Intent + Service + ContentProvider = Plugin Architecture - DevsBuild.It

stuckwarmersMobile - Wireless

Dec 14, 2013 (3 years and 10 months ago)

1,127 views

Intent + Service + Content Provider =
Android® Plugin Architecture



Jason Weiss

Learning Objectives


Understand the intrinsic relationships
between Intents, Services, and Content
Providers


Understand how to use the PackageManager
class to reflect system capabilities


Value of a Content Provider as a Plugin
Registrar


IPC with Intents or IBinder


Exported Services as a Plugin

Who is this guy?


Jason Weiss


jason DOT weiss AT tagdynamics DOT com


Developed Android apps for both the private
sector and the US government


Speaker at Large


Return AnDevCon speaker

Android® Architecture


PackageManager


Provides APK reflection capabilities


Access meta
-
data elements


Content Provider


Conceptually the model layer


Intent


Intra
-

and Inter
-
app messaging capabilities


May result in the display of an Activity


Service


Controller layer


May do work in the background on behalf of many disparate apps

PLUGIN METADATA

Plugin Declarations via
AndroidManifest

<meta
-
data> Tags

AndroidManifest Example


<service


android:name=".menu.InspectTagMenuPlugin"



android:enabled="true”



android:icon="@drawable/ic_btn_mm_tag_inspection”



android:label="@string/menu_label_inspect_tag”


android:description="@string/menu_description_inspect_tag"


android:exported="true"


android:permission="com.tagdynamics.permission.MENU" >



<meta
-
data android:name="@string/key_plugin_type"


android:value="menu" />



<meta
-
data android:name="@string/key_plugin_mime_type"


android:value="vnd.tagdynamics.plugin.menu/vnd.tagdynamics.inspecttag" />



<meta
-
data android:name="@string/key_plugin_action"


android:value="android.intent.action.VIEW" />



<meta
-
data android:name="@string/key_plugin_version"


android:value="1.0" />



<meta
-
data android:name="@string/key_guid"


android:value="09af85cc
-
cd00
-
4496
-
a126
-
ab17f98dbf25" />



<meta
-
data android:name="@string/key_vendor"


android:value=”@string/vendor_name" />

Publishing the Service


android:icon


Glyph used to advertise the plugin


android:label


Simple name used to advertise the plugin


android:description


Optional explanation of the plugin’s capabilities


All of these resources should be “@” defined
to preserve l10n capabilities


android:exported


Controls external access to the service


"false” restricts access to either components of the same application or applications
with the same user ID can start the service or bind to it.


Default is true,
or
false. From the docs:


The default value depends on whether the service contains intent
filters. The absence of any filters means that it can be invoked only by
specifying its exact class name. This implies that the service is intended
only for application
-
internal use (since others would not know the class
name). So in this case, the default value is "false". On the other hand,
the presence of at least one filter implies that the service is intended
for external use, so the default value is "true”.


For clarity, always set android:exported=“true” in manifest!

Optional Access Control


Carefully consider the ramifications of using
declarative access control


Forces apps to have
a priori

knowledge of a
specific class of plugins


Highly useful for apps built around a common
plugin framework


android:permission
="
com.tagdynamics.permission.MENU
" >



Metadata Name Keys


Widely disseminate mandatory and optional
<meta
-
data>
name

keys.


Consider adopting self
-
describing naming
conventions, e.g.
Optional
Copyright


Conveys developers should expect a missing value


Use metadata to capture everything not
already captured using standard android:
attributes

PackageManager


Easy access to those packages installed in the system


Context publishes getPackageManager()


Must pay attention to the flags


GET_INTENT_FILTERS


GET_META_DATA


GET_PERMISSIONS


GET_PROVIDERS


GET_SERVICES


(GET_ACTIVITIES)


Plugin UUID


Need a mechanism for universally unique plugin
identification


Separate from a revision/version concept that Android
uses for an APK


Package name is a concept already employed in the
Android architecture as a
bundle

or related
functionality, not a unique component per se


Suggest using a UUID


Native Java class for working with a UUID


Easy to generate


Universally familiar to programmers far and wide

How To: Identify Plugins

public static List<ServiceInfo> getDeclaredServices(Context context, PackageInfo pi) {


List<ServiceInfo> declaredServicesList = new ArrayList<ServiceInfo>();


PackageManager pm = context.getPackageManager();


ServiceInfo[] services = pi.services;


if (null != services) {



for (ServiceInfo service : services) {




ComponentName cn = new ComponentName(service.packageName, service.name);




try {





ServiceInfo metadataService = pm.getServiceInfo(cn, PackageManager.GET_META_DATA);





if (metadataService.metaData != null) {






if
(metadataService.metaData.containsKey(context.getResources().getString(R.string.key_guid) {







declaredServicesList.add(metadataService);






}





}




} catch (NameNotFoundException e)

{





// Handle this




}



}


}


return declaredServicesList;

}

Plugin Registration


PackageManager reads statically defined metadata


Cannot dynamically define a default plugin of a specific
type or an execution sequence


Think about SwiftKey and IME


System wide default IME is not defined in an AndroidManifest
entry, but registered by the user external to the APK


Identifying the plugins published by a package


Translate into ContentValues


Register the plugin with the authoritative plugin
registrar

How To: ServiceInfo Translation

private static final ContentValues createContentValues(Context context, ServiceInfo si) {


PackageManager packageManager = context.getPackageManager();


String uuid = (si.metaData.containsKey(MetadataKey.KEY_GUID))



? si.metaData.getString(MetadataKey.KEY_GUID)



: "
-

undefined
-

";



String label = packageManager.getText(si.packageName, si.labelRes, si.applicationInfo).toString();


String description = packageManager.getText(si.packageName, si.descriptionRes, si.applicationInfo).toString();



ContentValues values = new ContentValues();


values.put(BaseColumns._ID, (Integer) null);


values.put(PluginContract.Plugin.PLUGIN_PACKAGE, si.packageName);


values.put(PluginContract.Plugin.SERVICE_NAME, si.name);


values.put(PluginContract.Plugin.LABEL, label);


values.put(PluginContract.Plugin.PLUGIN_UUID, uuid);


values.put(PluginContract.Plugin.DESCRIPTION, description);



return values;

}

ServiceInfo Drawable Access


PackageManager provides easy access to the
ServiceInfo’s android:icon drawable resource


Only need to provide 3 pieces of information


Package Name


Icon Resource ID


ApplicationInfo object


Key Concept:
PackageManager looks up the
resource ID in the context of the target, not
the calling application

How To: Fetch Icon

public static Drawable lookupPluginIcon(Context context, String packageName, String serviceName) {


PackageManager packageManager = context.getPackageManager();


Drawable iconDrawable = null;


PackageInfo packageInfo;


try {



packageInfo = packageManager.getPackageInfo(packageName, PackageManager.GET_SERVICES);



ServiceInfo[] services = packageInfo.services;




for (ServiceInfo service : services) {




if (service.name.equals(serviceName)) {





iconDrawable = packageManager.getDrawable(packageName,








service.icon,








packageInfo.applicationInfo);





break;




}



}


} catch (NameNotFoundException e) {



// handle this


}


return iconDrawable;

}

Reading IntentFilter Definitions


No straight forward PackageManager method


Instantiate an
AssetManager

for the target


am = context.createPackageContext( packageName, 0).getAssets();


Instantiate a
Resources

for the target


resources = new Resources(assetManager,









context.getResources().getDisplayMetrics(), null);


Use
XmlResourceParser


am.openXmlResourceParser("AndroidManifest.xml");


Use pull parser architecture to re
-
build the entire
AndroidManifest.xml file, or just interesting elements

Plugin Metadata Summary


AndroidManifest.xml natively provides all the
requisite fields to formally define a plugin


Key Point:

Nothing “hacky” or unsupported here!


Manifest is static


Key Point:

Get the metadata out of the manifest and into a
ContentProvider where user defaults, ordering,
preferences, etc. can be cleanly managed


PackageManager is your friend, but the system is
always in a state of flux


Key Point:

Strong exception handling is a must; you never
know when a package may be uninstalled

PLUGIN REGISTRAR

Plugin Management via
ContentProvider

Content Providers


Intended to provide
single

source data
storage capabilities

across disparate apps


Think
one

set of contacts on the phone


App private (local) data
should not

immediately jump to a ContentProvider


Use files or a SQLite database directly instead


ContentProvider provides full CRUD support


Can optionally restrict operations

ContentProvider as Registrar


Accessing a ContentProvider is far more performant
than accessing PackageManager metadata each time
plugin definitions must be referenced


ContentProvider can be designed to allow the user to
establish order of precedence


Use this plugin first, then use this plugin


ContentProvider is the
preferred

means

of managing
an authoritative source of data on a given device


Photo Gallery or Contacts


one source, many apps access

Android “Contract” Pattern


Review the
android.provider

package


“Contract” nested interfaces


CalendarContract, ContactsContract


Every content provider needs to advertise its
structure and access path via URIs


Each advertised table maps to a “contract” nested
interface


Documents column names, data description and limits, etc.


Each physical structure maps to a static nested class


Documents CRUD behaviors

Components of a Contract Class


Authority Constants


PluginColumns Interface


Plugin Class


Builder Class (Optional)


Helper Methods (Optional)

Authority Constants


Provides the name of the authority, typically
using reverse domain notation


public static final String AUTHORITY =
"com.tagdynamics.providers.plugin”


Provides a
content://

style URI that forms the
basis for all content provider URIs


public static final Uri AUTHORITY_URI =
Uri.parse("content://" + AUTHORITY);


PluginColumns Interface


Provide a constant means of accessing the
various columns used to describe the plugin
architecture within the ContentProvider


Contain constants in the interface for each column
name, plus


A convenience array of the fields

Sample PluginColumns Interface

protected interface PluginColumns

{


String PLUGIN_UUID = "plugin_uuid";


String PLUGIN_TYPE = "plugin_type";


String PLUGIN_PACKAGE = "package";


String SERVICE_NAME = "name";


String LABEL = "label";


String DESCRIPTION = "description";


String[] FIELDS = new String[] { BaseColumns._ID, PLUGIN_TYPE,
PLUGIN_PACKAGE, SERVICE_NAME, LABEL, PLUGIN_UUID, DESCRIPTION };

}


Plugin Class


Standard POJO


Implement Comparable<?>


Specialized URIs as necessary


public static final Uri CONTENT_URI_BY_PERSISTENCE =
Uri.withAppendedPath(AUTHORITY_URI, "persistence");


public static final Uri CONTENT_URI_BY_WIDGET =
Uri.withAppendedPath(AUTHORITY_URI, "widget");

Builder Class


Extremely useful pattern in lieu of substantial
set of constructor arguments


Ensures that the POJO is instantiated with a
valid state


e.g. JSON, XML, etc.


Remember, Parcelable is
not
suitable for long term
persistence, e.g. inside of a SQLite DB!


See $SDK/docs/reference/android/os/Parcel.html

Helper Methods


Useful method unique to your plugin
architecture/design


e.g. toIntent(), or a toJson(), etc.

Separation of Concerns


The “Contract” class(es) should be built as an
Android library

project so that it can be
referenced by other plugin
-
enabled apps

Content Provider


Numerous examples abound on the physical
implementation of content providers.


Generally:


extends ContentProvider


Ensure methods are thread safe


Determine and document what operations your
provider will support (e.g. INSERT, UPDATE, etc.)
and what, if any, query strings may be utilized

Content Provider Summary


Provides the dynamic plugin management that isn’t possible
with a static AndroidManifest.xml file


Key Point:

Extra columns provide run
-
time configuration preferences
defined by the
user
, not the developer


Standardized Android mechanism for sharing data across all
installed apps


Key Point:

Globally accessible data documented by dozens of blogs,
books, and SDK examples


PackageManager is
still
your friend, but the system is always
in a state of flux


Key Point:

ContentProvider tells you how to find the plugin, but be
prepared to handle exceptional conditions

PLUGIN IMPLEMENTATION

Leveraging a Service to Fulfill External App Requests

Service, or Activity


There is no technical reason an Activity
couldn’t be treated as a plugin


Service provides a more powerful IPC
mechanism with its IBinder capability


No equivalent with an Activity


Service can run in the background either with
no explicitly user interface, or in the
foreground with an explicit user interface


Activity is explicitly a user interface

Service, or Activity

Service


android:export


android:icon


android:label


android:description


meta
-
data


Intent driven


IBinder


Background Processing*


Intrinsic Result Capability


Request Queuing*

Activity


android:export


android:icon


android:label


android:description


meta
-
data


Intent driven


IBinder


Background Processing


Intrinsic Result Capability


Request Queueing



* with
IntentService

Service + Activity


The
Service

onStartCommand()

is not
asynchronous


“…the system calls [onStartCommand] on your
service's main thread. A service's main thread is
the same thread where UI operations take place
for Activities running in the same process.”


Conclusion:

Nothing precludes the
Service

implementation from spawning an
Activity

from inside of
onStartCommand()

Service, or IntentService


IntentService extends Service


Similar, but used
very

differently


IntentService expressly designed to handle
asynchronous requests

via Intents, in the
order
received
, and can do so
on
-
demand


Use
startService(intent)

to trigger either


Caller shouldn’t have to reason about the
target, i.e. Service or IntentService

Service, or IntentService

Service


Plugin Extends:


Service


Implements Method:


onStartCommand()


Executes On:


Main UI Thread


Primarily Used For:


Plugin with direct user
interaction via an Activity


Shutdowns When:


Depends on return value

IntentService


Plugin Extends:


IntentService


Implements Method:


onHandleIntent()


Executes On:


Worker Thread


Primarily Used For:


Background task with no
direct user interaction


Shutdown When:


No work remains in queue

IntentService Considerations


IntentService is explicitly asychronous


How should we handle exceptions??


How should we handle success??


Option #1


Notify the
app

via
sendBroadcast

invocation and let the caller
handle success and failure equally


Option #2


Notify the
user

via the
NotificationManager

and document the
Intent an app should handle if the user taps on the notification for
more details


Option #3


Variations and combinations of the above

Plugin Controller Service


If Plugin Registrar (ContentProvider) design
allows user to define
default
,
order of
precedence
, or
sequenced

plugin execution,
consider implementing a Plugin Controller
Service


Conceptually similar to MVC


Incoming Intent can represent the model


Controller can inspect the user’s preferences (i.e.
the registrar represented by the ContentProvider)
and route the model to the preferred view

Plugin Implementation Summary


Determine if a given plugin metaphor must support direct
user interaction


Key Point:

Choose to extend Service or IntentService with a specific
design goal in mind


Empower the user by implementing a plugin controller service


Key Point:

Manifest driven Intent
-
Filter routing is static, but a
ContentProvider can capture user
-
defined, dynamic routing rules with
minimal work without intrusively prompting the user for a destination


Explicitly design feedback mechanisms into your IntentService
plugin implementations


Key Point:

Use sendBroadcast() for app interaction, or use the
NotificationManager for direct user interaction

PLUGIN INVOCATION

Plugin Communication via Intents, or
IBinder

against a Service

Intent Deep Dive


Intents are the de facto message bus built into the
Android® OS



An Intent provides a facility for performing late runtime
binding between the code in different applications.



Every Intent can define


Action


Data


Categories


Type


Extras

Intent’s Action


Can be one of the predefined Android® constants


android.intent.action.INSERT


Can be arbitrarily defined


com.tagdynamics.widget.CaptureNotePlugin


Consider using meta
-
data to self
-
describe what
action(s) the plugin will filter against:



<meta
-
data


android:name="@string/key_plugin_action”


android:value="android.intent.action.VIEW" />

Intent’s Data versus Extra


Data must be a Uri


Useful if the data is behind a ContentProvider


Alternatively, use a Parcelable Extra to provide
additional data to deliver with the Intent


Remember, Parcels are intended only as an IPC
mechanism and should not be used as a long
-
term
serialization mechanism

Intent Categories


Use a category to provide additional context
around the message being broadcast


An ACTION_INSERT indicates that the message
should result in an insertion


Consider the following category examples that
provide additional context about the
insertion:


com.tagdynamics.persistence.context.LOCAL


com.tagdynamics.persistence.context.NETWORK


Intent Categor
ies


0, 1, or many categories can added to an
Intent, whereas only a single Action, and a
single piece of Data can be defined on an
Intent


Useful when defining static Intent
-
Filter
definitions within a manifest


Allows the recipient to consider the context and
act accordingly


LOCAL versus NETWORK example from last slide:
mutually exclusive, or indicative of all supported
operations against the Intent?

Intent Type


Carefully consider your use of MIME types


Don’t unnecessarily invent MIME types


If you have to create a MIME type, follow the Android® design
patterns and rules:


vnd.android.cursor.item
/vnd.tagevent.pending


vnd.
tagdynamics.plugin.widget/vnd.tagdynamics.viewcamera


Review http://tools.ietf.org/rfc/rfc4288.txt


“Registrations in the vendor tree will be distinguished by the leading
facet "vnd.". That may be followed, at the discretion of the registrant,
by either a media subtype name from a well
-
known producer e.g.,
"vnd.mudpie") or by an IANA
-
approved designation of the producer's
name that is followed by a media type or product designation (e.g.,
vnd.bigcompany.funnypictures).”

Role of Intent Filtering


Intents representing the message bus powering a
plugin architecture


Message routing is of utmost importance and should
be carefully designed


Relying on manifest intent filtering mechanisms may lead
to a rigid coupling between application and plugin(s)


Not providing enough documentation can lead to missed
invocation opportunities or difficult debugging sessions


Weigh the pro’s/con’s of implementing a service controller
plugin carefully


May lead to non
-
standard or unintuitive message routing
scenarios if overly complex or inadequately documented

Intent Considerations


Intents are a powerful message bearing platform but are
generally one
-
way communication mechanims


Asynchronously delivered by the OS


No simple concept of a result,
per se


Intents may not reach their intended destination


Mismatched Intent
-
Filter


User is prompted to pick a destination, e.g. Browser or MyApp, and
they choose Browser and click the “Always use this” checkbox by
mistake


Call must explicitly choose delivery to a Service or an
Activity


startService()

versus
startActivity()

Introducing IBinder


Base interface for a remotable object


Android® preferred RPC mechanism


For plugins, should lean design toward AIDL



Using AIDL is necessary only if you allow clients from
different applications to access your service for IPC and
want to handle multithreading in your service.

If you do
not need to perform concurrent IPC across different
applications, you should create your interface by
implementing a Binder or, if you want to perform IPC, but
do not need to handle multithreading, implement your
interface using a Messenger.


Implementing IPC

1.
Write your AIDL in
file.aidl

inside of a valid
Java package directory


Eclipse will create stub for you in
gen

directory


Sample AIDL

package com.tagdynamics.sdk.ipc;


interface IUploadManager

{


int uploadAttachments();

}

Implementing IPC Service

@Override

public IBinder onBind(Intent intent)

{


return new IUploadManager.Stub()


{



@Override



public int uploadAttachments() throws
RemoteException



{




int uploadCount = 0;




// implementation here




return uploadCount;



}


};

}

Shared Preferences Tip


If you want to rely on a single set of
preferences between disparate contexts, use
getSharedPreferences() with an explicit name
and the MODE_MULTI_PROCESS flag


prefs = getSharedPreferences(



"com.tagdynamics.common_preferences",





Context.MODE_MULTI_PROCESS);

Implementing IPC Client

1.
Declare an instance variable in a caller class

2.
Create a ServiceConnection in the caller class
to monitor the the status of the service


Sample Caller Class

private IUploadManager upMgr;


private ServiceConnection connection =
new ServiceConnection()

{


@Override


public void onServiceDisconnected(ComponentName name)


{



upMgr = null;


}



@Override


public void onServiceConnected(ComponentName name, IBinder service)


{



upMgr =
IUploadMonitor.Stub.asInterface(service);


}

};

Implementing IPC Client

1.
Declare an instance variable in a caller class

2.
Create a ServiceConnection in the caller class
to monitor the the status of the service

3.
Invoke remote methods as
-
needed


Sample RPC Invocation

if (null != upMgr)

{


try


{



int uploadedCount = upMgr.uploadAttachments;




// do something with uploadedCount here



} catch (RemoteException e)


{



// Handle the exception


}

}


Plugin Invocation Summary


Carefully consider how you document plugin invocation


Key Point:

There are many ways to skin a cat, but some ways are
better than others. Communicate how your plugin(s) should be
invoked in an effective manner


Fully leverage the capabilities of Android® Intents


Key Point:

Action, Categories, MIME types, Intent
-
Filters, and Service
-
driven plugin Controller provide limitless invocation possiblities


Decide early on if one
-
way messaging is sufficient


Key Point:

AIDL provides a well
-
documented, supported method for
RPC between disparate Android® application processes

Class Summary


Componentized development on Android® is natively
supported by the platform itself


No special tools, frameworks, or hacks are needed


Leverage a plugin architecture to buffer internal messaging
implementations from the outside world


Use a Service as an entry
-
point into your application.


Your application’s concrete implementation can evolve and change
without regard to the public contract used by external applications


Consider a self
-
documenting approach with <meta
-
data> tags


Rely on a ContentProvider implementation as a plugin
registrar


Easily queried by applications to determine what, if any, specific plugin
capabilities are installed on the current device