当前位置:   article > 正文

Android原生系统开发如何优雅的提供系统级的API供第三方程序调用?_android 开发sdk怎么根据key开放接口使用

android 开发sdk怎么根据key开放接口使用

目录

故事背景

二、开发步骤

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代码

三、扩展扩展再扩展


【怒草 https://blog.csdn.net/visionliao?spm=1011.2124.3001.5113 未经允许严禁转载,请尊重作者劳动成果。】

故事背景

这篇文章是专门为马囧写的技术参考文档,主要涉及到Android系统的扩展开发,目的是让第三方应用可以很方便的调用一些高权限的系统级接口。有需要的小伙伴也可以参考。

那么,马囧是何许人也?当年马囧是我司软件部的经理,后来马走了牛上任,我就是在牛上任的时候进入的公司。马囧离开后自己开了家公司,干起了高新技术创新型企业的勾当,并且因为双方都认识,公司也就是隔壁或上下楼层的距离,所以两家公司关系比较好,我也就和马囧渐渐的熟络了起来。

创业公司嘛,你懂的。马囧身兼CEO、CTO、COO、CXXX.... 底层出身的马囧,不仅要做底层的技术,中间层和应用层的业务都要做,马囧长的很高,所以脸也很长,还要被客户时不时的按在地上摩擦摩擦,蓦然回首,发现马囧的脸被摩擦的更长了... 于是决定写下这篇文章,希望能给马囧带来一点帮助。


一、对外提供接口有哪些方式?

这个方式就很多了,至少有以下几种方式:

  • 系统添加好系统服务接口,和第三方应用开发这对接AIDL,需要第三方应用开发人员自行编写AIDL文件和接口打包到应用中
  • 简单的功能直接通过广播跨进程传递消息,系统收到消息后做相应的处理
  • 通过暴露ContentProivder接口给第三方开发者,让其通过ContentProvider跨进程通信

以上的方式各自有各自的优点,但是缺点也很明显,必需要双方亲密的合作无间(广播那个还好),所有的协议都需要双方沟通好,一旦一方出了一点差错,这个功能就完犊子了。这对于做事有目标有要求长的还帅气逼人马囧来说,这些方式就显得太Low了,马囧要的是那种用户只需要简单的调用一个API,一行代码即可实现复杂功能的完美主义者,要让用户用过了就会竖起大拇指:“诶,你这SDK好,这SDK妙,这SDK用的我呱呱叫”。

那要如何实现这么狂拽酷炫吊炸天的对外接口呢?我们先来参考参考Google原生的系统API

  1. private boolean setWifiEnable(boolean enable) {
  2. WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
  3. return wm.setWifiEnabled(enable);
  4. }
  1. Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE);
  2. vibrator.vibrate(1000);

上面是Google系统服务延续多年的标准使用方式,还有其它很多服务都是类似的使用方式。它们好用吗?当然好用(存在使用权限的情况下),至少在开发中调用起来还是肥肠不错的,但是这能让用户用起来得到"呱呱叫"的优良体验吗?显然不能,很明显的,这玩意还必须要有上下文(Context)才能发起调用,万一我想随时随地没有上下文的时候也调用呢?这就做不到了。

要实现这么牛逼的功能也是可以的,下面我们一起来开发一款对外SDK,可以让第三方应用调用需要系统级权限的接口,并且调用方式比Google的原生接口还要人性化,不需要Context,随时随地都可以调用。

 

二、开发步骤

  1. 添加一个属于自己的专属系统级服务
  2. 打包SDK,供第三方程序调用
  3. 再添加一个属于自己的专属系统级总控APP

Step1.添加一个专属系统级服务

2.1.1.模仿Android原生服务接口,如WifiManager,规划自己的Manager

在frameworks/base/core/java/com/目录下创建自己的目录层级 majiong/mdm/sdk,在这里创建MaJiongManager.java,为什么是在这个目录下呢?在其它的目录下行不行?其它目录下当然也行,但是这个目录有其特殊性,看Google framework的目录结构,这个目录下本身就是供厂商添加自己代码的地方,最重要的是,在这里添加代码编译系统的时候不用update-api,编译过系统源码的人都知道,update-api这是一个很繁琐的事,而且会增大系统SDK,作为ODM厂商,我们将自己需要打包进系统的代码放在这里,就可以避免update-api。

