赞
踩
目录
2.1.1.模仿Android原生服务接口,如WifiManager,规划自己的Manager
这篇文章是专门为马囧写的技术参考文档,主要涉及到Android系统的扩展开发,目的是让第三方应用可以很方便的调用一些高权限的系统级接口。有需要的小伙伴也可以参考。
那么,马囧是何许人也?当年马囧是我司软件部的经理,后来马走了牛上任,我就是在牛上任的时候进入的公司。马囧离开后自己开了家公司,干起了高新技术创新型企业的勾当,并且因为双方都认识,公司也就是隔壁或上下楼层的距离,所以两家公司关系比较好,我也就和马囧渐渐的熟络了起来。
创业公司嘛,你懂的。马囧身兼CEO、CTO、COO、CXXX.... 底层出身的马囧,不仅要做底层的技术,中间层和应用层的业务都要做,马囧长的很高,所以脸也很长,还要被客户时不时的按在地上摩擦摩擦,蓦然回首,发现马囧的脸被摩擦的更长了... 于是决定写下这篇文章,希望能给马囧带来一点帮助。
一、对外提供接口有哪些方式?
这个方式就很多了,至少有以下几种方式:
以上的方式各自有各自的优点,但是缺点也很明显,必需要双方亲密的合作无间(广播那个还好),所有的协议都需要双方沟通好,一旦一方出了一点差错,这个功能就完犊子了。这对于做事有目标有要求长的还帅气逼人马囧来说,这些方式就显得太Low了,马囧要的是那种用户只需要简单的调用一个API,一行代码即可实现复杂功能的完美主义者,要让用户用过了就会竖起大拇指:“诶,你这SDK好,这SDK妙,这SDK用的我呱呱叫”。
那要如何实现这么狂拽酷炫吊炸天的对外接口呢?我们先来参考参考Google原生的系统API
- private boolean setWifiEnable(boolean enable) {
- WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
- return wm.setWifiEnabled(enable);
- }
- Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
- vibrator.vibrate(1000);
上面是Google系统服务延续多年的标准使用方式,还有其它很多服务都是类似的使用方式。它们好用吗?当然好用(存在使用权限的情况下),至少在开发中调用起来还是肥肠不错的,但是这能让用户用起来得到"呱呱叫"的优良体验吗?显然不能,很明显的,这玩意还必须要有上下文(Context)才能发起调用,万一我想随时随地没有上下文的时候也调用呢?这就做不到了。
要实现这么牛逼的功能也是可以的,下面我们一起来开发一款对外SDK,可以让第三方应用调用需要系统级权限的接口,并且调用方式比Google的原生接口还要人性化,不需要Context,随时随地都可以调用。
在frameworks/base/core/java/com/目录下创建自己的目录层级 majiong/mdm/sdk,在这里创建MaJiongManager.java,为什么是在这个目录下呢?在其它的目录下行不行?其它目录下当然也行,但是这个目录有其特殊性,看Google framework的目录结构,这个目录下本身就是供厂商添加自己代码的地方,最重要的是,在这里添加代码编译系统的时候不用update-api,编译过系统源码的人都知道,update-api这是一个很繁琐的事,而且会增大系统SDK,作为ODM厂商,我们将自己需要打包进系统的代码放在这里,就可以避免update-api。
再有就是通常情况下,系统开发不会只添加一个服务和少量源文件,一般都会有大量的需求,所有添加的源文件会很多,我们不希望自己添加的东西过多的和系统原生的代码混杂在一起,创建独立的目录也可以更好的管理代码。
- package com.majiong.mdm.sdk;
-
- import android.os.majiong.IMaJiongService;
- import android.os.RemoteException;
- import android.os.ServiceManager;
- import android.util.Log;
-
- /*
- *
- * @author liaohongfei
- *
- * @Date 2019 2019-5-29 上午10:08:23
- *
- */
- public class MaJiongManager {
- private static final String TAG = "majiong";
- // 关闭
- public static final int CLOSE = 0;
- // 打开
- public static final int OPEN = 1;
- // 强制关闭用户不能打开
- public static final int FORCE_CLOSE = 2;
- // 强制打开用户不能关闭
- public static final int FORCE_OPEN = 3;
- // wifi状态
- public static final String WIFI_STATE = "wifi_state";
- // 蓝牙状态
- public static final String BLUETOOTH_STATE = "bluetooth_state";
-
- private static final String SERVICE_NAME = "majiong_service";
- private static MaJiongManager mInstance = null;
- private IMaJiongService mService;
-
- private MaJiongManager() {
- mService = IMaJiongService.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
- }
-
- public static synchronized MaJiongManager getInstance() {
- if (mInstance == null) {
- synchronized (MaJiongManager.class) {
- if (mInstance == null) {
- mInstance = new MaJiongManager();
- }
- }
- }
- return mInstance;
- }
-
- /**
- * 设置对应功能的管控方式
- * @param key
- * WIFI_STATE WIFI
- * BLUETOOTH_STATE 蓝牙
- * @param status
- * OPEN 打开状态
- * CLOSE 关闭状态
- * FORCE_OPEN 强制打开用户不能关闭
- * FORCE_CLOSE 强制关闭用户不能打开
- */
- public void setControlStatus(String key, int status) {
- if (mService == null) {
- Log.d(TAG, "MaJiongManager mService is null.");
- return;
- }
- try {
- mService.setControlStatus(key, status);
- } catch (RemoteException e) {
- Log.e(TAG, "MaJiongManager setControlStatus fail.");
- }
- }
-
- /**
- * 获取对应功能的管控状态
- * @param key
- * WIFI_STATE WIFI
- * BLUETOOTH_STATE 蓝牙
- * @return
- * OPEN 打开状态
- * CLOSE 关闭状态
- * FORCE_OPEN 强制打开用户不能关闭
- * FORCE_CLOSE 强制关闭用户不能打开
- */
- public int getControlStatus(String key) {
- if (mService == null) {
- Log.d(TAG, "MaJiongManager mService is null.");
- return OPEN;
- }
- try {
- return mService.getControlStatus(key);
- } catch (RemoteException e) {
- Log.e(TAG, "MaJiongManager getControlStatus fail.");
- return OPEN;
- }
- }
-
- }

