Skip to content

Instantly share code, notes, and snippets.

@5ec1cff
Last active February 28, 2022 12:46
Show Gist options
  • Save 5ec1cff/cc1f26d02a000d7d72d193ce032f59c5 to your computer and use it in GitHub Desktop.
Save 5ec1cff/cc1f26d02a000d7d72d193ce032f59c5 to your computer and use it in GitHub Desktop.
#Xposed Xposed 通过系统服务弹窗

Xposed 通过系统服务弹窗

想用 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 啦 ~ 效果如图:

qemu-system-x86_64_oVOcxgMV18

后续:

实际上不需要 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()
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment