【玩转Android无障碍】之微信老友导出

上一篇文章【玩转Android无障碍】之小试牛刀 现已简略介绍了一下完结一个功用需求的步骤,正所谓小试牛刀之后就能够大干一场了。本章咱们来介绍一下怎么获取微信老友列表吧。

需求剖析

  • 先来剖析一下微信老友列表在哪里,详细途径是什么。首要需求翻开微信主页,点击底部导航【通讯录】tab,就能够看到通讯录列表了

大约步操作流程是:微信主页 → 点击【通讯录】→ 获取页面节点信息 → 找到老友列表的父布局 → 循环解析每个子节点

详细完结

1、翻开微信主页
//直接经过发动微信主页Activity就能够翻开微信了
fun Context.goToWx() = Intent(Intent.ACTION_MAIN)
    .apply {
        addCategory(Intent.CATEGORY_LAUNCHER)
        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
        component = ComponentName(
            "com.tencent.mm",
            "com.tencent.mm.ui.LauncherUI",
        )
    }
    .apply(::startActivity)
2、点击【通讯录】tab
  • 要想点击【通讯录】这个节点就需求找到他的节点信息,使用节点速查东西,在微信主页打印一下一切节点信息,删除了一些无关的节点数据,底导节点内容如下
|       \--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/fj3 → description =  → isClickable = false → isScrollable = false → isEditable = false
|        \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|          +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kd_ → description =  → isClickable = true → isScrollable = false → isEditable = false
|          |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/f1f → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    |  +--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/f2a → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    |  \--- className = android.widget.TextView → text = 1 → id = com.tencent.mm:id/l0c → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    \--- className = android.widget.TextView → text = 微信 → id = com.tencent.mm:id/f2s → description =  → isClickable = false → isScrollable = false → isEditable = false
|          +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kd_ → description =  → isClickable = true → isScrollable = false → isEditable = false
|          |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/f1f → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/f2a → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    \--- className = android.widget.TextView → text = 通讯录 → id = com.tencent.mm:id/f2s → description =  → isClickable = false → isScrollable = false → isEditable = false
|          +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kd_ → description =  → isClickable = true → isScrollable = false → isEditable = false
|          |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/f1f → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/f2a → description =  → isClickable = false → isScrollable = false → isEditable = false
|          |    \--- className = android.widget.TextView → text = 发现 → id = com.tencent.mm:id/f2s → description =  → isClickable = false → isScrollable = false → isEditable = false
|          \--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kd_ → description =  → isClickable = true → isScrollable = false → isEditable = false
|            \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/f1f → description =  → isClickable = false → isScrollable = false → isEditable = false
|              +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|              |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/f2a → description =  → isClickable = false → isScrollable = false → isEditable = false
|              \--- className = android.widget.TextView → text = 我 → id = com.tencent.mm:id/f2s → description =  → isClickable = false → isScrollable = false → isEditable = false
  • 能够看到底部导航tab有四个,这四个tab的节点ID(com.tencent.mm:id/f2s)都是一样的,说明是复用的,没有仅有ID咱们就不能准确查找到当时节点,持续往上查找发现他们有一个公共的父布局,而且父布局的节点ID(com.tencent.mm:id/fj3)是仅有的,那么咱们就能够先获取仅有的父布局,在父布局中查找匹配到对应的text就能够了。
    //获取【通讯录】的节点
    val bottomNavId = "com.tencent.mm:id/fj3"
    val findContactTabNode = wxAccessibilityService?.rootInActiveWindow?.findNodesById(bottomNavId)?.firstOrNull { it.text == "通讯录" }
    //点击【我】的tab
    findContactTabNode.click()
    //click()是封装好的点击扩展办法,假如当时节点是不行点击的,就会往上查找父节点,让父节点调用click()办法,顺次递归一般都能找到一个能够点击的父节点的
    fun AccessibilityNodeInfo?.click(): Boolean {
        this ?: return false
        return if (isClickable) {
            performAction(AccessibilityNodeInfo.ACTION_CLICK)
        } else {
            parent?.click() == true
        }
    }
3、获取通讯录页面的节点信息
  • 这儿依然用咱们的节点速查东西查看我的页面的节点信息