我们先简单的规划两个多功能接口,一个设置的接口setControlStatus,和一个获取的接口getControlStatus,可以通过传入不同的参数设置/获取不同功能的开关状态,这里只有wifi 和蓝牙两个参数选择,开关选项也只有OPEN和CLOSE两个状态,当然这些都是可以扩展的,我们先把最简单的功能跑通,如果要扩展,后面也会讲到。这个类里定义了一些Constant常量类型,这样做的好处是,当把这个类打包成SDK后,开发这可以轻松知道SDK接口支持的参数和范围。
在frameworks/base/core/java/android/os/目录下新建目录majiong,在majiong目录下创建IMaJiongService.aidl文件
- package android.os.majiong;
-
- /** {@hide} */
- interface IMaJiongService
- {
- void setControlStatus(String key, int status);
- int getControlStatus(String key);
- }
为什么在os目录下新建文件夹以及文件?在2.1.1的目录下创建不行吗?答案当然是可以,还是那句话,为了方便维护和管理,Android系统的原生服务对应的AIDL文件绝大部分都是在android/os/目录下的,这里可以算的上是AIDL文件集中营,我们在这里新建自己的文件夹,存放自己的AIDL文件是科学且合理的。
在frameworks/base/services/core/java/com/android/server/目录下新建目录majiong,在majiong目录下创建MaJiongService.java,为什么在这里创建,理由和目的和上面两条如出一辙。
- package com.android.server.majiong;
-
- import com.majiong.mdm.sdk.MaJiongManager;
- import com.majiong.mdm.sdk.constant.MaJiongRestriction;
-
- import android.content.Context;
- import android.content.ContentValues;
- import android.content.ContentResolver;
- import android.content.Intent;
- import android.content.IntentFilter;
- import android.database.ContentObserver;
- import android.net.Uri;
- import android.os.Bundle;
- import android.os.Handler;
- import android.os.majiong.IMaJiongService;
- import android.provider.Settings;
- import android.util.Log;
-
- import android.net.wifi.WifiManager;
- import android.provider.Settings;
-
- public class MaJiongService extends IMaJiongService.Stub {
- private static final String TAG = "majiong";
- // True if systemReady() has been called.
- private boolean mSystemReady;
- private Context mContext;
- private ContentResolver mResolver = null;
- // 启用(默认)
- private static final int ENABLE = 0;
- // 禁用
- private static final int DISABLE = 1;
-
- public MaJiongService(Context context) {
- mContext = context;
- mResolver = mContext.getContentResolver();
- mResolver.registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.WIFI_ON), true,
- mWifiObserver);
- }
-
- private ContentObserver mWifiObserver = new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- // 这里可以监听wifi的状态,当wifi的管控状态为不可关闭/不可开启的时候,可以做相应的处理,阻止用户改变wifi状态
- }
- };
-
- public void setControlStatus(String key, int status) {
- Log.d(TAG, "setControlStatus key: " + key + ", status: " + status);
- setWifiEnable(status == MaJiongManager.OPEN ? true : false);
- }
-
- @Override
- public int getControlStatus(String key) {
- return getPersistedWifiOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
- }
-
- private boolean setWifiEnable(boolean enable) {
- WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
- return wm.setWifiEnabled(enable);
- }
-
- private boolean getPersistedWifiOn() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.WIFI_ON, 0) == 1;
- }
-
- public void systemRunning() {
- Log.d(TAG, "MaJiongService ready.");
- mSystemReady = true;
- }
-
- // 检查控制类属性 key 是否符合要求
- private boolean checkControlKey(String key) {
- if (null == key) {
- return false;
- }
- if (MaJiongManager.WIFI_STATE.equals(key)
- || MaJiongManager.BLUETOOTH_STATE.equals(key)) {
-
- return true;
- }
- return false;
- }
-
- // 检查控制类属性 value 是否符合要求
- private boolean checkControlValue(int status) {
- if (status < MaJiongManager.CLOSE || status > MaJiongManager.FORCE_OPEN) {
- return false;
- }
- return true;
- }
-
- }

