How to ensure that the Android library project code runs only in one of the installed applications that integrated it?

I am developing a library project that will be integrated into some popular Android applications that can be seen on Google Play.

Suppose a user can install two or more applications, and each of them can integrate my library. The library has specific code used to detect environmental changes. The status is simply sent to my server. The problem is that processing the state of the environment requires a lot of processor power, but for a short period of time. Processing cycles are triggered by the AlarmManager, using non-wake-up broadcasts that start their own IntentService.

My goal is to implement the library so that only one instance integrated into the application can do the job. I mean, only one library module should act as "active". If more applications are installed on the user device, then they should not overlap.

How to do it? I was thinking of some kind of permission check and cross-packet detection, but could not imagine how to implement it.

+4
source share
5 answers

I did some additional research and was able to find a satisfactory solution. Here it is:

The library should be designed so that every application that integrates it publishes a broadcast receiver with a known action, for example. com.mylib.ACTION_DETECT.

The library should have an additional service that publishes some AIDL interface, which helps to make a decision - if the current library instance can be activated. AIDL can have some useful methods, for example getVersion (), isActive (), getUUID ().

Decision template: if the current instance has a higher version number, the other will become active. If the current instance has a lower version, it deactivates itself or remains deactivated if it is already disabled. If the current instance has an equal version for another instance, then if the other instance is inactive and the other uuid library is lower (using the compareTo method), it activates itself. In another state, he deactivates himself. This cross-validation ensures that each library will make its own decision - there will be no ambiguous cases, because each library will extract the required data from the published AIDL service of other libary instances in other applications.

The next step is to prepare an IntentService, which is launched every time a new package is removed or added, or the library application is launched for the first time. IntentService requests all packets for broadcast receivers that implement com.mylib.ACTION_DETECT. It then iterates through the detected packages (rejecting its own package) and binds to the AIDL support of each other instance (the AIDL service class name will always be the same, only the application package will be different). After the binding is completed, we have a clear situation - if the result of applying the template is “positive” (our instance has a better version or a higher uuid or is already active), then this means that other cases considered themselves “negative” and deactivated themselves, Of course, The template should apply to each associated AIDL service.

I apologize for my poor English.

The work code is ConfictAvoidance: IntentService, which supports binding, so this is also the AIDL service mentioned above. There is also a BroadcastReceiver that runs conflict checking.

