Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to start a non existent Activity mentioned in Manifest?

Tags:

android

I am attempting to develop a "Dynamic" Android application.

Dynamic in the sense that I have an activity listed in the manifest that is "built" at runtime.

I can build the required activity fine, however, when I attempt to start it my application fails with...

    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.research.ps/com.research.Dynamic}: java.lang.ClassNotFoundException: 
Didn't find class "com.research.Dynamic" on path: DexPathList[[zip file "/data/app/com.research.ps-1/base.apk"],nativeLibraryDirectories=[/data/app/com.research.ps-1/lib/arm, 
/data/app/com.research.ps-1/base.apk!/lib/armeabi-v7a, /vendor/lib, /system/lib]]

Is there an approach I can take to successfully instantiate an Android Activity at runtime?

Is there a way I can add a "temporary" or "shell" activity onto my application path? and then replace the "temporary" activity with my dynamic instance?

UPDATE

My Manifest XML contains this entry

<activity
  android:name=".Dynamic"
  android:label="@string/title_activity_dynamic"
  android:theme="@style/AppTheme.NoActionBar" />

However, there is no Activity called "Dynamic" contained within my application.

I am using ByteBuddy to build my dynamic activity:-

  final Class<? extends android.support.v7.app.AppCompatActivity> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
            .subclass(android.support.v7.app.AppCompatActivity.class, IMITATE_SUPER_CLASS)
            .name("com.research.Dynamic")
            .make()
            .load(getClass().getClassLoader(), new AndroidClassLoadingStrategy.Wrapping(this.getDir("dexgen", Context.MODE_PRIVATE)))
            .getLoaded();

final Intent intent = new Intent(this, dynamicType);
startActivity(intent);
like image 726
Hector Avatar asked Nov 20 '17 10:11

Hector


People also ask

How do I add an activity to a manifest file?

To declare your activity, open your manifest file and add an <activity> element as a child of the <application> element. For example: <manifest ... > The only required attribute for this element is android:name, which specifies the class name of the activity.

How do I intent to start another activity?

To start an activity, use the method startActivity(intent) . This method is defined on the Context object which Activity extends. The following code demonstrates how you can start another activity via an intent. # Start the activity connect to the # specified class Intent i = new Intent(this, ActivityTwo.

Why should all your activities be declared in the manifest?

Declares an activity (an Activity subclass) that implements part of the application's visual user interface. All activities must be represented by elements in the manifest file. Any that are not declared there will not be seen by the system and will never be run.


2 Answers

Yes you CAN start such an Activity (assuming you have a dummy manifest Activity entry).
If you don't like this technique, use Fragments (they don't need entries in the manifest).
Alternatively use WebView and JavaScript like Apache Cordova et-al (cross platform too !).
ByteBuddy (kudos too @Rafael Winterhalter author of Byte Buddy) looks cool, maybe a learning curve involved. Why not download the linked project and try both techniques.
Here's how to include ByteBuddy in your Android Studio Gradle project (build.gradle):

android {
    compileSdkVersion 25
    buildToolsVersion '25'
    dependencies {
        compile 'com.android.support:appcompat-v7:25'
        compile 'net.bytebuddy:byte-buddy:1.7.9'
        compile 'net.bytebuddy:byte-buddy-android:1.7.9'
    }
}

how I can "find" my dynamically instantiate class at runtime?

External loading of DEX files (class byte code)

See my answer here and follow the links for source code and tutorial (Apache Ant {Eclipse compatible, build.xml} and Android Studio Gradle examples build.gradle of the same code, you need some custom build steps which these project's provide).
Code snippet:

        // Internal storage where the DexClassLoader writes the optimized dex file to.
        final File optimizedDexOutputPath = getDir(SECONDARY_DEX_INTERNAL_DIR, Context.MODE_PRIVATE);

        // Initialize the class loader with the secondary dex file.
        DexClassLoader cl = new DexClassLoader(dexInternalStoragePath.getAbsolutePath(),
                                                optimizedDexOutputPath.getAbsolutePath(),
                                                null,
                                                getClassLoader());
        Class libProviderClazz = null;//variable libProviderClazz of type Class

        try {
            // Load the library class from the class loader.
            libProviderClazz = cl.loadClass(PROVIDER_CLASS);

            // Cast the return object to the library interface so that the
            // caller can directly invoke methods in the interface.

            // Alternatively, the caller can invoke methods through reflection,
            // which is more verbose and slow.
            LibraryInterface lib = (LibraryInterface) libProviderClazz.newInstance();

        }
        catch (Exception exception)
        {
            // Handle exception gracefully here.
            exception.printStackTrace();
        }

Q: How do I add an Activity, I cannot add it to the manifest ?
A: Use Fragments, they don't need entries in the manifest.

like image 67
Jon Goodwin Avatar answered Oct 08 '22 12:10

Jon Goodwin


I have managed to call a dynamically instantiated Activity and set the required layout content using ByteBuddy.

Heres how

final DynamicType.Unloaded<? extends AppCompatActivity> dynamicType = new ByteBuddy(ClassFileVersion.JAVA_V8)
        .subclass(AppCompatActivity.class)
        .name(CLASS_NAME)
        .method(named("onCreate").and(takesArguments(1)))
        .intercept(MethodDelegation.to(TargetActivity.class).andThen(SuperMethodCall.INSTANCE))
        .make();

final Class<? extends AppCompatActivity> dynamicTypeClass = dynamicType.load(getClassLoader(), new AndroidClassLoadingStrategy.Injecting(this.getDir("dexgen", Context.MODE_PRIVATE))).getLoaded();

final Intent intent = new Intent(this, dynamicTypeClass);
startActivity(intent);

The method delegation class

public class TargetActivity {

    public static void intercept(Bundle savedInstanceState, @This AppCompatActivity thiz) {
        thiz.setContentView(R.layout.activity_fourth);
    }
}

Even though this gives the desired result it still has issues as the super.onCreate(savedInstanceState) call is made after I have called setContent (I think).

Using the excellent ByteBuddy library is a much better approach IMHO than working with DEX manipulation.

like image 38
Hector Avatar answered Oct 08 '22 13:10

Hector