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.

eros
  • 4,616
  • 17
  • 48
  • 77

6 Answers6

22

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.

Rich
  • 34,878
  • 31
  • 108
  • 151
  • 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) – eros Sep 14 '11 at 02:16
  • 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. – Rich Sep 14 '11 at 02:18
  • so you're saying that is impossible unless build it against the corresponding api level. – eros Sep 14 '11 at 03:43
  • 1
    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. – Rich Sep 14 '11 at 04:01
  • 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... – eros Sep 14 '11 at 04:07
8

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)
Alex Gitelman
  • 23,471
  • 7
  • 49
  • 48
  • 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. – Nathan Schwermann Sep 14 '11 at 01:18
  • Are you saying that Android classloader will crash instead of producing standard errors like NoClassDefFoundError? Have you tried it? – Alex Gitelman Sep 14 '11 at 01:28
  • 1
    The Dalvik VM will throw VerifyError. – Jeremy Edwards Sep 14 '11 at 02:15
  • is reflection possible in Android? Would you share code snippet for it? – eros Sep 14 '11 at 02:17
  • @Alex Gitelman you will never be able to load the class without nesting your API calls in a second class like in my answer. – Nathan Schwermann Sep 14 '11 at 03:24
  • 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. – Alex Gitelman Sep 14 '11 at 07:13
  • @Jeremy it does print some verify warning but it does not throw the error that would crash the app. – Alex Gitelman Sep 14 '11 at 07:14
  • @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. – Nathan Schwermann Sep 14 '11 at 07:26
5

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 "";
        }
     }
Nathan Schwermann
  • 30,406
  • 15
  • 78
  • 89
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.

Zsolt Safrany
  • 11,950
  • 5
  • 48
  • 57
  • 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. – ctrl-alt-delor Feb 26 '13 at 14:41
  • 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. – Zsolt Safrany Feb 26 '13 at 16:06
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());
  }
haimg
  • 4,458
  • 33
  • 47
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?)

m0she
  • 416
  • 4
  • 9