Thursday, June 20, 2013

Hacking (fixing) Toki Tori for ICS+ Devices

A while back I picked up one of the excellent +Humble Bundle collections for Android (actually I think I've bought them all!) and included in the bundle was an interesting little platformer called Toki Tori.

I'd actually forgotten about this game completely until the latest +Humble Bundle Android release. I was browsing the new games using the app they provide when I saw it and thought it would be a great game for the kids. Unfortunately after installing it I found that it wouldn't work! A Google search revealed:

Which was a bit disappointing, especially considering these articles mention ICS and JellyBean has already been out for ages...

So, what to do? Well I assumed that there must be some Android platform related change that was causing the issue. Maybe this could be patched or worked around? First thing then is to have a look the logcat output when the game crashed:

E/AndroidRuntime(24470): FATAL EXCEPTION: main
E/AndroidRuntime(24470): java.lang.NoClassDefFoundError: android/view/ViewRoot
E/AndroidRuntime(24470):        at com.polarbit.fuse.MainTask.processTouchpadAsPointer(Native Method)
E/AndroidRuntime(24470):        at com.polarbit.fuse.MainTask.access$200(MainTask.java:42)
E/AndroidRuntime(24470):        at com.polarbit.fuse.MainTask$RenderSurface.surfaceCreated(MainTask.java:338)

A bit of searching revealed this:

It seems that the Toki Tori developers (or porters or whatever) might have been a bit naughty and referred to an internal framework class that changed between GB and ICS as per this quote from the ViewRoot 2.2 source:

The top of a view hierarchy, implementing the needed protocol between View and the WindowManager. This is for the most part an internal implementation detail of WindowManagerImpl

At this point I had two choices:
  • try and reverse engineer the native method processTouchpadAsPointer() and attempt to patch it
  • see what would happen if I simply didn't call it.
I opted for the easier route :) This wasn't an entirely sloth based decision: several obscure searches pointed in the direction of this function being linked to the Xperia Play, which in my case (I have a Samsung S3) wasn't a relevant platform.

When reverse engineering Android code I usually just use the excellent command line utilities: APKTool and Dex2Jar and then run the resulting jar through JD-GUI to have a look.

However on this occasion I stumbled upon the most excellent Virtuous Ten Studio which automates most of the drudgery out of the process. I'd really recommend it and suggest you go and buy a licence to support the devs if you can afford it! So following the stack trace we get to the surfaceCreated method of the RenderSurface class which looks like this in smali:

.method public surfaceCreated(Landroid/view/SurfaceHolder;)V
    .locals 4
    .parameter "holder"

    .prologue
    const/4 v3, 0x1

    .line 332
    iget-object v2, p0, Lcom/polarbit/fuse/MainTask$RenderSurface;->this$0:Lcom/polarbit/fuse/MainTask;

    iput-boolean v3, v2, Lcom/polarbit/fuse/MainTask;->mHasSurface:Z

    .line 334
    iget-object v2, p0, Lcom/polarbit/fuse/MainTask$RenderSurface;->this$0:Lcom/polarbit/fuse/MainTask;

    iget-object v2, v2, Lcom/polarbit/fuse/MainTask;->mContext:Landroid/app/Activity;

    invoke-virtual {v2}, Landroid/app/Activity;->getWindow()Landroid/view/Window;

    move-result-object v2

    invoke-virtual {v2}, Landroid/view/Window;->getDecorView()Landroid/view/View;

    move-result-object v2

    invoke-virtual {v2}, Landroid/view/View;->getRootView()Landroid/view/View;

    move-result-object v0

    .line 335
    .local v0, root:Landroid/view/View;
    if-eqz v0, :cond_0

    .line 336
    invoke-virtual {v0}, Landroid/view/View;->getParent()Landroid/view/ViewParent;

    move-result-object v1

    .line 337
    .local v1, viewRoot:Landroid/view/ViewParent;
    if-eqz v1, :cond_0

    .line 338
    iget-object v2, p0, Lcom/polarbit/fuse/MainTask$RenderSurface;->this$0:Lcom/polarbit/fuse/MainTask;

    iget v2, v2, Lcom/polarbit/fuse/MainTask;->mInstance:I

    #calls: Lcom/polarbit/fuse/MainTask;->processTouchpadAsPointer(ILandroid/view/ViewParent;Z)Z
    invoke-static {v2, v1, v3}, Lcom/polarbit/fuse/MainTask;->access$200(ILandroid/view/ViewParent;Z)Z

    move-result v2

    if-ne v2, v3, :cond_0

    .line 350
    .end local v1           #viewRoot:Landroid/view/ViewParent;
    :cond_0
    return-void
.end method

The interesting bit being this line of code:

if-eqz v0, :cond_0

Which is a basic null check before calling some additional code and finally the nasty method we want to get rid of. Hmmm looks like an interesting candidate and an elegant patch point. If we change the code to this:

if-nez v0, :cond_0

Then the condition is inverted and the relevant code skipped....
Making the change and using Virtuous Ten Studio to rebuild the APK was a 30 second operation, and guess what... it WORKS!

That is to say: I've played several levels and not only does the game now start, but it appears to work flawlessly.

Obviously I can't share the APK, but if the +Humble Bundle guys / gals want to get hold of me, I'd be happy to help them through this process. Alternatively, you could give it a go yourself.

Until next time, happy hacking!
-(e)