4

Google Play에서 볼 수있는 인기있는 Android 애플리케이션에 통합 될 라이브러리 프로젝트를 개발 중입니다.어떻게 안드로이드 라이브러리 프로젝트 코드가 그것을 통합 한 설치된 응용 프로그램 중 하나에서만 실행되도록 보장할까요?

사용자가 두 개 이상의 응용 프로그램을 설치하고 각각 하나씩 내 라이브러리를 통합 할 수 있다고 가정 해 보겠습니다. 도서관에는 환경 상태의 변화를 감지하는 데 사용되는 특정 코드가 있습니다. 상태는 단순히 내 서버로 전송됩니다. 문제는 환경 상태 처리가 많은 CPU 전력을 소모하지만 짧은 시간 내에 발생한다는 것입니다. 처리주기는 적절한 IntentService를 시작하는 "깨우기 (non wake up)"브로드 캐스트를 사용하여 AlarmManager에 의해 시작됩니다.

내 목표는 응용 프로그램에 통합 된 하나의 인스턴스 만 작업을 수행 할 수 있도록 라이브러리를 구현하는 것입니다. 하나의 라이브러리 모듈 만 "활성"으로 작동해야합니다. 사용자의 장치에 더 많은 응용 프로그램이 설치되어있는 경우 겹치지 않아야합니다.

어떻게 구현합니까? 어떤 종류의 허가 유효성 검사와 교차 패키지 감지에 대해 생각하고 있었지만이를 구현하는 방법을 상상할 수 없었습니다.

+0

연구를 위해 시간을 보내고 작업 코드 샘플로 답변을 게시했습니다. 유용하다고 생각되면 의견을 남겨주세요. –

답변

0

나는 몇 가지 추가 조사를했으며 만족스러운 해결책을 찾을 수있었습니다. 여기에 오는 내용은 다음과 같습니다.

라이브러리는이를 통합하는 각 응용 프로그램이 알려진 조치로 방송 수신기를 게시하는 방식으로 개발되어야합니다. com.mylib.ACTION_DETECT.

라이브러리에는 라이브러리의 현재 인스턴스를 활성화 할 수있는 경우 결정을 내리는 데 도움이되는 AIDL 인터페이스를 게시하는 추가 서비스가 있어야합니다. AIDL은 getVersion(), isActive(), getUUID()와 같은 유용한 메소드를 가질 수 있습니다.

결정을 내리는 패턴은 현재 인스턴스에 더 높은 버전 번호가 있으면 다른 하나는 활성화됩니다. 현재 인스턴스의 버전이 낮 으면 - 자체를 비활성화하거나 이미 비활성화 된 경우 비활성화 상태를 유지합니다. 현재 인스턴스의 버전이 다른 인스턴스와 같으면 다른 인스턴스가 활성화되어 있지 않고 다른 라이브러리의 uuid가 (compareTo 메소드를 통해) 낮 으면 - 자체를 활성화합니다. 다른 조건에서는 - 자체를 비활성화합니다. 이 교차 검사는 각 라이브러리가 자체적으로 결정을 내릴 수 있도록합니다. 모호한 경우는 없습니다. 왜냐하면 각 라이브러리는 게시 된 AIDL에서 지원하는 다른 응용 프로그램의 libary 인스턴스에 대한 서비스에서 필요한 데이터를 가져올 것이기 때문입니다.

다음 단계는 새 패키지가 제거되거나 추가 될 때마다 시작되거나 라이브러리가있는 응용 프로그램이 처음으로 시작될 때 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 오버 헤드를 줄이기 위해 서식을 지원하는 사용자 지정 래퍼를 사용합니다.

0

기기의 ANDROID_ID (또는 휴대 전화에 대한 일종의 고유 식별자)를 사용하여 서버에 등록하고 라이브러리의 다른 인스턴스가 이미 해당 기기에서 실행되고있는 경우, 아무 것도하지 않는 이유는 무엇입니까?

당신은 응용 프로그램에 통합 된 하나의 인스턴스 만 일을 할 수있는, 다음 코드 조각

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

서버 측에서 이러한 것을 제어 할 수 없습니다. 애플리케이션을 제거하는 경우 어떻게해야합니까? 지금까지 내가 아는 한 - 응용 프로그램이 제거되는 동안 코드 블록을 실행하는 것은 불가능합니다. 서버에 응용 프로그램이 이미 제거되었다는 것을 어떻게 알릴 수 있습니까? –

+0

@MichaelP 왜 그렇게 생각하지 않습니까? 사용자는 안드로이드 라이브러리 프로젝트로 작업하기 위해 자신의 일부 응용 프로그램을 설치해야하며이 응용 프로그램은 원하는 것, 제안을 취해야합니다. – vrs

1

내 목표는 라이브러리와 같은 방법을 구현하는 것입니다하여 장치 식별자를 얻을 수 있습니다.

