Search code examples
androidandroid-activityjava-native-interfacenative-activity

Switching between two NativeActivities in Android


I need to have two separate activities in an Android app. As far as I understand this is done with having multiple <activity> tags in the <application> tag. Many things online indicate that you use something like this code:

 Intent intent = new Intent(this, mynativeactivity.class);
 startActivity(intent);

However, the android:name (and by extension, class) of both Native Activities is android.app.NativeActivity. They are just in separate .so files. Even if I use the JNI.

Is there a way of switching activities based on a string name, alias or label of the specific android activity?

My AndroidManifest.xml looks something like this:

    <application android:debuggable="true" android:hasCode="false" android:label="cnfgtest" tools:replace="android:icon,android:theme,android:allowBackup,label" android:icon="@mipmap/icon">
        <activity android:configChanges="keyboardHidden|orientation" android:label="app_i_want_to_launch" android:name="android.app.NativeActivity">
            <meta-data android:name="android.app.lib_name" android:value="app_i_want_to_launch"/>
            <intent-filter>
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:configChanges="keyboardHidden|orientation" android:label="app_that_currently_launches" android:name="android.app.NativeActivity">
            <meta-data android:name="android.app.lib_name" android:value="app_that_currently_launches"/>
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

EDIT: Within this build environment, there's really no way to build any additional Java code. All solutions would need to be with just NativeActivity.

EDIT2: To elaborate on my finally executed solution. I ended up compiling a separate Java file outside my tree and included the classes.dex inside my project and let the build system continue as expected. This allowed me to make separate class that was also exactly a NativeActivity but by a different name. I.e.

# javac -source 1.7 -target 1.7 -d bin/org/yourorg/cnfgtest/ -classpath ${ANDROID_HOME}/platforms/android-30/android.jar -sourcepath src src/org/yourorg/cnfgtest/MyOtherNativeActivity.java

# ~/android-sdk/build-tools/30.0.2/dx --output=makecapk/ --dex ./bin #makecapk is the root of my project.

Then use each class to define a new native activity.

Also, I was able to make use of parts of @dalmif 's answers below.


Solution

  • Create another class and extend NativeActivity like this

    public class MyOtherNativeActivity extends NativeActivity {}
    

    now you can put it in AndroidManifest.xml.

    <activity
        android:name="android.app.NativeActivity"
        android:configChanges="keyboardHidden|orientation"
        android:label="app_that_currently_launches">
        <meta-data
            android:name="android.app.lib_name"
            android:value="first_so_name" />
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    
    <!--SECOND NATIVE ACTIVITY-->
    <activity
        android:name=".MyOtherNativeActivity"
        android:configChanges="keyboardHidden|orientation"
        android:label="app_i_want_to_launch">
        <meta-data
            android:name="android.app.lib_name"
            android:value="second_so_name" />
    </activity>
    

    finally just startActivity with this new class.

    startActivity(new Intent(this, MyOtherNativeActivity.class))
    

    Update

    In general, you have two options for having a NativeActivity for each native library.

    1. Create a java class for each native library (as explained before).
    2. Use the singe NativeActivity declaration with modifiable <meta-data> tag. (In cases where you don't want to create Java files)

    the first option is explained above, but about the second, you can use this Application class

    import android.app.Activity;
    import android.app.Application;
    import android.app.NativeActivity;
    import android.content.ComponentName;
    import android.content.Context;
    import android.content.pm.ActivityInfo;
    import android.content.pm.PackageManager;
    import android.os.Bundle;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    /**
     * NativeActivity is an special activity for implementing in native libraries but
     * when you want to make more than one NativeActivity you have to create java classes
     * which extend NativeActivity and declare them in AndroidManifest.xml
     * and it's not possible in some situations but now using this Application class
     * you just put your library name which you want to load the activity from in
     * the intent like this
     *      <p><code>Intent intent = new Intent(this, NativeActivity.class); <br>
     *      intent.putExtra(NativeActivity.META_DATA_LIB_NAME, "my-native-library-name"); <br>
     *      startActivity(intent);</code></p>
     * Don't forget to 
     * 1. Add this class in the application tag of AndroidManifest.xml
     * 2. Add `android.app.NativeActivity` as an activity in AndroidManifest.xml
     */
    
    public class NativeActivityLoaderApplication extends Application implements InvocationHandler, Application.ActivityLifecycleCallbacks {
    
        private Object base;
        private String currentLibName;
    
        @Override
        public void onCreate() {
            super.onCreate();
            registerActivityLifecycleCallbacks(this);
        }
    
        @Override
        protected void attachBaseContext(Context base) {
            hookPackageManager(base);
            super.attachBaseContext(base);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // if currentLibName is equal to null then run in normal way, otherwise change the lib_name
            if (currentLibName != null  && "getActivityInfo".equals(method.getName())) {
                ComponentName componentName = (ComponentName) args[0];
                if (componentName.getClassName().equals(NativeActivity.class.getName())) {
                    ActivityInfo info = (ActivityInfo) method.invoke(base, args);
                    if (info != null) {
                        if (info.metaData == null) info.metaData = new Bundle();
                        info.metaData.putString(NativeActivity.META_DATA_LIB_NAME, currentLibName);
                    }
                    return info;
                }
            }
            return method.invoke(base, args);
        }
    
        private void hookPackageManager(Context context) {
            try {
                // get ActivityThread and currentActivityThread Method
                Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
                Method currentActivityThreadMethod =
                        activityThreadClass.getDeclaredMethod("currentActivityThread");
                Object currentActivityThread = currentActivityThreadMethod.invoke(null);
    
                // get PackageManager instance in the current activityThread
                Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
                sPackageManagerField.setAccessible(true);
                Object sPackageManager = sPackageManagerField.get(currentActivityThread);
    
                // get Class of IPackageManager
                Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
                this.base = sPackageManager;
    
                Object proxy = Proxy.newProxyInstance(
                        iPackageManagerInterface.getClassLoader(),  new Class<?>[]{ iPackageManagerInterface }, this);
    
                // set sPackageManager inside ActivityThread object to our hook
                sPackageManagerField.set(currentActivityThread, proxy);
    
                // set packageManager (mPM) to our hook
                PackageManager pm = context.getPackageManager();
                Field pmField = pm.getClass().getDeclaredField("mPM");
                pmField.setAccessible(true);
                pmField.set(pm, proxy);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void onActivityPreCreated(Activity activity, Bundle savedInstanceState) {
            if (activity instanceof NativeActivity) {
                currentLibName = activity.getIntent().getStringExtra(NativeActivity.META_DATA_LIB_NAME);
            }
            ActivityLifecycleCallbacks.super.onActivityPreCreated(activity, savedInstanceState);
        }
    
        @Override
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
    
        @Override
        public void onActivityStarted(Activity activity) {}
    
        @Override
        public void onActivityResumed(Activity activity) {}
    
        @Override
        public void onActivityPaused(Activity activity) {}
    
        @Override
        public void onActivityStopped(Activity activity) {}
    
        @Override
        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
    
        @Override
        public void onActivityDestroyed(Activity activity) {}
    }
    

    and declare this application class in your manifest

    <application
        android:name=".NativeActivityLoaderApplication"
        ....
    

    now you can simply use this code to set the lib name (<meta-data android:name="android.app.lib_name" ...>) in your code instead of AndroidManifest.xml

    Intent intent = new Intent(this, NativeActivity.class);
    intent.putExtra(NativeActivity.META_DATA_LIB_NAME, "native-activity");
    startActivity(intent);