8

Is it possible to use a fragment/activity from an external application and use as it is embedded?

For example: embed a PDF reader fragment from a PDF reader application.

ejuhjav
  • 2,660
  • 2
  • 21
  • 32
lujop
  • 13,504
  • 9
  • 62
  • 95

3 Answers3

41

May be a little bit late, but still feel that it can be added and might help others.

For activity, there's really no point to have it embedded, there's convenient way to use other apps activities - start it with intent. For fragments at might make sense in case of implementation some kind of 'plug-in' functionality inside the app.

There's an official way to use code from other applications (or load code from network) in Android Blog 'Custom Class Loading in Dalvik'. Please note, the android is not much different from other platforms/environments, so both parts (your app and fragment You want load into your app) should support some kind of contract. That means You cannot load any component from any application, which is quite common and there are number of reasons for it to be that way.

So, here's some small example of the implementation. It consists of 3 parts:

  1. Interfaces project - this project contains definitions of interfaces which should be loaded by main app in order to use external classes:

    package com.example.test_interfaces;
    
    import android.app.Fragment;
    
    /**
     * Interface of Fragment holder to be obtained from external application
     */
    public interface FragmentHolder {
        Fragment getFragment();
    }
    

    For this example we need only single interface just to demonstrate how to load the fragment.

  2. Plug-in application, which contains the code You need to load - in our case it's a fragment. Please note, that this project in your IDE should depend on Interface one using 'provided' type and without exporting, because it will be imported by main application.

    Fragment, we're going to load PlugInFragment:

    package com.sandrstar.plugin;
    
    import com.example.test_interfaces.FragmentHolder;
    
    public class PlugInFragment extends Fragment implements FragmentHolder {
    
        @Override
        public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) {
    
            // Note that loading of resources is not the same as usual, because it loaded actually from another apk
            final XmlResourceParser parser = container.getContext().getPackageManager().getXml("com.sandrstar.plugin", R.layout.fragment_layout, null);
    
            return inflater.inflate(parser, container, false);
        }
    
        @Override
        public Fragment getFragment() {
            return this;
        }
    }
    

    And it's layout fragment_layout.xml:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="@android:color/black">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="This is from fragment"
            android:textColor="@android:color/white"/>
    </LinearLayout>
    
  3. Main application which wants to load the fragment from another application. It should have Interface project imported:

    Activity itself MyActivity:

    public class MyActivity extends Activity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
    
            setContentView(R.layout.main);
    
            try {
                Class<?> requiredClass = null;
                final String apkPath = getPackageManager().getApplicationInfo("com.sandrstar.plugin",0).sourceDir;
                final File dexTemp = getDir("temp_folder", 0);
                final String fullName = "com.sandrstar.plugin.PlugInFragment";
                boolean isLoaded = true;
    
                // Check if class loaded
                try {
                    requiredClass = Class.forName(fullName);
                } catch(ClassNotFoundException e) {
                    isLoaded = false;
                }
    
                if (!isLoaded) {
                    final DexClassLoader classLoader = new DexClassLoader(apkPath,
                            dexTemp.getAbsolutePath(),
                            null,
                            getApplicationContext().getClassLoader());
    
                    requiredClass = classLoader.loadClass(fullName);
                }
    
                if (null != requiredClass) {
                    // Try to cast to required interface to ensure that it's can be cast
                    final FragmentHolder holder = FragmentHolder.class.cast(requiredClass.newInstance());
    
                    if (null != holder) {
                        final Fragment fragment = holder.getFragment();
    
                        if (null != fragment) {
                            final FragmentTransaction trans = getFragmentManager().beginTransaction();
    
                            trans.add(R.id.fragmentPlace, fragment, "MyFragment").commit();
                        }
                    }
                }
            } catch (PackageManager.NameNotFoundException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    

    And it's layout main.xml:

    <RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:clipChildren="false"
        android:id="@+id/root">
    
        <ImageView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/down_image" />
    
        <FrameLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/fragmentPlace"
            android:layout_centerInParent="true" />
    </RelativeLayout>
    

And the finally we able to observe the following on the real device:

enter image description here

Possible issues handling (thanks to @MikeMiller for the update):

  1. If you get the following error in the call to classLoader.loadClass:

java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation

Make sure the fragment modules are included in the main app (as 'compiled')

  1. If you get a NameNotFoundException in the call to context.getPackageManager().getApplicationInfo(packageName,0).sourceDir, then make sure the fragment is in an installed APPLICATION (not just a library dependency). Follow the steps below to make sure that's the case:

    1) In the main application's build.gradle, change apply plugin: 'android-library' to apply plugin: 'android' and make sure there's a dummy activity java file. In the main application, remove the dependency on the fragment module (It's not specified in step 3, but I had to add a dependency on the fragment module to the main application. But the fragment module is now an activity application, and you can't have dependencies on those) or you'll get this: Error:Dependency unspecified on project resolves to an APK archive which is not supported as a compilation dependency.

    2) Run the fragment module (which you can do now, because it's an activity application). That installs it in a way that the getApplicationInfo call can find it Revert build.gradle and add the dependency back in the main app (as a 'compile' dependency) Everything should work now. When you make updates to the fragment code, you won't need to go through this process again. You will, though, if you want to run on a new device or if you add a new fragment module. I hope this is able to save someone the time I spent trying to resolve the above errors.

Android L

Seems, based on normal multidex support with Android L, above steps are not needed, because class loading is different. Approach, described in multidex support can be used instead of Android Blog 'Custom Class Loading in Dalvik', because it clearly states that:

Note: The guidance provided in this document supersedes the guidance given in the Android Developers blog post Custom Class Loading in Dalvik.

Probably, changes in android.support.multidex might be needed to reuse that approach.

sandrstar
  • 12,503
  • 8
  • 58
  • 65
  • 8
    Why downvoted? Instead of previous answer (which actually has no 'official' proofs on why it's not possible to reuse) this one contains working example and links to official documentation. – sandrstar Jan 23 '14 at 02:54
  • This approach seems to perfectly suit my needs, but I'm stuck. Eclipse keeps telling me "Class resolved by unexpected DEX". As you really seem to have understood what your doing here maybe you can have a look on my code at http://stackoverflow.com/q/27230851/3960095 and help me? – Kevin Gebhardt Dec 01 '14 at 15:31
  • 1
    OK, forget about it, I'm just not able to read. Mark the libary as provided and it works like a charm. Thanks a lot for that awesome code! – Kevin Gebhardt Dec 01 '14 at 16:39
  • 1
    If you get the following error in the call to classLoader.loadClass: "java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation", Make sure the fragment modules are included in the main app (as 'compiled') If you get a NameNotFoundException in the call to context.getPackageManager().getApplicationInfo(packageName,0).sourceDir, Make sure the fragment is in an installed APPLICATION (not just a library dependency). Follow the steps below to make sure that's the case: – Mike Miller Jan 07 '15 at 03:28
  • (cont'd) In the main application's build.gradle, change "apply plugin: 'android-library'" to "apply plugin: 'android'" and make sure there's a dummy activity java file. In the main application, remove the dependency on the fragment module (It's not specified in step 3, but I had to add a dependency on the fragment module to the main application. But the fragment module is now an activity application, and you can't have dependencies on those) or you'll get this: Error:Dependency unspecified on project resolves to an APK archive which is not supported as a compilation dependency. – Mike Miller Jan 07 '15 at 03:29
  • (cont'd) Run the fragment module (which you can do now, because it's an activity application). That installs it in a way that the getApplicationInfo call can find it Revert build.gradle and add the dependency back in the main app (as a 'compile' dependency) Everything should work now. When you make updates to the fragment code, you won't need to go through this process again. You will, though, if you want to run on a new device or if you add a new fragment module. I hope this is able to save someone the time I spent trying to resolve the above errors. Thank you, sandrstar, for the code! – Mike Miller Jan 07 '15 at 03:30
  • @MikeMiller hm, with Gradle probably https://developer.android.com/tools/building/multidex.html will do the job? It actually says: "Note: The guidance provided in this document supersedes the guidance given in the Android Developers blog post Custom Class Loading in Dalvik." (link which I used as base). – sandrstar Jan 07 '15 at 03:44
  • @sandrstar That quote about superseding the advice given in the blog post refers to this line in the blog post: "there are situations where custom class loading can come in handy... Big apps can contain more than 64K method references... To get around this limitation, developers can partition part of the program into multiple secondary dex files, and load them at runtime." My goal, and lujop's goal, is to load fragments from an external, previously installed app. I don't think multidex will help with that. Or am I wrong? – Mike Miller Jan 07 '15 at 23:05
  • @MikeMiller main point is support of loading dex in runtime - same thing we're doing via DexClassLoader here. I think multidex support actually means 'better' support for dex loading. Unfortunately don't have any 5.0 device and platform/support lib source to check exact implementation details. Sure it cannot be used 'as it is' and some hacking probably will be needed for gradle build and / or android.support.multidex lib source modification to have things running. Same time this support lib seems to have some tools for easier class loading and integration into main app. – sandrstar Jan 07 '15 at 23:25
  • 1
    I'll add some additional insight as far as how to do this without using "provided" for the dependency. The key is to set the parent class loader to be that of the main application. – spy Mar 30 '16 at 10:56
  • Hi, your application will crash when android android system recreate the activity because at that time android system fail to find your fragment class. – User10001 Sep 20 '18 at 06:59
  • @User10001 true, recreation should be handled separately and carefully – sandrstar Sep 20 '18 at 16:02
  • @sandrstar as per my knowledge we are unable to handle case when android system kill the application while it is in background and at that time user launch the application from home screen. – User10001 Sep 21 '18 at 05:20
  • @User10001 You have options to handle it, refer to questions about that aspect. – sandrstar Sep 21 '18 at 06:52
  • @sandrstar, Yes we have option and i am just try to highlight the crash that we can not resolve, So if some one is using the above approach he always has the info that this application will crash at particular scenario. – User10001 Sep 21 '18 at 07:06
1

No, you can not "reuse" code from other applications. The only official way is to use Intent to invoke the whole Activity.

Peter Knego
  • 79,991
  • 11
  • 123
  • 154
0

I use a quite similar approach to sandrstart. Maybe it's less secure. I use a normal Classloader derived from a packagecontext created with the plugin package name. The package names of all plugins are loaded and saved along with other configurations from a config website.

Context ctx = createPackageContext(packetName, Context.CONTEXT_INCLUDE_CODE | 
               Context.CONTEXT_IGNORE_SECURITY);
ClassLoader cl = ctx.getClassLoader();
Class<?> c = cl.loadClass(className);
Fragment fragObj = (Fragment)c.newInstance();

But I wanted to stress that my approach and I think sandrstar's approach only work with the android internal android.app.Fragment class.

I was trying (again) as part for adopting to android 9 to switch from android.app.Fragment (depricated) to android.support.v4.Fragment but could'nt get it to work.

The reason is that: apk1:framework.class.A == apk2.framework.class.A but apk1:someJarOrAar.class.B != aps2.someJarOrAar.class.B even if the same exact jar/aar is used in both projects. So I will always get a ClassCastException on (Fragment)c.newInstance();.

I have tried to cast via the classloader from the plugin and cast via classloader from main package but to no avail. I think there is no way around it as it can't be guaranteed that jars/aars are really the same even if they have same names and same classnames in them so it's a security issue to treat them as different even if they are the same.

I surely hope for some workaround (other than keep on using the android.app.Fragment even under android 9 as I will do for now) but I will also be grateful for comments for why it is definetly not possible with shared classes (such as support.v4.Fragment).

FrankKrumnow
  • 501
  • 5
  • 13