想用 Xposed 实现一个纯注入系统的「去你大爷的内置浏览器」,需要实现一个「询问」功能,也就是启动内置浏览器 Activity 的时候并不总是替换,而是询问用户应该是用内置浏览器打开还是用户浏览器(显然内置浏览器往往不是 exported 的,因此不会出现在系统选择器中)。
既然是询问,肯定要有一个 Window 和用户交互,但是应该如何呈现这个 Window 很伤脑筋。一开始尝试把要启动的 Intent 用模块的 Activity 替换,模仿系统的 ResolverActivity 「转发」Intent,但是注意到这样处理多用户(工作空间)可能就有些棘手,此外「转发」也很麻烦。
于是换一种思路:hook 系统服务侧的 startActivity ,发现启动「内置浏览器」的 Intent 后,直接返回 0 (START_SUCCESS) ,同时显示一个 dialog 询问用户,根据用户操作再恢复 intent 的发送。
这个 dialog 自然是在系统服务最方便,但是系统服务不像普通的 app ,它没有 Activity ,又怎么弹 Dialog 呢?
一开始尝试直接用 systemUiContext 创建 AlertDialog ,然后直接 show:
val systemUIContext: Context by lazy {
Class.forName("android.app.ActivityThread")
.getDeclaredMethod("currentActivityThread")
.invoke(null)!!
.invokeMethod("getSystemUiContext") as Context
}
val myHandler: Handler by lazy {
val thread = HandlerThread("IntentForwarder")
thread.start()
Handler(thread.looper)
}
private fun askAndResendStartActivity(atm: Any, args: Array<Any>, userId: Int) {
val dialog = AlertDialog.Builder(systemUIContext)
.setPositiveButton("Direct") { _, _ ->
// ...
}
.setNegativeButton("Replace") { _, _ ->
// ...
}
.setMessage("IntentForwarder").show()
}
findMethod(ATMS) {
name == "startActivity"
}.hookBefore { param ->
val intent = param.args[3] as? Intent?: return@hookBefore
if (!checkIntent(intent)) return@hookBefore
myHandler.post { // 需要一个 handler
askAndResendStartActivity(
param.thisObject,
param.args,
Binder.getCallingUid() / 100000
)
}
param.result = 0 // ActivityManager.START_SUCCESS
}
这样果不其然报错了,原因是 token=null
WindowManager$BadTokenException: Unable to add window -- token null is not valid; is your activity running?
难道系统真的不能用 AlertDialog 吗?别急,实际上系统服务是有弹窗的——一个最容易找到的例子是输入法选择,这个 dialog 就来自 system_server 。
# 查找「选择输入法」的窗口
# dumpsys window | grep mFocus
mFocusedApp=ActivityRecord{7afae78 u0 com.tencent.mobileqq/.activity.SplashActivity t29514}
mFocusedWindow=Window{4c4ac88 mode=1 rootTaskId=1 u0 Select input method}
# dumpsys window | grep 'Window{4c4ac88'
Window #21 Window{4c4ac88 mode=1 rootTaskId=1 u0 Select input method}:
# dumpsys window | grep 'Window #21' -A 3
Window #21 Window{4c4ac88 mode=1 rootTaskId=1 u0 Select input method}:
mDisplayId=0 rootTaskId=1 mSession=Session{63e443b 1724:1000} mClient=android.view.ViewRootImpl$W@8b3007a
mOwnerUid=1000 showForAllUsers=true package=android appop=NONE
mAttrs={(0,0)(fillxwrap) gr=BOTTOM CENTER_VERTICAL sim={adjust=pan forwardNavigation} blurRatio=1.0 blurMode=0 ty=INPUT_METHOD_DIALOG fmt=TRANSLUCENT wanim=0x100d0104
# mSession 有 pid=1724, uid=1000 ,看看是谁:
# ps -p 1724
PID TTY TIME CMD
1724 ? 02:31:58 system_server
注意到它的 title 是 Select input method
,去 codesearch 搜一搜:
InputMethodMenuController.java - Android Code Search
frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodMenuController.java
(Android 11 在 frameworks/base/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
里面的 showInputMethodMenu
方法)
实际上系统的实现也是个 AlertDialog :
void showInputMethodMenu(boolean showAuxSubtypes, int displayId) {
// ...
synchronized (mMethodMap) {
// ...
final Context settingsContext = getSettingsContext(displayId);
mDialogBuilder = new AlertDialog.Builder(settingsContext);
mDialogBuilder.setOnCancelListener(dialog -> hideInputMethodMenu());
// ...
mSwitchingDialog = mDialogBuilder.create();
mSwitchingDialog.setCanceledOnTouchOutside(true);
final Window w = mSwitchingDialog.getWindow();
final WindowManager.LayoutParams attrs = w.getAttributes();
w.setType(WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG);
// Use an alternate token for the dialog for that window manager can group the token
// with other IME windows based on type vs. grouping based on whichever token happens
// to get selected by the system later on.
attrs.token = mSwitchingDialogToken;
attrs.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS;
attrs.setTitle("Select input method");
w.setAttributes(attrs);
mService.updateSystemUiLocked();
mSwitchingDialog.show();
}
}
public Context getSettingsContext(int displayId) {
if (mSettingsContext == null || mSettingsContext.getDisplayId() != displayId) {
final Context systemUiContext = ActivityThread.currentActivityThread()
.createSystemUiContext(displayId);
final Context windowContext = systemUiContext.createWindowContext(
WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG, null /* options */);
mSettingsContext = new ContextThemeWrapper(
windowContext, com.android.internal.R.style.Theme_DeviceDefault_Settings);
mSwitchingDialogToken = mSettingsContext.getWindowContextToken();
}
return mSettingsContext;
}
P.S. Android 11 的代码并不是上面这样
这里用了一个 createWindowContext 创建的 Context ,这是个公开 API 。此外它并没有立即调用 AlertDialog 的 show ,而是配置了一下 dialog 的 window ,设置了 token, type 之类的。
于是我们也照葫芦画瓢:
val windowContext: Context by lazy {
systemUIContext.createWindowContext(
WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG, null
)
}
private fun askAndResendStartActivity(atm: Any, args: Array<Any>, userId: Int) {
val dialog = AlertDialog.Builder(windowContext)
.setPositiveButton("Direct") { _, _ ->
// ...
}
.setNegativeButton("Replace") { _, _ ->
// ...
}
.setMessage("IntentForwarder").create()
dialog.window?.attributes?.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG // 这里必须设置,否则报错 token xxx is not for an application ,原因似乎是 create() 完成后 type 还是 APPLICATION
dialog.show()
}
这样就能愉快地在系统服务弹 dialog 啦 ~ 效果如图:
后续:
实际上不需要 Context#createWindowContext
也一样能用,事实上这是个 API 30 的方法,用了会没法兼容。参考 Android 11 的实现,直接 new Binder 即可(起码在 Android 11 还是能用的)
private val windowToken: IBinder by lazy {
Binder()
}
private fun askAndResendStartActivity(atm: Any, args: Array<Any>, userId: Int) {
val dialog = AlertDialog.Builder(systemUIContext) // ...
dialog.window?.let {
it.attributes?.type = WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG
it.attributes.token = windowToken
}
dialog.show()
}