Creating Android App with plugin Architecture – Tutorial

There are times when your apps become overloaded with functionality which all users don’t need. You can create a MAIN Application and rest of the functionality can be provided by child apps as plugins. User can download the plugin with proper functionality he wants .

This way application is light and user only have functionality he needs . This architecture is seen mostly in application which provide themes. There will be main app which contain functionality and plugin themes can be added later.

We will be using AIDL interface to achieve this functionality in our application. It allows you to define the programming interface that both the client and service agree upon in order to communicate with each other using interprocess communication (IPC). Core functionality will be implemented in plugins which will attach to the main application via AIDL so that main Application can utilize the functionality defined in plugin and perform the desired task.

In order to implement these functionalities, the following features of the Android framework were used.

  • Two applications will be created. One will act as main app and other will host two services which will act as plugins.
  • Each plugin is a service. Plugin exposes two of them. In order to identify our plugins, We defined a specific Intent (androidsrc.intent.action.PICK_PLUGIN) and We attached an intent filter listening to this intent in plugin1’s AndroidManifest.xml. In order to select a particular plugin, We abused the category field. One plugin can be accessed therefore by binding a service with an intent whose action is androidsrc.intent.action.PICK_PLUGIN and whose category equals to the category listed in AndroidManifest.xml of the package that exposes the plugin service.
  • Plugins are discovered using the Android PackageManager. Pluginapp asks the PackageManager to return the list of plugins that are bound to androidsrc.intent.action.PICK_PLUGIN action. Pluginapp then retrieves the category from this list and can produce an intent that is able to bind the selected plugin.
  • Pluginapp updates the plugin list dynamically by listening to ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and ACTION_PACKAGE_REPLACED intents. Whenever any of these intents are detected, it refreshes the plugin list.

Preview :

