Android: How do I call a method which is existing in other API Level?
I开发者_如何转开发 have application using Android 2.1 which utilize LocationManager to get the altitude. But now, I need to obtain the altitude using SensorManager which requires API Level 9 (2.3).
How can I put the SensorManager.getAltitude(float, float)
in my 2.1 android application by putting a condition and calling it by a function name (possible in normal Java)?
Thank you in advance
UPDATE 1 If you have noticed that my application need to be compiled using Android 2.1. That's why I'm looking for a way to call the function by name or in any other way that can be compiled.
You need to build against the highest api you require and then code alternate code paths conditionally for other levels you want to support
To check current API level at execution time, the latest recommendation from the Android docs is to do something like this:
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD)
{
...
Once you introduce this complexity though, you have to be very careful. There isn't currently an automatic way to check all code paths to make sure that all api level calls above the minSdkVersion have alternative calls to support all versions. Maybe someone can chime in if there exists a unit testing tool that might do something like this.
You can call the method using reflection and fail gracefully in case of errors (like missing class or methods). See java.lang.reflect
Other option is to compile code in level 9 but surround with try/catch to catch errors that would arise from execution on lower level. It could be fairly error prone, though, and I'd think twice about doing it.
Update
Here is test code
public void onCreate(Bundle savedInstanceState)
{
try {
// First we try reflection approach.
// Expected result
// in 2.3 we print some value in log but no exception
// in 2.2 we print NoSuchMethodException
// In both levels we get our screen displayed after catch
Method m = SensorManager.class.getMethod("getAltitude",Float.TYPE, Float.TYPE);
Float a = (Float)m.invoke(null, 0.0f, 0.0f);
Log.w("test","Result 1: " + a);
} catch (Throwable e) {
Log.e("test", "error 1",e);
}
try {
// Now we try compiling against 2.3
// Expected result
// in 2.3 we print some value in log but no exception
// in 2.2 we print NoSuchMethodError (Note that it is an error not exception but it's still caught)
// In both levels we get our screen displayed after catch
float b = SensorManager.getAltitude(0.0f, 0.0f);
Log.w("test","Result 2: " + b);
} catch (Throwable e) {
Log.e("test", "error 2",e);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
Results:
2.3
09-14 07:04:50.374: DEBUG/dalvikvm(589): Debugger has detached; object registry had 1 entries
09-14 07:04:50.924: WARN/test(597): Result 1: NaN
09-14 07:04:51.014: WARN/test(597): Result 2: NaN
09-14 07:04:51.384: INFO/ActivityManager(75): Displayed com.example/.MyActivity: +1s65ms
2.2
09-14 07:05:48.220: INFO/dalvikvm(382): Could not find method android.hardware.SensorManager.getAltitude, referenced from method com.example.MyActivity.onCreate
09-14 07:05:48.220: WARN/dalvikvm(382): VFY: unable to resolve static method 2: Landroid/hardware/SensorManager;.getAltitude (FF)F
09-14 07:05:48.220: DEBUG/dalvikvm(382): VFY: replacing opcode 0x71 at 0x0049
09-14 07:05:48.220: DEBUG/dalvikvm(382): VFY: dead code 0x004c-0064 in Lcom/example/MyActivity;.onCreate (Landroid/os/Bundle;)V
09-14 07:05:48.300: ERROR/test(382): error 1
java.lang.NoSuchMethodException: getAltitude
at java.lang.ClassCache.findMethodByName(ClassCache.java:308)
Skipped stack trace
09-14 07:05:48.300: ERROR/test(382): error 2
java.lang.NoSuchMethodError: android.hardware.SensorManager.getAltitude
at com.example.MyActivity.onCreate(MyActivity.java:35)
Skipped more stack trace
09-14 07:05:48.330: DEBUG/dalvikvm(33): GC_EXPLICIT freed 2 objects / 64 bytes in 180ms
09-14 07:05:48.520: INFO/ActivityManager(59): Displayed activity com.example/.MyActivity: 740 ms (total 740 ms)
You can take advantage of how class isn't loaded until it is accessed for an easy work around that doesn't require reflection. You use an inner class with static methods to use your new apis. Here is a simple example.
public static String getEmail(Context context){
try{
if(Build.VERSION.SDK_INT > 4) return COMPATIBILITY_HACK.getEmail(context);
else return "";
}catch(SecurityException e){
Log.w(TAG, "Forgot to ask for account permisisons");
return "";
}
}
//Inner class required so incompatibly phones won't through an error when this class is accessed.
//this is the island of misfit APIs
private static class COMPATIBILITY_HACK{
/**
* This takes api lvl 5+
* find first gmail address in account and return it
* @return
*/
public static String getEmail(Context context){
Account[] accounts = AccountManager.get(context).getAccountsByType("com.google");
if(accounts != null && accounts.length > 0) return accounts[0].name;
else return "";
}
}
When the question is "Do I have this class or method at the current API level?" then use branching like:
class SomeClass {
public void someMethod(){
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD)
{
//use classes and/or methods that were added in GINGERBREAD
}
}
}
For this you need to use an Android library that is Gingerbread or above. Otherwise the code won't compile with the classes added in Gingerbread.
This solution is MUCH more cleaner than the disgusting reflection stuff. Note that the dalvik will log a (not-lethal) error stating that he cannot find the classes added in GINGERBREAD when trying to load SomeClass but the app won't crash. It would only crash if we would try to USE that specific class and enter the IF branch - but we don't do that (unless we are on GINGERBREAD or later).
Note that the solution also works when you have a class that were there forever but a new method was added in Gingerbread. In runtime if you are running on pre-Gingerbread you just don't enter the IF branch and don't call that method thus the app will not crash.
Here how you do it using reflection (Calling StrictMode class from the level where it is not available:
try {
Class<?> strictmode = Class.forName("android.os.StrictMode");
Method enableDefaults = strictmode.getMethod("enableDefaults");
enableDefaults.invoke(null, new Object[] {});
} catch (Exception e) {
Log.i(TAG, e.getMessage());
}
I haven't tried it - but it should be possible, using some code generation, to create a proxy library (per API level) that will wrap the entire native android.jar and whose implementation will try to invoke the methods from android.jar.
This proxy lib will use either the above mentioned internal-static-class way or reflection to make the dalvikvm lazily link to the requested method.
It will let the user access all the API she wants (assuming she'll check for correct API level) and prevent the unpleasant dalvikvm log messages. You could also embed each method's API level and throw a usable exception (BadApiLevelException or something)
(Anyone knows why Google/Android don't already do something like that?)
精彩评论