MaJiongService继承了IMaJiongService.aidl的远程接口,实现其方法setControlStatus和getControlStatus,这也是Android AIDL的标准用法。为了方便测试,我们直接在setControlStatus方法中加上设置wifi开关状态的代码,在getControlStatus方法中添加读取wifi开关状态的代码。
仅仅将服务写好是不够的,系统中的每个原生服务都需要注册,相当于给系统留一个备案,方便需要使用的时候快速查找,否则即使编译通过,也是无法找到这个服务的。在frameworks/base/core/java/android/app/SystemServiceRegistry.java文件中添加如下注册代码:
- registerService("majiong_service", MaJiongManager.class,
- new StaticServiceFetcher<MaJiongManager>() {
- @Override
- public MaJiongManager createService() {
- return MaJiongManager.getInstance();
- }});
上面一步我们已经将服务注册到系统了,这仅仅是有了备案信息而已,并不会让服务启动运行,系统级服务都是随着设备开机自启动的,那么如何初始化服务并启动它运行起来呢?Google为这些系统服务准备了一个特定的地方 -- frameworks/base/services/java/com/android/server/SystemServer.java,所有的系统服务都在这里初始化。
- diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
- index ed5c65ffabe..11f362d4cf8 100644
- --- a/services/java/com/android/server/SystemServer.java
- +++ b/services/java/com/android/server/SystemServer.java
- import com.android.server.job.JobSchedulerService;
- import com.android.server.lights.LightsService;
- +import com.android.server.majiong.MaJiongService;
- import com.android.server.media.MediaResourceMonitorService;
- import com.android.server.media.MediaRouterService;
- import com.android.server.media.MediaUpdateService;
- @@ -766,6 +767,7 @@ public final class SystemServer {
- final Context context = mSystemContext;
- VibratorService vibrator = null;
- + MaJiongService mjs = null;
-
- IStorageManager storageManager = null;
- NetworkManagementService networkManagement = null;
- @@ -943,6 +945,16 @@ public final class SystemServer {
- }
- traceEnd();
-
- + traceBeginAndSlog("StartMaJiongService");
- + try {
- + Slog.i(TAG, "add MaJiong Service");
- + mjs = new MaJiongService(context);
- + ServiceManager.addService("majiong_service", mjs);
- + } catch (Throwable e) {
- + reportWtf("starting MaJiong Service", e);
- + }
- + traceEnd();
- +
- traceBeginAndSlog("StartDeviceSettingsService");
- try {
- Slog.i(TAG, "add DeviceSettings Service");
- @@ -1923,6 +1935,7 @@ public final class SystemServer {
- final MmsServiceBroker mmsServiceF = mmsService;
- final IpSecService ipSecServiceF = ipSecService;
- final WindowManagerService windowManagerF = wm;
- + final MaJiongService mjsF = mjs;
-
- // We now tell the activity manager it is okay to run third party
- // code. It will call back into us once it has gotten to the state
- @@ -2145,6 +2158,14 @@ public final class SystemServer {
- reportWtf("Notifying DeviceRestrictionService running", e);
- }
- traceEnd();
- +
- + traceBeginAndSlog("MaJiongServiceReady");
- + try {
- + if (mjsF != null) mjsF.systemRunning();
- + } catch (Throwable e) {
- + reportWtf("Notifying MaJiongService running", e);
- + }
- + traceEnd();
-
- traceBeginAndSlog("DeviceSettingsServiceReady");

这个文件添加的内容比较分散,就直接上patch了,不过也是清晰易懂的,就是系统在开机的过程中,会按顺序挨个把这些服务都初始化好,并通知相关组件已经开机,可以工作了。
我们所添加的所有java文件都不用操心编译规则,这些文件所在的路径都已经默认包含到编译规则里了,所以都会编译到,唯一要注意的是AIDL文件,它是一个比较特殊的文件,Android自己搞出来的玩意,我们要做的只是配置这个文件的编译规则,否则系统不编译AIDL,那么服务也就编译不过了。
很简单,只需要在frameworks/base/Android.bp(Android O以下的代码为Android.mk文件) 中添加如下patch中的增量代码即可:
- diff --git a/Android.bp b/Android.bp
- index 6097d4052b4..5c7de32dee6 100755
- --- a/Android.bp
- +++ b/Android.bp
- @@ -225,6 +225,7 @@ java_library {
- "core/java/android/se/omapi/ISecureElementSession.aidl",
- + "core/java/android/os/majiong/IMaJiongService.aidl",
- "core/java/android/os/IBatteryPropertiesListener.aidl",
- "core/java/android/os/IBatteryPropertiesRegistrar.aidl",
一个新的服务已经添加好了,如果这是在Android5.0之前的系统上,这就可以用起来了,但是后来Google对系统权限管理的越来越严格,引入了SELinux权限,所以到目前为止,以上代码编译是没问题的,运行也没问题,但是要让其它的进程调用它就有问题了,因为这个服务没有权限让其它进程调用。先看看我在system/sepolicy目录下修改的文件
这一大票都是神马玩意??不仅你看了烦,我看了也烦。简单来说就是将我们添加的服务公开化,让其它的进程能够引用它。否则这个服务初始化都不会成功,更别提让其它进程调用了。由于修改的文件众多,没办法全部贴出来,已经打好patch,有需要的猛戳这里下载。
系统服务已经添加好了,为了验证服务的可用性,我们要让第三方应用调用我们的服务,最好的方式是提供一个开发用的SDK,开发人员只需要在Android工程里引用这个SDK就可以了,可以说肥肠方便了。
打包SDK的方法也有多种:
以上的方式都是可行的,我就以Eclipse为例说一下打包过程
新建一个Android工程,名字无所谓,随便取,然后新建工程包名,这个包名一定要和我们系统源码中的MaJiongManager.java的包名一致,然后将系统中的MaJiongManager.java文件拷贝到这个包下,如下图所示
然后修改MaJiongManager.java的内容,改成如下:
- package com.majiong.mdm.sdk;
-
- /*
- *
- * @author liaohongfei
- *
- * @Date 2019 2019-5-29 上午10:08:23
- *
- */
- public class MaJiongManager {
- // 关闭
- public static final int CLOSE = 0;
- // 打开
- public static final int OPEN = 1;
- // 强制关闭用户不能打开
- public static final int FORCE_CLOSE = 2;
- // 强制打开用户不能关闭
- public static final int FORCE_OPEN = 3;
- // wifi状态
- public static final String WIFI_STATE = "wifi_state";
- // 蓝牙状态
- public static final String BLUETOOTH_STATE = "bluetooth_state";
-
- public static synchronized MaJiongManager getInstance() {
- throw new RuntimeException("API not supported!");
- }
-
- /**
- * 设置对应功能的管控方式
- * @param key
- * WIFI_STATE WIFI
- * BLUETOOTH_STATE 蓝牙
- * @param status
- * OPEN 打开状态
- * CLOSE 关闭状态
- * FORCE_OPEN 强制打开用户不能关闭
- * FORCE_CLOSE 强制关闭用户不能打开
- */
- public void setControlStatus(String key, int status) {
- throw new RuntimeException("API not supported!");
- }
-
- /**
- * 获取对应功能的管控状态
- * @param key
- * WIFI_STATE WIFI
- * BLUETOOTH_STATE 蓝牙
- * @return
- * OPEN 打开状态
- * CLOSE 关闭状态
- * FORCE_OPEN 强制打开用户不能关闭
- * FORCE_CLOSE 强制关闭用户不能打开
- */
- public int getControlStatus(String key) {
- throw new RuntimeException("API not supported!");
- }
-
- }

这样就看明白了吧?这个代码其实就是系统源码中MaJiongManager.java的副本,只保留常量类型供开发人员引用,方法全部直接抛出异常,因为它只是个副本,真正调用的是系统源码中的正文。这样做的好处是,这个SDK对于其它平台其它设备都是没有丝毫价值的,只在我们自己的系统和平台上才有价值,因为这是我们的专属服务,别人没法用,也看不到我们的源码是怎么写的。
然后就是制作SDK开发包咯,IDE都有现成的工具,一键生成
右键项目名称,选择Export,再弹出来的窗口再选择JAR file,然后按下图的方式选择
点击finish,就可在桌面上生成majiong-mdm-sdk.jar,然后这个JAR开发包就可以在开发中使用了。
让我们试验一下,看看SDK的使用效果如何,顺便系统添加的服务也需要验证是否没有问题,我们再编写一个测试APK,在工程中引用majiong-mdm-sdk.jar,并且调用它的相关接口
这个测试程序布局文件很简单,界面上就一个Button,给它加上点击事件,调用我们的系统服务,主要代码如下:
- package com.example.majiongtest;
-
- import com.majiong.mdm.sdk.MaJiongManager;
-
- import android.app.Activity;
- import android.os.Bundle;
- import android.util.Log;
- import android.view.View;
- import android.view.View.OnClickListener;
- import android.widget.Button;
-
- public class MainActivity extends Activity {
- private static final String TAG = "majiong";
- private Button btn;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- btn = (Button) findViewById(R.id.btn_set);
- btn.setOnClickListener(new OnClickListener() {
-
- @Override
- public void onClick(View view) {
- int wifiState = MaJiongManager.getInstance().getControlStatus(MaJiongManager.WIFI_STATE);
- Log.d(TAG, "wifiState = " + wifiState);
- if (wifiState == MaJiongManager.CLOSE) {
- MaJiongManager.getInstance().setControlStatus(MaJiongManager.WIFI_STATE, MaJiongManager.OPEN);
- } else {
- MaJiongManager.getInstance().setControlStatus(MaJiongManager.WIFI_STATE, MaJiongManager.CLOSE);
- }
- }
- });
- }
-
- }

界面就长这样
代码非常简单,就是点击那个丑丑的图片按钮的时候先获取WiFi的状态,如果wifi关闭了就打开,反之则关闭。
再看看我们的调用系统服务的方式,根本不需要Context上下文,一行代码一个功能,是不是肥肠不错?
”bia唧“一下点击按钮,通过log会发现代码最终调用到MaJiongService的getControlStatus和setControlStatus,说明我们的服务没问题,服务相关的SELinux权限也没问题,整个流程算是跑通了。但是很遗憾,在设置wifi状态的时候抛出了异常
神马?权限错误?我们已经是系统级服务了,还不能开关一个WiFi??对的,还就是不让你好好玩了,这个问题就又牵扯到Android的权限分类管理了
- /**
- * Android M 及之后的版本将系统权限分为四类:
- * 1. 普通权限:对用户数据安全不敏感的权限,比如获取网络状态、WIFI状态等权限,只需在AndroidManifest配置即可,在运行中不需要再通过用户去手动赋予权限。
- * 2. 危险权限:对用户数据安全很敏感的权限,比如获取通讯录、获取地理位置等权限,需要在AndroidManifest文件中配置相关权限,并且在运行中必须由用户动态的决定,是
- 否赋予该程序打开这项权限,否则程序将异常退出。
- * 3. 特殊权限:特别敏感的权限,主要有两个:SYSTEM_ALERT_WINDOW -- 设置系统级别的悬浮窗(比如系统开关机时的提示窗口,始终保持在最上层视图,用户无法关闭);WRITE_SETTINGS -- 修改系统设置。
- * 4. 其它权限: 一些很少用到的权限
- */
总之就是这属于敏感权限,System Service也无权操作,那什么样的进程才有权操作呢?我们看一下系统的Settings,这玩意就能操作,因为Settings的android:sharedUserId="android.uid.system",并且它被置于/system/app/ or /system/priv-app/ 目录下,一个应用进程要同时满足这两个条件,那么系统中的大多数敏感权限都可以规避。当然还有少量特殊操作也是不能做的,这里不做分析。
这样一来下一步该怎么做就很明显了,再编写一个android:sharedUserId="android.uid.system"、编译到system/app/ 的超级管理APP,当然这种程序都是无界面在后台运行的,用户不能感知它的存在,也是我们的系统专属管控应用,它所具有的权限非常的高。
我们已经有了一个MaJiongService,这是一个独立进程;现在要添加一个MaJiongMdmControl APP,这也是一个独立进程,这两个进程要进行交互,就不可避免的又要用到跨进程通信。MaJiongService的调用接口我们已经封装的很完美了,只需要调用MaJiongManager.getInstance().xxx就可以调用它的方法,实现跨进程通信,这可以说是肥肠便捷了。那么MaJiongService如何将消息传递给MaJiongMdmControl呢?其实Android系统中有一些没有公开的API,用起来也很方便,我们可以通过隐藏API很方便的实现这一点。
ContentProvider我想大家肯定都用过,这是Android 四大组件之一,专门用来跨进程通信的。ContentProvider有一个call方法,可以很方便的实现跨进程通信,不需要专门暴露接口等复杂的操作,还要配合一个系统隐藏接口,ContentResolver 的acquireProvider方法。
实现了以上两点MaJiongService和MaJiongMdmControl 之间就可以毫无障碍的通信了,我们通过这样的设计可以实现很多很多的功能,现在因为只是演示简单的代码功能,就用最简单的方式来实现。我们先来实现MaJiongMdmControl的代码编写。
我们的总控APP MaJiongMdmControl需要放在源码中编译进系统system/app/下,所以我们需要编写一个Android.mk
- LOCAL_PATH:= $(call my-dir)
-
- include $(CLEAR_VARS)
- LOCAL_MODULE_TAGS := optional
- LOCAL_SRC_FILES := $(call all-java-files-under, src)
- LOCAL_MODULE_PATH := $(TARGET_OUT_SYSTEM_APPS)
-
- LOCAL_PACKAGE_NAME := MaJiongMdmControl
- LOCAL_CERTIFICATE := platform
-
- LOCAL_DEX_PREOPT := false
- LOCAL_PROGUARD_ENABLED := disabled
- #LOCAL_PROGUARD_ENABLED := full obfuscation
- #LOCAL_PROGUARD_FLAG_FILES := proguard.flags
-
- LOCAL_PRIVATE_PLATFORM_APIS := true
-
- include $(BUILD_PACKAGE)
- include $(call all-makefiles-under, $(LOCAL_PATH))

然后再来编写APK的代码,APK代码可以借助IDE快速编写完成,还能排除掉不必要的错误,编写完后删除不必要的文件再放到系统中编辑即可
AndroidManifest.xml
- <?xml version="1.0" encoding="utf-8"?>
- <manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.majiong.mdm.control"
- android:sharedUserId="android.uid.system" >
-
- <application
- android:name="com.majiong.mdm.control.MaJiongMdmApplication"
- android:allowBackup="true"
- android:icon="@drawable/ic_launcher"
- android:label="@string/app_name" >
-
- <!-- Device Restriction -->
- <provider android:name="com.majiong.mdm.control.provider.MaJiongProvider"
- android:authorities="com.mdm.MaJiongProvider"
- android:multiprocess="false"
- android:exported="true" >
- </provider>
- </application>
- </manifest>

程序入口类MaJiongMdmApplication.java
- package com.majiong.mdm.control;
-
- import android.app.Application;
- import android.content.Context;
- import android.util.Log;
-
- /*
- *
- * @author liaohongfei
- *
- * @Date 2019 2019-6-28 下午7:21:17
- * 应用程序入口类
- */
- public class MaJiongMdmApplication extends Application {
- private static final String TAG = "majiong";
- // 全局Context
- private static Context mContext;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Log.d(TAG, "MaJiongMdmApplication onCreate.");
- mContext = getApplicationContext();
- }
-
- public static Context getContext() {
- return mContext;
- }
-
- }

进程间通信的ContentProvider
- package com.majiong.mdm.control.provider;
-
- import com.majiong.mdm.sdk.constant.MaJiongRestriction;
- import com.majiong.mdm.sdk.MaJiongManager;
-
- import android.bluetooth.BluetoothAdapter;
- import android.content.Context;
- import android.content.ContentProvider;
- import android.content.ContentResolver;
- import android.content.ContentValues;
- import android.content.Intent;
- import android.database.Cursor;
- import android.net.Uri;
- import android.net.wifi.WifiManager;
- import android.os.Bundle;
- import android.provider.Settings;
- import android.util.Log;
-
- /*
- *
- * @author liaohongfei
- *
- * @Date 2018 2018-10-12 下午4:59:54
- *
- */
- public class MaJiongProvider extends ContentProvider {
- private static final String TAG = "majiong";
- private ContentResolver mResolver;
- private Context mContext;
-
- @Override
- public boolean onCreate() {
- mContext = getContext();
- mResolver = mContext.getContentResolver();
- return true;
- }
-
- @Override
- public int delete(Uri uri, String selection, String[] selectionArgs) {
- return 0;
- }
-
- @Override
- public String getType(Uri uri) {
- return null;
- }
-
- @Override
- public Uri insert(Uri uri, ContentValues values) {
- return null;
- }
-
- @Override
- public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
- String sortOrder) {
- return null;
- }
-
- @Override
- public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
- return 0;
- }
-
- @Override
- public Bundle call(String method, String arg, Bundle extras) {
- switch (method) {
- case MaJiongRestriction.SET_CONTROL_STATUS:
- if (extras != null) {
- String key = extras.getString(MaJiongRestriction.CONTROL_STATUS_KEY);
- int status = extras.getInt(MaJiongRestriction.CONTROL_STATUS_VALUE);
- setControlStatus(key, status);
- }
- break;
- case MaJiongRestriction.GET_CONTROL_STATUS:
- if (extras != null) {
- String key = extras.getString(MaJiongRestriction.CONTROL_STATUS_KEY);
- extras = new Bundle();
- extras.putInt(MaJiongRestriction.CONTROL_STATUS_VALUE, getControlStatus(key));
- return extras;
- }
- break;
- }
- return null;
- }
-
- private void setControlStatus(String key, int status) {
- Log.d(TAG, "马囧身高一米九,脸长一米二");
-
- boolean enable = false;
- switch (key) {
- case MaJiongManager.WIFI_STATE:
- if (status == MaJiongManager.OPEN || status == MaJiongManager.FORCE_OPEN) {
- enable = true;
- }
- setWifiEnable(enable);
- break;
- case MaJiongManager.BLUETOOTH_STATE:
- if (status == MaJiongManager.OPEN || status == MaJiongManager.FORCE_OPEN) {
- enable = true;
- }
- setBluetoothEnable(enable);
- break;
- }
- }
-
- private int getControlStatus(String key) {
- Log.d(TAG, "马囧身高一米九,脸长一米二");
-
- int state = MaJiongManager.CLOSE;
- switch (key) {
- case MaJiongManager.WIFI_STATE:
- state = getPersistedWifiOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
- break;
- case MaJiongManager.BLUETOOTH_STATE:
- state = getPersistedBluetoothOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
- break;
- }
- return state;
- }
-
- private void setBluetoothEnable(boolean enable) {
- BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
- boolean blueToothState = bluetoothAdapter.isEnabled();
- if (enable) {
- if (!blueToothState)
- bluetoothAdapter.enable();
- } else {
- if (blueToothState)
- bluetoothAdapter.disable();
- }
- }
-
- private boolean getPersistedBluetoothOn() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.BLUETOOTH_ON, 0) == 1;
- }
-
- private boolean setWifiEnable(boolean enable) {
- WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
- return wm.setWifiEnabled(enable);
- }
-
- private boolean getPersistedWifiOn() {
- return Settings.Global.getInt(mContext.getContentResolver(),
- Settings.Global.WIFI_ON, 0) == 1;
- }
-
- }

