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的应用列表,就像这样:

Android之Lollipop DevicePolicyManager学习(下)_bundle

 

那么有没有办法只让这个消息传到其他的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  
    lic 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的系统应用进行具体功能限制,而对第三方应用而言,只能在账户层面上做一些限制而已。

 

 

参考代码与本项目源码

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