How to choose which service to handle an intent - android

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.)

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.

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.

Related

Why do we get these "invalid" intents?

We have an app with an Activity that can be started in two ways:
From another Activity - always with some extra data filled in
From deep linking
As far as I can see this is always working just fine. We either get the Intent.ACTION_VIEW with a data URI, or we get some string extras.
However, we have a very few instances where action is Intent.ACTION_MAIN and there are no extra data.
The toString() of the Intent is as follows (class name changed):
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10400000 cmp=com.example.OurActivity }
Intent.getExtras() returns null, Intent.getDataString() returns null.
In which cases can this happen? Why is the category for the Activity Intent.CATEGORY_LAUNCHER? How can we get the data needed to show the user the right contents?
launchMode is not specified for the Activity. The only IntentFilter in AndroidManifest.xml is for the deep linking (and not the launcher category).
The issue happens on Android 4-6 on a wide range of devices.
Edit: Forgot to mention the flags:
As the print out suggests the flags for the Intent are FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_BROUGHT_TO_FRONT. I suppose that could be relevant here.
I believe, I nailed it:
There're launchers, like Nova Launcher which allows users to start with any of app's Activities, instead of the normal flow:
For example, you can add a shortcut on your desktop to start Gmail client with Account setup Activity.
And in this case, Activity is being started with empty Extras and technically it becomes a launcher's Activity.
Now that AndroidManifest.xml is manipulated by the build system, it often happens that libraries that you include also add things to the manifest, which I suspect may be happening here.
Although you state that there is only one IntentFilter in the manifest, have you actually checked the installed app to see what its manifest says (rather than relying on what you think you put in your source code)?
Various apps are available in the Play Store to show you the manifest of an installed app - including App Detective (which I wrote).

Can an app start a component in another app through explicit intent?

Can App A start a component in App B through explicit intent?
I understand that general use of an explicit intent is to start a component within the same app. Implicit intents are used to cross process boundary.
Scenario:
App B defines and uses a custom permission "foo"
App A uses
PackageManager API to identify App B (app which defined "foo")
Can App A start a component within App B through explicit intent?
I don't think this is possible because explicit intent needs- app package and component name.
if (PackageManager.PERMISSION_GRANTED == packageManager
.checkPermission("foo",
pk.packageName))
results.add("package name: "+pk.applicationInfo.packageName+ " class name:
"+pk.applicationInfo.classNa);
}
Class name is null. So, per my understanding there is no way inter-app communication is possible through explicit intent just by relying on PackageManager APIs. In Android 5, you can call AIDL Services only through explicit intent but at dev-time the caller needs to know package and component name of app exposing the AIDL. Is my understanding correct?
Can App A start a component within App B?
App A can try. Whether App A will succeed depends on several things, including whether the component is exported and whether the component is secured by a permission.
I don't think this is possible because explicit intent needs- app package and component name.
The application ID and component name are both strings. Apps can use strings. Correctly getting the application ID and component name may take some work, depending upon the relationship between App A and App B.
As a counter-example, the only way to bind to a service on Android 5.0+ is via an explicit Intent. Hence, if App A wishes to bind to a service exported by App B, App A has no choice but to create an explicit Intent.
No way. If you want to start something that's outside your app you have to declare a implicit intent. Otherwise android will look for it inside your package, probably not gonna find anything and crash.
From the docs:
You'll typically use an explicit intent to start a component in your
own app, because you know the class name of the activity or service
you want to start.
Under "Intent Types" - http://developer.android.com/guide/components/intents-filters.html
So, typically you will not... but you can do this however it is not straightforward. To create a proper explicit intent, you will need the application context and the class.
To get the context of another application:
createPackageContext (String packageName, int flags)
http://developer.android.com/reference/android/content/Context.html#createPackageContext(java.lang.String,%20int)
For the class, you can request 'Activity' and 'Service' classes from package manager. You could also try to use reflection. You can also use the fully qualified string name of the package and assume it is correct and/or will not change. If you want, you can include a stub of the class in your app, thereby obtaining a reference to it (if you have access to the source or a stub library).
If there are permission restrictions, you will need to request permission.

Pasing data through app after install without launching