完整的工程猛戳这里下载!!
还记得MaJiongService里面的方法吗?就是第三方APK引用SDK后调用的setControlStatus和getControlStatus方法,之前我们已经尝试过了在MaJiongService中直接操作WiFi权限上是通不过的,所以我们要把操作WiFi发代码移到总控APP的MaJiongProvider中来,如上代码所示。相应的,MaJiongService里应该调用MaJiongProvider里的方法,我们把MaJiongService里的方法稍微改一下:
- @Override
- public void setControlStatus(String key, int status) {
- Log.d(TAG, "setControlStatus key: " + key + ", status: " + status);
- if (mResolver.acquireProvider(MaJiongRestriction.MAJIONG_MDM_URI) != null) {
- Bundle extras = new Bundle();
- extras.putString(MaJiongRestriction.CONTROL_STATUS_KEY, key);
- extras.putInt(MaJiongRestriction.CONTROL_STATUS_VALUE, status);
- mResolver.call(MaJiongRestriction.MAJIONG_MDM_URI, MaJiongRestriction.SET_CONTROL_STATUS, null, extras);
- } else {
- Log.d(TAG, "Could Not Find Provider " + MaJiongRestriction.MAJIONG_MDM_URI);
- }
- }
-
- @Override
- public int getControlStatus(String key) {
- if (mResolver.acquireProvider(MaJiongRestriction.MAJIONG_MDM_URI) != null) {
- Bundle extras = new Bundle();
- extras.putString(MaJiongRestriction.CONTROL_STATUS_KEY, key);
- extras = mResolver.call(MaJiongRestriction.MAJIONG_MDM_URI, MaJiongRestriction.GET_CONTROL_STATUS, null, extras);
- if (extras != null)
- return extras.getInt(MaJiongRestriction.CONTROL_STATUS_VALUE, DISABLE);
- } else {
- Log.d(TAG, "Could Not Find Provider " + MaJiongRestriction.MAJIONG_MDM_URI);
- }
- return DISABLE;
- }

