poniedziałek, 23 lutego 2015

Android - Loading Activity from additional apk

( update: source code can be found here: https://github.com/marcing-dev/APK_loading )

Some time ago I've found a way of executing Activities in Android that are not located in original apk. Here’s a little story about that!

Every Android developer who was writing big app probably had a problem with limitation of number of references in one apk file. There're few ways to deal with it for example something called Multidexing (https://developer.android.com/tools/building/multidex.html). Developer can also create an instance of ClassLoader that will load classes from another apk, and then just use these classes.
Code below shows that:

Here’s a class I’ve created in additional apk:

and a log:
>_ term
02-20 12:26:01.466  15412-15412/pl.com.marcing.android.dynamicactivityloader I/DynamicClassLoadActivity﹕ Trying to load new class from apk.
02-20 12:26:01.466  15412-15412/pl.com.marcing.android.dynamicactivityloader I/DynamicClassLoadActivity﹕ dexInternalStoragePath: /data/data/pl.com.marcing.android.dynamicactivityloader/app_dex/test.apk
02-20 12:26:01.466  15412-15412/pl.com.marcing.android.dynamicactivityloader I/DynamicClassLoadActivity﹕ New apk found!
02-20 12:26:01.466  15412-15412/pl.com.marcing.android.dynamicactivityloader I/DynamicClassLoadActivity﹕ New object has class: pl.com.marcing.android.customdex.NewObject
02-20 12:26:01.466  15412-15412/pl.com.marcing.android.dynamicactivityloader I/DynamicClassLoadActivity﹕ Invoking getInfo on new object: New object info
This approach is nice, but has its limitations. For example you can't run new Activities using it, why? Because when you start new Activity original application ClassLoader is used.
ClassLoaders are written in Java and they are normal objects inside app, so one can get access to them. Of course they are written in such way that normal access is impossible. You can't just simply grab a ClassLoader object and start setting attributes inside it. I could end my story here but fortunately there is a thing called Java Reflection API. It gives you possibility to do almost anything with Java objects. Let's check an example class:
This class has an attribute named "guardedAttribute". It's private and there's no way you can change it normally. So here's an output of test app using that class: 
>_ term
guardedAttribute: I'm private, u can't change me!
guardedAttribute: Oh really?
guardedAttribute value changed :) Here's code of this app:
I think there's no need for explanation here. If field is final you can also change its value but if it’s primitive type u basically gain nothing cause classes will use old value. But if that final attribute is some object and you change it to new one, class will use the new one.
Let's go back to Android...I have created new apk containing Activity like this one:

I modified method for loading custom classes to look like that:

When I’ve tried to run new Activity from new apk this error showed up:

>_ term
android.content.ActivityNotFoundException: Unable to find explicit activity class {pl.com.marcing.android.dynamicactivityloader/pl.com.marcing.android.customdex.TestActivity}; have you declared this activity in your AndroidManifest.xml?
Activity is not specified in AndroidManifest.xml...So let's specify it (there was an error....ignored it).

Let's check it again:
>_ term
    Process: pl.com.marcing.android.dynamicactivityloader, PID: 14987
    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{pl.com.marcing.android.dynamicactivityloader/pl.com.marcing.android.customdex.TestActivity}: java.lang.ClassNotFoundException: Didn't find class "pl.com.marcing.android.customdex.TestActivity" on path: DexPathList[[zip file "/data/app/pl.com.marcing.android.dynamicactivityloader-2.apk"], nativeLibraryDirectories=[
/data/app-lib/pl.com.marcing.android.dynamicactivityloader-2, /system/lib]]
Still error but this time different.  Going through source code of BaseDexClassLoader I’ve found that attribute holding paths to apks and libs is named “pathList”.  So I created some custom ClassLoader and then got out its “pathList” and set it to application ClassLoader. This will force app ClassLoader to load classes from my apk. Everything looks like that:

In addition I'm saving old classpath and adding it to the new one so there are no errors. Let's see what will happen when I'll try to load new Activity:

>_ term
02-20 11:51:46.742  15359-15359/pl.com.marcing.android.dynamicactivityloader I/DynamicActivityLoadActivity﹕ Test: dalvik.system.DexClassLoader[DexPathList[[zip file "/data/data/pl.com.marcing.android.dynamicactivityloader/app_dex/test.apk", zip file "/data/app/pl.com.marcing.android.dynamicactivityloader-1.apk"],nativeLibraryDirectories=[/data/app-lib/pl.com.marcing.android.dynamicactivityloader-1, /system/lib, /system/lib]]]
02-20 11:51:46.750  15359-15359/pl.com.marcing.android.dynamicactivityloader I/TestActivity﹕ ::onStart
02-20 11:51:46.750  15359-15359/pl.com.marcing.android.dynamicactivityloader I/TestActivity﹕ action inside TestActivity
This time everything went smooth and new Activity has been started :) Impossible is nothing! 

That's all, I think that people will find some interesting tricks that can be done using this technique. I for example, have made an app that gives user option to add new features to it. User clicks button, app connects to server, downloads required apk and runs it. Activity loader can be inside new apk. Application loads loader class from apk and fires it passing context to it. The best thing about that approach is that if u load “different” apk you can get different results. Have fun!

Source code can be found here: https://github.com/marcing-dev/APK_loading

7 komentarzy:

  1. Hey Marcin.
    Could You please upload the sources. I would love to try this thing. And Great article - cheers!

  2. Ten komentarz został usunięty przez autora.

  3. Ten komentarz został usunięty przez autora.

  4. Hello Marcin,
    First of all thanks for this great job.
    I got one question:
    is there an ability to load not only classes but resources from "additional apk"?
    For example i need that "activity in a additional apk" should be able to work with it's own R., load it's own R.layout and use it's own strings. I know that i can put all resources in "loader apk" but what about to keep all resources for activities in "additional apk" ?

    Thanks for your answer.

    1. Hi,
      I've looked briefly into resources but without any success. They are handled differently, right now I'm not able to look into this. Try checking source code to see how they deal with it (I think it will probably go to some manager which is a service running in OS). If there is no easy way of patching this you can create your own resource handler and overwrite the original one. If not...handle everything in Java without using R

  5. In my opinion we need to overload resource handling routines and redirect them to work with resources in "additional apk" but how?

  6. Hi Marcin

    I Want Write Code Protector(Or Packer) For My App By This Method

    Can I Encrypt Bytes Orginale Apk And Add It To Apk Loader And In Apk Loader When Apk Loader Run In Phone
    Apk Loader Decrypt Orginale Apk And Run It And When Apk Loader Closed Delete Decrypted Orginale Apk.