Android如何优雅监听第三方App中某Activity的启动

背景

前段时间遇到了一个比较有意思的问题点:Android 10 ~ 12无法创建携带FLAG_TRUSTED标识virtualdisplay,也就是说我们没有权限直接将SecondaryActivity启动到virtualdisplay上,这个时候只能App自己想办法解决了。

解题思路

样板间App是通过MainActivity启动SecondaryActivity的,如果能监听到MainActivity启动,Android副屏(模拟器)App通过某种方式启动SecondaryActivity即可。

思路一

想要监听第三方App是否启动最直接的办法就是ps,哪想要监听第三方App中acitivity是否启动最直接的办法就是am stack list | grep MainActivity,那么我们启动一个线程,每间隔2s就检测一下MainActivity是否已启动,如果没有启动,我们就等待下次检测,如果已经启动,我们就通过am start -n SecondaryActivity --display displayId命令将SecondaryActivity启动起来。
伪代码大致如下:
private static boolean activityRunning(String activity) {
String cmd = "am stack list | grep " + activity;
Shell.Result sr = Shell.execCommand(cmd);
return sr.mResult == 0;
}

private static void startActivity(String activity, int displayId) {
    StringBuilder sb = new StringBuilder();

    sb.append("am start -n ");
    sb.append(activity);
    sb.append(" --display ");
    sb.append(displayId);

    String text = sb.toString();
    Shell.Result sr = Shell.execCommand(text);
    if (sr.mResult == 0) {
        System.out.println("am start secondActivity success");
    }
}

private boolean isReady() {
    return activityRunning(MainActivity) && !activityRunning(SecondActivity);
}

class DetectorThread extends Thread {
    DetectorThread() {
        super("DetectorThread");
    }
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                if (isReady()) {
                    Thread.sleep(2000);

                    if (isReady()) {
                        startActivity(mSecondActivity, mDisplayId);
                    }
                }

                Thread.sleep(2000);
            } catch (Exception e) {
                System.out.println("detector exception:" + e);
            }
        }
    }
}

上述代码简单易懂,经过测试非常OK。
上述代码虽然能达到目的,但是并不优雅,第一需要借助ADB shell,am stack list会很耗时,第二需要启动一个线程,且此线程需要一直跑着,哪Android有没有专门监听acitivy启动的API呢?

思路二

要想找到监听activity的API,首先想到的是android.app.AcivityManager或com.android.server.am.ActivityManagerService。果然在ActivityManagerService中找到了IActivityController

@Override
    public void setActivityController(IActivityController controller, boolean imAMonkey) {
        if (controller != null) {
            Binder.allowBlocking(controller.asBinder());
        }
        mActivityTaskManager.setActivityController(controller, imAMonkey);
    }

/*
**
** Copyright 2009, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License"); 
** you may not use this file except in compliance with the License. 
** You may obtain a copy of the License at 
**
**     http://www.apache.org/licenses/LICENSE-2.0 
**
** Unless required by applicable law or agreed to in writing, software 
** distributed under the License is distributed on an "AS IS" BASIS, 
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
** See the License for the specific language governing permissions and 
** limitations under the License.
*/

package android.app;

import android.content.Intent;

/**
 * Testing interface to monitor what is happening in the activity manager
 * while tests are running.  Not for normal application development.
 * {@hide}
 */
interface IActivityController
{
    /**
     * The system is trying to start an activity.  Return true to allow
     * it to be started as normal, or false to cancel/reject this activity.
     */
    boolean activityStarting(in Intent intent, String pkg);
    
    /**
     * The system is trying to return to an activity.  Return true to allow
     * it to be resumed as normal, or false to cancel/reject this activity.
     */
    boolean activityResuming(String pkg);
    
    /**
     * An application process has crashed (in Java).  Return true for the
     * normal error recovery (app crash dialog) to occur, false to kill
     * it immediately.
     */
    boolean appCrashed(String processName, int pid,
            String shortMsg, String longMsg,
            long timeMillis, String stackTrace);
    
    /**
     * Early call as soon as an ANR is detected.
     */
    int appEarlyNotResponding(String processName, int pid, String annotation);

    /**
     * An application process is not responding.  Return 0 to show the "app
     * not responding" dialog, 1 to continue waiting, or -1 to kill it
     * immediately.
     */
    int appNotResponding(String processName, int pid, String processStats);

    /**
     * The system process watchdog has detected that the system seems to be
     * hung.  Return 1 to continue waiting, or -1 to let it continue with its
     * normal kill.
     */
    int systemNotResponding(String msg);
}

IActivityController.activityStarting就能监听到任何activity的启动。
Android副屏(模拟器)App项目按照Android原生路径android.app引入IActivityController.aidl进行编译,以欺骗sdk。
ActivityManagerService可通过android.app.ActivityManagerNative或者android.app.IActivityManager获得,然后可直接调用setActivityController接口设置自己监听器。

public void setActivityController(IActivityController.Stub stub) {
  try {
      Class<?> cls = Class.forName("android.app.ActivityManagerNative");
      Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
      IInterface am = (IInterface) getDefaultMethod.invoke(null);
      Method method = am.getClass().getMethod("setActivityController", IActivityController.class, boolean.class);
      method.invoke(mManager, stub, true);
  } catch (Exception e) {
      e.printStackTrace();
  }
}

编译通过后,进行测试,非常OK。
然后可以通过原生startActivityAsUser API替代am start,此处不在赘述。

彩蛋

IActivityController.activityStarting的注释:The system is trying to start an activity. Return true to allowt to be started as normal, or false to cancel/reject this activity.
返回true允许activity启动,返回false就是取消或者拒绝activity启动。
那么我们只要有ADB权限就可以有针对的禁止任意APP的任意activity启动,比如:游戏、支付、直播等等,这不就是专治不听话熊孩子的神器吗,程序员爸爸/妈妈快点行动起来吧,开发一个专治熊孩的APP。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容