天天看點

Lollipop DevicePolicyManager學習(下)

3.      如何在主賬戶與被管理者賬戶之間做資料通信。

a)        什麼是userID

剛才提到,Lollipop用來區分主賬戶與被管理賬戶的其實是一個int型數值userID。

從UserHandler.class可以看到,這個userID是通過對uid作整除得到的:

public static final int PER_USER_RANGE =100000;

/**
     *Returns the user id for a given uid.
     *@hide
     */
   public static final int getUserId(int uid) {
       if (MU_ENABLED) {
           return uid / PER_USER_RANGE;
        }else {
           return 0;
        }
}
           

是以100000以内的uid對應的userID都是0,而超過這個數值的再取其整除結果。注意,這個隻是Google為了辨識主賬戶與被管理賬戶所做的設計,并不是Unix底層帶上來的參數。

而這個userID的作用剛才也提到了。在service程序對應的方法裡會進行參數校驗,一般來說,隻有系統應用才能調用一些涉及到其他profile的方法。

b)        兩個賬戶之前通信的先決條件

由于Profile之間資料通信的互相隔離,導緻任何一個Profile中的消息發送隻能被自己Profile中的元件所捕獲。這樣一來,雖然從根本上解決了兩個Profile之間因為資料交流所可能産生的隐私暴露的問題,但是也為我們的資料共享帶來了不便。

當然,Google也考慮了這方面的問題,通過一個授權處理方法addCrossProfileIntentFilter(),指定一個用于處理對應消息的Intentfilter,既可以讓被管理者賬戶的消息可以透傳到主賬戶,也可以在被管理者賬戶中接收到主賬戶的消息。

其中的參數FLAG_MANAGED_CAN_ACCESS_PARENT對應前者, FLAG_PARENT_CAN_ACCESS_MANAGED 對應後者。

c)        驗證可行的通信方式

Android常見的元件之間通信的方式無外乎Intent,通過Intent我們可以啟動Activity,Service或者是進行Broadcast等。

但是在兩個Profile之間進行元件的啟動,我隻成功嘗試了startActivity一種……

先說startService。Android5.0之後,Google對于startService限制更加嚴格,已經不允許以隐式Intent的方式啟動一個service,不管它是不是本程序的。雖然我在建立Intent對象的同時既指定了service class,也指定了對應的action,但是通過這個action建立的intentfilter仍然無法像Activity那樣被其他Profile對應的Service元件捕獲。

而Broadcast也有同樣的問題,無論是靜态注冊的還是動态注冊,都無法接收到其他Profile發出的廣播資訊。

這個實在非常奇怪,如果有人找到了解決的辦法務必給我留言,多謝。

至于說通過startActivity的方式來透傳消息,有人可能認為這會造成設計上的不美觀,因為跳轉到其他Profile相關應用都會首先展現一個Activity。這個其實可以解決,在Manifest中對這個跳轉用的activity做一些調整:

<activity
           android:name=".ui.PackageEnabledActivity"
            <strong>android:theme="@android:style/Theme.NoDisplay</strong>">
            <intent-filter>
                            …
            </intent-filter>
        </activity>
           

就可以了,所顯示的Activity完全被隐藏。之後通過這個Activity在啟動此應用所在的Profile的其他元件,就沒有任何的問題。

當兩邊的通信方式确立了之後,可能還存在一個有趣的問題,那就是如何隻讓某些Intent透傳到其他Profile而不被本Profile的同名元件所捕獲。

說起來有點繞,舉個簡單的例子就明白了。我們現在知道,當android系統中已經建立被管理者賬戶時,一些應用既可以存在于主賬戶側,又可以在被管理賬戶中有一個同名的拷貝。那麼問題來了,這些應用發給自身某些元件的消息,比如說啟動某個Activity的Intent,如果被允許透傳的話,兩邊Profile的同名應用都會接收到這個Intent,而且會啟動可以處理該Intent的應用清單,就像這樣:

Lollipop DevicePolicyManager學習(下)

那麼有沒有辦法隻讓這個消息傳到其他的Profile中,而本Profile的元件不做處理?

其實很好解決,不需要而且也不可能通過Intent的标志位來處理,因為這是完全相同的兩個鏡像應用。解決這個問題的辦法是禁用目前Profile中的這個元件就可以了:

public static void disableCurrentProfileComponent(Context context, Class component, PackageManagerpm) {
        final ComponentName activity = newComponentName(context, component);
        pm.setComponentEnabledSetting(activity,
                PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                PackageManager.DONT_KILL_APP);
}
           

禁用了目前Profile的這個元件,那麼自然消息隻能被對面Profile的同名元件來處理。

PS:當然,還有一個更簡單的方法,就是利用PackageManager.SKIP_CURRENT_PROFILE标志位來禁止在本Profile内的使用,譬如:

pm.addCrossProfileIntentFilter(callEmergency,managedProfileUserId, parentUserId,
               PackageManager.SKIP_CURRENT_PROFILE);
           

d)        賬戶之間的大量資料傳輸

解決了兩個Profile之間消息傳輸的方式之後,最後來看如何攜帶大量資料。

這個問題其實不難解決,因為即使Profile之間資料區互相獨立,但是Intent本身是可以通過Bundle來攜帶鍵值對的。隻要Intent能夠傳過去,自然也能在對應的Activity元件中解析出Bundle資料來。

但是一旦要透傳某些檔案類的資料,比如說圖檔或音樂,或者說Profile雙方需要共同維護一個資料庫,比如一個聯系人庫。這個時候,單靠Bundle就很難完成工作。

是以,Profile之間的資料互動不能僅限于鍵值對的方式,以往的檔案類型和資料庫類型的共享仍然要走通才可以。

File類型的資料共享

Google的幫助文檔中提到了用于共享資料檔案的方法,這是通過FileProvider庫提供的方法來完成的操作。具體的思路就是:

1)  将待傳輸的檔案ContentUri通過FileProvider.getUriForFile()取出來。

2)  把ContentUri與Type通過setDataAndType()加載到Intent中。

3)  一定要在Intent中加上這個Flag——Intent.FLAG_GRANT_READ_URI_PERMISSION,這個Flag決定了Receiver是否具有這個Uri的臨時通路權限。這點非常重要。

4)  startActivity成功之後,通過getFileDescriptor()方法得到待傳輸檔案的檔案描述符,之後解析出這個檔案即可。

File類型檔案傳輸的難點并不是如何從Uri中解析檔案,而在于Intent傳輸過程。我查閱的大量資料中都建議在檔案的ContentUri擷取之後,通過grantUriPermission()賦予其對應的讀寫權限,但是這個方法是不成的,隻有在Intent中加上對應Flag才行。

資料庫類型共享

雖然在Google的幫助文檔中沒有說明不同的Profile可以共享ContentProvider,但是通過檔案類型的資料共享可以看出,從原理上說ContentProvider也應是可以共享的,因為FileProvider正式ContentP的一個子類。

關于ContentProvider的共享我走了點彎路,先把解決問題的要點說出來:

使用ContentProvider時我們都會維護一個static常量CONTENT_URI,這個常量一般是由幾部分拼成的:

//Content Url
public static final Uri CONTENT_URI =Uri.parse("content://" + AUTHORITY + "/item");
           

通常,需要使用資料庫的其他元件直接解析這個Uri就能得到db檔案的确切位址,使用對應的方法就能讀寫資料庫檔案。