[su_youtube url=”http://youtu.be/vzhehTK6dAA”]

Lets get started :

1. Creating MainApplication

1. Create a new project in Eclipse by navigating to File ⇒ New Android ⇒ Application Project and fill required details.

2. Now create package in src folder with name “com.example.aidl” and create a file named “IOperation.aidl” in this package. This file with same package name will be used in plugin app also. This will act as interface between app and plugin. Copy following code in this file.

package com.example.aidl;

interface IOperation {
  int operation( in int i1, in int i2 );
}

3. Now add following code to activity_main.xml 

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent">

  <ListView android:id="@+id/android:list"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>

  <TextView android:id="@+id/android:empty"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:text="No Plugins"/>

</LinearLayout>

4. Now create file serviceinvoker.xml and copy this code. This layout will be shown in while operating on plugin operations.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >

    <EditText android:id="@+id/num1"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
		android:numeric="decimal|signed"/>

    <EditText android:id="@+id/num2"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
		android:numeric="decimal|signed"/>

	<Button android:id="@+id/op"
        	android:text="operate"
			android:enabled="false"
        	android:layout_width="wrap_content"
        	android:layout_height="wrap_content"/>

	<TextView
        android:id="@+id/result"
    	android:layout_width="fill_parent"
    	android:layout_height="wrap_content"
    	android:text=""/>
</LinearLayout>

5. Now create service_row.xml and copy following code.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content"
     android:orientation="vertical">

     <TextView android:id="@+id/pkg"
         android:textSize="12dp"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>

     <TextView android:id="@+id/servicename"
         android:textSize="12dp"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>

     <TextView android:id="@+id/actions"
         android:textSize="12dp"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>

     <TextView android:id="@+id/categories"
         android:textSize="dp"
         android:layout_width="fill_parent"
         android:layout_height="wrap_content"/>

</LinearLayout>

6. Now copy following code to ActivityMain.java

package com.example.mainapplication;

import android.app.ListActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

public class ActivityMain extends ListActivity {
    public static final String ACTION_PICK_PLUGIN = "androidsrc.intent.action.PICK_PLUGIN";
    static final String KEY_PKG = "pkg";
    static final String KEY_SERVICENAME = "servicename";
    static final String KEY_ACTIONS = "actions";
    static final String KEY_CATEGORIES = "categories";
    static final String BUNDLE_EXTRAS_CATEGORY = "category";

    static final String LOG_TAG = "PluginApp";

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fillPluginList();
        itemAdapter =
                new SimpleAdapter(this,
                        services,
                        R.layout.services_row,
                        new String[]{KEY_PKG, KEY_SERVICENAME, KEY_ACTIONS, KEY_CATEGORIES},
                        new int[]{R.id.pkg, R.id.servicename, R.id.actions, R.id.categories}
                );
        setListAdapter(itemAdapter);

        packageBroadcastReceiver = new PackageBroadcastReceiver();
        packageFilter = new IntentFilter();
        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        packageFilter.addCategory(Intent.CATEGORY_DEFAULT);
        packageFilter.addDataScheme("package");
    }

    protected void onStart() {
        super.onStart();
        Log.d(LOG_TAG, "onStart");
        registerReceiver(packageBroadcastReceiver, packageFilter);
    }

    protected void onStop() {
        super.onStop();
        Log.d(LOG_TAG, "onStop");
        unregisterReceiver(packageBroadcastReceiver);
    }

    protected void onListItemClick(ListView l, View v, int position, long id) {
        Log.d(LOG_TAG, "onListItemClick: " + position);
        String category = categories.get(position);
        if (category.length() > 0) {
            Intent intent = new Intent();
            intent.setClassName(
                    "com.example.mainapplication",
                    "com.example.mainapplication.InvokeOp");
            intent.putExtra(BUNDLE_EXTRAS_CATEGORY, category);
            startActivity(intent);
        }
    }

    private void fillPluginList() {
        services = new ArrayList<HashMap<String, String>>();
        categories = new ArrayList<String>();
        PackageManager packageManager = getPackageManager();
        Intent baseIntent = new Intent(ACTION_PICK_PLUGIN);
        baseIntent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
        List<ResolveInfo> list = packageManager.queryIntentServices(baseIntent,
                PackageManager.GET_RESOLVED_FILTER);
        Log.d(LOG_TAG, "fillPluginList: " + list);
        for (int i = 0; i < list.size(); ++i) {
            ResolveInfo info = list.get(i);
            ServiceInfo sinfo = info.serviceInfo;
            IntentFilter filter = info.filter;
            Log.d(LOG_TAG, "fillPluginList: i: " + i + "; sinfo: " + sinfo + ";filter: " + filter);
            if (sinfo != null) {
                HashMap<String, String> item = new HashMap<String, String>();
                item.put(KEY_PKG, sinfo.packageName);
                item.put(KEY_SERVICENAME, sinfo.name);
                String firstCategory = null;
                if (filter != null) {
                    StringBuilder actions = new StringBuilder();
                    for (Iterator<String> actionIterator = filter.actionsIterator(); actionIterator.hasNext(); ) {
                        String action = actionIterator.next();
                        if (actions.length() > 0)
                            actions.append(",");
                        actions.append(action);
                    }
                    StringBuilder categories = new StringBuilder();
                    for (Iterator<String> categoryIterator = filter.categoriesIterator();
                         categoryIterator.hasNext(); ) {
                        String category = categoryIterator.next();
                        if (firstCategory == null)
                            firstCategory = category;
                        if (categories.length() > 0)
                            categories.append(",");
                        categories.append(category);
                    }
                    item.put(KEY_ACTIONS, new String(actions));
                    item.put(KEY_CATEGORIES, new String(categories));
                } else {
                    item.put(KEY_ACTIONS, "<null>");
                    item.put(KEY_CATEGORIES, "<null>");
                }
                if (firstCategory == null)
                    firstCategory = "";
                categories.add(firstCategory);
                services.add(item);
            }
        }
        Log.d(LOG_TAG, "services: " + services);
        Log.d(LOG_TAG, "categories: " + categories);
    }

    private PackageBroadcastReceiver packageBroadcastReceiver;
    private IntentFilter packageFilter;
    private ArrayList<HashMap<String, String>> services;
    private ArrayList<String> categories;
    private SimpleAdapter itemAdapter;

    class PackageBroadcastReceiver extends BroadcastReceiver {
        private static final String LOG_TAG = "PackageBroadcastReceiver";

        public void onReceive(Context context, Intent intent) {
            Log.d(LOG_TAG, "onReceive: " + intent);
            services.clear();
            fillPluginList();
            itemAdapter.notifyDataSetChanged();
        }
    }
}

7. Now create file named “InvokeOp.java” in main package of the application. Copy following code.

package com.example.mainapplication;

import android.app.ListActivity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

