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

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

上一篇文章【玩转Android无障碍】之小试牛刀已经简单介绍了一下实现一个功能需要的步骤,正所谓小试牛刀之后就可以大干一场了。本章我们来介绍一下如何获取微信好友列表吧。

需求分析

  • 先来分析一下微信好友列表在哪里,具体路径是什么。首先需要打开微信首页,点击底部导航【通讯录】tab,就可以看到通讯录列表了

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

具体实现

1、打开微信首页
kotlin 复制代码
//直接通过启动微信首页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
  • 要想点击【通讯录】这个节点就需要找到他的节点信息,使用节点速查工具,在微信首页打印一下所有节点信息,删除了一些无关的节点数据,底导节点内容如下
ini 复制代码
|       \--- 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就可以了。
kotlin 复制代码
    //获取【通讯录】的节点
    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、获取通讯录页面的节点信息
  • 这里仍然用我们的节点速查工具查看我的页面的节点信息
ini 复制代码
+--- 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就是好友的微信昵称。
kotlin 复制代码
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每次滑动取到的子节点数据是有可能重复的,可能是上一页的最后一条,这就会出现数据重复的问题,所有需要对取出的数据做一下过滤,如果已经取出了就不添加到列表中

kotlin 复制代码
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循环,咦?哪里不对吗,无奈,刚开始游戏只能暂时挂机了,太对不起我的队友了🙏🏻。

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

kotlin 复制代码
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
}
  • 好在队友给力,依然在严防死守高地,游戏还没有结束,最终在我不懈的努力下还是输掉了游戏,成功收获几个举报信息。。。

最后

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

预告

相关推荐
Estar.Lee22 分钟前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯1 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!4 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟5 小时前
Android音频采集
android·音视频
小白也想学C6 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程6 小时前
初级数据结构——树
android·java·数据结构
闲暇部落8 小时前
‌Kotlin中的?.和!!主要区别
android·开发语言·kotlin
诸神黄昏EX10 小时前
Android 分区相关介绍
android
大白要努力!11 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle