Android辅助功能开发——一键优化

作者: yooking丶毓 | 来源:发表于2020-08-24 09:41 被阅读0次

    个人博客地址:https://blog.yookingh.cn
    该文章地址:https://blog.yookingh.cn/dev/200424-accessibilityService.html

    前言

    公司项目需求:为App提供一键优化功能(一键获取相关所有权限),确保App在优化过后能够常驻手机,为用户提供服务。——商户类App,用户非主动关闭时常驻手机合情合理

    阅读源码

    1. getWindows()和getRootInActiveWindow()

      List<AccessibilityNodeInfo> getWindows() 即:获取当前页面所有的节点——包括顶部状态栏、底部系统按钮等各自的root节点。

      AccessibilityNodeInfo getRootInActiveWindow()即:获取当前活动页面(Activity)中的root节点。

      getRootInActiveWindow()方法在页面未加载完成时可能为null,而getWindows()一直有值。

    2. AccessibilityNodeInfo

      AccessibllityNodeInfo类是含有链表结构的类(应该是这样称呼吧)。

      譬如:

      class AccessibilityNodeInfo implements Parcelable{
          ...
          public AccessibilityNodeInfo getParent() {
              throw new RuntimeException("Stub!");
          }
          
          public AccessibilityNodeInfo getChild(int index) {
              throw new RuntimeException("Stub!");
          }
          ...
      }
      

      AccessibllityNodeInfo页包含了对View的操作(仅包含有用到的部分):

      class AccessibilityNodeInfo implements Parcelable{
          ...
          //点击事件
          public static final int ACTION_CLICK = 16;
          //朝后滚动 performAction返回false即滑动到顶部
          public static final int ACTION_SCROLL_BACKWARD = 8192;
          //朝前滚动 performAction返回false即滑动到底部
          public static final int ACTION_SCROLL_FORWARD = 4096;
          ...
       public boolean performAction(int action) {
              throw new RuntimeException("Stub!");
          }
      
          public boolean performAction(int action, Bundle arguments) {
              throw new RuntimeException("Stub!");
          }
          ...
      }
      
    3. 全局事件

      public abstract class AccessibilityService extends Service {
          ...
          //返回键
          public static final int GLOBAL_ACTION_BACK = 1;
          //Home键
          public static final int GLOBAL_ACTION_HOME = 2;
          //菜单键
          public static final int GLOBAL_ACTION_RECENTS = 3;
          ...
          public final boolean performGlobalAction(int action) {
              throw new RuntimeException("Stub!");
          }
          ...
      }
      

    封装工具

    了解完AccessibilityService后,可以对Accessibility进行简单的封装,方便使用。

    1. 获取root节点

      由上可知,在寻找目标的root节点这方面,getRootInActiveWindow()会根据有优势,所以

      /**
       * @return 获取root节点
       */
      private AccessibilityNodeInfo findRoot() {
         return getRootInActiveWindow();
      }
      
    2. 获取所有当前Activity可见节点

      遍历多叉树

      /**
       * 遍历多叉树 全遍历
       */
      private List<AccessibilityNodeInfo> iteratorTree(AccessibilityNodeInfo parent) {
          List<AccessibilityNodeInfo> childList = new ArrayList<>();
          if (parent == null) return childList;
          for (int i = 0; i < parent.getChildCount(); i++) {
              AccessibilityNodeInfo child = parent.getChild(i);
              childList.add(child);
              if (child.getChildCount() > 0) {
                  //利用递归方法
                  childList.addAll(iteratorTree(child));
              }
          }
          return childList;
      }
      

      获取全部可见节点

      /**
       * 遍历当前页面所有节点
       */
      private List<AccessibilityNodeInfo> findAllView() {
          AccessibilityNodeInfo parent = findRoot();
          List<AccessibilityNodeInfo> list = new ArrayList<>();
          list.add(parent);
          list.addAll(iteratorTree(parent));
          return list;
      }
      
    3. 查找第一个匹配节点

      马后炮:在Activity中,可能存在ListViewRecyclerView滑动控件使得目标节点出现超出屏幕情况,所以我们应当先在当前可见区域内查找是否有目标节点,如果没有,则向下滚动,再次查找目标节点——直至滑动到屏幕底部。为防止当前界面并非从最顶部开始向下滑动,故应该再向上滑动到顶部一次。

      查找滑动组件代码如下:

      private static final List<String> SCROLL_CLASS_NAME_ARRAY = Arrays.asList(
                  "android.widget.ListView",
                  "android.support.v7.widget.RecyclerView",
                  "androidx.recyclerview.widget.RecyclerView"
      );
      /**
       * 查找可滑动的view
       */
      private List<AccessibilityNodeInfo> findCanScrollView() {
          List<AccessibilityNodeInfo> viewList = findAllView();
          List<AccessibilityNodeInfo> scrollViewList = new ArrayList<>();
          for (AccessibilityNodeInfo info : viewList) {
              if (info != null)
                  //info.isScrollable判断是否为可滚动控件
                  if (info.isScrollable()) {
                      //其中滚动控件还包含了spinner、viewpage等,而这些控件目前业务中并没有需要(且会影响业务内容),因此做了个限定
                      for (String scrollClassName : SCROLL_CLASS_NAME_ARRAY) {
                          if (scrollClassName.equals(info.getClassName().toString())) {
                              scrollViewList.add(info);
                              setLog("------------------------------------------------------------");
                              setLog("查询到可滑动组件" + info.getViewIdResourceName());
                              setLog("查询到可滑动组件" + info.getClassName());
                              setLog("------------------------------------------------------------");
                          }
                      }
                  }
          }
          return scrollViewList;
      }
      

      遍历多叉树,根据条件查寻当前可见部分的节点

      /**
       * 遍历多叉树 查询方法
       * 找到控件会抛出异常,异常中包含目标数据
       */
      private void findIteratorTree(AccessibilityNodeInfo parent, String text) throws StopIteratorException {
          if (parent != null)
              for (int i = 0; i < parent.getChildCount(); i++) {
                  AccessibilityNodeInfo child = parent.getChild(i);
                  if (child != null)
                      if (multiCriteriaMatching(child, text)) {
                          throw new StopIteratorException(child);
                      } else if (child.getChildCount() > 0) {
                          findIteratorTree(child, text);
                      }
              }
      }
      
      /**
       * 自定义报错 用于中断递归
       */
      private static class StopIteratorException extends RuntimeException {
          private AccessibilityNodeInfo info;
          private StopIteratorException(AccessibilityNodeInfo info) {
              this.info = info;
          }
          private AccessibilityNodeInfo getInfo() {
              return info;
          }
      }
      
      /**
       * 多条件匹配 multiCriteriaMatching
       *
       * @param info 目前包含getViewIdResourceName、getClassName、getText
       * @param text 匹配词
       * @return 返回匹配结果
       */
      private boolean multiCriteriaMatching(AccessibilityNodeInfo info, String text) {
          return text.equals(info.getViewIdResourceName())
                  || text.equals(info.getClassName() == null ? null : info.getClassName().toString())
                  || text.equals(info.getText() == null ? null : info.getText().toString());
      }
      

      查找第一个匹配节点代码如下:

      /**
       * @param text 要查找目标组件的标识 (className/resourceId/text)
       * @return 返回查找到的目标组件 null则表示查找失败
       */
      private AccessibilityNodeInfo findFirst(String text) {
          AccessibilityNodeInfo info = null;
          try {
              findIteratorTree(findRoot(), text);
      
              int searchNum = 0;
              boolean scroll = true, isForward = true;
              List<AccessibilityNodeInfo> scrollViewList = new ArrayList<>();
              while (scroll) {//利用关键字scroll和Exception跳出循环
                  //找不到控件,向下滑动继续找
                  if (scrollViewList.size() == 0) {
                      if (searchNum < MAX_SEARCH_NUM) {
                          ++searchNum;
                          scrollViewList.addAll(findCanScrollView());
                      } else {
                          //找不到可滑动组件
                          setLog("找不到可滑动组件,结束循环体,查找次数:" + MAX_SEARCH_NUM);
                          scroll = false;
                      }
                  }
      
                  if (scrollViewList.size() != 0) {
                      //如果查找到滑动组件,则滑动滑动组件
                      //下、上各滑动一次,确保找到控件
                      for (AccessibilityNodeInfo scrollViewInfo : scrollViewList) {
                          if (isForward) {
                              if (!scrollViewInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)) {
                                  isForward = false;//滑动到底部,修改滑动状态,再滑一次
                              }
                          } else {
                              if (!scrollViewInfo.performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)) {
                                  scroll = false;
                              }
                          }
                      }
                  }
                  //滑动后 睡眠 之后查找组件
                  SystemClock.sleep(TIME_SLEEP);
                  findIteratorTree(findRoot(), text);
              }
          } catch (StopIteratorException e) {
              info = e.getInfo();
              if (info != null) {
                  setLog("------------------------------------------------------------");
                  setLog("查询到目标组件id:" + info.getViewIdResourceName());
                  setLog("查询到目标组件text:" + info.getText());
                  setLog("查询到目标组件clsName:" + info.getClassName());
                  setLog("------------------------------------------------------------");
              }
          }
          return info;
      }
      

      补充说明:(可不看)在查看AccessibilityNodeInfo类中有如下方法

      class AccessibilityNodeInfo implements Parcelable{
          ...
          public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String text) {
              throw new RuntimeException("Stub!");
          }
      
          public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByViewId(String viewId) {
              throw new RuntimeException("Stub!");
          }
          ...
      }
      

      由此联想到findIteratorTree(AccessibilityNodeInfo parent, String text)或许可以改成:(以下代码未尝试,只是临时想到的改进方案)

      private AccessibilityNodeInfo findIteratorTree(AccessibilityNodeInfo parent, String text){
          List<AccessibilityNodeInfo> infoList = new ArrayList<>();
          infoList.addAll(parent.findAccessibilityNodeInfosByText(text));
          if(info.size() == 0){
              infoList.addAll(parent.findAccessibilityNodeInfosByViewId(text));
          }
          AccessibilityNodeInfo info = null;
          if(infoList.size() > 0){
           info = infoList.get(0);
          }
          return info;
      }
      
    4. 点击事件

      找到目标节点后,自然是要对目标节点进行操作——这里仅封装点击事件

      /**
       * 点击事件
       *
       * @param info 要点击的目标View
       * @return 点击结果(成功/失败)
       */
      private boolean clickView(AccessibilityNodeInfo info) {
          if (info != null)
              if (info.isClickable()) {
                  info.performAction(AccessibilityNodeInfo.ACTION_CLICK);
                  return true;
              } else {
                  AccessibilityNodeInfo parent = info.getParent();
                  if (parent != null) {
                      boolean b = clickView(parent);
                      parent.recycle();
                      return b;
                  }
              }
          return false;
      }
      

      将查询节点与点击节点组合起来就是:

      /**
       * 点击事件(点击本身)
       *
       * @param text 要点击的目标节点标识
       * @return 点击结果(true/false)
       */
      public boolean clickFirstView(String text) {
          AccessibilityNodeInfo info = findFirst(text);
          if (info == null) {
              setLog(text + "找不到匹配的控件");
              return false;
          }
          return clickView(info);
      }
      

      那如果是CheckBox之类的点击事件(如下图)应该怎么处理?


      1.jpg

      首先转化为查询该CheckBox的同级节点,再通过该同级节点反查父节点,之后再查询父节点中的子节点中包含isCheckable的节点:

      private AccessibilityNodeInfo findBrotherCheckBox(AccessibilityNodeInfo child) {
          AccessibilityNodeInfo checkBoxInfo = null;
          AccessibilityNodeInfo parent = child.getParent();
          if (parent != null) {
              List<AccessibilityNodeInfo> infoList = iteratorTree(parent);
              setLog("------------------------------------------------------------");
              for (AccessibilityNodeInfo info : infoList) {
                  if (info.isCheckable()) {
                      checkBoxInfo = info;
                      setLog("------------------------------------------------------------");
                      setLog("查询到目标组件id:" + info.getViewIdResourceName());
                      setLog("查询到目标组件text:" + info.getText());
                      setLog("查询到目标组件clsName:" + info.getClassName());
                      setLog("------------------------------------------------------------");
                      break;
                  }
              }
              setLog("------------------------------------------------------------");
          }
          return checkBoxInfo;
      }
      

      CheckBox节点中有isChecked字段用于判断是( true )否( false )选中,因此有:

      /**
       * checkBox的点击事件(带选中状态)
       *
       * @param brotherText 要点击目标的兄弟节点标识
       * @param setChecked  将checkBox的状态修改为true/false
       * @return 返回点击事件触发结果(true/false)
       */
      public boolean clickCheckBox(String brotherText, boolean setChecked) {
          AccessibilityNodeInfo brotherInfo = findFirst(brotherText);
          if (brotherInfo == null) {
              setLog(brotherText + "找不到匹配的控件");
              return false;
          }
          AccessibilityNodeInfo checkBoxInfo = findBrotherCheckBox(brotherInfo);
          if (checkBoxInfo == null) {
              setLog(brotherText + "找不到匹配的checkBox控件");
              return false;
          }
          if (checkBoxInfo.isChecked() != setChecked) {
              return clickView(checkBoxInfo);
          } else {
              setLog("checkBox控件为目标状态,不点击");
          }
          return false;
      }
      
    5. 补充

      静态变量、日志与返回

      //静态变量
      private static final int TIME_SLEEP = 300;//单位毫秒
      private static final int MAX_SEARCH_NUM = 20;//查找循环体次数
      
      //日志
      protected abstract boolean isDebugger();
      private void setLog(String msg) {
          if (isDebugger()) {
              String tag = "AManagerLog";
              Log.i(tag, msg);
          }
      }
      
      //系统返回键
      public void goBack() {
          performGlobalAction(GLOBAL_ACTION_BACK);
      }
      

      以上就是整个工具的封装了

    如何使用

    1. 辅助功能触发点的设置

      辅助功能顾名思义,就是辅助用户使用当前App,甚至可以延伸为使用当前手机。比如:

      • 监测App接收到的通知消息,根据消息对手机进行辅助操作
      • 监测文件下载状态,根据下载状态进行自动安装、打开等操作
      • 当用户点击了某个位置时,根据点击的位置做出反馈
      • 为用户自动开启相关权限等
      • ...

      这时候:监测到App接收通知消息和监测到文件下载状态即为触发点;用户点击事件即为出发点;而开启权限,则可以是在辅助功能权限开启的时候。

      触发点为辅助功能权限开启:辅助功能权限开启的时候会调用初始化Service方法:onServiceConnected(),这里可以作为一个监测点,利用sendBroadcast(Intent intent)服务启动的消息广播出去。而广播接收器就是真实的触发点

      触发点为某种状态

      if(该状态可回调){
          if(辅助功能权限开启){
              直接执行辅助功能
          }else{
              弹出无障碍设置页面,引导用户开启辅助功能权限-触发点转为辅助功能权限开启时
          }
      }else{
          监测状态改变引发的界面变化
          ...
      }
      

      触发点为用户点击事件:如果点击的是自己的App,在点击事件触发辅助功能即可。如果点击的不为自己的App,可以在onAccessibilityEvent(AccessibilityEvent event)触发。

    2. 辅助功能执行

      辅助功能是一个富有想象力的功能,TA的执行能力取决于脑洞。比如模拟用户点击,让用户的操作更快捷(比如自动抢红包、一键连招等)或者取代繁琐操作(自动下载并安装等)甚至也可以是智能分词翻译(锤子BigBang)等等...

      这里以“取代繁琐操作”为例——征得用户同意后,替用户勾选一些无法直接申请的权限:

      任务:AssignmentEntity

      public class AssignmentEntity {
          private Queue<StepEntity> queue = new LinkedList();  //步骤队列
          private String name;                             //任务名称
      
          public AssignmentEntity() {
          }
      
          public String getName() {
              return this.name;
          }
      
          public AssignmentEntity setName(String name) {
              this.name = name;
              return this;
          }
      
          public Queue<StepEntity> getQueue() {
              return this.queue;
          }
      
          /**
           * 添加步骤
           **/
          public AssignmentEntity addStep(StepEntity entity) {
              this.queue.add(entity);
              return this;
          }
      }
      

      具体步骤:StepEntity

      public class StepEntity {
          public static final int TYPE_INTENT = 0;    //页面跳转
          public static final int TYPE_CLICK = 1;     //点击事件
          public static final int TYPE_CHECKED = 2;   //选中事件
          public static final int TYPE_BACK = 3;      //返回事件
          private String name;      //目标控件名称
          private int type;         //事件类型
          private boolean checked;  //选中事件-true:选中-false:反选
          private Intent intent;    //跳转事件-Intent
      
          public StepEntity() {
          }
      
          public String getName() {
              return this.name;
          }
      
          public void setName(String name) {
              this.name = name;
          }
      
          public int getType() {
              return this.type;
          }
      
          public void setType(int type) {
              this.type = type;
          }
      
          public Intent getIntent() {
              return this.intent;
          }
      
          public void setIntent(Intent intent) {
              this.intent = intent;
          }
      
          public boolean isChecked() {
              return this.checked;
          }
      
          public void setChecked(boolean checked) {
              this.checked = checked;
          }
      }
      

      步骤辅助类StepHelper

      public class StepHelper {
          public StepHelper() {
          }
      
          /**
           * 跳转事件
           **/
          public static StepEntity intentStep(Intent intent) {
              return getStep("页面跳转", 0, intent, (Boolean)null);
          }
      
          /**
           * 点击事件
           **/
          public static StepEntity clickStep(String text) {
              return getStep(text, 1, (Intent)null, (Boolean)null);
          }
      
          /**
           * 选中事件
           **/
          public static StepEntity checkStep(String text, boolean setChecked) {
              return getStep(text, 2, (Intent)null, setChecked);
          }
      
          /**
           * 返回事件
           **/
          public static StepEntity backStep() {
              return getStep("返回上一页", 3, (Intent)null, (Boolean)null);
          }
      
          private static StepEntity getStep(String name, int type, @Nullable Intent intent, @Nullable Boolean setChecked) {
              StepEntity step = new StepEntity();
              step.setName(name);
              step.setType(type);
              if (0 == type) {
                  if (intent == null) {
                      throw new IllegalArgumentException("代码错误,Type为TYPE_INTENT时Intent参数不能为空");
                  }
      
                  step.setIntent(intent);
              } else if (2 == type) {
                  if (setChecked == null) {
                      throw new IllegalArgumentException("代码错误,Type为TYPE_CHECKED时setChecked参数不能为空");
                  }
      
                  boolean b = setChecked;
                  step.setChecked(b);
              }
      
              return step;
          }
      }
      

      添加任务示例:

      public class AssignmentFactory {
          ...
          public static Queue<AssignmentEntity> create() {
              Queue<AssignmentEntity> queue = new LinkedList<>();
              if (RomUtils.isHuawei()) {
                  queue.addAll(HuaweiFactory.create());
              } else if (RomUtils.isMiui()) {
                  queue.addAll(XiaomiFactory.create());
              } else if (RomUtils.isOppo()) {
      
              } else if (RomUtils.isMeizu()) {
                  queue.addAll(MeizuFactory.create());
              }
              return queue;
          }
          ...
              
          static class HuaweiFactory {
              
              private static int getVersion() {
                  int version = -1;
                  try {
                      String systemProperty = RomUtils.getSystemProperty("ro.build.version.emui");
                      if (systemProperty != null) {
                          String trim = systemProperty.replace("EmotionUI", "").replace("_", "").trim();
                          if (trim.contains(".")) {
                              trim = trim.substring(0, trim.indexOf("."));
                          }
                          version = Integer.valueOf(trim);
                      }
                  } catch (Exception ignored) {
      
                  }
                  return version;
              }
      
              static Queue<AssignmentEntity> create() {
                  L.i("手机版本号:" + getVersion()); //目前适配10
                  Queue<AssignmentEntity> queue = new LinkedList<>();
                  queue.add(ignoreBatteryOptimization());
                  queue.addAll(newMessageNotification());
                  queue.add(selfStarting());
                  return queue;
              }
      
              //忽略电池优化
              private static AssignmentEntity ignoreBatteryOptimization() {
                  AssignmentEntity assignment = new AssignmentEntity();
                  assignment.setName("忽略电池优化");
      
                  assignment.addStep(StepHelper.intentStep(IntentUtils.hightPowerManger()))
                          .addStep(StepHelper.clickStep("不允许"))
                          .addStep(StepHelper.clickStep("所有应用"))
                          .addStep(StepHelper.clickStep(BaseApplication.getInstance().getAppName()))
                          .addStep(StepHelper.clickStep("不允许"))
                          .addStep(StepHelper.clickStep("确定"))
                          .addStep(StepHelper.backStep());
      
                  return assignment;
              }
      
              //新消息通知
              private static Queue<AssignmentEntity> newMessageNotification() {
                  AssignmentEntity ae1 = new AssignmentEntity();
                  ae1.setName("新消息通知");
                  AssignmentEntity ae2 = new AssignmentEntity();
                  ae2.setName("通知锁屏显示");
      
                  ae1.addStep(StepHelper.intentStep(IntentUtils.huaweiNotification()))
                          .addStep(StepHelper.clickStep(BaseApplication.getInstance().getAppName()))
                          .addStep(StepHelper.checkStep("允许通知", true))
                          .addStep(StepHelper.backStep());
                  ae2.addStep(StepHelper.clickStep("锁屏通知"))
                          .addStep(StepHelper.clickStep("显示所有通知"))
                          .addStep(StepHelper.clickStep("更多通知设置"))
                          .addStep(StepHelper.checkStep("通知亮屏提示", true))
                          .addStep(StepHelper.backStep())
                          .addStep(StepHelper.backStep());
      
                  Queue<AssignmentEntity> queue = new LinkedList<>();
                  queue.add(ae1);
                  queue.add(ae2);
                  return queue;
              }
      
              //自启动
              private static AssignmentEntity selfStarting() {
                  AssignmentEntity assignment = new AssignmentEntity();
                  assignment.setName("自启动");
      
                  assignment.addStep(StepHelper.intentStep(IntentUtils.huaweiStartupNormalApp()))
                          .addStep(StepHelper.checkStep(BaseApplication.getInstance().getAppName(), true))
                          .addStep(StepHelper.checkStep(BaseApplication.getInstance().getAppName(), false))
                          .addStep(StepHelper.checkStep("允许自启动", true))
                          .addStep(StepHelper.checkStep("允许关联启动", true))
                          .addStep(StepHelper.checkStep("允许后台活动", true))
                          .addStep(StepHelper.clickStep("确定"))
                          .addStep(StepHelper.backStep());
      
                  return assignment;
              }
          }
          
          ...
      }
      

      执行任务示例:

      public class AssignmentFactory {
          ...
          public static void run(Activity activity, Queue<AssignmentEntity> queue) {
              L.i("队列开始执行");
              int assignmentSize = queue.size();
      
              CompositeDisposable co = new CompositeDisposable();
              Disposable subscribe = Observable.create(
                      (ObservableOnSubscribe<AssignmentEntity>) emitter -> {
                          int progressSize = 0;
                          for (int i = 0; i < assignmentSize; i++) {
                              AssignmentEntity entity = queue.poll();
                              if (entity != null) {
                                  Queue<StepEntity> stepQueue = entity.getQueue();
                                  if (stepQueue != null) {
                                      int stepSize = stepQueue.size();
                                      for (int j = 0; j < stepSize; j++) {
                                          progressSize++;
                                          emitter.onNext(entity);
                                      }
                                  }
                              }
                          }
                          //FloatWindowView.getInstance().progressBar.setMax(progressSize);
                          emitter.onComplete();
                      })
                      .subscribeOn(Schedulers.io())//执行在io线程
                      .observeOn(Schedulers.io())//回调在io线程  //主线程阻塞将无法更新ui AndroidSchedulers.mainThread()
                      .subscribe(
                              assignment -> {
                                  //FloatWindowView.getInstance().progressAdd();
                                  //L.i("当前进度:" + assignment.getName() + "===" + FloatWindowView.getInstance().getProgress());
                                  poll(activity, assignment.getQueue().poll());
                              },
                              throwable -> L.e("", throwable),
                              () -> //FloatWindowView.getInstance().stopFloatWindow()
                      );
              co.add(subscribe);
          }
      
          private static void poll(Activity activity, StepEntity poll) {
              if (poll == null) return;
              switch (poll.getType()) {
                  case StepEntity.TYPE_INTENT://跳转事件
                      activity.startActivity(poll.getIntent());
                      break;
                  case StepEntity.TYPE_CLICK://点击事件
                      MyAccessibilityService.getInstance().clickFirstView(poll.getName());
                      break;
                  case StepEntity.TYPE_CHECKED://选中事件
                      MyAccessibilityService.getInstance().clickCheckBox(poll.getName(), poll.isChecked());
                      break;
                  case StepEntity.TYPE_BACK://返回事件
                      MyAccessibilityService.getInstance().goBack();
                      break;
              }
              SystemClock.sleep(POST_DELAY_MILLIS);
          }
          ...
      }
      

      链接

      完整demo:Github

    相关文章

      网友评论

        本文标题:Android辅助功能开发——一键优化

        本文链接:https://www.haomeiwen.com/subject/ynnrjktx.html