再有就是通常情况下,系统开发不会只添加一个服务和少量源文件,一般都会有大量的需求,所有添加的源文件会很多,我们不希望自己添加的东西过多的和系统原生的代码混杂在一起,创建独立的目录也可以更好的管理代码。

  1. package com.majiong.mdm.sdk;
  2. import android.os.majiong.IMaJiongService;
  3. import android.os.RemoteException;
  4. import android.os.ServiceManager;
  5. import android.util.Log;
  6. /*
  7. *
  8. * @author   liaohongfei
  9. * @Date  2019 2019-5-29  上午10:08:23
  10. *
  11. */
  12. public class MaJiongManager {
  13. private static final String TAG = "majiong";
  14. // 关闭
  15. public static final int CLOSE = 0;
  16. // 打开
  17. public static final int OPEN = 1;
  18. // 强制关闭用户不能打开
  19. public static final int FORCE_CLOSE = 2;
  20. // 强制打开用户不能关闭
  21. public static final int FORCE_OPEN = 3;
  22. // wifi状态
  23. public static final String WIFI_STATE = "wifi_state";
  24. // 蓝牙状态
  25. public static final String BLUETOOTH_STATE = "bluetooth_state";
  26. private static final String SERVICE_NAME = "majiong_service";
  27. private static MaJiongManager mInstance = null;
  28. private IMaJiongService mService;
  29. private MaJiongManager() {
  30. mService = IMaJiongService.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
  31. }
  32. public static synchronized MaJiongManager getInstance() {
  33. if (mInstance == null) {
  34. synchronized (MaJiongManager.class) {
  35. if (mInstance == null) {
  36. mInstance = new MaJiongManager();
  37. }
  38. }
  39. }
  40. return mInstance;
  41. }
  42. /**
  43. * 设置对应功能的管控方式
  44. * @param key
  45. * WIFI_STATE WIFI
  46. * BLUETOOTH_STATE 蓝牙
  47. * @param status
  48. * OPEN 打开状态
  49. * CLOSE 关闭状态
  50. * FORCE_OPEN 强制打开用户不能关闭
  51. * FORCE_CLOSE 强制关闭用户不能打开
  52. */
  53. public void setControlStatus(String key, int status) {
  54. if (mService == null) {
  55. Log.d(TAG, "MaJiongManager mService is null.");
  56. return;
  57. }
  58. try {
  59. mService.setControlStatus(key, status);
  60. } catch (RemoteException e) {
  61. Log.e(TAG, "MaJiongManager setControlStatus fail.");
  62. }
  63. }
  64. /**
  65. * 获取对应功能的管控状态
  66. * @param key
  67. * WIFI_STATE WIFI
  68. * BLUETOOTH_STATE 蓝牙
  69. * @return
  70. * OPEN 打开状态
  71. * CLOSE 关闭状态
  72. * FORCE_OPEN 强制打开用户不能关闭
  73. * FORCE_CLOSE 强制关闭用户不能打开
  74. */
  75. public int getControlStatus(String key) {
  76. if (mService == null) {
  77. Log.d(TAG, "MaJiongManager mService is null.");
  78. return OPEN;
  79. }
  80. try {
  81. return mService.getControlStatus(key);
  82. } catch (RemoteException e) {
  83. Log.e(TAG, "MaJiongManager getControlStatus fail.");
  84. return OPEN;
  85. }
  86. }
  87. }

我们先简单的规划两个多功能接口,一个设置的接口setControlStatus,和一个获取的接口getControlStatus,可以通过传入不同的参数设置/获取不同功能的开关状态,这里只有wifi 和蓝牙两个参数选择,开关选项也只有OPEN和CLOSE两个状态,当然这些都是可以扩展的,我们先把最简单的功能跑通,如果要扩展,后面也会讲到。这个类里定义了一些Constant常量类型,这样做的好处是,当把这个类打包成SDK后,开发这可以轻松知道SDK接口支持的参数和范围。

2.1.2.为我们的Manager生成AIDL

在frameworks/base/core/java/android/os/目录下新建目录majiong,在majiong目录下创建IMaJiongService.aidl文件

  1. package android.os.majiong;
  2. /** {@hide} */
  3. interface IMaJiongService
  4. {
  5. void setControlStatus(String key, int status);
  6. int getControlStatus(String key);
  7. }

为什么在os目录下新建文件夹以及文件?在2.1.1的目录下创建不行吗?答案当然是可以,还是那句话,为了方便维护和管理,Android系统的原生服务对应的AIDL文件绝大部分都是在android/os/目录下的,这里可以算的上是AIDL文件集中营,我们在这里新建自己的文件夹,存放自己的AIDL文件是科学且合理的。

2.1.3.编写系统级服务

