Tuesday, July 6, 2010

Access Internal classes in Android

So long before, i was blogging too much, then i stopped and told you guys i'll be doing more Android and Frontend, then i quit OpenRice and now back to doing Android as hobby.

Senario
Say you want to access some internal class in Android which are not public and you really want to use that feature, what do you do?

Notes
* This is on how to access non public classes thus are restricted classes which are not recommended, use this knowledge at your own risk and i'm NOT to be held responsible to any harm it might cause.
* This is quite advance and i hope to explain it as much as possible while keeping it simple.
* The following codes might not work as you/I expected it to work thus this is for reference only

Introduction
As i was trying to update my tutorial on Localization i was looking at how the locale setting was done and i found it at the LocalePicker.java under the onListItemClick function, if you try this code in your app you would realized that ActivityManagerNative and IActivityManager are internal classes. So how to use this piece of code?
IActivityManager am = ActivityManagerNative.getDefault();
Configuration config = am.getConfiguration();
Loc loc = mLocales[position];
config.locale = loc.locale;
final String language = loc.locale.getLanguage();
final String region = loc.locale.getCountry();
am.updateConfiguration(config);



What Do I Need
In Manafiest.xml
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />

Codes
DexFile  df = new DexFile(new File("/system/app/Settings.apk"));
String name = "com.android.settings.LocalePicker"; 
ClassLoader cl = getClassLoader();     
Class LocalePicker = df.loadClass(name, cl);
Class ActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
Class IActivityManager = Class.forName("android.app.IActivityManager");

Method getDefault =  ActivityManagerNative.getMethod("getDefault", null);
Object am = IActivityManager.cast(getDefault.invoke(ActivityManagerNative, null));

Method getConfiguration =  am.getClass().getMethod("getConfiguration", null);

Configuration config = (Configuration) getConfiguration.invoke(am, null);
Locale locale = new Locale(languageToLoad);
Locale.setDefault(locale);
config.locale = locale;

Class[] args = new Class[1];
args[0] = Configuration.class;
Method updateConfiguration =  am.getClass().getMethod("updateConfiguration", args);
updateConfiguration.invoke(am, config);


Explanation
DexFile df = new DexFile(new File("/system/app/Settings.apk"));
You need to load the package from where the class is from
NOTE: The dex don't directly open or readed in this line but they're memory-mapped read-only by the VM.

String name = "com.android.settings.LocalePicker";
ClassLoader cl = getClassLoader();
Class LocalePicker = df.loadClass(name, cl);

Load the class where your target classes were imported.

Class ActivityManagerNative = Class.forName("android.app.ActivityManagerNative");
Class IActivityManager = Class.forName("android.app.IActivityManager");

Load the classes you want to use

Method getDefault = ActivityManagerNative.getMethod("getDefault", null);
To use a method you have to get the method first. http://developer.android.com/reference/java/lang/reflect/Method.html

Object am = IActivityManager.cast(getDefault.invoke(ActivityManagerNative, null));
Execute the getDefault method using the invoke class, and since it has a return of IActivityManager, cast the returned object.

Method getConfiguration = am.getClass().getMethod("getConfiguration", null);
Since am is an instance of IActivityManager we could use getClass() to reflect on its method.

Configuration config = (Configuration) getConfiguration.invoke(am, null);
Execute the getConfiguration method, you can see here we use the regular casting (Configuration), you could also use Configuration.Class.cast

ocale locale = new Locale(languageString);
Locale.setDefault(locale);
config.locale = locale;

Just set a new Locale configuration

Class[] args = new Class[1];
args[0] = Configuration.class;
Method updateConfiguration = am.getClass().getMethod("updateConfiguration", args);
updateConfiguration.invoke(am, config);

To reflect on a method with arguements you have to do it this way, the second argument is a list of Class type of the required arguments. Optionally you could refer to another approach found in Calling private methods in Android

Conclusion
Reflecting a class could be lead to unknown results, like this class would set System Wide language change and not application wise and thus executing would lead to your whole system being reconfigured on the language (your home screen would HANG after you change and click Home), this is a powerful tool thus "We great knowledge comes great responsibility" :)

References
Google Groups
ClassTest
LocalePicker.java
Calling private methods in Android
http://developer.android.com/reference/java/lang/reflect/Method.html
Post a Comment