可能大家又有疑问了,在MaJiongProvider和MaJiongService 的代码中都看到了MaJiongRestriction这个类的引用,MaJiongRestriction是个什么瘪犊子玩意儿?请看代码:
- package com.majiong.mdm.sdk.constant;
-
- import android.net.Uri;
-
- /*
- *
- * @author liaohongfei
- *
- * @Date 2018 2018-10-12 下午4:59:54
- * Device Restriction Constant
- */
- public class MaJiongRestriction {
- public static final String AUTHORITY = "com.mdm.MaJiongProvider";
- public static final String CONTENT_TYPE =
- "vnd.android.cursor.dir/vnd.com.mdm.MaJiongProvider";
- public static final String CONTENT_ITEM_TYPE =
- "vnd.android.cursor.item/vnd.com.mdm.MaJiongProvider";
-
- public static final class Table {
- public static final String TABLE_NAME = "test_table";
-
- public static final String ID = "_id";
- public static final String KEY = "key";
- public static final String VALUE = "value";
-
- public static final int COLUMN_IDX_ID = 0;
- public static final int COLUMN_IDX_KEY = 1;
- public static final int COLUMN_IDX_VALUE = 2;
-
- public static final String DEFAULT_SORT_ORDER = "_id asc";
-
- public static final int ITEM = 1;
- public static final int ITEM_NAME = 2;
-
- public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
- }
-
- public static final Uri MAJIONG_MDM_URI = Uri.parse("content://com.mdm.MaJiongProvider");
- // Control
- public static final String SET_CONTROL_STATUS = "set_control_status";
- public static final String GET_CONTROL_STATUS = "get_control_status";
- public static final String CONTROL_STATUS_KEY = "control_key";
- public static final String CONTROL_STATUS_VALUE = "control_value";
-
- }

