3

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.

Martin Zeitler
  • 1
  • 19
  • 155
  • 216
  • When I had to participate one of the GDG Events and their `Bundles` feature (which I think, somehow related to the same question, because you need to split everything as much as possible to different packages). They said, that right now you can call Activity from another package with reflection and have few workarounds for that (actually the same reflection, but in nicer way). – GensaGames Oct 29 '19 at 16:36
  • @GensaGames I know how to start them at run-time, but this doesn't help with (automated) testing. Since adding the feature module, there are 2 package names, which is the root problem - and both having the same package name does not seem right either. When trying to add them into debug `AndroidManifest.xml`, the name-space is not resolved either. – Martin Zeitler Oct 29 '19 at 16:51
  • Question above is `How to run the activities from dynamic features modules?` – GensaGames Oct 29 '19 at 16:55
  • but it is tagged `run-configuration` (since the run-time approach is documented). One possible workaround might be, to add an debug activity with an intent-filter for the class name, which then starts these feature module activities at run-time, through reflection - but starting these with `ActitivtiyRule` for automated testing still seems complicated. Manually constructing the `Intent` for the run-configuration might also be an option. – Martin Zeitler Oct 29 '19 at 17:02
  • Thanks. If I understand it correctly for testing purpose we can use `bundletool`, which basically just install all the modules one by one. Depending on the configuration run. Here is documentation on this tool https://developer.android.com/studio/command-line/bundletool – GensaGames Oct 29 '19 at 17:11
  • @GensaGames for manual testing purposes, one can create run-configurations, which have the feature module whether deployed - or not deployed. See my answer on how to start such a feature module `Activity`, with an `Intent`, from a run-configuration. – Martin Zeitler Oct 30 '19 at 01:25

1 Answers1

2

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:

Martin Zeitler
  • 1
  • 19
  • 155
  • 216