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.
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.
1 2 3 4 5 |
package com.example.aidl; interface IOperation { int operation( in int i1, in int i2 ); } |
3. Now add following code to activity_main.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<?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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
<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.
1 2 3 4 5 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?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.