Android原生系统开发如何优雅的提供系统级的API供第三方程序调用?

目录

故事背景

二、开发步骤

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

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,随时随地都可以调用。

 

二、开发步骤

  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。

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

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接口支持的参数和范围。

2.1.2.为我们的Manager生成AIDL

在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文件是科学且合理的。

2.1.3.编写系统级服务

在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开关状态的代码。

2.1.4.注册服务

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

registerService("majiong_service", MaJiongManager.class,
        new StaticServiceFetcher<MaJiongManager>() {
    @Override
    public MaJiongManager createService() {
        return MaJiongManager.getInstance();
    }});

2.1.5.初始化服务

上面一步我们已经将服务注册到系统了,这仅仅是有了备案信息而已,并不会让服务启动运行,系统级服务都是随着设备开机自启动的,那么如何初始化服务并启动它运行起来呢?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了,不过也是清晰易懂的,就是系统在开机的过程中,会按顺序挨个把这些服务都初始化好,并通知相关组件已经开机,可以工作了。

2.1.6.添加编译规则

我们所添加的所有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",

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的内容,改成如下:

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开发包就可以在开发中使用了。

2.2.2.使用SDK

让我们试验一下,看看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,当然这种程序都是无界面在后台运行的,用户不能感知它的存在,也是我们的系统专属管控应用,它所具有的权限非常的高。

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

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

 

 

  • 19
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
要在Qt for Android调用原生系统摄像头进行录像并保存输出,可以使用Android NDK来实现。下面是实现步骤: 1. 在Qt项目中添加一个Java文件,文件名为CameraRecorder.java,代码如下: ``` package com.example.myapplication; import android.hardware.Camera; import android.media.CamcorderProfile; import android.media.MediaRecorder; import android.os.Environment; import android.util.Log; import android.view.SurfaceHolder; import java.io.File; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; public class CameraRecorder implements SurfaceHolder.Callback { private static final String TAG = "CameraRecorder"; private Camera mCamera; private MediaRecorder mMediaRecorder; private SurfaceHolder mHolder; private boolean mIsRecording = false; public CameraRecorder(SurfaceHolder holder) { mHolder = holder; mHolder.addCallback(this); } public void startRecording() { if (mCamera == null) { mCamera = getCameraInstance(); } try { mCamera.setPreviewDisplay(mHolder); mCamera.startPreview(); } catch (IOException e) { Log.e(TAG, "Error starting camera preview: " + e.getMessage()); } mMediaRecorder = new MediaRecorder(); mCamera.unlock(); mMediaRecorder.setCamera(mCamera); mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA); CamcorderProfile profile = CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH); mMediaRecorder.setProfile(profile); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss"); String currentDateAndTime = dateFormat.format(new Date()); File mediaFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/Camera/", "VID_" + currentDateAndTime + ".mp4"); mMediaRecorder.setOutputFile(mediaFile.getAbsolutePath()); try { mMediaRecorder.prepare(); } catch (IOException e) { Log.e(TAG, "Error preparing media recorder: " + e.getMessage()); } mMediaRecorder.start(); mIsRecording = true; } public void stopRecording() { if (mIsRecording) { mMediaRecorder.stop(); mMediaRecorder.release(); mCamera.lock(); mCamera.release(); mCamera = null; mIsRecording = false; } } private Camera getCameraInstance() { Camera camera = null; try { camera = Camera.open(); } catch (Exception e) { Log.e(TAG, "Error opening camera: " + e.getMessage()); } if (camera != null) { Camera.Parameters params = camera.getParameters(); List<Camera.Size> sizes = params.getSupportedPreviewSizes(); Camera.Size size = sizes.get(0); params.setPreviewSize(size.width, size.height); camera.setParameters(params); } return camera; } @Override public void surfaceCreated(SurfaceHolder holder) { } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { } @Override public void surfaceDestroyed(SurfaceHolder holder) { stopRecording(); } } ``` 2. 在Qt项目的AndroidManifest.xml文件中添加如下权限: ``` <uses-permission android:name="android.permission.CAMERA"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ``` 3. 在Qt项目中添加一个QML页面,页面中添加一个SurfaceView控件,并在控件上叠加一个按钮,代码如下: ``` import QtQuick 2.0 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.2 import QtQuick.Dialogs 1.2 import QtAndroidExtras 1.4 Rectangle { id: root width: 360 height: 640 color: "white" SurfaceView { id: surfaceView anchors.fill: parent } Button { id: btnRecorder text: "Start Recording" width: 200 height: 50 anchors.centerIn: parent onClicked: { if (btnRecorder.text === "Start Recording") { var recorder = QtAndroid.createQmlObject('import com.example.myapplication 1.0; CameraRecorder {id: recorder;}', surfaceView); recorder.startRecording(); btnRecorder.text = "Stop Recording"; } else { recorder.stopRecording(); btnRecorder.text = "Start Recording"; } } } } ``` 4. 编译并运行Qt项目,在页面中点击按钮即可开始录制视频。视频文件保存在SD卡的DCIM/Camera目录下。 以上代码仅为示例代码,具体实现可以根据自己的需求进行修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值