public class ConflictAvoidance extends IntentService { private static final String TAG = ConflictAvoidance.class.getSimpleName(); private static final String PREFERENCES = "mylib_sdk_prefs"; private static final int VERSION = 1; private static final String KEY_BOOLEAN_PRIME_CHECK_DONE = "key_bool_prime_check_done"; private static final String KEY_BOOLEAN_ACTIVE = "key_bool_active"; private static final String KEY_LONG_MUUID = "key_long_muuid"; private static final String KEY_LONG_LUUID = "key_long_luuid"; private WakeLock mWakeLock; private SharedPreferences mPrefs; public ConflictAvoidance() { super(TAG); } private final IRemoteSDK.Stub mBinder = new IRemoteSDK.Stub() { @Override public boolean isActive() throws RemoteException { return mPrefs.getBoolean(KEY_BOOLEAN_ACTIVE, false); } @Override public long[] getUUID() throws RemoteException { return getLongUUID(); } @Override public int getSdkVersion() throws RemoteException { return 1; } }; @Override public IBinder onBind(Intent intent) { return mBinder; } @Override public void onCreate() { //#ifdef DEBUG Log.i(TAG, "onCreate()"); //#endif mWakeLock = ((PowerManager) getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); mWakeLock.acquire(); mPrefs = getSharedPreferences(PREFERENCES, MODE_PRIVATE); super.onCreate(); } @Override public void onDestroy() { //#ifdef DEBUG Log.i(TAG, "onDestroy()"); //#endif mWakeLock.release(); super.onDestroy(); } @Override protected void onHandleIntent(Intent arg) { //#ifdef DEBUG Log.d(TAG, "Conflict check"); //#endif final String packageName = getPackageName(); //#ifdef DEBUG Log.v(TAG, "Current package name: %s", packageName); //#endif final ArrayList<String> packages = new ArrayList<String>(20); final PackageManager man = getPackageManager(); //#ifdef DEBUG Log.v(TAG, "Querying receivers: com.mylib.android.sdk.ACTION_DETECT_LIB"); //#endif final List<ResolveInfo> receivers = man.queryBroadcastReceivers(new Intent("com.mylib.android.sdk.ACTION_DETECT_LIB"), 0); for (ResolveInfo receiver : receivers) { if (receiver.activityInfo != null) { final String otherPackageName = receiver.activityInfo.packageName; //#ifdef DEBUG Log.v(TAG, "Checking package: %s", otherPackageName); //#endif if (!packageName.equals(otherPackageName)) { packages.add(otherPackageName); } } } if (packages.isEmpty()) { //#ifdef DEBUG Log.i(TAG, "No other libraries found"); //#endif setup(true); } else { //#ifdef DEBUG Log.v(TAG, "Querying other packages"); //#endif final UUID uuid = getUUID(); for (String pkg : packages) { final Intent intent = new Intent(); intent.setClassName(pkg, "com.mylib.android.sdk.utils.ConflictAvoidance"); final RemoteConnection conn = new RemoteConnection(uuid); try { if (bindService(intent, conn, BIND_AUTO_CREATE)) { if (!conn.canActivateItself()) { setup(false); return; } } } finally { unbindService(conn); } } setup(true); } } private UUID getUUID() { final long[] uuid = getLongUUID(); return new UUID(uuid[0], uuid[1]); } private synchronized long[] getLongUUID() { if (mPrefs.contains(KEY_LONG_LUUID) && mPrefs.contains(KEY_LONG_MUUID)) { return new long[] { mPrefs.getLong(KEY_LONG_MUUID, 0), mPrefs.getLong(KEY_LONG_LUUID, 0) }; } else { final long[] uuid = new long[2]; final UUID ruuid = UUID.randomUUID(); uuid[0] = ruuid.getMostSignificantBits(); uuid[1] = ruuid.getLeastSignificantBits(); mPrefs.edit().putLong(KEY_LONG_MUUID, uuid[0]).putLong(KEY_LONG_LUUID, uuid[1]).commit(); return uuid; } } private void setup(boolean active) { //#ifdef DEBUG Log.v(TAG, "setup(active:%b)", active); //#endif mPrefs.edit().putBoolean(KEY_BOOLEAN_ACTIVE, active).putBoolean(KEY_BOOLEAN_PRIME_CHECK_DONE, true).commit(); } public static StatusInfo getStatusInfo(Context context) { final SharedPreferences prefs = context.getSharedPreferences(PREFERENCES, MODE_PRIVATE); return new StatusInfo(prefs.getBoolean(KEY_BOOLEAN_ACTIVE, false), prefs.getBoolean(KEY_BOOLEAN_PRIME_CHECK_DONE, false)); } public static class DetectionReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { context.startService(new Intent(context, ConflictAvoidance.class)); } } public static class StatusInfo { public final boolean isActive; public final boolean primeCheckDone; public StatusInfo(boolean isActive, boolean primeCheckDone) { this.isActive = isActive; this.primeCheckDone = primeCheckDone; } } protected static class RemoteConnection implements ServiceConnection { private final ConditionVariable var = new ConditionVariable(false); private final UUID mUuid; private final AtomicReference<IRemoteSDK> mSdk = new AtomicReference<IRemoteSDK>(); public RemoteConnection(UUID uuid) { super(); this.mUuid = uuid; } @Override public void onServiceConnected(ComponentName name, IBinder service) { //#ifdef DEBUG Log.v(TAG, "RemoteConnection.onServiceConnected(%s)", name.getPackageName()); //#endif mSdk.set(IRemoteSDK.Stub.asInterface(service)); var.open(); } @Override public void onServiceDisconnected(ComponentName name) { //#ifdef DEBUG Log.w(TAG, "RemoteConnection.onServiceDisconnected(%s)", name); //#endif var.open(); } public boolean canActivateItself() { //#ifdef DEBUG Log.v(TAG, "RemoteConnection.canActivateItself()"); //#endif var.block(30000); final IRemoteSDK sdk = mSdk.get(); if (sdk != null) { try { final int version = sdk.getSdkVersion(); final boolean active = sdk.isActive(); final UUID uuid; { final long[] luuid = sdk.getUUID(); uuid = new UUID(luuid[0], luuid[1]); } //#ifdef DEBUG Log.v(TAG, "Other library: ver: %d, active: %b, uuid: %s", version, active, uuid); //#endif if (VERSION > version) { return true; } else if (VERSION < version) { return false; } else { if (active) { return false; } else { return mUuid.compareTo(uuid) == 1; } } } catch (Exception e) { return false; } } else { return false; } } } } 

AIDL File:

 package com.mylib.android.sdk; interface IRemoteSDK { boolean isActive(); long[] getUUID(); int getSdkVersion(); } 

Manifest example:

 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.mylib.android.sdk" android:versionCode="1" android:versionName="1.0" > <uses-sdk android:minSdkVersion="4" android:targetSdkVersion="4" /> <service android:name="com.mylib.android.sdk.utils.ConflictAvoidance" android:exported="true" /> <receiver android:name="com.mylib.android.sdk.utils.ConflictAvoidance$DetectionReceiver" > <intent-filter> <action android:name="com.mylib.android.sdk.ACTION_DETECT_LIB" /> </intent-filter> <intent-filter> <action android:name="android.intent.action.PACKAGE_ADDED" /> <action android:name="android.intent.action.PACKAGE_REMOVED" /> <action android:name="android.intent.action.PACKAGE_DATA_CLEARED" /> <action android:name="android.intent.action.PACKAGE_REPLACED" /> <data android:scheme="package" /> </intent-filter> </receiver> </application> </manifest> 

Act:

 <action android:name="com.mylib.android.sdk.ACTION_DETECT_LIB" /> 

This is a common action that is used to discover other applications in the library.

Using logs may seem odd, but I'm using a custom wrapper that supports formatting to reduce StringBuffers' debugging overhead.

0
source

I would try something related to the CSMA / CD collision detection method that was used (or used most often) on the network.

You do not want to transfer a specific instance so that it always does the work, since you do not know whether this file will be deleted. So instead, make the decision again each time (since it really doesn't matter what it does at any given time).

This gets a little complicated because it is not a trivial problem to solve, but I like the idea of ​​someone, perhaps generalizing this solution to anyone who uses (open source, what are you doing with this?).

When the initial broadcast begins, send a custom broadcast (defined as coming from your specific application) that you are also listening to. If you do not receive any other of the same broadcast within, say, a second, then continue and do this work, since there should not be other instances of your library who want to do this work.

If you receive a message from at least one other library (keep track of everything you hear), wait a while. If you receive a message from another library that says “I will do this” within this period of time, immediately send the message “OK, you will do it.” If you don’t do this, send a message “I will do it” and wait until every other library with which you received a message from the very beginning sends a message “ok, you do it”. Then do the work.

If you send the message “I will do it”, but also receive the message “I will do it” from another library, then start the process. The fact that each library is waiting for a random time to send “I will do it” means that there are rarely such collisions, and of course they should not be repeated often several times in a row.

I hope I explained it well enough so you can do it. If not, ask for clarification or see how this is done in the network world. What I'm trying to describe is similar to what is called "Collision Detection", for example, as indicated here: https://en.wikipedia.org/wiki/CSMA/CD

+2
source

My goal is to implement the library so that only one instance integrated into the application can do the job.

This will be quite difficult, and the results are likely to be unreliable.

I would recommend a variation on the Ian theme. Change the definition of your problem to read: "I want the work to be performed only every N minutes / hours / independently." There are some background work tools to determine when the work was the last (file on external storage, a request made from your web service, whatever), and then skip this work if it is too soon. Thus, it does not matter how many applications are installed in your library, in what order they are installed or when they are removed.

+1
source

Why can’t you use the ANDROID_ID device (or some unique identifier for your phone), register it on the server, and if another instance of the library is already running on this device, do nothing.

You can get the device ID from the following code snippet

 Secure.getString(context.getContentResolver(), Secure.ANDROID_ID); 
0
source

Isn't ContentProvider friendly way to share data for apps? You can use a single row SQLite table to implement an atomic timestamp. Replace the alarm manager schema with the thread created during the initialization of the library, which polls the ContentProvider every few seconds. The CP replies “yes, please send the state of the environment”, which means that it has already updated the table with current data / time or “no, not yet”. The provider consults the table and system clock to decide when to say yes.

0
source

All Articles