4

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

假设用户可以安装两个或多个应用程序,并且每个应用程序都可以集成我的库。该库有一些特定的代码用于检测环境状态的变化。状态只是发送到我的服务器。问题是环境状态处理会占用大量 CPU 资源,但时间很短。处理周期由 AlarmManager 启动,使用启动适当 IntentService 的“非唤醒”广播。

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

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

4

5 回答 5

2

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

于 2012-06-19T16:29:59.900 回答
1

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

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

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

于 2012-06-18T15:33:08.217 回答
0

为什么您不能使用设备的 ANDROID_ID(或某种电话的唯一标识符),将其注册到服务器,并且如果该库的另一个实例已经在该设备上运行 - 什么也不做。

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

Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
于 2012-06-15T18:20:02.773 回答
0

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

于 2012-06-22T03:33:51.590 回答
0

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

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

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

做出决定的模式是:如果当前实例具有更高的版本号,则另一个 - 它将变为活动状态。如果当前实例的版本较低 - 它会自行停用,如果已经停用,则保持停用状态。如果当前实例与其他实例具有相同的版本,那么如果其他实例未处于活动状态,并且其他库的 uuid 较低(通过 compareTo 方法) - 它会自行激活。在其他情况下 - 它会自行停用。这种交叉检查确保每个库都将自行做出决定 - 不会出现模棱两可的情况,因为每个库将从其他应用程序中其他库实例的已发布 AIDL 支持的服务中获取所需的数据。

下一步是准备一个 IntentService,它在每次删除或添加新包时启动,或者第一次启动带有库的应用程序。IntentService 查询所有包以查找实现 com.mylib.ACTION_DETECT 的广播接收器。然后它遍历检测到的包(拒绝它自己的包),并绑定到其他实例的 AIDL 支持的服务(AIDL 服务的类名将始终相同,只有应用程序包会不同)。完成绑定后——我们有明确的情况——如果应用模式的结果是“积极的”(我们的实例有更好的版本或更高的 uuid,或者已经处于活动状态),那么这意味着其他实例认为自己是“消极的”,并自行停用. 当然,该模式必须应用于每个绑定的 AIDL 服务。

我为我糟糕的英语道歉。

工作冲突避免解决方案的代码: 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 开销。

于 2012-06-23T13:03:27.490 回答