这个类是在frameworks/base/core/java/com/majiong/mdm/sdk/constant目录下创建的,和MaJiongManager也算是同级目录了,可以看到这个类中定义的都是一些常量类型的数据,为的是方便系统其它进程引用,不至于每个地方还要单独定义变量引起变量。那么为什么不直接定义在MaJiongManager 里面呢?MaJiongManager里面定义的常量类是为了公开给第三方开发这调用的,而这些常量是需要对外隐藏的,只有系统知道,只有系统能引用。
可以看到里面还定义了一张表的内部类,这个暂时不讲,后面的扩展里面会说到。目前我们只需要用到MAJIONG_MDM_URI 等几个常量数据。
到这里为止,系统服务添加了,总控APP MaJiongMdmControl写好了,切编译到/system/app/下了,双方的交互规则也定义好了,SDK开发包也提供了,测试程序也写好了,一切准备就绪,来测试一波。
再次打开MaJiongTest APP, ”bia唧“一下点击按钮,会发现WiFi打开了,再点击一下WiFi又关闭了。查看打印日志,可以看到输出了"马囧身高一米九,脸长一米二"的日志,只要”bia唧“一下按钮,不仅可以实现WiFi的开关操作,还可以知道马囧不仅高脸还长的事实,惊不惊喜?刺不刺激?这么体面的设计,谁用谁说好。
我们回顾一下调用流程,当”bia唧“一下按钮,会调用SDK的接口,这里实际上通过Binder进程间通信(AIDL)调用到了我们的专属服务MaJiongService,然后MaJiongService 再通过Binder进程间通信(ContentProvider)调用到了我们的总控APP MaJiongMdmControl,因为MaJiongMdmControl的android:sharedUserId="android.uid.system"并且存在system/app/目录下,所以它具有和系统设置一样高的权限,完全有能力操控WiFi的状态。
上面实际上已经把整个流程打通,并实现了想要的功能。那就会有童鞋要说了:你就实现了个开关WiFi的功能,至于搞这么一大堆复杂的东西吗?我用个反射一样的实现了。
我只能说这个仅仅是一个很简单的功能演示代码,没有添加其它的复杂功能,一个Android操作系统,涉及到需要管理的功能项有多少?一百?几百个都不止,这些功能如果都要做难道全部用反射?这不科学也不现实,况且很多功能你是射不了的(哈哈,原谅我)。
为什么要开发这些限制设备功能的API?因为有需求,有市场,有钱挣,所以会有人做。 据我所知,小米、华为等大型手机厂商,都做了这种SDK,专业一点的名词叫MDM(Mobile Device Manager)。用智能手机的不能仅仅有像你我这样的普通用户,各行各业都会越来越离不开智能设备,那么很多行业的只能设备都是需要管控的,或为了保证业务机密性或为了防止员工开小差,各行各业有各行各业的需求,普通大众的智能手机满足不了行业需求,这样行业定制机就应运而生,厂商会提供一系列管控SDK,用来控制设备的各项功能。目前来看MDM接口做的最全面应该是华为,功能几乎涵盖了整个操作系统的方方面面。
不管是华为还是小米,实现这种MDM功能管控的对外SDK,流程大致上都和我上述的代码流程一致,因为目前为止对Android操作系统来说这可能是最优方案了。
说了这么多,还没说到底怎么扩展。还记得MaJiongRestriction中的Table内部类吧?这个其实是我们预留的扩展的一部分。比如说WiFi,上面的代码我们只实现了开关功能,很多行业都需要四种状态的管理:
OPEN、CLOSE、FORCE_OPEN、FORCE_CLOSE。啥意思?就是不仅可以控制wifi的开关,有些场景下还要强制打开Wifi不能让用户关闭,或者取反。那么我们的Table就有用了,这就是个方便跨进程操作DB的引子,我们可以通过这些定义很方便的将管控的值存进数据库,然后再到framework中开关wifi的代码里添加我们的判断流程:
假如管理人员对设备下发了FORCE_OPEN的指令,那我们的数据库中对应的wifi state 状态就应该存储FORCE_OPEN;设备在用户(员工)手中,他可以自由使用手机,这个时候的工作场景因为需要是不能关闭WiFi的,但用户进入设置里主动去关闭wifi,关闭WiFi的动作必然会framework中的相关流程,我们可以在这里做拦截处理,通过我们添加的service和总控app,可以轻松的读取到我们保存的wifi state,此时的状态是FORCE_OPEN,就直接return 用户关闭WiFi的非法操作了。
Android操作系统的功能众多,每个行业的需求也不一致,如果要兼顾到每一个功能和需求,那么我们添加的MDM代码会深入到系统源码的每一个角落。当然万变不离其宗,按照我们定义的这套框架、流程去做,绝大部分的问题都能迎刃而解。
【怒草 https://blog.csdn.net/visionliao?spm=1011.2124.3001.5113 未经允许严禁转载,请尊重作者劳动成果。】
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。