如何确保android库项目代码只在其中一个集成它的已安装应用程序中执行?

时间:2012-06-15 14:12:25

标签: android shared-libraries android-library mutual-exclusion

我正在开发一个库项目,该项目将集成到一些流行的Android应用程序中,可以在Google Play中看到。

假设用户可以安装两个或更多应用程序,并且每个应用程序都可以集成我的库。该库具有一些用于检测环境状态变化的特定代码。状态只是发送到我的服务器。问题是环境状态处理需要很多CPU能力,但需要很短的时间。处理循环由AlarmManager启动,使用“非唤醒”广播启动正确的IntentService。

我的目标是以这种方式实现库,只有一个集成到应用程序中的实例才能完成工作。我的意思是只有一个库模块应该充当“活动”。如果用户设备上安装了更多应用程序 - 那么它们不应重叠。

如何实现呢?我正在考虑某种权限验证和跨包检测,但无法想象如何实现它。

5 个答案:

答案 0 :(得分:2)

我会尝试与CSMA / CD碰撞检测技术相关的东西,这种技术在网络中使用(或者经常使用)。

您不希望提交特定实例以始终执行工作,因为您不知道是否会卸载该实例。因此,每次都要重新做出决定(因为无论在任何给定时间,它都无关紧要。)

它有点复杂,因为它不是一个需要解决的微不足道的问题,但我喜欢有人可能将这个解决方案推广给任何人使用的想法(开源你用这个做什么?)。

当初始广播到达时,发送您正在收听的自定义广播(标识为来自您的特定应用)。如果你在一秒钟之内没有收到任何其他相同的广播,那么请继续进行工作,因为你的图书馆必须没有其他实例愿意开展这项工作。

如果您从至少一个其他库中获取消息(跟踪您听到的所有消息),请等待一段随机时间。如果您在这段时间内收到来自另一个图书馆的消息说“我会这样做”,那么请立即发出一条消息,意思是“好的,你这样做”。如果你,那么发出一条消息说“我会这样做”,并等待你从一开始收到消息的每个其他库发送一个“好吧,你做到了“ 信息。然后做好工作。

如果您发送“我会做”的消息,但也从另一个库获得“我会做”消息,然后开始执行该过程。每个库等待随机时间发送“我会这样做”的事实意味着很少会发生这样的冲突,并且它们肯定不会经常连续多次发生。

我希望我已经很好地解释了这一点,你可以实现它。如果没有,请询​​问澄清,或者看看在网络世界中如何做到这一点。我想要描述的就是所谓的“碰撞检测”,例如:https://en.wikipedia.org/wiki/CSMA/CD

答案 1 :(得分:1)

  

我的目标是以这种方式实现库,只有一个集成到应用程序中的实例才能完成工作。

这将是相当复杂的,结果可能不可靠。

我会推荐Ian主题的变体。将问题的定义更改为“我希望工作只能每N分钟/小时/无论什么时候完成”。有一些后台作业的方法来检测上次完成工作的时间(外部存储上的文件,Web服务的请求,等等),如果太快,则跳过该工作。这样,与您的库安装了多少个应用程序,安装它们的顺序或卸载它们无关紧要。

答案 2 :(得分:0)

为什么不能使用设备的ANDROID_ID(或电话的某种唯一标识符),向服务器注册,如果该设备的另一个实例已在该设备上运行,则不执行任何操作。

您可以通过以下代码获取设备标识符

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

答案 3 :(得分:0)

不是ContentProvider应用分享数据的友好方式吗?您可以使用一行SQLite表来实现原子时间戳。将警报管理器方案替换为库初始化期间创建的线程,该线程每隔几秒轮询一次ContentProvider。 CP回复'是,请发送环境状态',这意味着它已经用当前数据/时间或'否,尚未'更新了表。提供商正在咨询表格和系统时钟,以决定何时说是。

答案 4 :(得分:0)

我做了一些额外的研究,并设法找到了令人满意的解决方案。 它来了:

必须以某种方式开发库,每个应用程序集成它 - 发布具有已知操作的广播接收器,例如。 com.mylib.ACTION_DETECT。

库必须有额外的服务,它发布一些AIDL接口,这有助于做出决定 - 如果库的当前实例可以被激活。 AIDL可以有一些有用的方法,例如getVersion(),isActive(),getUUID()。

决策模式是:如果当前实例具有更高版本号,那么另一个 - 它将变为活动状态。如果当前实例具有较低版本 - 它将自行停用,或者如果已停用则保持停用状态。如果当前实例与其他实例具有相同的版本,那么如果其他实例未激活,并且其他库的uuid较低(通过compareTo方法) - 它将自动激活。在其他条件 - 它将自行停用。这种交叉检查确保每个库都能自行做出决策 - 不存在歧义,因为每个库都将从其他应用程序中其他libary实例的已发布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开销。