Refering to this post I need to ask something else.
I have an application "A" which download and install another app "B".
I want B to "transfer" data to A and then A will use this data to do work.
I know that we can transfer data with intent.
After installing B app with A, Android provide a choice with "Ok" or "Launch" ; my question is :
Is that possible to pass data from B to A when we click on "Ok"? (So we stay in A app without launching B)
If yes, how? Is that possible to "invisible" launch B? How should I code B to get this comportement?
I know that might be hard to understand, you can try to check my previous draw (here again).
EDIT:
I use this code to launch B installation from A.
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Uri uri = Uri.fromFile(new File(Environment.getExternalStorageDirectory().toString() + "/downloadedfile.apk"));
Intent.setDataAndType(uri, "application/vnd.android.package-archive");
getApplicationContext().startActivity(intent);
There are many ways to handle this, here is one that (I believe) is quite simple to implement. Since your A app [presumably] knows what it is installing:
App A: Add a BroadcastReceiver to react to the installation, though by default it is off.
Android: BroadcastReceiver on application install / uninstall
App B: Add a Service for background communication.
Note: A Service must be exported to be accessible to other apps via explicit intent, but this creates a security concern, as it is open to all other apps.
When a user of App A clicks to install App B:
Start the BroadcastReceiver with a filter set to detect the install:
stackoverflow...android-broadcastreceiver-on-application-install-uninstall
App A starts the install.
When the BroadcastReceiver detects the package has been added (the package name will be in the received intent,) it can stop the BroadcastReceiver, and can send an explicit Intent naming the Service in AppB. You can pass whatever data you need in the intent.
When the AppB service receives the intent, it can act in any way you'd like.
Service is always created using a non-null Intent, though the 'action' of explicit Intents is null.
Service.onStartCommand() might receive a null Intent if the service was re-created.
I'd fill in more of the code, but I have a day job ;)
Note:
Intent.ACTION_PACKAGE_ADDED called when a package is installed.
Intent.ACTION_PACKAGE_INSTALL was never used, and was deprecated in API 14.
http://developer.android.com/reference/android/content/BroadcastReceiver.html
http://developer.android.com/reference/android/content/Intent.html

Does the LocalBroadcastManager depend on app package names?

I'm creating an app using the new Android build system, and using a LocalBroadcastManager to fire notifications from an IntentService back to an Activity.
I've defined the following product flavours as part of the build for 3 separate apps;
productFlavours {
london {
packageName 'com.example.lo'
}
dubai {
packageName 'com.example.du'
}
}
When I build the app using either of these product flavours I am not getting any broadcasts through the manager. I've confirmed that both the service and the activity are running in the same process.
A broadcast receiver is bound as required in the Activity#onCreate method of my activity but does not receive any events. I have also confirmed by printing out the process ID of both the activity and service and both are reporting that this is the same process, so this can't be an issue.
As a test I removed the product flavours and built the app with just the single packagename as defined in the app manifest file and now the broadcast manager seems to function as expected. On the intents that I'm sending I am only setting the action.
Do I need to set anything else?
If you look at the source code of LocalBroadcastManager you'll see that if you give your Intent the flag Intent.FLAG_DEBUG_LOG_RESOLUTION (by using Intent.addFlags(int)), it will print in the logcat pretty useful information: you might want to try that with and without the flavours and compare the result to see what goes wrong.
I have org.cmpny.app and org.cmpny.app.additional namespaces in same project; LocalBroadcastManager works fine, nothing is required to be tuned in AndroidManifest (except that LocalBroadcastManager can't be used with ":remote" services, but that's another point)

android : how does the OS chose a component implementation when multiple implementation respond to the same intent?

I have a free and a premium version of the same app (almost same code, same classes with "if"s here and there, different packages in the manifest, same process name in the manifest). The main activity calls a service to execute some stuff, using an IMPLICIT intent.
When I install both apps on the phone, it turns out that the premium activity actually starts a "free" service sometimes and a "premium" service another.
I have been playing around with categories and the packagemanager but it seems too complicated.
Questions :
how does Android handle multiple components responding to a same Intent?
how would you do what I am trying to do: I have the same service in multiple apps and I only want one instance being called from all the apps?
I guess you could add an extra boolean isPremium to the intent. Naturally you will need some more of those "if's".
Activity:
//send broadcast
Intent serviceStarted = new Intent(Actions.ACTION_START_SERVICE);
serviceStarted.putExtra(Extras.EXTRA_PREMIUM_VERSION, PREMIUM_VERSION);
sendBroadcast(serviceStarted);
Receiver:
if (!intent.getExtras().getBoolean(Extras.EXTRA_PREMIUM_VERSION)) {
Log.v(TAG, " - ignoring wrong version");
return;
}

Categories

Resources