Как обеспечить выполнение кода проекта библиотеки Android только в одном из установленных приложений, которые его интегрируют?

Я занимаюсь разработкой библиотечного проекта, который будет интегрирован в некоторые популярные приложения для Android, которые можно увидеть в Google Play.

Предположим, что у пользователя может быть установлено два или более приложений, и каждое может интегрировать мою библиотеку. В библиотеке есть определенный код, используемый для обнаружения изменений состояния среды. Состояние просто отправляется на мой сервер. Проблема в том, что обработка состояния среды занимает много ресурсов процессора, но за короткий промежуток времени. Циклы обработки запускаются AlarmManager с использованием широковещательных рассылок без пробуждения, которые запускают надлежащий IntentService.

Моя цель - реализовать библиотеку таким образом, чтобы только один экземпляр, интегрированный в приложение, мог выполнять свою работу. Я имею в виду, что только один библиотечный модуль должен действовать как "активный". Если на устройстве пользователя установлено больше приложений - они не должны перекрываться.

Как этого добиться? Я думал о какой-то проверке разрешений и обнаружении перекрестных пакетов, но не мог представить, как это реализовать.

5 ответов

Решение

Я сделал несколько дополнительных исследований, и мне удалось найти удовлетворительное решение. Вот оно:

Библиотека должна быть разработана таким образом, чтобы каждое приложение, которое ее интегрирует, публиковало широковещательный приемник с известным действием, например. com.mylib.ACTION_DETECT.

У библиотеки должен быть дополнительный Сервис, который публикует некоторый интерфейс AIDL, который помогает в принятии решения - можно ли сделать текущий экземпляр библиотеки активным. AIDL может иметь несколько полезных методов, например, getVersion(), isActive(), getUUID().

Шаблон для принятия решения: если текущий экземпляр имеет более высокий номер версии, то другой - он станет активным. Если текущий экземпляр имеет более низкую версию - он деактивируется сам или остается деактивированным, если он уже деактивирован. Если текущий экземпляр имеет версию, равную другому экземпляру, то если другой экземпляр не активен, а uuid другой библиотеки ниже (с помощью метода CompareTo) - он активируется сам. В другом состоянии - деактивируется. Эта перекрестная проверка гарантирует, что каждая библиотека примет решение самостоятельно - не будет неоднозначных случаев, потому что каждая библиотека будет извлекать необходимые данные из опубликованной службы AIDL, поддерживаемой другими экземплярами библиотеки в других приложениях.

Следующим шагом является подготовка IntentService, который запускается при каждом удалении или добавлении нового пакета или при первом запуске приложения с библиотекой. IntentService запрашивает все пакеты для широковещательных приемников, которые реализуют com.mylib.ACTION_DETECT. Затем он выполняет итерацию по обнаруженным пакетам (отклоняя свой собственный пакет) и связывается со службой, поддерживаемой AIDL каждого другого экземпляра (имя класса службы AIDL всегда будет одинаковым, только пакет приложения будет другим). После завершения связывания - у нас ясная ситуация - если применяемый шаблон дает "положительный результат" (у нашего экземпляра лучшая версия или более высокий uuid, или он уже был активен), то это означает, что другие экземпляры считали себя "отрицательными" и деактивировали себя, Конечно, шаблон должен применяться к каждой связанной услуге AIDL.

Я прошу прощения за мой плохой английский.

Код работающего решения ConfictAvoidance: класс IntentService, который поддерживает связывание, поэтому это также служба с поддержкой AIDL, упомянутая выше. Существует также BroadcastReceiver, который запускает проверку конфликтов.

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:

package com.mylib.android.sdk;

interface IRemoteSDK
{
    boolean isActive();
    long[] getUUID();
    int getSdkVersion();
}

Образец манифеста:

<?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 android:name="com.mylib.android.sdk.ACTION_DETECT_LIB" />

Это общее действие, которое используется для обнаружения других приложений с библиотекой.