在frameworks/base/services/core/java/com/android/server/目录下新建目录majiong,在majiong目录下创建MaJiongService.java,为什么在这里创建,理由和目的和上面两条如出一辙。

  1. package com.android.server.majiong;
  2. import com.majiong.mdm.sdk.MaJiongManager;
  3. import com.majiong.mdm.sdk.constant.MaJiongRestriction;
  4. import android.content.Context;
  5. import android.content.ContentValues;
  6. import android.content.ContentResolver;
  7. import android.content.Intent;
  8. import android.content.IntentFilter;
  9. import android.database.ContentObserver;
  10. import android.net.Uri;
  11. import android.os.Bundle;
  12. import android.os.Handler;
  13. import android.os.majiong.IMaJiongService;
  14. import android.provider.Settings;
  15. import android.util.Log;
  16. import android.net.wifi.WifiManager;
  17. import android.provider.Settings;
  18. public class MaJiongService extends IMaJiongService.Stub {
  19. private static final String TAG = "majiong";
  20. // True if systemReady() has been called.
  21. private boolean mSystemReady;
  22. private Context mContext;
  23. private ContentResolver mResolver = null;
  24. // 启用(默认)
  25. private static final int ENABLE = 0;
  26. // 禁用
  27. private static final int DISABLE = 1;
  28. public MaJiongService(Context context) {
  29. mContext = context;
  30. mResolver = mContext.getContentResolver();
  31. mResolver.registerContentObserver(
  32. Settings.Global.getUriFor(Settings.Global.WIFI_ON), true,
  33. mWifiObserver);
  34. }
  35. private ContentObserver mWifiObserver = new ContentObserver(new Handler()) {
  36. @Override
  37. public void onChange(boolean selfChange) {
  38. // 这里可以监听wifi的状态,当wifi的管控状态为不可关闭/不可开启的时候,可以做相应的处理,阻止用户改变wifi状态
  39. }
  40. };
  41. public void setControlStatus(String key, int status) {
  42. Log.d(TAG, "setControlStatus key: " + key + ", status: " + status);
  43. setWifiEnable(status == MaJiongManager.OPEN ? true : false);
  44. }
  45. @Override
  46. public int getControlStatus(String key) {
  47. return getPersistedWifiOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
  48. }
  49. private boolean setWifiEnable(boolean enable) {
  50. WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
  51. return wm.setWifiEnabled(enable);
  52. }
  53. private boolean getPersistedWifiOn() {
  54. return Settings.Global.getInt(mContext.getContentResolver(),
  55. Settings.Global.WIFI_ON, 0) == 1;
  56. }
  57. public void systemRunning() {
  58. Log.d(TAG, "MaJiongService ready.");
  59. mSystemReady = true;
  60. }
  61. // 检查控制类属性 key 是否符合要求
  62. private boolean checkControlKey(String key) {
  63. if (null == key) {
  64. return false;
  65. }
  66. if (MaJiongManager.WIFI_STATE.equals(key)
  67. || MaJiongManager.BLUETOOTH_STATE.equals(key)) {
  68. return true;
  69. }
  70. return false;
  71. }
  72. // 检查控制类属性 value 是否符合要求
  73. private boolean checkControlValue(int status) {
  74. if (status < MaJiongManager.CLOSE || status > MaJiongManager.FORCE_OPEN) {
  75. return false;
  76. }
  77. return true;
  78. }
  79. }

MaJiongService继承了IMaJiongService.aidl的远程接口,实现其方法setControlStatus和getControlStatus,这也是Android AIDL的标准用法。为了方便测试,我们直接在setControlStatus方法中加上设置wifi开关状态的代码,在getControlStatus方法中添加读取wifi开关状态的代码。

2.1.4.注册服务

仅仅将服务写好是不够的,系统中的每个原生服务都需要注册,相当于给系统留一个备案,方便需要使用的时候快速查找,否则即使编译通过,也是无法找到这个服务的。在frameworks/base/core/java/android/app/SystemServiceRegistry.java文件中添加如下注册代码:

  1. registerService("majiong_service", MaJiongManager.class,
  2. new StaticServiceFetcher<MaJiongManager>() {
  3. @Override
  4. public MaJiongManager createService() {
  5. return MaJiongManager.getInstance();
  6. }});

2.1.5.初始化服务

