androidandroid-activityrun-configurationgoogle-play-coredynamic-feature-module

How to start an Activity in a dynamic feature module?


When typing to start an Activity within a dynamic feature module trough an Android Studio run-configuration, I get the following warning: The activity 'SomeActivity' is not declared in AndroidManifest.xml. (because it is being declared in the AndroidManifest.xml of the dynamic feature module). For reference, this is the library being used:

// https://developer.android.com/guide/app-bundle/playcore
api "com.google.android.play:core:1.6.4"

The run configuration shows & deploys both modules, but it only recognizes the activities from the base module AndroidManifest.xml. How to start an Activity in a dynamic features module?


Side Note: When trying to install the deployed feature module, it doesn't seem to install:

I/PlayCore: SplitInstallListenerRegistry : registerListener
I/PlayCore: SplitInstallInfoProvider : No metadata found in AndroidManifest.
I/PlayCore: SplitInstallService : startInstall([feature_module],[])
I/PlayCore: SplitInstallService : Initiate binding to the service.
I/PlayCore: SplitInstallService : ServiceConnectionImpl.onServiceConnected(ComponentInfo{com.android.vending/com.google.android.finsky.splitinstallservice.SplitInstallService})
I/PlayCore: SplitInstallService : linkToDeath
I/PlayCore: SplitInstallService : onError(-5)
I/PlayCore: SplitInstallService : Unbind from service.