Использование журнала может показаться странным, но я использую пользовательскую оболочку, которая поддерживает форматирование, чтобы уменьшить накладные расходы StringBuffers при отладке.

Я бы попробовал кое-что, связанное с техникой обнаружения столкновений CSMA/CD, которая используется (или использовалась чаще) в сети.

Вы не хотите фиксировать конкретный экземпляр, чтобы он всегда выполнял свою работу, поскольку вы не знаете, будет ли этот экземпляр удален. Поэтому вместо этого принимайте решение заново каждый раз (поскольку на самом деле не имеет значения, что делает это в любой момент времени).

Это становится немного сложнее, потому что это не тривиальная проблема, которую нужно решить, но мне нравится идея кого-то, возможно, обобщающего это решение, чтобы кто-нибудь мог его использовать (с открытым исходным кодом, что вы делаете с этим?).

Когда начальная трансляция поступит, отправьте пользовательскую трансляцию (определенную как исходящую из вашего конкретного приложения), которую вы также слушаете. Если вы не получите никакой другой такой же трансляции, скажем, за секунду, тогда продолжайте работу и выполняйте работу, поскольку не должно быть других экземпляров вашей библиотеки, готовых выполнить эту работу.

Если вы получаете сообщение хотя бы из одной другой библиотеки (отслеживайте все, что вы слышали), подождите случайное количество времени. Если в течение этого времени вы получите сообщение из другой библиотеки, в котором будет написано "Я сделаю это", немедленно отправьте сообщение, означающее "хорошо, вы делаете это". Если вы этого не сделаете, то отправьте сообщение "Я сделаю это" и подождите, пока каждая другая библиотека, из которой вы получили сообщение в начале, отправит сообщение "Хорошо, вы делаете это". Тогда делай работу.

Если вы отправите сообщение "Я сделаю это", но получите сообщение "Я сделаю это" и из другой библиотеки, то начните процесс заново. Тот факт, что каждая библиотека ожидает произвольное время, чтобы отправить сообщение "Я сделаю это", означает, что подобные коллизии редко бывают такими, и они, конечно, не должны часто происходить несколько раз подряд.

Надеюсь, я объяснил это достаточно хорошо, чтобы вы могли это сделать. Если нет, пожалуйста, попросите разъяснений или посмотрите, как это делается в мире сетей. То, что я пытаюсь описать, похоже на то, что называется "Обнаружение столкновения", например, как указано здесь: https://en.wikipedia.org/wiki/CSMA/CD

Моя цель - реализовать библиотеку таким образом, чтобы только один экземпляр, интегрированный в приложение, мог выполнять свою работу.

Это будет довольно сложно, и результаты, вероятно, будут ненадежными.

Я бы порекомендовал вариацию на тему Яна. Измените определение вашей проблемы следующим образом: "Я хочу, чтобы работа выполнялась только каждые N минут / часов / что угодно". Имейте некоторые средства фоновой работы, чтобы определить, когда работа была выполнена в последний раз (файл во внешнем хранилище, запрос, сделанный вашим Web-сервисом и т. Д.), А затем пропустите эту работу, если это слишком рано. Таким образом, не имеет значения, сколько приложений установлено в вашей библиотеке, в каком порядке они установлены или когда они удалены.

Почему вы не можете использовать ANDROID_ID устройства (или какой-то уникальный идентификатор телефона), зарегистрировать его на сервере, и если на этом устройстве уже запущен другой экземпляр библиотеки - ничего не делать.

Вы можете получить идентификатор устройства по следующему коду

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

Не ContentProvider удобный способ обмена данными между приложениями? Вы можете использовать однострочную таблицу SQLite для реализации атомарной метки времени. Замените схему диспетчера тревог потоком, созданным во время инициализации библиотеки, который опрашивает ContentProvider каждые несколько секунд. CP отвечает "да, пожалуйста, отправьте состояние среды", что означает, что он уже обновил таблицу с текущими данными / временем, или "нет, еще нет". Поставщик обращается к столу и системным часам, чтобы решить, когда сказать "да".

Другие вопросы по тегам