How to ensure that android library project code is executed only in one of installed applications which integrate it?

StackOverflow https://stackoverflow.com/questions/11052347

Question

I'm developing a library project that will be integrated into some popular android applications which can be seen in Google Play.

Let assume that user can have two or more applications installed, and each one can integrate my library. The library have some specific code used to detect changes of environment's state. The state is simply sent to my server. The problem is that environment state processing takes a lot of CPU power, but in a short period of time. Cycles of processing are started by AlarmManager, using "non wake up" broadcasts that launch proper IntentService.

My goal is to implement the library such way, that only one instance integrated into application can do the work. I mean only one library module should act as "active". If there are more applications installed on user's device - then they shouldn't overlap.

How to achieve it? I was thinking about some kind of permission validation, and cross package detection, but couldn't imagine how to implement it.

Was it helpful?

Solution 5

I have made some additional researches, and managed to find a satisfying solution. Here it comes:

A library has to be developed in a way, that each application which integrates it - publishes broadcast receiver with known action, eg. com.mylib.ACTION_DETECT.

The library has to have additional Service, that publishes some AIDL interface, which helps with making decision - if current instance of library can be made active. The AIDL can have some useful methods for example getVersion(), isActive(), getUUID().

The pattern for making decision is: if current instance has higher version number, that other one - it will become active. If current instance has lower version - it will deactivate itself, or stay deactivated if it is already deactivated. If current instance has equal version to other instance, then if other instance is not active, and other library's uuid is lower (through compareTo method) - it will activate itself. In other condition - it will deactivate itself. This cross checking ensures, that each library will make decision on its own - there will be no ambiguous cases, because each library will fetch required data from published AIDL backed Service of other libary instances in other apps.

Next step is to prepare an IntentService, that is started each time new package is removed or added, or the application with library is started first time. The IntentService queries all packages for broadcast receivers, which implement com.mylib.ACTION_DETECT. Then it iterates through detected packages (rejecting it's own package), and binds to AIDL backed service of each other instance (the class name of AIDL service will be always the same, only application package would be different). After completing binding - we have clear situation - if applied pattern results "positive" (our instance has better version or higher uuid, or has been active already) then it implies, that other instances figured out themselves as "negative", and deactivated themselves. Of course the pattern has to be applied on each bound AIDL service.

I apologize for my bad English.

Code of working ConfictAvoidance solution: IntentService class, that supports binding, so it is also AIDL backed service mentioned above. There is also BroadcastReceiver, which starts conflict checks.

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();
}

Sample manifest:

<?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>

Action:

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

It is the common action, which is used to detect other apps with the library.

Log usage may look weird, but I use custom wrapper, which supports formatting, to decrease StringBuffers overhead when debugging.

OTHER TIPS

I'd try something related to the CSMA/CD collision detection technique that's used (or used to be used more often) in networking.

You don't want to commit to a specific instance to be always doing the work, since you don't know if that one would get uninstalled. So instead, make the decision anew each time (since it really doesn't matter which does it at any given time).

It gets a little complicated, because it's not a trivial problem to solve, but I like the idea of someone perhaps generalizing this solution for anyone to use (open-source what you do with this?).

When the initial broadcast arrives, send out a custom broadcast (identified as coming from your particular app) that you're also listening for. If you don't receive any other of that same broadcast within, say, a second, then go ahead and do the work, since there must be no other instances of your library willing to do the work.

If you do get a message from at least one other library (keep track of all of them that you hear from), wait a random amount of time. If you receive a message from another library saying "I'll do it" within that amount of time, then immediately send out a message meaning "okay, you do it". If you don't, then send out a message saying "I'll do it", and wait for every other library you received a message from at the beginning to send a "okay, you do it" message. Then do the work.

If you send a "I'll do it" message, but get an "I'll do it" message from another library as well, then start the process over. The fact that each library waits a random time to send the "I'll do it" means there will rarely be collisions like this, and they certainly shouldn't often happen multiple times in a row.

I hope I've explained this well enough that you can make it happen. If not, please ask for clarification, or look at how this is done in the networking world. What I'm trying to describe is like what's called "Collision Detection", for example as referenced here: https://en.wikipedia.org/wiki/CSMA/CD

My goal is to implement the library such way, that only one instance integrated into application can do the work.

That is going to be rather complicated, and the results are likely to be unreliable.

I would recommend a variation on Ian's theme. Change the definition of your problem to be "I want the work to only be done every N minutes/hours/whatever". Have some means of the background job to detect when the work was last done (file on external storage, request made of your Web service, whatever), and then skip that work if it is too soon. That way, it does not matter how many apps are installed with your library, what order they are installed in, or when they are uninstalled.

Why can't you use the device's ANDROID_ID (or some kind of unique identifier for the phone), register that with the server, and if another instance of the library is already running on that device - do nothing.

You can get a device identifier by the following piece of code

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

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

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top