Search code examples
androidandroid-intentandroid-activitymodulebundle

Main activity class of loaded Dynamic feature is not found


I am trying to add dynamic feature support to my app, so I created a test app.

The test app has a main app part that loads a dynamic feature and try to execute it.

The feature module has MainActivityCalled as main activity.

What I get is that the feature loading process works because I get successful log messages, and I get the list of installed modules.

Note that the app is run on a virtual device and no real download happens, I thinks everything gets installed automatically by deployment procedure from AndroidStudio.

The fact is that I get this kind of error when trying to call the main activity of the module:

W/System.err: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.example.dynamicfeature1/MainActivityCalled}; have you declared this activity in your AndroidManifest.xml?
W/System.err:     at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:1805)
    at android.app.Instrumentation.execStartActivity(Instrumentation.java:1523)
    at android.app.Activity.startActivityForResult(Activity.java:4225)
    at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767)
    at android.app.Activity.startActivityForResult(Activity.java:4183)
    at androidx.fragment.app.FragmentActivity.startActivityForResult(FragmentActivity.java:754)
    at android.app.Activity.startActivity(Activity.java:4522)
    at android.app.Activity.startActivity(Activity.java:4490)
    at com.example.mymodules.MainActivity$2.onClick(MainActivity.java:227)
    at android.view.View.performClick(View.java:5637)
    at android.view.View$PerformClick.run(View.java:22429)
    at android.os.Handler.handleCallback(Handler.java:751)
    at android.os.Handler.dispatchMessage(Handler.java:95)

Everything is done as in the documentation at https://developer.android.com/guide/app-bundle/playcore#java except for the explicit call to the module activity that I guessed has to be performed with an intent (no example in the above linked page).

Important part of the code you find below are:

new OnSuccessListener<Integer>() {
                        @Override
                        public void onSuccess(Integer result) {
                            Log.d("request feature load","success "+result);
                            mySessionId=result;
                            Set<String> installedModules = splitInstallManager.getInstalledModules();
                            String[] modules = new String[installedModules.size()];

                            installedModules.toArray(modules);
                            for (int i=0;i<modules.length;i++)
                            {
                                Log.d("module",modules[i]);
                            }


                        }
                    })

that is OK.

Then

Button button2 = findViewById(R.id.fab2);
    button2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {



            Log.d("feature1","called");
            Intent intent=new Intent();
            intent.setClassName("com.example.dynamicfeature1","MainActivityCalled");

            try{  startActivity(intent);}
            catch (Exception e){
                e.printStackTrace();
            }

        }
    });

All main activities has this overidden method

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this);
    Log.d("attachBaseContext",base.getPackageName().toString());
}

that is not called in the dynamic feature module when installed.

What is wrong with my code?

This is the Mainactivity of the app

package com.example.mymodules;

...imports...

public class MainActivity extends AppCompatActivity {
private static int MY_REQUEST_CODE=1;
Activity activity;
int mySessionId;

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of future on demand modules using SplitCompat.
    SplitCompat.install(this);
 Log.d("attachBaseContext",base.getPackageName().toString());

}

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == MY_REQUEST_CODE) {
        // Handle the user's decision. For example, if the user selects "Cancel",
        // you may want to disable certain functionality that depends on the module.
    }
}


