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

最后

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

预告

相关推荐
CCTV果冻爽1 小时前
Android 源码集成可卸载 APP
android
秋月霜风2 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教3 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python
编程乐学4 小时前
基于Android Studio 蜜雪冰城(奶茶饮品点餐)—原创
android·gitee·android studio·大作业·安卓课设·奶茶点餐
problc5 小时前
Android中的引用类型:Weak Reference, Soft Reference, Phantom Reference 和 WeakHashMap
android
IH_LZH5 小时前
Broadcast:Android中实现组件及进程间通信
android·java·android studio·broadcast
去看全世界的云5 小时前
【Android】Handler用法及原理解析
android·java
机器之心6 小时前
o1 带火的 CoT 到底行不行?新论文引发了论战
android·人工智能
机器之心6 小时前
从架构、工艺到能效表现,全面了解 LLM 硬件加速,这篇综述就够了
android·人工智能
AntDreamer6 小时前
在实际开发中,如何根据项目需求调整 RecyclerView 的缓存策略?
android·java·缓存·面试·性能优化·kotlin