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

那么有没有办法只让这个消息传到其他的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

/********************************************转载请注明来源***********************************/

 

时间: 2024-08-09 09:06:17

Lollipop DevicePolicyManager学习(下)的相关文章

Lollipop DevicePolicyManager学习下届

Android 5.0(lollipop)发布之后,看特性文档增加了不少有趣的东西. 最近花了一些时间,研究了下其中Managed Profile的概念,简称MP,记录下来作为一些经验,有需要的同学请参考. 简介 Managed Profile,简称被管理者账户.这个概念并不是什么新东西,因为早在4.2版本中,Android就引入了多用户机制来解决平板使用上的问题.而如今5.0新加入的这个被管理者账户功能,可以理解成为是为了解决用户本人对于应用进行分类的需求问题而做的细化吧. 存在于被管理者账户

android学习---下拉刷新组建

Google官方的下拉刷新组建 activity代码实现: /** * The SwipeRefreshLayout should be used whenever the user * can refresh the contents of a view via a vertical swipe gesture. * */public class MainActivity extends Activity implements SwipeRefreshLayout.OnRefreshListe

Python学习—面向对象学习下

面向对象下 1.构造方法与析构方法 class People(object): # 构造方法:__init__(),在实例化对象时自动执行的方法 def __init__(self, name, age): self.name = name self.age = age print("创建对象成功.......") # 析构函数:__del__(), 当你删除对象时,自动调用的方法 # 删除对象:del 对象名 def __del__(self): print("删除对象成功

高手的C++学习忠告,虚心学习下~~[转载]

1.把C++当成一门新的语言学习(和C没啥关系!真的.): 2.看<Thinking In C++>,不要看<C++变成死相>: 3.看<The C++ Programming Language>和<Inside The C++ Object Model>,不要因为他们很难而我们自己是初学者所以就不看: 4.不要被VC.BCB.BC.MC.TC等词汇所迷惑——他们都是集成开发环境,而我们要学的是一门语言: 5.不要放过任何一个看上去很简单的小编程问题——他们

学习下关于ViewStub实例的用法及带Drawable的TextView的妙用

在项目中,我们可能有多种数据来源比如: 里面有ListView也有当获得数据为空的时候显示的空信息.根据点击的项目还是差事不同,显示的空消息也不同.a.没有收藏的项目,b目前没有收藏的差事. 其实实现方法很多.也都可以实现.不过用viewStub在这里最恰当不过了. 先看此Activity的布局吧.布局文件: < FrameLayout android:id="@+id/layoutFrm" android:layout_width="match_parent"

黑马高强度学习下的一些学习方法

通过多个班级的教学,以及与同学们的交流,发现很多同学学不好,不是学不动,而是不会学习,从而导致一些同学学习起来吃力,甚至痛苦,所以基于个人想法,对同学们的学习作了一些个人的总结,希望有益于大家. 以下学习方法不针对所有人群使用,只是给长期在编程高压力中学习的同学们的友情帮助,如有问题或者更好的意见,请联系我(鄙人贾乐飞),进一步完善内容. 课上: 问题1: 一些同学喜欢上课勤记笔记,这是一种好的学习态度,但不是一种适合目前这种该密度学习的状况下. 说明: 由于知识的密度比较高,所以会出现一不小心

PMP概略学习下--主体内容

4  知识主体 4.1 主要知识简介 PMP所有的知识围绕五大过程组和十大知识领域展开.五大过程组包括启动.规划.执行.监控.结尾.启动的内容主要是定义项目或阶段.获得授权以及正式开始:规划的内容主要是明确项目范围.优化目标和制定方案:执行的内容是完成计划工作以及满足项目规范:监控的内容是跟踪.审查.调整进展与绩效,识别变更,控制变更等:结尾的内容是正式结束项目或阶段.其中,规划.执行和监控应该是一个满足PDCA环的过程,是不断迭代进行的. 十大知识领域,包括整合.范围.进度.成本.质量.资源.

目标检测——深度学习下的小目标检测(检测难的原因和Tricks)

小目标难检测原因 主要原因 (1)小目标在原图中尺寸比较小,通用目标检测模型中,一般的基础骨干神经网络(VGG系列和Resnet系列)都有几次下采样处理,导致小目标在特征图的尺寸基本上只有个位数的像素大小,导致设计的目标检测分类器对小目标的分类效果差. (2)小目标在原图中尺寸比较小,通用目标检测模型中,一般的基础骨干神经网络(VGG系列和Resnet系列)都有几次下采样处理,如果分类和回归操作在经过几层下采样处理的 特征层进行,小目标特征的感受野映射回原图将可能大于小目标在原图的尺寸,造成检测

php 温故而知新 好久不用 又得继续学习下

1.php注释:/* */.//.#等三种方式2.echo:向浏览器输出字符串,echo其实是一个函数:返回值:无3.print:向浏览器输出字符串,它也是一个函数:返回值:整型.            echo与print的功能几乎相同,但echo的运行速度比print要快,因为echo无返回值,而print有返回值 4.printf功能:向浏览器输出字符串:返回值:字符串的长度. 5.php变量的创建:格式 :$+标识符.变量的数据类型有整型.浮点型.字符串.布尔型.数组.对象.php是弱类