但是在跨Profile操作時不能這麼做。因為如果直接解析這個常量,得到的隻是db檔案的相對存儲位址而已,比如說同樣将資料庫儲存在應用内部,主Profile可能是/data/data/companyName/databases/*.db,但在被管理Profile裡,則變成了/data/user/11/companyName/databases/*.db。

是以即使我們知道db檔案的ContentUri,也必須通過Intent攜帶上述臨時通路權限(Intent.FLAG_GRANT_READ_URI_PERMISSION)發到其他Profile的元件中去。在對方的環境裡解析出正确的db位址來。

至于ContentProvider其他的共享細節與FileProvider無異。隻是query資料的時候,記得使用我們Intent攜帶的Uri而不要用static常量直接解析。

到此為止,AP與MP之間的通信可以由我們自己完全控制,哪些消息可以通過,哪些消息會被禁止都由我們自己來界定。接下來說說被管理者賬戶中的那些應用都可以做哪些操作。

4.      如何對MP賬戶中的應用進行限制

安裝于MP賬戶中的應用,可以從兩個方面進行限制。

一個是賬戶使用者層面的限制。DevicePlicyManager類提供了一組用來限制被管理者賬戶某些功能的方法addUserRestriction()/clearUserRestriction(),通過給定的key來限制對應賬戶的某些功能。

值得注意的是,這原本不是什麼新功能,為了改善JB多使用者功能的體驗Google在4.3就添加了這個Restrict Profile功能。但是當時的情形是,平闆的使用者在主賬戶中對訪客賬戶做某些限制,當平闆的使用者切換到一個訪客賬戶時,這些功能就不能再被使用了。而現在的情況是,被管理者賬戶與主賬戶同處于一個Launch裡,可以對被管理者賬戶進行限制但不應該影響到主賬戶的同樣功能。

這個功能比較坑,以限制撥打電話功能為例。如果我不希望訪客賬戶或者被管理者賬戶的應用撥打電話,那麼勢必要在MP賬戶下通過以下方法禁止撥電話功能:

<p>myDeviceManaged.addUserRestriction(myDeviceName,UserManager. DISALLOW_OUTGOING_CALLS)</p>
           

注意到Android檢查這個disallow标志是在CallActivity的processOutgoingCallIntent方法中進行的:

privatevoid processOutgoingCallIntent(Intent intent) {
….
        if(userManager.hasUserRestriction(UserManager.DISALLOW_OUTGOING_CALLS)
                &&!TelephonyUtil.shouldProcessAsEmergency(this, handle)) {
            // Only emergency calls are allowedfor users with the DISALLOW_OUTGOING_CALLS
            // restriction.
                    …
           }
}
           

喚起這個Activity的是Intent.ACTION_CALL,而Google在CrossProfileIntentFiltersHelper中自作主張的為ACTION_CALL添加了SKIP_CURRENT_PROFILE的條件:

publicstatic void setFilters(PackageManager pm, int parentUserId, intmanagedProfileUserId) {
…
IntentFilter callVoicemail = new IntentFilter();
       callVoicemail.addAction(Intent.ACTION_DIAL);
       callVoicemail.addAction(Intent.ACTION_CALL);
       callVoicemail.addAction(Intent.ACTION_VIEW);
        callVoicemail.addCategory(Intent.CATEGORY_DEFAULT);
       callVoicemail.addCategory(Intent.CATEGORY_BROWSABLE);
       callVoicemail.addDataScheme("voicemail");
       pm.addCrossProfileIntentFilter(callVoicemail, managedProfileUserId,parentUserId,
               PackageManager.SKIP_CURRENT_PROFILE);
…
IntentFilter smsMms = new IntentFilter();
       smsMms.addAction(Intent.ACTION_VIEW);
       smsMms.addAction(Intent.ACTION_SENDTO);
       smsMms.addCategory(Intent.CATEGORY_DEFAULT);
        smsMms.addCategory(Intent.CATEGORY_BROWSABLE);
       smsMms.addDataScheme("sms");
       smsMms.addDataScheme("smsto");
       smsMms.addDataScheme("mms");
       smsMms.addDataScheme("mmsto");
       pm.addCrossProfileIntentFilter(smsMms, managedProfileUserId,parentUserId,
               PackageManager.SKIP_CURRENT_PROFILE);
…
}
           

導緻這個Activity實際上調用的是AP賬戶中的那個,而我們所做的限制在AP中并不生效。

最終的結論就是,對賬戶所做的限制,也隻有在本賬戶内執行的有效,實際調用主賬戶完成的操作并不能實作。

另一個則是應用層面的限制。DevicePolicyManager類同樣提供了一組用來限制被管理者賬戶中具體應用的某些功能的方法setApplicationRestrictions()/getApplicationRestrictions(),該方法是通過指定具體的應用包名,以及一組用于限制應用功能的Bundle串來限制具體的應用功能。

可以看到UserManagerService的實作方法:

public voidsetApplicationRestrictions(String packageName, Bundle restrictions,
            int userId) {
        if(UserHandle.getCallingUserId() != userId
                || !UserHandle.isSameApp(Binder.getCallingUid(),getUidForPackage(packageName))) {
           checkManageUsersPermission("Only system can set restrictions forother users/apps");
        }
        synchronized(mPackagesLock) {
            if (restrictions == null|| restrictions.isEmpty()) {
               cleanAppRestrictionsForPackage(packageName, userId);
            } else {
                // Write therestrictions to XML
               writeApplicationRestrictionsLocked(packageName, restrictions, userId);
            }
        }
 
        if(isPackageInstalled(packageName, userId)) {
            // Notify package ofchanges via an intent - only sent to explicitly registered receivers.
            Intent changeIntent =new Intent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED);
           changeIntent.setPackage(packageName);
           changeIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
           mContext.sendBroadcastAsUser(changeIntent, new UserHandle(userId));
        }
}
           

Google将限制的功能以及對應包名注冊到一個xml檔案中,然後重新啟動以限制功能的方式重新喚起這個元件,這個元件在啟動之後會載入用以限制功能的xml,實作限制具體功能的目的。

這個功能出發點本身是非常好的,因為作為被管理者賬戶中的某個單獨應用,很可能存在某些特定的功能需求,比如說不允許使用某些應用特定功能(例如内購),或者是必須打開預設的通路頁面等。這些功能的實作都有賴于具體的限制方法。但實際上,這個功能又比較難以完成。原因有兩個。

首先,用于限制應用具體功能的Bundle字串是如何擷取的。根據Google官方的參考demoBasicManagedProfile可以了解到,Google的系統應用Chrome是如何進行定制的,但是反過來作為非系統層面的開發人員,你該如何擷取Google系統應用具體支援的定制功能串呢?在沒有官方文檔的前提下,我想隻能通過反編譯這些應用,通過源碼才能找到具體的功能字串名,以及該如何修改這些功能的方法。

再者,Google系統應用之是以能夠通過這類Bundle鍵值對修改具體的功能,前提是它已經預留好了接口給開發者,讓我們能夠通過setApplicationRestrictions()方法修改具體的應用。如果是沒有預留這些接口的第三方應用,則根本不可能完成這類功能。

是以如果希望對MP賬戶中應用進行限制,目前看起來行之有效的隻有對Google的系統應用進行具體功能限制,而對第三方應用而言,隻能在賬戶層面上做一些限制而已。

最後歡迎所有希望了解DevicePolicyManager的人給我留言,我們可以一塊讨論并學習這部分功能。

參考代碼與本項目源碼

1.      參考代碼

Google官方demo:

BasicManagedProfile

https://github.com/googlesamples/android-BasicManagedProfile.git

AppRestrictionEnforcer

https://github.com/googlesamples/android-AppRestrictionEnforcer.git

AppRestrictionSchema

https://github.com/googlesamples/android-AppRestrictionSchema.git

2.      本人測試代碼

DevicePolicyTest

https://github.com/guiyu/DevicePolicyTest.git