4

My app is called MyNiceApp. MyNiceApp is mostly just a core that loads a view called coreView in the MainActivity onCreate. coreView gets populated by views from other plugins which the user downloads as wishes. I define the various areas on the core view that can be populated by the plugins via Interfaces in MyNiceApp. How can I load and pass Views from plugins into the coreView ?

I've been told that RemoteViews are a good option, but I don't know how to implement it. What other options are there?

Are RemoteViews the best way to go? I'm willing to try out anything that will work, even if not the best approach. A hack will do. Anything that could service this functionality will suffice, for the moment. Improvements could be made later.

Thank you all in advance.

UPDATE

I'm thinking of having them hosted on my private server. They will be downloaded to a dedicated folder called /data/app/com.myniceapp.plugins

I'm thinking it would be better organized if I had a folder created under /data/app/com.myniceapp./plugins, then have DexClassLoader crawl /data/app/com.myniceapp/plugins for downloaded plugins, then I could call my Class implementations, and dynamically load the plugin views to the core view at runtime.

TEMPORARY UPDATE

Hi @lelloman, and everyone else. I've been trying to make your solution work, but I've been unsuccessful so far.

I created a new project called Test View. It has an XML layout which I try to inflate and send to the Core View as follows:

package rev.ca.testview;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;

import ca.rev.libs.core.MyViewCreator;

public class TestView implements MyViewCreator {
    @Override
    public View createMyView(Context context) {
        LayoutInflater revInfl = LayoutInflater.from(context);
        View toolBarItemsLL = revInfl.inflate(R.layout.layout, null, false);

        Button button = (Button) toolBarItemsLL.findViewById(R.id.testButton);
        return button;
    }
}  

This, however doesn't work. Here's the rest of it:

In the MainActivity view, which is supposed to get the views from the plugins:

NavigationView navigationView = (NavigationView) findViewById(R.id.nav_view);
navigationView.setNavigationItemSelectedListener(this);

String dexPath = "/data/app/rev.ca.testview";
String optimizedDirectory = this.getCacheDir().getAbsolutePath();
String libraryPath = null;

DexClassLoader dexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, null, ClassLoader.getSystemClassLoader());
DexFile dexFile = null;
try {
    dexFile = DexFile.loadDex(dexPath, File.createTempFile("opt", "dex", this.getCacheDir()).getPath(), 0);

    for (Enumeration<String> classNames = dexFile.entries(); classNames.hasMoreElements(); ) {
        String className = classNames.nextElement();
        Class myClass = dexClassLoader.loadClass(className);
        if (myClass.isAssignableFrom(MyViewCreator.class)) {
            MyViewCreator creator = (MyViewCreator) myClass.getConstructor().newInstance();
            View myView = creator.createMyView(this);
                    // add myView wherever you want
            navigationView.addView(myView);
        }
    }
} catch (IOException e) {
    e.printStackTrace();
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (InvocationTargetException e) {
    e.printStackTrace();
} catch (NoSuchMethodException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}  

DRAWER LAYOUT IMPLEMENTATION ISSUES UPDATE

Hi @lelloman again. I've been trying to implement your solution into my main project which has a Drawer Layout. It breaks at final View toolBarItemsLL = revInfl.inflate(R.layout.activity_main, null, false);.

Why won't it work with Drawer Layout. If you add a Navigation Drawer Activity (android-plugins/MyNiceApps/app/src/main/ : New -> Activity -> Navigation Drawer Activity) into the, that is when it all falls apart. Hope you can help.

Here is the StackTrace:

08-14 21:44:27.564 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f0b005e, entry index(94) is beyond type entryCount(9)
08-14 21:44:27.564 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f0b005e (t=10 e=94) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f0a002c, entry index(44) is beyond type entryCount(5)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f0a002c (t=9 e=44) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: For resource 0x7f060022, entry index(34) is beyond type entryCount(1)
08-14 21:44:27.565 13390-13390/rev.ca.revcore W/ResourceType: Failure getting entry for 0x7f060022 (t=5 e=34) (error -75)
08-14 21:44:27.565 13390-13390/rev.ca.revcore D/AndroidRuntime: Shutting down VM
08-14 21:44:27.566 13390-13390/rev.ca.revcore E/AndroidRuntime: FATAL EXCEPTION: main
                                                                Process: rev.ca.revcore, PID: 13390
                                                                android.view.InflateException: Binary XML file line #7: Binary XML file line #7: Error inflating class TextView
                                                                Caused by: android.view.InflateException: Binary XML file line #7: Error inflating class TextView
                                                                Caused by: java.lang.UnsupportedOperationException: Can't convert to ComplexColor: type=0x1
                                                                    at android.content.res.ResourcesImpl.loadComplexColorForCookie(ResourcesImpl.java:879)
                                                                    at android.content.res.ResourcesImpl.loadComplexColorFromName(ResourcesImpl.java:756)
                                                                    at android.content.res.ResourcesImpl.loadColorStateList(ResourcesImpl.java:835)
                                                                    at android.content.res.Resources.loadColorStateList(Resources.java:1002)
                                                                    at android.content.res.TypedArray.getColorStateList(TypedArray.java:531)
                                                                    at android.widget.TextView.<init>(TextView.java:1076)
                                                                    at android.widget.TextView.<init>(TextView.java:704)
                                                                    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:62)
                                                                    at android.support.v7.widget.AppCompatTextView.<init>(AppCompatTextView.java:58)
                                                                    at android.support.v7.app.AppCompatViewInflater.createView(AppCompatViewInflater.java:103)
                                                                    at android.support.v7.app.AppCompatDelegateImplV9.createView(AppCompatDelegateImplV9.java:1029)
                                                                    at android.support.v7.app.AppCompatDelegateImplV9.onCreateView(AppCompatDelegateImplV9.java:1087)
                                                                    at android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC.onCreateView(LayoutInflaterCompatHC.java:47)
                                                                    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:769)
                                                                    at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:727)
                                                                    at android.view.LayoutInflater.rInflate(LayoutInflater.java:858)
                                                                    at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:821)
                                                                    at android.view.LayoutInflater.inflate(LayoutInflater.java:518)
                                                                    at android.view.LayoutInflater.inflate(LayoutInflater.java:426)
                                                                    at rev.ca.revbags.MyViewCreator.createView(MyViewCreator.java:24)
                                                                    at rev.ca.revcore.rev_plugin_loader.RevPluginLoader.revLoadView(RevPluginLoader.java:36)
                                                                    at rev.ca.revcore.RevCoreMainActivity$1.handleMessage(RevCoreMainActivity.java:21)
                                                                    at android.os.Handler.dispatchMessage(Handler.java:102)
                                                                    at android.os.Looper.loop(Looper.java:154)
                                                                    at android.app.ActivityThread.main(ActivityThread.java:6119)
                                                                    at java.lang.reflect.Method.invoke(Native Method)
                                                                    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
                                                                    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)
Program-Me-Rev
  • 6,184
  • 18
  • 58
  • 142
  • how is the user downloading plugins? – lelloman Aug 02 '17 at 07:13
  • I'm thinking of having the plugins hosted on my private server. @lelloman – Program-Me-Rev Aug 02 '17 at 07:17
  • I mean, what format are these views, are they actual Views compiled into Dex files? Json models that define how the views are structured? Do you manage the download process within your app? – lelloman Aug 02 '17 at 07:19
  • I'm thinking it would be better organized if I had a folder created under /data/app/com.myniceapp./plugins, then have DexClassLoader crawl /data/app/com.myniceapp/plugins for downloaded plugins, then I could call my Class implementations, and dynamically load the plugin views to the core view at runtime. They will be mostly in APK and .Jar format - @lelloman – Program-Me-Rev Aug 02 '17 at 07:24
  • so you don't need remoteviews, you can instantiate them directly in your process and attach them to the view hierarchy of your activity. when you say they are in apk format, you can load classes at runtime but I don't think you can define resources – lelloman Aug 02 '17 at 07:30
  • Could you please give a sample implementation in an answer if you can. – Program-Me-Rev Aug 02 '17 at 07:34
  • I gave you an example, but you need to clarify exactly what you want to achieve. there could be so many methods and some of them are more painful than others – lelloman Aug 02 '17 at 08:17

1 Answers1

3

First thing, it seems you don't need RemoteViews at all, you can instantiate the view in your process and attach them to the Activity.

Then you need to clarify with yourself one thing: do you need to load classes at runtime? This would be the case if you want to use new classes in your app without updating it. This is not trivial, you will be forced to use some already defined interfaces in your app or crawl your way through it with reflection. It would be a pain.

Another, much simpler option would be to download an xml layout for each of your views and associated with that a configuration file which could describe some behaviors. If you decide to load classes at runtime from an external plugin, you could go this way:

  • define a ViewCreator class inside a library
  • when you want to create a plugin you need to make an apk which contains one (or more?) of these ViewCreator class
  • in your app you then load the apk with DexClassLoader, find the ViewCreator class and instantiate it
  • the ViewCreator instance can then generate your View

This approach gives you a tremendous power, far beyond mere View instantiation, but it brings also a lot of complexity. If you're doing this for fun, well, I think you're on the right track, however I wouldn't recommend to do this for a commercial project. I created a sample repository with a minimal working example here.

The bulk of this approach is that you create a "plugins" library which will contain the common interfaces for your app and each plugin. In my sample, the library contains only one class:

public abstract class AbstractViewCreator {

    private final Context context;

    public AbstractViewCreator(Context context) {
        this.context = context;
    }

    public abstract View createView();

    protected Context getContext() {
        return context;
    }
}

Then you need to create a "plugin" app, in the sample is PluginA. This app must contain one implementation of AbstractViewCreator. Then the NiceApp needs to download the plugin apk, in the sample the apk is copied into the assets folder. After that you need to load the apk:

DexClassLoader dexClassLoader = new DexClassLoader(apkPath, codeCachePath, librariesPath, parentClassLoader);

then you need to load a class, you could derive the name of the class you want to load, for instance:

String className = "com.lelloman." + assetsFileName.replace(".apk", "").toLowerCase() + ".MyViewCreator");
Class fooClass = dexClassLoader.loadClass(className);

Then get the constructor and instantiate the ViewCreator via reflection

Constructor constructor = myClass.getConstructor(Context.class);
AbstractViewCreator creator = (AbstractViewCreator) constructor.newInstance(context);

then you can create your View

View viewFromPlugin = creator.createView();
lelloman
  • 13,883
  • 5
  • 63
  • 85
  • Hi @lelloman. I've been trying to make your solution work, but I've been unsuccessful so far. I have added an update with what I've been trying so far. Could you please advise me on what the reasons for the failure could be? Thank you. – Program-Me-Rev Aug 03 '17 at 05:55
  • As I said in previous comment, please define your requirements. The solution I'm proposing is very painful to implement, there is a good chance you don't need to go this far. What about downloading xml layout files of your views? would that be an option? – lelloman Aug 03 '17 at 06:20
  • The plugins will bring in their own functionalities to the app. They will have forms with their own persistence implementations, which means their own independent actions and Buttons, etc. So I think a fully qualified independent module would seem appropriate, unless there's some other approach. Your approach seems the best and very workable, if only I can get to load the Dex files, which I feel I'm very close to. It would handle most of what I need done. – Program-Me-Rev Aug 03 '17 at 06:40
  • you should compile the plugin in a separate apk, did you do that? you're passing /data/app/rev.ca.testview as path to the classloader, that's no apk – lelloman Aug 03 '17 at 06:43
  • Sorry for the late reply. I saw your update, and tried and studied the git example that you posted, but wasn't able to reply & thank you in time. I should have accepted your answer sooner. I think it's what I've been looking for. However, don't understand why you are stressing that you *'wouldn't recommend to do this for a commercial project'*. Why is that so? Would you mind outlining other available options. I'm, however, content with the solution you've posted. I think that it's an important area if one is interested in building highly modular apps. Again, thank you. – Program-Me-Rev Aug 06 '17 at 05:50
  • 1
    np, actually I was recently asking myself more or less the same question. I wouldn't recommend using this solution because I think there are many possibilities for things to go wrong, and in very unexpected ways. For instance, try to do this: in the implementation of createView() in the plugin, set an OnClickListener on the output VIew and show a Toast in onClick. – lelloman Aug 06 '17 at 06:24
  • Hi @lelloman again. I've been trying to implement your solution into my main project which has a Drawer Layout. It breaks at `final View toolBarItemsLL = revInfl.inflate(R.layout.activity_main, null, false);`. Why won't it work with Drawer Layout. If you add a **Navigation Drawer Activity** *(`android-plugins/MyNiceApps/app/src/main/` : New -> Activity -> Navigation Drawer Activity)* into the, that is when it all falls apart. Hope you can help. I added an update in the question under **DRAWER LAYOUT IMPLEMENTATION ISSUES UPDATE** – Program-Me-Rev Aug 14 '17 at 19:04
  • 1
    from the stacktrace it seems that the problem is not DrawerLayout but with a TextView. the actual error is `java.lang.UnsupportedOperationException: Can't convert to ComplexColor: type=0x1` remember when I said with this solution things could go wrong in many and unexpected ways? here it is one :) – lelloman Aug 14 '17 at 19:10
  • Any possible fix for this you can think of? – Program-Me-Rev Aug 14 '17 at 19:15
  • 1
    the problem might be with the LayoutInflater you're using to inflate that View. if you want to inflate Views from the plugin apk, you need to use a custom LayoutInflater as you see in my github sample (with custom Assets Resources and Theme). you can't inflate a View defined in the custom apk with your host app LayoutInflater – lelloman Aug 14 '17 at 19:20
  • Please note that your [sample project](https://github.com/lelloman/android-plugins) works well until I add a Navigation Drawer Activity. I don't even call it *(the Navigation Drawer Activity)*. Just adding it spoils everything. I don't try to inflate anything. I don't even try to reference it. – Program-Me-Rev Aug 14 '17 at 19:31
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/151919/discussion-between-lelloman-and-program-me-rev). – lelloman Aug 14 '17 at 19:43