@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    activity = this;
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    Button button1 = findViewById(R.id.button1);
    button1.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {

            // Creates an instance of SplitInstallManager.
            final SplitInstallManager splitInstallManager =
                    SplitInstallManagerFactory.create(activity);

// Creates a request to install a module.
            SplitInstallRequest request =
                    SplitInstallRequest
                            .newBuilder()
                            // You can download multiple on demand modules per
                            // request by invoking the following method for each
                            // module you want to install.
                            .addModule("dynamicfeature1")

                            .build();

// Creates a listener for request status updates.
            SplitInstallStateUpdatedListener listener =new SplitInstallStateUpdatedListener() {
                @Override
                public void onStateUpdate(SplitInstallSessionState state) {
                    if (state.status() == SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION) {
                        // Displays a dialog for the user to either “Download”
                        // or “Cancel” the request.
                        try {
                            splitInstallManager.startConfirmationDialogForResult(
                                    state,
                                    /* activity = */ activity,
                                    // You use this request code to later retrieve the user's decision.
                                    /* requestCode = */ MY_REQUEST_CODE);
                        } catch (IntentSender.SendIntentException e) {
                            e.printStackTrace();
                        }
                    }

                    if (state.sessionId() == mySessionId) {
                        switch (state.status()) {

                            case SplitInstallSessionStatus.INSTALLED:
                                Context tempNewContext=null;
                                try {
                                    tempNewContext = activity.createPackageContext(activity.getPackageName(), 0);
                                } catch (PackageManager.NameNotFoundException e) {

                                }
                                final Context newContext =tempNewContext;
                                // If you use AssetManager to access your app’s raw asset files, you’ll need
                                // to generate a new AssetManager instance from the updated context.
                                AssetManager am = newContext.getAssets();


                                if (BuildCompat.isAtLeastO()) {
                                    // Updates the app’s context with the code and resources of the
                                    // installed module.
                                    SplitInstallHelper.updateAppInfo(newContext);
                                    new Handler().post(new Runnable() {
                                        @Override public void run() {
                                            // Loads contents from the module using AssetManager
                                            AssetManager am = newContext.getAssets();

                                        }
                                    });
                                } else
                                {SplitInstallHelper.updateAppInfo(newContext);}

                        }
                    }
                }

            } ;
            splitInstallManager.registerListener(listener);
            splitInstallManager
                    // Submits the request to install the module through the
                    // asynchronous startInstall() task. Your app needs to be
                    // in the foreground to submit the request.
                    .startInstall(request)
                    // You should also be able to gracefully handle
                    // request state changes and errors. To learn more, go to
                    // the section about how to Monitor the request state.
                    .addOnSuccessListener(new OnSuccessListener<Integer>() {
                        @Override
                        public void onSuccess(Integer result) {
                            Log.d("request feature load","success "+result);
                            mySessionId=result;
                            Set<String> installedModules = splitInstallManager.getInstalledModules();
                            String[] modules = new String[installedModules.size()];

                            installedModules.toArray(modules);
                            for (int i=0;i<modules.length;i++)
                            {
                                Log.d("module",modules[i]);
                            }


                        }
                    })
                    .addOnFailureListener(new OnFailureListener() {
                        void checkForActiveDownloads() {
                            splitInstallManager
                                    // Returns a SplitInstallSessionState object for each active session as a List.
                                    .getSessionStates()
                                    .addOnCompleteListener(
                                            new OnCompleteListener<List<SplitInstallSessionState>>() {
                                                @Override
                                                public void onComplete(Task<List<SplitInstallSessionState>> task) {
                                                    if (task.isSuccessful()) {
                                                        // Check for active sessions.
                                                        for (SplitInstallSessionState state : task.getResult()) {
                                                            if (state.status() == SplitInstallSessionStatus.DOWNLOADING) {
                                                                // Cancel the request, or request a deferred installation.
                                                            }
                                                        }
                                                    }
                                                }


                                            });
                        }

                        @Override
                        public void onFailure(Exception e) {
Log.d("request feature load","failure "+e.getMessage());
                            switch (((SplitInstallException) e).getErrorCode()) {
                                case SplitInstallErrorCode.NETWORK_ERROR:
                                    // Display a message that requests the user to establish a
                                    // network connection.
                                    break;
                                case SplitInstallErrorCode.ACTIVE_SESSIONS_LIMIT_EXCEEDED:
                                    checkForActiveDownloads();
                            }

                        }
                    });




        }
    });
   Button button2 = findViewById(R.id.button2);
    button2.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Log.d("feature1","called");
 Intent intent=new Intent();
            intent.setClassName("com.example.dynamicfeature1","MainActivityCalled");

            try{  startActivity(intent);}
            catch (Exception e){
                e.printStackTrace();
            }

        }
    });
   Button button3 = findViewById(R.id.button3);
    button3.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Log.d("feature2","called");
        }
    });
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.menu_main, menu);
    return true;
}



@Override
public boolean onOptionsItemSelected(MenuItem item) {
    // Handle action bar item clicks here. The action bar will
    // automatically handle clicks on the Home/Up button, so long
    // as you specify a parent activity in AndroidManifest.xml.
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.action_settings) {
        return true;
    }

    return super.onOptionsItemSelected(item);
}
}

here's the main app manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.mymodules">

<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:name="com.google.android.play.core.splitcompat.SplitCompatApplication"
android:theme="@style/AppTheme">
    <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:theme="@style/AppTheme.NoActionBar">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

</application>
</manifest>

Then there is the MainActivity of the dyamic feature module

package com.example.dynamicfeature1;

...imports...

public class MainActivityCalled extends AppCompatActivity {
@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    // Emulates installation of on demand modules using SplitCompat.
    SplitCompat.installActivity(this);
    Log.d("attachBaseContext",base.getPackageName().toString());
}

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    Toolbar toolbar = findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    FloatingActionButton fab = findViewById(R.id.fab);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Snackbar.make(view, "1-Replace with your own action", Snackbar.LENGTH_LONG)
                    .setAction("Action", null).show();
        }
    });
}

}

and the module manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
package="com.example.dynamicfeature1">

<dist:module
    dist:instant="false"
    dist:title="@string/title_dynamicfeature1">
    <dist:delivery>
        <dist:on-demand />
    </dist:delivery>

    <dist:fusing dist:include="false" />
</dist:module>

<application>
    <activity
        android:name=".MainActivityCalled"
        android:label="@string/title_activity_main"
        android:theme="@style/AppTheme.NoActionBar"></activity>
</application>

</manifest>

Solution

  • The right instruction to call the activity is

    intent. setClassName(BuildConfig.APPLICATION_ID, "com.example.dynamicfeature.MainActivityCalled");
    

    It works, the activity gets called.

    Note that BuildConfig.APPLICATION_ID is

    com.example.mymodules
    

    If this string is used in the dynamic modules too, you can make cross-calls:

    -from one module to another

    -from one module to the main app

    but you do not want to use BuildConfig.APPLICATION_ID in modules because there it is a different value, so the main string value has to be put into a variable.