public class ActivityMain extends ListActivity {
    public static final String ACTION_PICK_PLUGIN = "androidsrc.intent.action.PICK_PLUGIN";
    static final String KEY_PKG = "pkg";
    static final String KEY_SERVICENAME = "servicename";
    static final String KEY_ACTIONS = "actions";
    static final String KEY_CATEGORIES = "categories";
    static final String BUNDLE_EXTRAS_CATEGORY = "category";

    static final String LOG_TAG = "PluginApp";

    /**
     * Called when the activity is first created.
     */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        fillPluginList();
        itemAdapter =
                new SimpleAdapter(this,
                        services,
                        R.layout.services_row,
                        new String[]{KEY_PKG, KEY_SERVICENAME, KEY_ACTIONS, KEY_CATEGORIES},
                        new int[]{R.id.pkg, R.id.servicename, R.id.actions, R.id.categories}
                );
        setListAdapter(itemAdapter);

        packageBroadcastReceiver = new PackageBroadcastReceiver();
        packageFilter = new IntentFilter();
        packageFilter.addAction(Intent.ACTION_PACKAGE_ADDED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_REPLACED);
        packageFilter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        packageFilter.addCategory(Intent.CATEGORY_DEFAULT);
        packageFilter.addDataScheme("package");
    }

    protected void onStart() {
        super.onStart();
        Log.d(LOG_TAG, "onStart");
        registerReceiver(packageBroadcastReceiver, packageFilter);
    }

    protected void onStop() {
        super.onStop();
        Log.d(LOG_TAG, "onStop");
        unregisterReceiver(packageBroadcastReceiver);
    }

    protected void onListItemClick(ListView l, View v, int position, long id) {
        Log.d(LOG_TAG, "onListItemClick: " + position);
        String category = categories.get(position);
        if (category.length() > 0) {
            Intent intent = new Intent();
            intent.setClassName(
                    "com.example.mainapplication",
                    "com.example.mainapplication.InvokeOp");
            intent.putExtra(BUNDLE_EXTRAS_CATEGORY, category);
            startActivity(intent);
        }
    }

    private void fillPluginList() {
        services = new ArrayList<HashMap<String, String>>();
        categories = new ArrayList<String>();
        PackageManager packageManager = getPackageManager();
        Intent baseIntent = new Intent(ACTION_PICK_PLUGIN);
        baseIntent.setFlags(Intent.FLAG_DEBUG_LOG_RESOLUTION);
        List<ResolveInfo> list = packageManager.queryIntentServices(baseIntent,
                PackageManager.GET_RESOLVED_FILTER);
        Log.d(LOG_TAG, "fillPluginList: " + list);
        for (int i = 0; i < list.size(); ++i) {
            ResolveInfo info = list.get(i);
            ServiceInfo sinfo = info.serviceInfo;
            IntentFilter filter = info.filter;
            Log.d(LOG_TAG, "fillPluginList: i: " + i + "; sinfo: " + sinfo + ";filter: " + filter);
            if (sinfo != null) {
                HashMap<String, String> item = new HashMap<String, String>();
                item.put(KEY_PKG, sinfo.packageName);
                item.put(KEY_SERVICENAME, sinfo.name);
                String firstCategory = null;
                if (filter != null) {
                    StringBuilder actions = new StringBuilder();
                    for (Iterator<String> actionIterator = filter.actionsIterator(); actionIterator.hasNext(); ) {
                        String action = actionIterator.next();
                        if (actions.length() > 0)
                            actions.append(",");
                        actions.append(action);
                    }
                    StringBuilder categories = new StringBuilder();
                    for (Iterator<String> categoryIterator = filter.categoriesIterator();
                         categoryIterator.hasNext(); ) {
                        String category = categoryIterator.next();
                        if (firstCategory == null)
                            firstCategory = category;
                        if (categories.length() > 0)
                            categories.append(",");
                        categories.append(category);
                    }
                    item.put(KEY_ACTIONS, new String(actions));
                    item.put(KEY_CATEGORIES, new String(categories));
                } else {
                    item.put(KEY_ACTIONS, "<null>");
                    item.put(KEY_CATEGORIES, "<null>");
                }
                if (firstCategory == null)
                    firstCategory = "";
                categories.add(firstCategory);
                services.add(item);
            }
        }
        Log.d(LOG_TAG, "services: " + services);
        Log.d(LOG_TAG, "categories: " + categories);
    }

    private PackageBroadcastReceiver packageBroadcastReceiver;
    private IntentFilter packageFilter;
    private ArrayList<HashMap<String, String>> services;
    private ArrayList<String> categories;
    private SimpleAdapter itemAdapter;

    class PackageBroadcastReceiver extends BroadcastReceiver {
        private static final String LOG_TAG = "PackageBroadcastReceiver";

        public void onReceive(Context context, Intent intent) {
            Log.d(LOG_TAG, "onReceive: " + intent);
            services.clear();
            fillPluginList();
            itemAdapter.notifyDataSetChanged();
        }
    }

}

8. finally update AndroidManifest.xml with this code.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mainapplication"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="21" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".ActivityMain"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".InvokeOp"
            android:label="Operate" />
    </application>

</manifest><br>

2. Creating plugin app :

1. Create a new project in Eclipse by navigating to File ⇒ New Android ⇒ Application Project and fill required details.

2. Now create package in src folder with name “com.example.aidl” and create a file named “IOperation.aidl” in this package. This file with same package name is also created in Main application. This will act as interface between app and plugin. Copy following code in this file.

package com.example.aidl;

interface IOperation {
  int operation( in int i1, in int i2 );
}<br>

3. Now create file PluginService1.java for first plugin and copy following code.

package com.example.plugin;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import com.example.aidl.IOperation;

public class PluginService1 extends Service {
    static final String LOG_TAG = "PluginService1";

    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
    }

    public void onDestroy() {
        super.onDestroy();
    }

    public IBinder onBind(Intent intent) {
        return addBinder;
    }

    private final IOperation.Stub addBinder =
            new IOperation.Stub() {
                public int operation(int i1, int i2) {
                    return i1 + i2;
                }
            };
}

4. Now create PluginService2.java and copy following code.

package com.example.plugin;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

import com.example.aidl.IOperation;

public class PluginService2 extends Service {
    static final String LOG_TAG = "PluginService2";

    public void onStart(Intent intent, int startId) {
        super.onStart(intent, startId);
    }

    public void onDestroy() {
        super.onDestroy();
    }

    public IBinder onBind(Intent intent) {
        return mulBinder;
    }

    private final IOperation.Stub mulBinder =
            new IOperation.Stub() {
                public int operation(int i1, int i2) {
                    return i1 * i2;
                }
            };
}

5. Finally update AndroidManifest.xml for plugin.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example.plugin"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:label="@string/app_name">
        <service android:name="com.example.plugin.PluginService1"
                  android:exported="true">
            <intent-filter>
                <action android:name="androidsrc.intent.action.PICK_PLUGIN" />
                <category android:name="androidsrc.intent.category.ADD_PLUGIN" />
            </intent-filter>
        </service>
        <service android:name="com.example.plugin.PluginService2"
                  android:exported="true">
            <intent-filter>
                <action android:name="androidsrc.intent.action.PICK_PLUGIN" />
                <category android:name="androidsrc.intent.category.MUL_PLUGIN" />
            </intent-filter>
		</service>
    </application>
</manifest><br>

6. Now build and run the project.

GuRu

Technology enthusiast. Loves to tinker with things. Always trying to create something wonderful using technology. Loves coding for Android, Raspberry pi, Arduino , Opencv and much more.

You may also like...

6 Responses

  1. CoDeSigns says:

    Doesn’t appear to be working on my Nexus 5 / Android 5.1 due to an
    implicit intent. Logcat states the Service intent must be explicit –
    coming from bindService in InvokeOp. I understand explicit intents are
    now enforced in Lollipop but when I drop sdk to 19 only the addition plugin seems to be working and not the multiplication.

    • guru says:

      Yes there is changes in API for “Using an implicit intent to start a service”. Using an implicit intent to start a service is a security hazard because you cannot be certain what service will respond to the intent, and the user cannot see which service starts. Please refer to this page for more info.

    • AndroidSRC . says:

      Yes there is changes in API for “Using an implicit intent to start a service”. Using an implicit intent to start a service is a security hazard because you cannot be certain what service will respond to the intent, and the user cannot see which service starts. Please refer to this page for more info.

  2. Khoi Boo says:

    Hi, Android Studio says “Duplicate Class found in MainActivity.java and InvokeOp.java” How can I solve this 🙁
    There is “public class ActivityMain extends ListActivity” in both MainActivity.java and InvokeOp.java

  3. Super Pride says:

    Why we duplicate ActivityMain.java code in InvokeOp.java file?

Leave a Reply

Your email address will not be published. Required fields are marked *