Where -5 means SplitInstallErrorCode.API_NOT_AVAILABLE (likely because it's a debug build); nevertheless getInstalledModules() should find the deployed feature module ...which it doesn't. SplitInstallInfoProvider : No metadata found in AndroidManifest seems to be this issue - when depoying "Default APK" instead of "APK from app bundle", the feature module gets installed.


Solution

  • Since one cannot reference feature module activities in the base module's AndroidManifest.xml, I've wrote a SplitInstallActivity, which resides in the debug source-set of the base module, where it is also available to tests. It can be called with a run-configuration, which passes launch flags:

    -e "moduleName" "feature_module" -e "className" "com.acme.feature.SomeActivity"
    

    It either installs the feature module by moduleName and/or starts the Activity by className.

    This at least works while deploying the "Default APK" instead of "APK from app bundle".

    ArgumentKeys.java

    public class ArgumentKeys {
    
        /** {@link SplitInstallActivity} dynamic features, the module name */
        public static final String ARGUMENT_FEATURE_MODULE_MODULE_NAME = "moduleName";
    
        /** {@link SplitInstallActivity} dynamic features, the activity class name to launch */
        public static final String ARGUMENT_FEATURE_MODULE_CLASS_NAME = "className";
    }
    

    SplitInstallActivity.java

    /**
     * Split-Install {@link AppCompatActivity}.
     * @author Martin Zeitler
    **/
    public class SplitInstallActivity extends AppCompatActivity implements SplitInstallStateUpdatedListener {
    
        private static final String LOG_TAG = SplitInstallActivity.class.getSimpleName();
    
        private SplitInstallRequest request;
        private SplitInstallManager sim;
    
        private String moduleName;
        private String className;
    
        public SplitInstallActivity() {}
    
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
    
            super.onCreate(savedInstanceState);
    
            /* instance the {@link SplitInstallManager}: */
            this.sim = SplitInstallManagerFactory.create(this.getApplicationContext());
    
            /* obtain the feature module & class name from arguments */
            if(this.getIntent() != null) {
                Bundle extras = this.getIntent().getExtras();
                if(extras != null) {
                    this.moduleName = extras.getString(ArgumentKeys.ARGUMENT_FEATURE_MODULE_MODULE_NAME);
                    this.className = extras.getString(ArgumentKeys.ARGUMENT_FEATURE_MODULE_CLASS_NAME);
                    if(this.moduleName != null && this.className != null) {
                        this.startFeatureActivity(this.moduleName, this.className);
                    } else {
                        Log.e(LOG_TAG, "module and class are required.");
                    }
                }
            }
        }
    
        /** it listens for the split-install session state */
        @Override
        public void onStateUpdate(SplitInstallSessionState state) {
            if(state.errorCode() == SplitInstallErrorCode.NO_ERROR && state.status() == SplitInstallSessionStatus.INSTALLED) {
                Log.d(LOG_TAG, "dynamic feature " + this.moduleName + " had been installed.");
                this.startFeatureActivity(this.moduleName, this.className);
            } else {
                // this.OnSplitInstallStatus(state);
            }
        }
    
        /** it checks if the dynamic feature module is installed and then either installs it - or starts the desired activity */
        private void startFeatureActivity(@NonNull String moduleName, @NonNull String className) {
            if (this.sim.getInstalledModules().contains(moduleName)) {
                Log.d(LOG_TAG, "dynamic feature module " + moduleName + " already installed.");
                Intent intent = this.getIntent();
                intent.setClassName(BuildConfig.APPLICATION_ID, className);
                this.startActivity(intent);
                this.finish();
            } else {
                Log.d(LOG_TAG, "dynamic feature module " + moduleName + " is not installed.");
                this.installFeatureModule(moduleName);
            }
        }
    
        /** it installs a dynamic feature module on demand */
        private void installFeatureModule(@NonNull String moduleName) {
            Log.d(LOG_TAG, "dynamic feature module " + moduleName + " will be installed.");
            this.request = SplitInstallRequest.newBuilder().addModule(moduleName).build();
            this.sim.registerListener(this);
            this.sim.startInstall(this.request);
        }
    
        ...
    }
    

    The launch of a specific Activity can be automated with an ActivityTestRule<?>:

    @Rule
    public ActivityTestRule<SplitInstallActivity> mRule = new ActivityTestRule<SplitInstallActivity>(SplitInstallActivity.class) {
    
        @Override
        protected Intent getActivityIntent() {
            Intent intent = new Intent();
            Bundle extras = new Bundle();
            extras.putString(ArgumentKeys.ARGUMENT_FEATURE_MODULE_MODULE_NAME, "feature_module");
            extras.putString(ArgumentKeys.ARGUMENT_FEATURE_MODULE_CLASS_NAME, "com.acme.feature.SomeActivity");
            intent.putExtras(extras);
            return intent;
        }
    };
    

    For testing one has to provide these dependencies in the feature module's build.gradle:

    androidTestDebugImplementation "com.google.android.gms:play-services-basement:17.1.1"
    androidTestDebugImplementation "com.google.android.play:core:1.6.4"
    

    Else it cannot link the resources of the test application and fails with:

    > Task :feature_module:processDebugAndroidTestResources FAILED
    AGPBI: {"kind":"error","text":"Android resource linking failed","sources":[{"file":"/home/user/.gradle/caches/transforms-2/files-2.1/7435b27a13269cffdd35a7dd69f0b9d2/core-1.6.4/AndroidManifest.xml","position":{"startLine":8,"startColumn":4,"endColumn":277}}],"original":"/home/user/.gradle/caches/transforms-2/files-2.1/7435b27a13269cffdd35a7dd69f0b9d2/core-1.6.4/AndroidManifest.xml:9:5-278: AAPT: error: resource style/Theme.PlayCore.Transparent (aka com.acme.feature.test:style/Theme.PlayCore.Transparent) not found.","tool":"AAPT"}
    AGPBI: {"kind":"error","text":"Android resource linking failed","sources":[{"file":"/home/user/.gradle/caches/transforms-2/files-2.1/c1b8b45e2f49fbe83ea45d80000bd6e9/jetified-play-services-basement-17.0.0/AndroidManifest.xml","position":{"startLine":22,"startColumn":8,"endLine":24,"endColumn":68}}],"original":"/home/user/.gradle/caches/transforms-2/files-2.1/c1b8b45e2f49fbe83ea45d80000bd6e9/jetified-play-services-basement-17.0.0/AndroidManifest.xml:23:9-25:69: AAPT: error: resource integer/google_play_services_version (aka com.acme.feature.test:integer/google_play_services_version) not found.","tool":"AAPT"}
    

    For testing, there's also: