benkc
benkc

Reputation: 3382

How to choose which service to handle an intent

I have an app which uses a service that is included in the application's package.

I am now making a new app which uses a newer version the same service, bundled within its own application package.

If only one app is installed on the device, this works fine. However, if both apps are installed on the device, they both use the service from whichever package was least recently installed -- i.e., the oldest one. This is problematic for two reasons: 1) the old service doesn't handle the new calls (obviously) 2) when using the service bundled with the other app, the service ends up using that other app's saved data instead of its own.

Short-term, I really just want each app to talk to its own version of the service. Is it possible to do this without making each bundled service respond to a unique intent, and having each app use that unique intent?

Long-term, it would be nice to optionally have both apps talk to the newest version of the service, and have them share data, and play nice with either app getting uninstalled. What's the right way to do that? The data is primarily in an SQLite database, which gets stored where Context.getDatabasePath() specifies, which is of course application-specific and wiped if that application is uninstalled.

Here's what I've tried so far:

I can use PackageManager.queryIntentServices() to get a list of ResolveInfo with both versions of the service. By looking at ResolveInfo.serviceInfo.applicationInfo.packageName, I can tell which is which. However, there doesn't seem to be any way to use that ResolveInfo to specify which of the two services I want to handle the intent.

This seems like it should do the right thing:

serviceIntent.setPackage(context.getApplicationContext().getApplicationInfo().packageName);

as that packageName is the same as the one I want to match. Indeed, when I call queryIntentServices with that modified intent I get a list back containing only the service within my package. However, when I try to bindService with that intent, it throws a security exception.

java.lang.RuntimeException: Unable to bind to service my.service.package.name.MyServiceClass@40531558 with Intent { act=my.intent.string pkg=my.app.package.name }: java.lang.SecurityException: Not allowed to start service Intent { act=RuntimeException: Unable to bind to service my.service.package.name.MyServiceClass } without permission private to package

Based on some googling, I have also tried creating the intent by doing:

Intent serviceIntent = new Intent().setClassName(
    "my.service.package.name",
    "my.service.package.name.ServiceClassName");

That doesn't resolve to any services at all.

I have also tried changing android:exported= in the service entry in the AndroidManifest.xml to true, false, or not specified, in various combinations in both apps, to no avail. (I was hoping that if I set them both to false, then each app would only be able to see its own copy of the service.)

Upvotes: 2

Views: 1449

Answers (2)

Douglas Jones
Douglas Jones

Reputation: 2542

Here is a solution that works:

In the manifest add a meta-data for the Service. The name should be something like "service launcher" and the value must be identical to the name of the action in the intent filter.

<meta-data android:name="Service Launcher"
           android:value="your.service.action.string.here" />

<intent-filter>
    <action android:name="your.service.action.string.here" />
</intent-filter>

Then in your code add an access method to get the meta-data and use that to build the Intent.

public static String getServiceIntentAction(Context context) {
    String action = "";
    try {
        PackageManager pm = context.getPackageManager();
        android.content.ComponentName cn = new android.content.ComponentName(context.getPackageName(), "com.your.ServiceClass");
        android.content.pm.ServiceInfo si = pm.getServiceInfo(cn, PackageManager.GET_META_DATA);
        action = si.metaData.getString("Service Launcher");
    } catch(android.content.pm.PackageManager.NameNotFoundException nnfe) {}
    return action;
}

Then anything that is building an Intent to launch the service calls this method. Every user of the service in their app can have a different action to only communicate to their app with just one version of the library and only a change to the manifest.

To have both apps use the same service the service should probably be install as its own entity.

Upvotes: 3

Douglas Jones
Douglas Jones

Reputation: 2542

Have you tried using categories? For every category in an intent, the potentially receiving component has to match each category to be considered a receiver.

Service A has category "1" and intents to category "1" go there. Service B has category "1" and category "2" (assuming it still supports everything in version "1". With this an older app could talk to the newer service but a newer app could not communicate with an older service.

Upvotes: 0

Related Questions