이것은 다소 복잡 할 것이며 그 결과는 신뢰할 수 없습니다.

나는 이안의 테마에 변형을 추천합니다. 문제의 정의를 "N 분 /시/모든 작업 만 수행하도록합니다"로 변경하십시오. 백그라운드 작업을 통해 작업이 마지막으로 완료되었을 때 (외부 저장소에 파일, 웹 서비스 요청 등)를 감지 한 다음 작업이 너무 빠르면 건너 뜁니다. 그렇게하면 라이브러리에 설치된 앱의 수, 설치 순서 또는 설치 제거시기가 중요하지 않습니다.

+0

외부 파일 잠금은 훌륭하게 들리지만 외부 저장소가 항상 존재하고 탑재되어 있다고 가정 할 수는 없습니다. 실제 문제는 AlarmManager에 등록하는 문제입니다. 나는 USER_PRESENT 브로드 캐스트 리시버를 사용하여 AlarmManager에 PendingIntent를 등록합니다. 그것은 내가 생각할 수 없기 때문입니다 - 그 사용자는 도서관과 함께 응용 프로그램을 시작합니다. USER_PRESENCE 브로드 캐스트는 라이브러리를 통합하는 모든 응용 프로그램에서 수신되기 때문에 경쟁 조건이있을 수 있습니다. 경쟁 조건을 없애고 미래에 하나의 앱에서만 라이브러리를 사용할 수 있기를 바랍니다. –

2

나는 네트워크에서 사용되는 CSMA/CD 충돌 감지 기술과 관련하여 뭔가를 시도해 보았습니다.

작업 인스턴스를 제거할지 여부를 모르기 때문에 특정 인스턴스를 항상 커밋하고 싶지는 않습니다. 대신 매번 새로운 결정을 내리십시오. (어떤 주어진 시간에 어떤 일을하는지는 중요하지 않으므로)

문제가 해결할 수있는 간단한 문제는 아니기 때문에 조금 복잡해집니다.하지만 누군가가 사용할 수 있도록이 솔루션을 일반화하는 아이디어를 좋아합니다 (오픈 소스에서이 작업을 어떻게 수행합니까?).

초기 브로드 캐스트가 도착하면 사용자 정의 브로드 캐스트 (사용자의 특정 앱에서 온 것으로 확인 됨)가 이고이기 때문에들을 수 있습니다. 같은 방송을 몇 초 이내에받지 못하면 다른 일을 할 의사가없는 도서관이 없어야합니다.

적어도 하나의 다른 라이브러리에서 메시지를 받으면 (듣고있는 모든 것을 추적하십시오) 임의의 시간 동안 기다리십시오. 그 시간 내에 "내가 할게"이라고 말하는 다른 도서관의 메시지를 받으면 즉시 "좋아, 할 수있다"는 의미의 메시지를 보냅니다. 이 아니면 "나만 할거야"라는 메시지를 보내고 처음부터 메시지를받은 다른 모든 라이브러리가 "알았어요, 할 수 있어요"라는 메시지를 기다립니다. 그런 다음 일하십시오.

"I 'll do it"메시지를 보내고 다른 라이브러리의 "I 'll do it"메시지가 나타나면 프로세스를 다시 시작하십시오. 각 도서관이 "I 'll do it"이라는 무작위 시간을 기다려야한다는 사실은 이런 종류의 충돌이 거의 없으며 여러 번 연속해서 발생하지 않아야한다는 것을 의미합니다.

나는 이것을 충분히 설명하여 당신이 그렇게 할 수 있기를 바랍니다. 그렇지 않은 경우 명확한 설명을 요청하거나 이것이 네트워킹 세계에서 어떻게 이루어 졌는지 확인하십시오. 내가 설명하려고하는 것은 "충돌 감지"와 같습니다 (예 : 여기에서 참조). https://en.wikipedia.org/wiki/CSMA/CD

0

ContentProvider은 앱이 데이터를 공유하는 친숙한 방법이 아닙니까? 한 행의 SQLite 테이블을 사용하여 원자 시간 소인을 구현할 수 있습니다. 라이브러리 초기화 중에 생성 된 스레드로 알람 관리자 구성표를 바꾸어 ContentProvider을 몇 초마다 폴링합니다. CP는 '예, 환경 상태를 보내주십시오'라고 응답합니다. 즉, 현재 데이터/시간 또는 '아니요, 아직'로 테이블을 이미 업데이트했음을 의미합니다. 공급자는 테이블 및 시스템 시계를 참조하여 예를 말할시기를 결정합니다.

+0

ContentProvider는 고유 한 콘텐츠 URI를 가져야하므로 라이브러리의 다른 인스턴스이지만 다른 앱에서는 동일한 콘텐츠 공급자를 가질 수 없습니다. –

관련 문제