上面一步我们已经将服务注册到系统了,这仅仅是有了备案信息而已,并不会让服务启动运行,系统级服务都是随着设备开机自启动的,那么如何初始化服务并启动它运行起来呢?Google为这些系统服务准备了一个特定的地方 -- frameworks/base/services/java/com/android/server/SystemServer.java,所有的系统服务都在这里初始化。

  1. diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
  2. index ed5c65ffabe..11f362d4cf8 100644
  3. --- a/services/java/com/android/server/SystemServer.java
  4. +++ b/services/java/com/android/server/SystemServer.java
  5. import com.android.server.job.JobSchedulerService;
  6. import com.android.server.lights.LightsService;
  7. +import com.android.server.majiong.MaJiongService;
  8. import com.android.server.media.MediaResourceMonitorService;
  9. import com.android.server.media.MediaRouterService;
  10. import com.android.server.media.MediaUpdateService;
  11. @@ -766,6 +767,7 @@ public final class SystemServer {
  12. final Context context = mSystemContext;
  13. VibratorService vibrator = null;
  14. + MaJiongService mjs = null;
  15. IStorageManager storageManager = null;
  16. NetworkManagementService networkManagement = null;
  17. @@ -943,6 +945,16 @@ public final class SystemServer {
  18. }
  19. traceEnd();
  20. + traceBeginAndSlog("StartMaJiongService");
  21. + try {
  22. + Slog.i(TAG, "add MaJiong Service");
  23. + mjs = new MaJiongService(context);
  24. + ServiceManager.addService("majiong_service", mjs);
  25. + } catch (Throwable e) {
  26. + reportWtf("starting MaJiong Service", e);
  27. + }
  28. + traceEnd();
  29. +
  30. traceBeginAndSlog("StartDeviceSettingsService");
  31. try {
  32. Slog.i(TAG, "add DeviceSettings Service");
  33. @@ -1923,6 +1935,7 @@ public final class SystemServer {
  34. final MmsServiceBroker mmsServiceF = mmsService;
  35. final IpSecService ipSecServiceF = ipSecService;
  36. final WindowManagerService windowManagerF = wm;
  37. + final MaJiongService mjsF = mjs;
  38. // We now tell the activity manager it is okay to run third party
  39. // code. It will call back into us once it has gotten to the state
  40. @@ -2145,6 +2158,14 @@ public final class SystemServer {
  41. reportWtf("Notifying DeviceRestrictionService running", e);
  42. }
  43. traceEnd();
  44. +
  45. + traceBeginAndSlog("MaJiongServiceReady");
  46. + try {
  47. + if (mjsF != null) mjsF.systemRunning();
  48. + } catch (Throwable e) {
  49. + reportWtf("Notifying MaJiongService running", e);
  50. + }
  51. + traceEnd();
  52. traceBeginAndSlog("DeviceSettingsServiceReady");

这个文件添加的内容比较分散,就直接上patch了,不过也是清晰易懂的,就是系统在开机的过程中,会按顺序挨个把这些服务都初始化好,并通知相关组件已经开机,可以工作了。

2.1.6.添加编译规则

我们所添加的所有java文件都不用操心编译规则,这些文件所在的路径都已经默认包含到编译规则里了,所以都会编译到,唯一要注意的是AIDL文件,它是一个比较特殊的文件,Android自己搞出来的玩意,我们要做的只是配置这个文件的编译规则,否则系统不编译AIDL,那么服务也就编译不过了。

很简单,只需要在frameworks/base/Android.bp(Android O以下的代码为Android.mk文件) 中添加如下patch中的增量代码即可:

  1. diff --git a/Android.bp b/Android.bp
  2. index 6097d4052b4..5c7de32dee6 100755
  3. --- a/Android.bp
  4. +++ b/Android.bp
  5. @@ -225,6 +225,7 @@ java_library {
  6. "core/java/android/se/omapi/ISecureElementSession.aidl",
  7. + "core/java/android/os/majiong/IMaJiongService.aidl",
  8. "core/java/android/os/IBatteryPropertiesListener.aidl",
  9. "core/java/android/os/IBatteryPropertiesRegistrar.aidl",

2.1.7.为新服务添加SELinux权限

一个新的服务已经添加好了,如果这是在Android5.0之前的系统上,这就可以用起来了,但是后来Google对系统权限管理的越来越严格,引入了SELinux权限,所以到目前为止,以上代码编译是没问题的,运行也没问题,但是要让其它的进程调用它就有问题了,因为这个服务没有权限让其它进程调用。先看看我在system/sepolicy目录下修改的文件

这一大票都是神马玩意??不仅你看了烦,我看了也烦。简单来说就是将我们添加的服务公开化,让其它的进程能够引用它。否则这个服务初始化都不会成功,更别提让其它进程调用了。由于修改的文件众多,没办法全部贴出来,已经打好patch,有需要的猛戳这里下载

Step2.打包SDK,供第三方程序调用

2.2.1.打包SDK

系统服务已经添加好了,为了验证服务的可用性,我们要让第三方应用调用我们的服务,最好的方式是提供一个开发用的SDK,开发人员只需要在Android工程里引用这个SDK就可以了,可以说肥肠方便了。

打包SDK的方法也有多种:

  • Eclipse生成SDK
  • AS生成SDK
  • Android源码中编写Android.mk文件编译出jar包

以上的方式都是可行的,我就以Eclipse为例说一下打包过程

新建一个Android工程,名字无所谓,随便取,然后新建工程包名,这个包名一定要和我们系统源码中的MaJiongManager.java的包名一致,然后将系统中的MaJiongManager.java文件拷贝到这个包下,如下图所示

然后修改MaJiongManager.java的内容,改成如下:

  1. package com.majiong.mdm.sdk;
  2. /*
  3. *
  4. * @author   liaohongfei
  5. * @Date  2019 2019-5-29  上午10:08:23
  6. *
  7. */
  8. public class MaJiongManager {
  9. // 关闭
  10. public static final int CLOSE = 0;
  11. // 打开
  12. public static final int OPEN = 1;
  13. // 强制关闭用户不能打开
  14. public static final int FORCE_CLOSE = 2;
  15. // 强制打开用户不能关闭
  16. public static final int FORCE_OPEN = 3;
  17. // wifi状态
  18. public static final String WIFI_STATE = "wifi_state";
  19. // 蓝牙状态
  20. public static final String BLUETOOTH_STATE = "bluetooth_state";
  21. public static synchronized MaJiongManager getInstance() {
  22. throw new RuntimeException("API not supported!");
  23. }
  24. /**
  25. * 设置对应功能的管控方式
  26. * @param key
  27. * WIFI_STATE WIFI
  28. * BLUETOOTH_STATE 蓝牙
  29. * @param status
  30. * OPEN 打开状态
  31. * CLOSE 关闭状态
  32. * FORCE_OPEN 强制打开用户不能关闭
  33. * FORCE_CLOSE 强制关闭用户不能打开
  34. */
  35. public void setControlStatus(String key, int status) {
  36. throw new RuntimeException("API not supported!");
  37. }
  38. /**
  39. * 获取对应功能的管控状态
  40. * @param key
  41. * WIFI_STATE WIFI
  42. * BLUETOOTH_STATE 蓝牙
  43. * @return
  44. * OPEN 打开状态
  45. * CLOSE 关闭状态
  46. * FORCE_OPEN 强制打开用户不能关闭
  47. * FORCE_CLOSE 强制关闭用户不能打开
  48. */
  49. public int getControlStatus(String key) {
  50. throw new RuntimeException("API not supported!");
  51. }
  52. }

这样就看明白了吧?这个代码其实就是系统源码中MaJiongManager.java的副本,只保留常量类型供开发人员引用,方法全部直接抛出异常,因为它只是个副本,真正调用的是系统源码中的正文。这样做的好处是,这个SDK对于其它平台其它设备都是没有丝毫价值的,只在我们自己的系统和平台上才有价值,因为这是我们的专属服务,别人没法用,也看不到我们的源码是怎么写的。

然后就是制作SDK开发包咯,IDE都有现成的工具,一键生成

右键项目名称,选择Export,再弹出来的窗口再选择JAR file,然后按下图的方式选择

点击finish,就可在桌面上生成majiong-mdm-sdk.jar,然后这个JAR开发包就可以在开发中使用了。

2.2.2.使用SDK

让我们试验一下,看看SDK的使用效果如何,顺便系统添加的服务也需要验证是否没有问题,我们再编写一个测试APK,在工程中引用majiong-mdm-sdk.jar,并且调用它的相关接口

这个测试程序布局文件很简单,界面上就一个Button,给它加上点击事件,调用我们的系统服务,主要代码如下:

  1. package com.example.majiongtest;
  2. import com.majiong.mdm.sdk.MaJiongManager;
  3. import android.app.Activity;
  4. import android.os.Bundle;
  5. import android.util.Log;
  6. import android.view.View;
  7. import android.view.View.OnClickListener;
  8. import android.widget.Button;
  9. public class MainActivity extends Activity {
  10. private static final String TAG = "majiong";
  11. private Button btn;
  12. @Override
  13. protected void onCreate(Bundle savedInstanceState) {
  14. super.onCreate(savedInstanceState);
  15. setContentView(R.layout.activity_main);
  16. btn = (Button) findViewById(R.id.btn_set);
  17. btn.setOnClickListener(new OnClickListener() {
  18. @Override
  19. public void onClick(View view) {
  20. int wifiState = MaJiongManager.getInstance().getControlStatus(MaJiongManager.WIFI_STATE);
  21. Log.d(TAG, "wifiState = " + wifiState);
  22. if (wifiState == MaJiongManager.CLOSE) {
  23. MaJiongManager.getInstance().setControlStatus(MaJiongManager.WIFI_STATE, MaJiongManager.OPEN);
  24. } else {
  25. MaJiongManager.getInstance().setControlStatus(MaJiongManager.WIFI_STATE, MaJiongManager.CLOSE);
  26. }
  27. }
  28. });
  29. }
  30. }

界面就长这样

代码非常简单,就是点击那个丑丑的图片按钮的时候先获取WiFi的状态,如果wifi关闭了就打开,反之则关闭。

再看看我们的调用系统服务的方式,根本不需要Context上下文,一行代码一个功能,是不是肥肠不错?

”bia唧“一下点击按钮,通过log会发现代码最终调用到MaJiongService的getControlStatus和setControlStatus,说明我们的服务没问题,服务相关的SELinux权限也没问题,整个流程算是跑通了。但是很遗憾,在设置wifi状态的时候抛出了异常

神马?权限错误?我们已经是系统级服务了,还不能开关一个WiFi??对的,还就是不让你好好玩了,这个问题就又牵扯到Android的权限分类管理了

  1. /**
  2. * Android M 及之后的版本将系统权限分为四类:
  3. * 1. 普通权限:对用户数据安全不敏感的权限,比如获取网络状态、WIFI状态等权限,只需在AndroidManifest配置即可,在运行中不需要再通过用户去手动赋予权限。
  4. * 2. 危险权限:对用户数据安全很敏感的权限,比如获取通讯录、获取地理位置等权限,需要在AndroidManifest文件中配置相关权限,并且在运行中必须由用户动态的决定,是
  5. 否赋予该程序打开这项权限,否则程序将异常退出。
  6. * 3. 特殊权限:特别敏感的权限,主要有两个:SYSTEM_ALERT_WINDOW -- 设置系统级别的悬浮窗(比如系统开关机时的提示窗口,始终保持在最上层视图,用户无法关闭);WRITE_SETTINGS -- 修改系统设置。
  7. * 4. 其它权限: 一些很少用到的权限
  8. */

总之就是这属于敏感权限,System Service也无权操作,那什么样的进程才有权操作呢?我们看一下系统的Settings,这玩意就能操作,因为Settings的android:sharedUserId="android.uid.system",并且它被置于/system/app/ or /system/priv-app/ 目录下,一个应用进程要同时满足这两个条件,那么系统中的大多数敏感权限都可以规避。当然还有少量特殊操作也是不能做的,这里不做分析。

这样一来下一步该怎么做就很明显了,再编写一个android:sharedUserId="android.uid.system"、编译到system/app/ 的超级管理APP,当然这种程序都是无界面在后台运行的,用户不能感知它的存在,也是我们的系统专属管控应用,它所具有的权限非常的高。

Step3.再添加一个专属系统级总控APP

2.3.1.进程与进程间的交互

我们已经有了一个MaJiongService,这是一个独立进程;现在要添加一个MaJiongMdmControl APP,这也是一个独立进程,这两个进程要进行交互,就不可避免的又要用到跨进程通信。MaJiongService的调用接口我们已经封装的很完美了,只需要调用MaJiongManager.getInstance().xxx就可以调用它的方法,实现跨进程通信,这可以说是肥肠便捷了。那么MaJiongService如何将消息传递给MaJiongMdmControl呢?其实Android系统中有一些没有公开的API,用起来也很方便,我们可以通过隐藏API很方便的实现这一点。

ContentProvider我想大家肯定都用过,这是Android 四大组件之一,专门用来跨进程通信的。ContentProvider有一个call方法,可以很方便的实现跨进程通信,不需要专门暴露接口等复杂的操作,还要配合一个系统隐藏接口,ContentResolver 的acquireProvider方法。

实现了以上两点MaJiongService和MaJiongMdmControl 之间就可以毫无障碍的通信了,我们通过这样的设计可以实现很多很多的功能,现在因为只是演示简单的代码功能,就用最简单的方式来实现。我们先来实现MaJiongMdmControl的代码编写。

2.3.2.编写总控APP代码

我们的总控APP MaJiongMdmControl需要放在源码中编译进系统system/app/下,所以我们需要编写一个Android.mk

  1. LOCAL_PATH:= $(call my-dir)
  2. include $(CLEAR_VARS)
  3. LOCAL_MODULE_TAGS := optional
  4. LOCAL_SRC_FILES := $(call all-java-files-under, src)
  5. LOCAL_MODULE_PATH := $(TARGET_OUT_SYSTEM_APPS)
  6. LOCAL_PACKAGE_NAME := MaJiongMdmControl
  7. LOCAL_CERTIFICATE := platform
  8. LOCAL_DEX_PREOPT := false
  9. LOCAL_PROGUARD_ENABLED := disabled
  10. #LOCAL_PROGUARD_ENABLED := full obfuscation
  11. #LOCAL_PROGUARD_FLAG_FILES := proguard.flags
  12. LOCAL_PRIVATE_PLATFORM_APIS := true
  13. include $(BUILD_PACKAGE)
  14. include $(call all-makefiles-under, $(LOCAL_PATH))

然后再来编写APK的代码,APK代码可以借助IDE快速编写完成,还能排除掉不必要的错误,编写完后删除不必要的文件再放到系统中编辑即可

AndroidManifest.xml

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3. package="com.majiong.mdm.control"
  4. android:sharedUserId="android.uid.system" >
  5. <application
  6. android:name="com.majiong.mdm.control.MaJiongMdmApplication"
  7. android:allowBackup="true"
  8. android:icon="@drawable/ic_launcher"
  9. android:label="@string/app_name" >
  10. <!-- Device Restriction -->
  11. <provider android:name="com.majiong.mdm.control.provider.MaJiongProvider"
  12. android:authorities="com.mdm.MaJiongProvider"
  13. android:multiprocess="false"
  14. android:exported="true" >
  15. </provider>
  16. </application>
  17. </manifest>

程序入口类MaJiongMdmApplication.java

  1. package com.majiong.mdm.control;
  2. import android.app.Application;
  3. import android.content.Context;
  4. import android.util.Log;
  5. /*
  6. *
  7. * @author   liaohongfei
  8. * @Date  2019 2019-6-28  下午7:21:17
  9. * 应用程序入口类
  10. */
  11. public class MaJiongMdmApplication extends Application {
  12. private static final String TAG = "majiong";
  13. // 全局Context
  14. private static Context mContext;
  15. @Override
  16. public void onCreate() {
  17. super.onCreate();
  18. Log.d(TAG, "MaJiongMdmApplication onCreate.");
  19. mContext = getApplicationContext();
  20. }
  21. public static Context getContext() {
  22. return mContext;
  23. }
  24. }

进程间通信的ContentProvider

  1. package com.majiong.mdm.control.provider;
  2. import com.majiong.mdm.sdk.constant.MaJiongRestriction;
  3. import com.majiong.mdm.sdk.MaJiongManager;
  4. import android.bluetooth.BluetoothAdapter;
  5. import android.content.Context;
  6. import android.content.ContentProvider;
  7. import android.content.ContentResolver;
  8. import android.content.ContentValues;
  9. import android.content.Intent;
  10. import android.database.Cursor;
  11. import android.net.Uri;
  12. import android.net.wifi.WifiManager;
  13. import android.os.Bundle;
  14. import android.provider.Settings;
  15. import android.util.Log;
  16. /*
  17. *
  18. * @author   liaohongfei
  19. * @Date  2018 2018-10-12  下午4:59:54
  20. *
  21. */
  22. public class MaJiongProvider extends ContentProvider {
  23. private static final String TAG = "majiong";
  24. private ContentResolver mResolver;
  25. private Context mContext;
  26. @Override
  27. public boolean onCreate() {
  28. mContext = getContext();
  29. mResolver = mContext.getContentResolver();
  30. return true;
  31. }
  32. @Override
  33. public int delete(Uri uri, String selection, String[] selectionArgs) {
  34. return 0;
  35. }
  36. @Override
  37. public String getType(Uri uri) {
  38. return null;
  39. }
  40. @Override
  41. public Uri insert(Uri uri, ContentValues values) {
  42. return null;
  43. }
  44. @Override
  45. public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
  46. String sortOrder) {
  47. return null;
  48. }
  49. @Override
  50. public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
  51. return 0;
  52. }
  53. @Override
  54. public Bundle call(String method, String arg, Bundle extras) {
  55. switch (method) {
  56. case MaJiongRestriction.SET_CONTROL_STATUS:
  57. if (extras != null) {
  58. String key = extras.getString(MaJiongRestriction.CONTROL_STATUS_KEY);
  59. int status = extras.getInt(MaJiongRestriction.CONTROL_STATUS_VALUE);
  60. setControlStatus(key, status);
  61. }
  62. break;
  63. case MaJiongRestriction.GET_CONTROL_STATUS:
  64. if (extras != null) {
  65. String key = extras.getString(MaJiongRestriction.CONTROL_STATUS_KEY);
  66. extras = new Bundle();
  67. extras.putInt(MaJiongRestriction.CONTROL_STATUS_VALUE, getControlStatus(key));
  68. return extras;
  69. }
  70. break;
  71. }
  72. return null;
  73. }
  74. private void setControlStatus(String key, int status) {
  75. Log.d(TAG, "马囧身高一米九,脸长一米二");
  76. boolean enable = false;
  77. switch (key) {
  78. case MaJiongManager.WIFI_STATE:
  79. if (status == MaJiongManager.OPEN || status == MaJiongManager.FORCE_OPEN) {
  80. enable = true;
  81. }
  82. setWifiEnable(enable);
  83. break;
  84. case MaJiongManager.BLUETOOTH_STATE:
  85. if (status == MaJiongManager.OPEN || status == MaJiongManager.FORCE_OPEN) {
  86. enable = true;
  87. }
  88. setBluetoothEnable(enable);
  89. break;
  90. }
  91. }
  92. private int getControlStatus(String key) {
  93. Log.d(TAG, "马囧身高一米九,脸长一米二");
  94. int state = MaJiongManager.CLOSE;
  95. switch (key) {
  96. case MaJiongManager.WIFI_STATE:
  97. state = getPersistedWifiOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
  98. break;
  99. case MaJiongManager.BLUETOOTH_STATE:
  100. state = getPersistedBluetoothOn() ? MaJiongManager.OPEN : MaJiongManager.CLOSE;
  101. break;
  102. }
  103. return state;
  104. }
  105. private void setBluetoothEnable(boolean enable) {
  106. BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
  107. boolean blueToothState = bluetoothAdapter.isEnabled();
  108. if (enable) {
  109. if (!blueToothState)
  110. bluetoothAdapter.enable();
  111. } else {
  112. if (blueToothState)
  113. bluetoothAdapter.disable();
  114. }
  115. }
  116. private boolean getPersistedBluetoothOn() {
  117. return Settings.Global.getInt(mContext.getContentResolver(),
  118. Settings.Global.BLUETOOTH_ON, 0) == 1;
  119. }
  120. private boolean setWifiEnable(boolean enable) {
  121. WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
  122. return wm.setWifiEnabled(enable);
  123. }
  124. private boolean getPersistedWifiOn() {
  125. return Settings.Global.getInt(mContext.getContentResolver(),
  126. Settings.Global.WIFI_ON, 0) == 1;
  127. }
  128. }

完整的工程猛戳这里下载!!

还记得MaJiongService里面的方法吗?就是第三方APK引用SDK后调用的setControlStatus和getControlStatus方法,之前我们已经尝试过了在MaJiongService中直接操作WiFi权限上是通不过的,所以我们要把操作WiFi发代码移到总控APP的MaJiongProvider中来,如上代码所示。相应的,MaJiongService里应该调用MaJiongProvider里的方法,我们把MaJiongService里的方法稍微改一下:

  1. @Override
  2. public void setControlStatus(String key, int status) {
  3. Log.d(TAG, "setControlStatus key: " + key + ", status: " + status);
  4. if (mResolver.acquireProvider(MaJiongRestriction.MAJIONG_MDM_URI) != null) {
  5. Bundle extras = new Bundle();
  6. extras.putString(MaJiongRestriction.CONTROL_STATUS_KEY, key);
  7. extras.putInt(MaJiongRestriction.CONTROL_STATUS_VALUE, status);
  8. mResolver.call(MaJiongRestriction.MAJIONG_MDM_URI, MaJiongRestriction.SET_CONTROL_STATUS, null, extras);
  9. } else {
  10. Log.d(TAG, "Could Not Find Provider " + MaJiongRestriction.MAJIONG_MDM_URI);
  11. }
  12. }
  13. @Override
  14. public int getControlStatus(String key) {
  15. if (mResolver.acquireProvider(MaJiongRestriction.MAJIONG_MDM_URI) != null) {
  16. Bundle extras = new Bundle();
  17. extras.putString(MaJiongRestriction.CONTROL_STATUS_KEY, key);
  18. extras = mResolver.call(MaJiongRestriction.MAJIONG_MDM_URI, MaJiongRestriction.GET_CONTROL_STATUS, null, extras);
  19. if (extras != null)
  20. return extras.getInt(MaJiongRestriction.CONTROL_STATUS_VALUE, DISABLE);
  21. } else {
  22. Log.d(TAG, "Could Not Find Provider " + MaJiongRestriction.MAJIONG_MDM_URI);
  23. }
  24. return DISABLE;
  25. }

可能大家又有疑问了,在MaJiongProvider和MaJiongService 的代码中都看到了MaJiongRestriction这个类的引用,MaJiongRestriction是个什么瘪犊子玩意儿?请看代码:

  1. package com.majiong.mdm.sdk.constant;
  2. import android.net.Uri;
  3. /*
  4. *
  5. * @author   liaohongfei
  6. * @Date  2018 2018-10-12  下午4:59:54
  7. * Device Restriction Constant
  8. */
  9. public class MaJiongRestriction {
  10. public static final String AUTHORITY = "com.mdm.MaJiongProvider";
  11. public static final String CONTENT_TYPE =
  12. "vnd.android.cursor.dir/vnd.com.mdm.MaJiongProvider";
  13. public static final String CONTENT_ITEM_TYPE =
  14. "vnd.android.cursor.item/vnd.com.mdm.MaJiongProvider";
  15. public static final class Table {
  16. public static final String TABLE_NAME = "test_table";
  17. public static final String ID = "_id";
  18. public static final String KEY = "key";
  19. public static final String VALUE = "value";
  20. public static final int COLUMN_IDX_ID = 0;
  21. public static final int COLUMN_IDX_KEY = 1;
  22. public static final int COLUMN_IDX_VALUE = 2;
  23. public static final String DEFAULT_SORT_ORDER = "_id asc";
  24. public static final int ITEM = 1;
  25. public static final int ITEM_NAME = 2;
  26. public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/item");
  27. }
  28. public static final Uri MAJIONG_MDM_URI = Uri.parse("content://com.mdm.MaJiongProvider");
  29. // Control
  30. public static final String SET_CONTROL_STATUS = "set_control_status";
  31. public static final String GET_CONTROL_STATUS = "get_control_status";
  32. public static final String CONTROL_STATUS_KEY = "control_key";
  33. public static final String CONTROL_STATUS_VALUE = "control_value";
  34. }

这个类是在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 未经允许严禁转载,请尊重作者劳动成果。】

 

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/羊村懒王/article/detail/265165
推荐阅读
相关标签
  

闽ICP备14008679号