+--- className = androidx.recyclerview.widget.RecyclerView → text =  → id = com.tencent.mm:id/js → description =  → isClickable = false → isScrollable = true → isEditable = false
|  +--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/eau → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |    \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/eb7 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/eap → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  +--- className = android.widget.ImageView → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/br8 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |        \--- className = android.widget.TextView → text = 新的朋友 → id = com.tencent.mm:id/knx → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/br0 → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  |    \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/ki → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kj → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  +--- className = android.widget.ImageView → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/br8 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |        \--- className = android.widget.TextView → text = 仅谈天的朋友 → id = com.tencent.mm:id/kk → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/br0 → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  |    \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/ki → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kj → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  +--- className = android.widget.ImageView → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/br8 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |        \--- className = android.widget.TextView → text = 群聊 → id = com.tencent.mm:id/kk → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/br0 → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  |    \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/ki → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/kj → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  +--- className = android.widget.ImageView → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/br8 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |        \--- className = android.widget.TextView → text = 标签 → id = com.tencent.mm:id/kk → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/br0 → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  |    \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/a_u → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/a_t → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  +--- className = android.widget.ImageView → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      |  \--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/br8 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |      \--- className = android.widget.LinearLayout → text =  → id = com.tencent.mm:id/aev → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |        \--- className = android.widget.TextView → text = 公众号 → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |    \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |  +--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/bqp → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  |  \--- className = android.widget.TextView → text = A → id = com.tencent.mm:id/bqq → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |  \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |    \--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/bqy → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |      \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |        +--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/a27 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |        \--- className = android.widget.TableLayout → text =  → id = com.tencent.mm:id/hg2 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |          \--- className = android.widget.TableRow → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |            \--- className = android.widget.TextView → text = A-马可波罗 → id = com.tencent.mm:id/hg4 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |   \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = true → isScrollable = false → isEditable = false
|  |     \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |       \--- className = android.widget.RelativeLayout → text =  → id = com.tencent.mm:id/bqy → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |         \--- className = android.widget.LinearLayout → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |           +--- className = android.widget.ImageView → text =  → id = com.tencent.mm:id/a27 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |           \--- className = android.widget.TableLayout → text =  → id = com.tencent.mm:id/hg2 → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |             \--- className = android.widget.TableRow → text =  → id =  → description =  → isClickable = false → isScrollable = false → isEditable = false
|  |               \--- className = android.widget.TextView → text = A-王昭君 → id = com.tencent.mm:id/hg4 → description =  → isClickable = false → isScrollable = false → isEditable = false
  • 经过剖析上边的节点信息,能够发现这个页面是用RecyclerView做的,他的节点ID(com.tencent.mm:id/js)是仅有的,列表中老友节点是TextView,ID是com.tencent.mm:id/hg4,所以咱们找到recyclerView然后遍历他的子节点中ID为com.tencent.mm:id/hg4的节点然后取出来text就是老友的微信昵称。
fun AccessibilityService?.findChildNodes(parentViewId: String, childViewId: String): List<AccessibilityNodeInfo> {
    this ?: return listOf()
    val rootNode = rootInActiveWindow
    val parentNode: AccessibilityNodeInfo =
        rootNode.findNodesById(parentViewId).firstOrNull() ?: return listOf()
    val findList = mutableListOf<AccessibilityNodeInfo>()
    val size = parentNode.childCount
    if (size <= 0) return emptyList()
    for (index in 0 until size) {
        parentNode.getChild(index).findNodesById(childViewId).firstOrNull()?.let {
            Log.d("printNodeInfo", "当时页parentNode可见的元素=======${it.text}")
            findList.add(it)
        }
    }
    return findList
}
  • 经过咱们封装好的打印节点的办法就能够找到当时RecyclerView中可见的子节点的数据了,由于RV有缓存的战略,一切不会一下子把全部老友列表都找到的,这个时分就需求咱们经过滑动屏幕去找到下一屏中的数据,顺次类推,每滑动一下就获取一次数据,当滑动都低的时分就全部获取完了
4、主动滑动屏幕
  • 滑动屏幕的前提是当时布局支撑翻滚,咱们这个布局是RecyclerView所以是能够翻滚的,控制节点翻滚能够调用performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)就能够滑动到下一屏了。

  • 这儿在真正完结的时分其实有很多细节需求处理的,调用节点的ACTION_SCROLL_FORWARD每次是滑动整一屏的,而RecyclerView每次滑动取到的子节点数据是有或许重复的,或许是上一页的最终一条,这就会呈现数据重复的问题,一切需求对取出的数据做一下过滤,假如现已取出了就不添加到列表中

suspend fun AccessibilityService?.findAllChildByScroll(
    parentViewId: String,
    childViewId: String,
): List<AccessibilityNodeInfo> {
    this ?: return listOf()
    val rootNode = rootInActiveWindow
    val list = mutableListOf<AccessibilityNodeInfo>()
    val finds = findAllChildByFilter(parentViewId, childViewId) { filter ->
        //倒叙查找能够提升查找功率,由于新增的数据是在列表后边的,比较上次插入的数据和即即将插入的数据就能够了
        //过滤掉现已添加过的节点
        list.findLast { it.text.default() == filter.text.default() } != null
    }
    list.addAll(finds)
    val parentNode = rootNode.findNodeById(parentViewId) ?: return list
    while (parentNode.isScrollable) {
        parentNode.scrollForward()
        delay(500)//时间太短的话有时分会获取不到节点信息
        val findNextNodes = findAllChildByFilter(parentViewId, childViewId) { filter ->
            list.findLast { it.text.default() == filter.text.default() } != null
        }
        list.addAll(findNextNodes)
    }
    return list
}
  • 上面的代码现已处理了翻滚查找的问题,可是什么时分去终止翻滚呐,列表总会翻滚到最底部的,一切咱们需求一个跳出while的条件,刚开始做的时分我发现微信老友列表最底部有个XX个朋友的元素,能够在每次翻滚完结之后判别一次当时屏幕内有没有XX个朋友这个节点就能够了,由于XX个朋友这个节点的ID也是仅有的,也是在页面最底部的。尽管这种办法能够处理眼下的问题,可是只适用于微信老友列表这种特别的状况,要是某个页面列表最终没有相似这样的标记怎么办,一切仍是要持续寻找处理办法。

  • 已然需求寻找列表是否翻滚到底的判别办法,只能持续回到方才循环的地方剖析了,已然翻滚是否到底没有直接的判别方法,那么咱们能够比较两次翻滚结果,首要想到的是假如当时翻滚后的取到的最终一个元素是上次翻滚的最终一个元素是不是就说明翻滚到底了呐,由于翻滚到底后持续翻滚的话取出来的数据是上次一样的。感觉发现了新大陆,赶紧预备在代码中实验一番,三下五去二干完代码编译项目,等待打印获取的老友列表,发现数据全部获取到了,哈哈,搞定,然后立马翻开王者选好英豪预备排位一局大杀四方歇息一下。无意间又瞄了一下控制台,whf。。。 怎么还在履行循环句子,翻滚完毕后并没有跳出while循环,咦?哪里不对吗,无法,刚开始游戏只能暂时挂机了,太对不住我的队友了。

  • 经过认真剖析发现了问题所在,由于咱们现已写好了每次翻滚获取到的数据都是经过过滤的,那么当翻滚后获取到的数据是空的话,有用空列表的数据跟上次数据对比肯定永久都不行能相等的,突然灵光一现,已然每次翻滚获取到的数据都是经过过滤的,是不是就意味着当翻滚后取到的数据是空的时分就说明现已全部提取玩了吗,答案确实这样的,也彻底无需对数据对什么对比了。

suspend fun AccessibilityService?.findAllChildByScroll(
    parentViewId: String,
    childViewId: String,
): List<AccessibilityNodeInfo> {
    this ?: return listOf()
    val rootNode = rootInActiveWindow
    val list = mutableListOf<AccessibilityNodeInfo>()
    val finds = findAllChildByFilter(parentViewId, childViewId) { filter ->
        //倒叙查找能够提示查找功率,由于新增的数据是在列表后边的
        list.findLast { it.text.default() == filter.text.default() } != null
    }
    list.addAll(finds)
    val parentNode = rootNode.findNodeById(parentViewId) ?: return list
    var isStop = false
    while (parentNode.isScrollable && !isStop) {
        parentNode.scrollForward()
        delay(500)//时间太短的话有时分会获取不到节点信息
        val findNextNodes = findAllChildByFilter(parentViewId, childViewId) { filter ->
            list.findLast { it.text.default() == filter.text.default() } != null
        }
        isStop = findNextNodes.isEmpty()
        if (isStop) break
        list.addAll(findNextNodes)
    }
    return list
}
  • 好在队友给力,依然在严防死守高地,游戏还没有完毕,最终在我不懈的尽力下仍是输掉了游戏,成功收成几个告发信息。。。

最终

  • 咱们获取到的老友列表是有几个特别的老友的,一般老友列表里边会有自己微信团队文件传输助手,这三条数据对咱们其实是没啥用的,应该过滤掉,上一篇咱们现已取到了自己的微信昵称,一切用在这儿再好不过了。

预告

  • 已然现已拿到了老友列表,是不是就能够对每个老友做一些针对性的动作,下一篇【玩转Android无障碍】之微信老友状况查看《假转账方法》让咱们期待一下怎么对获取到的老友进行检测吧