Android: How do I call a method which is existing in other API Level?
Asked Answered
I

6

12

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.

Inroad answered 14/9, 2011 at 0:53 Comment(0)
C
9

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)
Carlie answered 14/9, 2011 at 1:11 Comment(8)
you cant just try to catch errors like that, the class loader will crash the app if you try to access apis that don't exist.Dicta
Are you saying that Android classloader will crash instead of producing standard errors like NoClassDefFoundError? Have you tried it?Carlie
The Dalvik VM will throw VerifyError.Carol
is reflection possible in Android? Would you share code snippet for it?Inroad
@Alex Gitelman you will never be able to load the class without nesting your API calls in a second class like in my answer.Dicta
I added test code and results that show that everything works exactly as I described. @schwiz application does not crash and no additional class is needed (at least in this basic form). If it was your down vote it would be prudent of you to remove it.Carlie
@Jeremy it does print some verify warning but it does not throw the error that would crash the app.Carlie
@Alex Gitelman I down voted for the second option you give, reflection is an option, try/catch blocks with missing APIs is not an options without an extra class to keep the class loader from reaching those lines.Dicta
B
23

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.

Busk answered 14/9, 2011 at 1:20 Comment(5)
You are right for checking the API level but I am asking on how to call a function which is not belong to compiling platform (in my case 2.1, the function is 2.3)Inroad
That is answered in my first sentence. You can't. You have to build against api level 9 in your case (Android 2.3) or the method just doesn't exist.Busk
so you're saying that is impossible unless build it against the corresponding api level.Inroad
yes. If you import ClassA from a library in which the method doesn't exist, it doesn't exist. The method you are trying to call is not even defined in the context in which you're trying to call it. In order for that method to even exist, you have to import the library containing the version of the class where the method exists.Busk
oh yeah you're right. even if it allows to call a function by name, still generate an error because it doesn't exist. So if I build it using API level 9, is @schwiz 's answer the best approach? what do you think guys...Inroad
C
9

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)
Carlie answered 14/9, 2011 at 1:11 Comment(8)
you cant just try to catch errors like that, the class loader will crash the app if you try to access apis that don't exist.Dicta
Are you saying that Android classloader will crash instead of producing standard errors like NoClassDefFoundError? Have you tried it?Carlie
The Dalvik VM will throw VerifyError.Carol
is reflection possible in Android? Would you share code snippet for it?Inroad
@Alex Gitelman you will never be able to load the class without nesting your API calls in a second class like in my answer.Dicta
I added test code and results that show that everything works exactly as I described. @schwiz application does not crash and no additional class is needed (at least in this basic form). If it was your down vote it would be prudent of you to remove it.Carlie
@Jeremy it does print some verify warning but it does not throw the error that would crash the app.Carlie
@Alex Gitelman I down voted for the second option you give, reflection is an option, try/catch blocks with missing APIs is not an options without an extra class to keep the class loader from reaching those lines.Dicta
D
4

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 "";
        }
     }
Dicta answered 14/9, 2011 at 1:25 Comment(6)
looks cool but don't know how to implement it using SensorManager. Would you share code snippet for it?Inroad
@Inroad I have provided a snippet already, what do you not understand? Make a static method inside of the COMPATIBILITY_HACK class that uses the API you want.Dicta
SensorManager.getAltitude(float, float) doesn't appear in the intellisense because I am using 2.1. Is it still possible?Inroad
based on @Busk comments, your answer still need the correct API level. is it correct?Inroad
you have to use the 2.2 sdk or higherDicta
Maybe on the above code but SensorManager.getAltitude requires API Level 9 up(2.3).Inroad
M
3

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.

Mussulman answered 25/10, 2012 at 12:49 Comment(2)
Is there a way to do it so lint in eclipse knows what is happening? I have code like above but lint warns me about it. I could just tell it to be quiet. But would rather do it in a clean way, so I don't miss anything.Carmagnole
Use the @TargetApi annotation - it is meant to give Lint hints about your intention. It overrides your manifest minimum API level for the section of code you are annotating. For instance, if you annotate a method with @TargetApi(Build.VERSION_CODES.GINGERBREAD) then you tell Lint that you are aware of that you are using something only available since Gingerbread and Lint can suppress its warnings.Mussulman
S
0

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());
  }
Stairwell answered 14/9, 2011 at 2:51 Comment(0)
S
0

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?)

Slav answered 10/10, 2011 at 18:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.