Android Framework权限篇一之RuntimePermission整体流程
Android Framework权限篇二之RuntimePermission数据结构解析
Android Framework权限篇三之后台定位权限源码分析
Android Framework权限篇四之AppOps机制
Android Framework权限篇五之实现敏感权限行为提醒
概述
继之前第三篇文章的后台定位权限源码分析中提到的敏感权限行为提醒,这片文章讨论下之前笔者在坚果手机上是如何实现这样的效果的。
实现思路
这里的实现思路是在系统中新增一个系统应用app:安全中心;在这里去监听各个系统服务的回调,根据规则计算出当前应该展示哪个应用的哪个敏感权限,通知给SystemUI进行展示,点击的时候将对应应用移到前台
录音状态查询
录音使用原生提供接口,可以监听录音状态;List<AudioRecordingConfiguration>
可以拿到当前正在使用录音的应用列表,这个是被动监听方式;
private AudioManager.AudioRecordingCallback mRecordingCallback = new AudioManager.AudioRecordingCallback() {
@Override
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
super.onRecordingConfigChanged(configs);
...
}
};
mAudioManager.registerAudioRecordingCallback(mRecordingCallback, null);
由于需要考虑安全中心应用挂掉之后重启恢复数据,所以还需要通过接口主动查询;
List<AudioRecordingConfiguration> configs = mAudioManager.getActiveRecordingConfigurations();
相机状态查询
- 相机没有像录音的原生接口,需要相机Framework服务侧提供支持
- 实现:相机服务会将使用相机的应用持久化到SystemProperties,安全中心再根据camera 回调onCameraAvailable(close camera)/onCameraUnavailable(open camera) 在这两个回调时机时去查询SystemProperties得到正在使用相机的应用列表
- 这两个方法会在首次注册的时候就收到回调,可以当作安全中心应用挂掉重启后进行主动查询
private CameraManager.AvailabilityCallback mListener = new CameraManager.AvailabilityCallback() {
@Override
public void onCameraAvailable(String cameraId) {
...
}
@Override
public void onCameraUnavailable(String cameraId) {
...
}
};
mCameraManager.registerAvailabilityCallback(mListener, null);
定位状态查询
- 定位服务没有原生接口,Framework侧会在开始/结束定位时发送广播,广播中携带正在使用定位的应用列表
- 定位广播是粘性广播,在安全中心挂掉重启以后可以再响应最近的一次广播拿到正在使用定位的应用列表进行更新
String LOCATION_APPS_ACTION = "smartisan.intent.action.LOCATION_LISTENER_LIST";
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (LOCATION_APPS_ACTION.equals(action)) {
...
}
}
};
维护前台列表
- 由于需要判断应用是否在后台使用相机/录音/定位服务,所以需要维护一个前台应用列表,如果正在使用服务的应用不在前台列表中,则认为是在后台使用服务,需要通知SystemUI展示bar
- 这里通过ActivityManager监听resume则将应用加入前台列表,pause则移除前台列表;
private IActivityLifeCycleObserver mLifeCycleObserver = new IActivityLifeCycleObserver.Stub() {
@Override
public void onActivityResumeForeground(String pkg, String activity, int uid) {
...
}
@Override
public void onActivityPauseBackground(String pkg, String activity, int uid) {
...
}
}
ActivityManager.getService().registerActivityLifeCycleObserver(mLifeCycleObserver);
同样当安全中心挂掉重启时需要主动查询接口:
public LinkedHashSet<String> getRunningForegroundPkgList(boolean ignoreSystem) {
LinkedHashSet<String> forePkgList = new LinkedHashSet<>();
final List<ActivityManager.RunningAppProcessInfo> processInfos =
mAm.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo processInfo : processInfos) {
if (processInfo.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
&& processInfo.processState == ActivityManager.START_TASK_TO_FRONT) {
for (String pkg : processInfo.pkgList) {
...
forePkgList.add(pkg);
}
}
}
return forePkgList;
}
进程结束状态
- 应用进程退出时(可能是用户手动清理后台或者崩溃退出)相机/录音/定位等服务的接口会回调状态更新,从而更新对应正在使用服务的应用列表
- 前台列表的
ActivityManager pasue resume
接口不会收到回调,所以需要额外处理下这种场景case,通过以下接口进行监听,通过onProcessDied拿到的uid可以查询到应用包名,再将其从前台列表移除
private IProcessObserver mProcessObserver = new IProcessObserver.Stub() {
@Override
public void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities) {
}
@Override
public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) {
}
@Override
public void onProcessDied(int pid, int uid) {
...
}
};
ActivityManager.getService().registerProcessObserver(mProcessObserver);
展示规则计算
- 前面通过各种服务回调拿到相关数据列表,进行判断以后更新到map中
- 当有多个应用使用多个权限时,显示的时候按照如下规则展示
- 优先按照权限优先级:相机>录音>定位
- 其次按照最近使用的时间对应用进行排序
这里使用LinkedHashMap:键是包名,值是对应的权限列表的数据结构进行存储
//这里第三个参数设置为true可以保证map新增/更新的item会放在末尾处,以保证顺序
private LinkedHashMap<String, List<Integer>> mPkgAndPermData = new
LinkedHashMap<>(INIT_CAPACITY, LOAD_FACTOR, true);
权限 | 标识 | 优先级(数值越大越高) |
---|---|---|
定位 | location | 1 |
录音 | audio | 2 |
相机 | camera | 3 |
如果map当中存储数据如下:
String:pkg | List<Integer>: permissionList |
---|---|
com.tencent.mm | 1,2 |
com.tencent.qq | 2,3 |
则根据如下规则计算,由于map是按照最近使用的应用排序的,即最近更新的应用会排在末尾,所以遍历完之后,会得到权限级别最高和对应的最近应用:如上得到的结果是:QQ-相机权限 通知给SystemUI
String packageName = null;
int permissionLevel = PermissionDataManager.PERMISSION_NULL;
for (Map.Entry<String, List<Integer>> entry : data.entrySet()) {
List<Integer> valueList = new ArrayList<>(entry.getValue());
int level = getHighestLevel(valueList, !isGpsAllowed);
if (level >= permissionLevel) {
packageName = entry.getKey();
permissionLevel = level;
}
}
SystemUI通信
最终bar的展示是在SystemUI,安全中心与SystemUI进行通信,将需要展示的权限和点击跳转的包名传给SystemUI
通信方式
SystemUI应用通过bindService的形式,绑定安全中心提供的service进行通信;定义aidl接口;
public class SensitivePermissionService extends Service {
@Override
public void onCreate() {
super.onCreate();
...
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private Binder mBinder = new ISensitivePermission.Stub() {
@Override
public void registerLatestPermissionObserver(IPermissionObserver observer, boolean sticky)
throws RemoteException {
...
}
@Override
public void unregisterLatestPermissionObserver(IPermissionObserver observer)
throws RemoteException {
...
}
};
}
aidl接口IPermissionObserver如下,对应的参数描述如下:
interface IPermissionObserver {
void onPermissionChanged(String packageName, in String[] permissions, int userId);
}
import com.smartisanos.securitycenter.IPermissionObserver;
interface ISensitivePermission {
void registerLatestPermissionObserver(IPermissionObserver observer, boolean sticky);
void unregisterLatestPermissionObserver(IPermissionObserver observer);
}
参数名称 | 类型 | 描述 | 空情况 |
---|---|---|---|
packageName | String | 应用包名 | "" |
permissions | String[] | 权限名:默认情况下只有一个权限 audio/camera/location | new String[] { } |
userId | int | 区分双开应用,默认是0,双开则为10 | 0 |
SystemUI侧通过bindService方式绑定服务,注册监听,当服务端有变化时会通知客户端;
void bindService() {
Intent intent = new Intent(ACTION);
intent.setPackage(PACKAGE);
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
这里具体的使用参考aidl接口通信,提供整体思路,不提供源码,避免泄漏安全问题;
结语
坚果手机上的整体思路方案即是如此,其他手机厂商感兴趣的可以借鉴看下。