目录
故事背景
二、开发步骤
Step1.添加一个专属系统级服务
2.1.1.模仿Android原生服务接口,如WifiManager,规划自己的Manager
2.1.2.为我们的Manager生成AIDL
2.1.3.编写系统级服务
2.1.4.注册服务
2.1.5.初始化服务
2.1.6.添加编译规则
2.1.7.为新服务添加SELinux权限
Step2.打包SDK,供第三方程序调用
2.2.1.打包SDK
2.2.2.使用SDK
Step3.再添加一个专属系统级总控APP
2.3.1.进程与进程间的交互
2.3.2.编写总控APP代码
三、扩展扩展再扩展
这篇文章是专门为马囧写的技术参考文档,主要涉及到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() {
@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
程序入口类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 未经允许严禁转载,请尊重作者劳动成果。】