安卓开发-应用锁功能的实现
需求
能够对指定的应用上锁,上锁之后启动这些应用需要先验证密码,密码验证通过才能打开。
注意:开发这个功能需要有源码环境,或者有改动系统级别应用的权限;下面的内容是在这两项前提下进行的,如果没有上面的条件,需要找下其他方法来实现。
思路分析
- 启动应用实际上就是启动对应的Activity,要在应用启动之前做密码验证,可以在Activity启动过程中进行拦截。
- 在启动一个Activity时,目标Activity的相关信息会封装在Intent中,系统也是根据Intent的信息去解析出要启动的Activity。所以可以根据Intent中携带的包名等信息去做判断和拦截。
- 注意1:显式启动Activity的Intent中含有包名信息,但也存在隐式启动的情况,隐式启动Activity的Intent中是不包含包名信息的,这种情况下就需要想办法拿到待启动Activity所属的包名,否则无法判断该Activity是否需要拦截。
- 需要写一个LockApp用来执行验证密码等操作,拦截Activity后,将原始的Intent转交给LockApp来处理,若是密码验证通过再使用这个Intent重新执行启动流程;若是密码验证失败就不启动应用。
- 注意2:Activity在启动过程中,原始的Intent是会被更改的,在解析过程中,会加入一些Flag等信息,被修改后的Intent无法再用来重新启动对应的Activity。所以要在合适的地方,拿到原始的Intent传递给LockApp。
- 在拦截时,需要排除同一个应用内,Activity相互启动的情况。如某App有ActivityA和ActivityB,用户启动app默认打开ActivityA,这时候已经验证过密码了,随后用户在app内点击跳转到ActivityB时,就不应该再对ActivityB进行拦截。
- 在拦截时,需要排除LockApp的请求。LockApp不会主动去启动App,来自LockApp的启动请求,已经通过密码验证了,需要正常启动。
方法一:在Activity启动源码中拦截
经过分析,在Activity启动过程中,会调用ActivityStarter.execute()方法,在这个方法内部可以同时拿到原Intent和解析后的包名信息,所以可以在这个方法中做处理:
// frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
// 关键方法,其他代码省略
int execute() {
try {
// add by lll --AppLock -2023/12/08+++
// 在方法的开头通过mRequest.intent拿到原Intent
Intent originalIntent = mRequest.intent;
// end add by lll --AppLock -2023/12/08---
// ...
// 在这里解析原Intent,要在这之前保存一下原Intent
if (mRequest.activityInfo == null) {
mRequest.resolveActivity(mSupervisor);
}
// ...
// 解析完成后,会将信息保存在mRequest.resolveInfo中
if (mRequest.intent != null && mRequest.resolveInfo != null) {
// 将要启动Activity的包名
android.util.Log.d(TAG, "execute: mRequest.resolveInfo package =" + mRequest.resolveInfo.activityInfo.packageName);
// action信息
android.util.Log.d(TAG, "execute: intentAction " + mRequest.intent.getAction());
// Activity的调用者
android.util.Log.d(TAG, "execute: callingPackage " + mRequest.callingPackage);
// add by lll --AppLock -2023/12/08+++
// 需要满足两个条件:
// 1. 启用了应用锁,这个属性可以作为应用锁的总开关
// 2. 不同应用间的启动才需要验证密码,同一个应用的Activity互相启动需要跳过
if (SystemProperties.getBoolean("persist.sys.app.lock.state", false)
&& !originalIntent.toString().contains(mRequest.callingPackage)) {
String check_packageName = mRequest.resolveInfo.activityInfo.packageName;
// 通过包名判断是否要拦截处理,这里只写了一个app,可以拓展到多个应用
if ("com.xxx.settings".equals(check_packageName)) {
android.util.Log.d(TAG, "execute: startActivity-");
Intent interceptIntent = new Intent();
interceptIntent.setComponent(new ComponentName("com.xxx.appLock", "com.xxx.appLock.MainActivity"));
// 将原Intent传给LockApp
interceptIntent.putExtra("originalIntent", originalIntent);
interceptIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动LockApp
mService.getUiContext().startActivity(interceptIntent);;
return START_ABORTED;
}
}
// end add by lll --AppLock -2023/12/08---
}
// ...
return getExternalResult(res);
}
} finally {
onExecutionComplete();
}
}
LockApp的关键代码:
/**
* 验证密码
*/
public class FirstFragment extends Fragment {
private Activity mActivity;
private Intent mIntent;
// ...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
mActivity = getActivity();
// 拿到ActivityStarter传过来的原Intent
mIntent = (Intent) mActivity.getIntent().getParcelableExtra("originalIntent");
init(view);
}
private void init(View view) {
// ...
NumberKeypad.Listener numberListener = new NumberKeypad.Listener() {
@Override
public void onFinish(String password) {
Log.d(TAG, "onFinish: " + password);
new Handler().postDelayed(() -> {
if (password.equals(SystemProperties.get("persist.sys.safety.lock.password", ""))) {
// 密码验证通过
Log.d(TAG, "run: intent " + mIntent);
if (mIntent != null) {
// 这里最好是加上FLAG_ACTIVITY_NEW_TASK,否则启动的Activity会跟验证密码的app在同一个栈中
mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(mIntent);
}
// 关闭密码验证页面
System.exit(0);
} else {
Toast.makeText(getActivity(), getString(R.string.password_wrong), Toast.LENGTH_SHORT).show();
numberKeypad.reset();
}
}, 50);
}
};
}
}
方法二:继承IActivityController.Stub来监控Activity的状态
IActivityController.aidl是系统自带的aidl,在Am的内部类MyActivityController有实现这个aidl接口,主要用于app状态监听控制。继承IActivityController.Stub需要实现六个方法:
- activityStarting:当系统正在启动一个activity时会触发,当返回true,表示允许启动。当返回状态noraml/false分别表示停止/拒绝启动activity。
- activityResuming:当系统正在返回一个activity时会触发,当返回true,表示允许返回。当返回状态noraml/false分别表示停止/拒绝返回activity
- appCrashed:当一个应用进程已经崩溃会触发,当返回true时,表示可以重启,当返回false时,表示立即杀死它(进程)。
- appEarlyNotResponding:系统检测到一个应用程序可能即将变得无响应,但在达到正式的ANR状态之前。这给了应用程序一个机会去恢复,或者让系统进行进一步的检查以确认是否真的无响应
- appNotResponding:当一个应用进程出现ANR时就会触发,当返回0时,表示会弹出应用无响应的dialog,如果返回1时,表示继续等待,如果返回-1时,表示立即杀死进程。
- systemNotResponding:当系统检测到整个系统似乎无响应时,会调用此方法。如果返回1,表示继续等待,如果返回-1,就让系统进行正常的自杀
由上面的内容可知:可以通过自定义ActivityController类,重写activityStarting方法来监听Activity的启动。
使用:
只是继承IActivityController.Stub接口重写activityStarting方法,还不能让系统在启动Activity时触发对应的方法,需要将我们自定义的ActivityController类注册到系统中:在系统级别应用中调用IActivityManager.setActivityController()方法来设置我们自定义的ActivityController:
// 这里选择的是Launcher的onCreate中
@Override
protected void onCreate(Bundle savedInstanceState) {
lastTime = SystemClock.currentThreadTimeMillis();
super.onCreate(savedInstanceState);
this.savedInstanceState = savedInstanceState;
mContext = this;
setContentView(R.layout.activity_main);
// 将我们自定义的ActivityController类注册到系统中
IActivityManager am = ActivityManagerNative.getDefault();
try {
Log.d(TAG, "setActivityController: ");
am.setActivityController(new ActivityController(this),true);
} catch (RemoteException e) {
Log.d(TAG, "setActivityController: error " + e.getMessage());
e.printStackTrace();
}
}
这里需求是拦截应用的启动,所以重点关注activityStarting方法即可,自定义的ActivityController类实现如下:
/**
* @author liang
* @description: ActivityController
* @date 2024/6/6 8:39
*/
public class ActivityController extends IActivityController.Stub {
private Context mContext;
public ActivityController(Context context) {
mContext = context;
}
/**
* 当系统正在启动一个activity时会触发
* @author liang
* @createTime 2024/6/6 8:40
* @param intent: 启动Activity的Intent
* @param pkg: 将要启动Activity所属的包名
* @return : boolean:当返回true,表示允许启动。当返回状态noraml/false分别表示停止/拒绝启动activity
*/
@Override
public boolean activityStarting(Intent intent, String pkg) throws RemoteException {
// 应用锁已启用
if (SystemProperties.getBoolean("persist.sys.app.lock.state", false)){
Log.d(TAG, "activityStarting: intent " + intent);
Log.d(TAG, "activityStarting: pkg " + pkg);
// 是否从LockApp启动,由LockApp启动的不需要拦截
boolean openByLockApp = SystemProperties.getBoolean("persist.sys.open.by.lock.app", false);
// 该应用是否在前台,在前台表示是同一个应用的启动,无需拦截
boolean appOpened = appInForeground(pkg);
Log.d(TAG, "activityStarting: openByLockApp " + openByLockApp + "; appOpened " + appOpened);
// 由LockApp启动或由同一个应用启动的 不需要拦截
if (!openByLockApp && !appOpened) {
// 通过包名判断是否要拦截处理,这里只写了一个app,可以拓展到多个应用
if ("com.xxx.settings".equals(pkg)) {
Log.d(TAG, "activityStarting: intercept " + pkg);
Intent interceptIntent = new Intent();
interceptIntent.setComponent(new ComponentName("com.xxx.appLock", "com.xxx.appLock.MainActivity"));
// 将原Intent传给LockApp
interceptIntent.putExtra("originalIntent", intent);
interceptIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 启动LockApp
mContext.startActivity(interceptIntent);
SystemProperties.setString("persist.sys.open.by.lock.app", "false");
return false;
}
}
}
return true;
}
@Override
public boolean activityResuming(String pkg) throws RemoteException {
return true;
}
@Override
public boolean appCrashed(String processName, int pid, String shortMsg, String longMsg, long timeMillis, String stackTrace) throws RemoteException {
return true;
}
@Override
public int appEarlyNotResponding(String processName, int pid, String annotation) throws RemoteException {
return 0;
}
@Override
public int appNotResponding(String processName, int pid, String processStats) throws RemoteException {
return 0;
}
@Override
public int systemNotResponding(String msg) throws RemoteException {
return 0;
}
}
activityStarting(Intent intent, String pkg)方法传入两个参数分别是启动Activity的Intent,和Activity的数所包名。根据传进来的pkg参数判断该Activity是否需要拦截;拦截之后,将启动Activity的intent传递给LockApp即可。后续的处理于方法一类似,这里不再列出LockApp的代码。
相比第一种方式,这里少了一个关键信息:启动Activity的app的包名(调用者的包名,不是被启动Activity的包名),所以不能直接判断是不是由同一个应用启动,也不能直接判断是不是由LockApp启动的。所以这里需要用额外的方式去处理:
- 判断被启动的app是否在前台,在前台的话意味着是同一个应用内的Activity,无需拦截。
- 使用一个标志来标志Activity是否由LockApp启动,由LockApp启动的应用,被视为是已经通过密码验证,不需要拦截。需要在LockApp中做处理:
- 启动LockApp时设置persist.sys.open.by.lock.app为false
- 密码验证正确、重新启动Activity时,设置persist.sys.open.by.lock.app为true
方式二补充说明:
activityStarting()方法的调用,其实也是在frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java这个文件中的,并且就是在方式一处理的时间节点后面一点点,源码如下:
int execute() {
try {
// ...
// 解析Intent
if (mRequest.activityInfo == null) {
mRequest.resolveActivity(mSupervisor);
}
// 尝试为关闭或重启添加检查点,记录原始Intent和包名
if (mRequest.intent != null) {
String intentAction = mRequest.intent.getAction();
String callingPackage = mRequest.callingPackage;
if (intentAction != null && callingPackage != null
&& (Intent.ACTION_REQUEST_SHUTDOWN.equals(intentAction)
|| Intent.ACTION_SHUTDOWN.equals(intentAction)
|| Intent.ACTION_REBOOT.equals(intentAction))) {
ShutdownCheckPoints.recordCheckPoint(intentAction, callingPackage, null);
}
}
int res;
synchronized (mService.mGlobalLock) {
// ...
// 调用executeRequest方法
res = executeRequest(mRequest);
// ...
}
} finally {
onExecutionComplete();
}
}
private int executeRequest(Request request) {
// ...
// 这里拿到的Intent是修改后的intent
Intent intent = request.intent;
// 这里的mService是ActivityTaskManagerService,mController是其内部类型为IActivityController的变量
if (mService.mController != null) {
try {
// 提供给观察者的Intent已经剥离了额外的数据,拿到的是原Intent,可以直接转发使用
Intent watchIntent = intent.cloneFilter();
// 调用IActivityController的activityStarting方法
abort |= !mService.mController.activityStarting(watchIntent,
aInfo.applicationInfo.packageName);
} catch (RemoteException e) {
mService.mController = null;
}
}
// ...
}
前面方式一有说到,启动Activity的过程中,Intent会被加入一些Flag参数之类的,被修改后的Intent无法再次用来启动Activity。executeRequest()方法的调用在解析Intent之后,所以在executeRequest()方法中拿到的Intent是修改后的intent。
调用activityStarting之前,通过intent.cloneFilter()拿到的Intent,是已经剥离了额外的数据,相当于是原Intent,所以可以用来重新启动Activity。
再看下 将自定义ActivityController类注册到系统中的代码:
// 将我们自定义的ActivityController类注册到系统中
IActivityManager am = ActivityManagerNative.getDefault();
try {
Log.d(TAG, "setActivityController: ");
am.setActivityController(new ActivityController(this),true);
} catch (RemoteException e) {
Log.d(TAG, "setActivityController: error " + e.getMessage());
e.printStackTrace();
}
简单来说就是调用了IActivityManager的setActivityController()方法,追踪源码,发现IActivityManager的setActivityController()方法的调用逻辑如下:
// frameworks/base/core/java/android/app/IActivityManager.aidl
void setActivityController(in IActivityController watcher, boolean imAMonkey);
// frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
@Override
public void setActivityController(IActivityController controller, boolean imAMonkey) {
if (controller != null) {
Binder.allowBlocking(controller.asBinder());
}
mActivityTaskManager.setActivityController(controller, imAMonkey);
}
// frameworks/base/core/java/android/app/IActivityTaskManager.aidl
void setActivityController(in IActivityController watcher, boolean imAMonkey);
// frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@Override
public void setActivityController(IActivityController controller, boolean imAMonkey) {
mAmInternal.enforceCallingPermission(android.Manifest.permission.SET_ACTIVITY_WATCHER,
"setActivityController()");
synchronized (mGlobalLock) {
// 这里拿到的mController就是ActivityStarter.java里面的mService.mController
mController = controller;
mControllerIsAMonkey = imAMonkey;
// 设置Watchdog中的IActivityController
Watchdog.getInstance().setActivityController(controller);
}
}
所以到这里,逻辑就联系起来了,在我们的应用中调用了IActivityManager的setActivityController()方法,实际上就是将自定义的ActivityController对象传递到ActivityTaskManagerService中,然后在Activity启动过程中,会回调我们重写的setActivityController方法()。
小结
上面提供了两种实现应用锁的思路,可能还有一些其他的方法可以实现同样的效果,但核心的逻辑还是通过packageName去过滤应用,然后重新启动Activity。这里只是简单介绍了应用锁的思路和实现步骤,具体的原理没有讲解,有能力的同学可以尝试分析下framework中Activity启动过程的源码,这样就可以理解为什么是在ActivityStarter.execute()方法中添加拦截的逻辑。
与第一种方式相比,第二种方法的优点是不需要改动framework源码,只在系统的app代码中就可以实现,并且比较方便地管理上锁的应用(在应用内使用List动态管理);缺点是缺少关键信息:启动Activity的应用的信息,所以就要做更多的逻辑去判断各种情况,这也意味着维护成本的增加。两种方式各有优劣,可以根据自己的使用场景去做选择。
由于个人能力有限,上面的分析和讲解难免有错漏之处,欢迎各位批评指正;如有其他比较好的实现方法,同时也欢迎大家相互交流学习,一起进步,共勉!!!