├── .gitignore
├── README.md
├── accessibility
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── android
│ └── accessibility
│ └── ext
│ ├── AsyncAccessibilityService.kt
│ ├── ContextExt.kt
│ ├── FormatExt.kt
│ ├── acc
│ ├── AccessibilityEventExt.kt
│ ├── AccessibilityNodeActionExt.kt
│ ├── AccessibilityNodeInfoExt.kt
│ ├── AccessibilityServiceExt.kt
│ ├── GestureExt.kt
│ └── PrintNodeInfo.kt
│ ├── data
│ └── NodeWrapper.kt
│ └── task
│ ├── RetryTask.kt
│ ├── RetryTaskWithLog.kt
│ └── i
│ └── ITaskTracker.kt
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── lygttpod
│ │ └── android
│ │ └── auto
│ │ ├── App.kt
│ │ ├── MainActivity.kt
│ │ ├── MainFragment.kt
│ │ ├── ktx
│ │ ├── ContextKtx.kt
│ │ └── LogUtils.kt
│ │ └── update
│ │ ├── UpdateApp.kt
│ │ └── VersionTipDialog.kt
│ └── res
│ ├── drawable
│ └── bg_tip_dialog.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── content_main.xml
│ ├── dialog_version_tip.xml
│ └── fragment_main.xml
│ ├── mipmap-xxxhdpi
│ └── icon_logo.png
│ ├── navigation
│ └── nav_graph.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
│ └── xml
│ ├── backup_rules.xml
│ └── data_extraction_rules.xml
├── auto-ad
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── lygttpod
│ │ └── android
│ │ └── auto
│ │ └── ad
│ │ ├── FuckAdManager.kt
│ │ ├── accessibility
│ │ └── FuckADAccessibility.kt
│ │ ├── data
│ │ ├── FilterKeywordData.kt
│ │ └── FuckAdAppsData.kt
│ │ ├── ktx
│ │ ├── ContextExt.kt
│ │ └── GsonExt.kt
│ │ ├── task
│ │ └── FuckADTask.kt
│ │ └── ui
│ │ ├── FuckAdMainFragment.kt
│ │ └── adapter
│ │ └── AppConfigAdapter.kt
│ └── res
│ ├── layout
│ ├── dialog_app_config.xml
│ ├── dialog_filter_keyword.xml
│ ├── fragment_fuck_ad_main.xml
│ └── item_app.xml
│ ├── values
│ └── strings.xml
│ └── xml
│ └── fuck_ad_accessible_service_config.xml
├── auto-tools
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── assets
│ └── auto_tools_index.html
│ ├── java
│ └── com
│ │ └── lygttpod
│ │ └── android
│ │ └── auto
│ │ └── tools
│ │ ├── AutoTools.kt
│ │ ├── accessibility
│ │ └── AutoToolsAccessibility.kt
│ │ ├── ktx
│ │ ├── ContextExt.kt
│ │ └── IpAddress.kt
│ │ ├── manager
│ │ ├── ContentManger.kt
│ │ └── FloatManager.kt
│ │ ├── service
│ │ └── AutoToolsService.kt
│ │ ├── ui
│ │ └── ToolsMainFragment.kt
│ │ └── utils
│ │ └── SPUtils.kt
│ └── res
│ ├── layout
│ ├── fragment_tools_main.xml
│ └── widget_float_print.xml
│ ├── values
│ └── strings.xml
│ └── xml
│ └── tools_accessible_service_config.xml
├── auto-wx
├── .gitignore
├── build.gradle
├── consumer-rules.pro
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── lygttpod
│ │ └── android
│ │ └── auto
│ │ └── wx
│ │ ├── adapter
│ │ └── FriendInfoAdapter.kt
│ │ ├── data
│ │ ├── NodeInfo.kt
│ │ └── WxUserInfo.kt
│ │ ├── em
│ │ ├── CheckStatus.kt
│ │ └── FriendStatus.kt
│ │ ├── helper
│ │ ├── FriendStatusHelper.kt
│ │ ├── HBTaskHelper.kt
│ │ ├── TaskByGroupHelper.kt
│ │ └── TaskHelper.kt
│ │ ├── ktx
│ │ └── FormatExt.kt
│ │ ├── page
│ │ ├── IPage.kt
│ │ ├── WXChattingPage.kt
│ │ ├── WXContactInfoPage.kt
│ │ ├── WXContactPage.kt
│ │ ├── WXHBPage.kt
│ │ ├── WXHomePage.kt
│ │ ├── WXMinePage.kt
│ │ ├── WXRemittancePage.kt
│ │ └── group
│ │ │ ├── WXCreateGroupPage.kt
│ │ │ ├── WXGroupChatPage.kt
│ │ │ ├── WXGroupInfoPage.kt
│ │ │ └── WXGroupManagerPage.kt
│ │ ├── service
│ │ └── WXAccessibility.kt
│ │ ├── ui
│ │ └── WxMainFragment.kt
│ │ └── version
│ │ ├── WeChatNodesImpl.kt
│ │ └── WeChatVersionSupport.kt
│ └── res
│ ├── layout
│ ├── fragment_wx_main.xml
│ ├── item_friend.xml
│ └── item_version.xml
│ ├── values
│ ├── colors.xml
│ └── strings.xml
│ └── xml
│ └── wx_accessible_service_config.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── we_chat_tools.jks
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | local.properties
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## AndroidAuto
2 | Android自动化工具
3 |
4 | ### [下载demo](https://www.pgyer.com/androidAuto)体验
5 |
6 | ### PC端浏览器效果
7 | 
8 |
9 | ### 相关文章介绍(按顺序发布)
10 |
11 | * 一、[【玩转Android自动化】开篇序言](https://juejin.cn/post/7266076520111276069)
12 | * 二、[【玩转Android自动化】布局节点速查器](https://juejin.cn/post/7266087487939035192)
13 | * 三、[【玩转Android自动化】小试牛刀](https://juejin.cn/post/7266326381649821755)
14 | * 四、[【玩转Android自动化】微信好友导出](https://juejin.cn/post/7266332124663185463)
15 | * 五、[【玩转Android自动化】微信好友状态检查(假转账方式)](https://juejin.cn/post/7268539925096464444)
16 | * 六、[【玩转Android自动化】微信好友状态检查(拉群方式)](https://juejin.cn/post/7268590610188976165)
17 | * 七、[【玩转Android自动化】微信自动抢红包](https://juejin.cn/post/7269032845786513463)
18 | * 八、[【玩转Android自动化】微信版本适配](https://juejin.cn/post/7269046568211202103)
19 | * 九、[【玩转Android自动化】自动跳过APP启动页广告](https://juejin.cn/post/7277799132119416890)
20 |
--------------------------------------------------------------------------------
/accessibility/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/accessibility/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.android.accessibility.ext'
8 | compileSdk 32
9 |
10 | defaultConfig {
11 | minSdk 26
12 | targetSdk 32
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | }
32 |
33 | dependencies {
34 | implementation 'androidx.core:core-ktx:1.7.0'
35 | implementation 'androidx.appcompat:appcompat:1.4.1'
36 | implementation 'com.google.android.material:material:1.5.0'
37 | implementation 'androidx.navigation:navigation-fragment-ktx:2.4.1'
38 | }
--------------------------------------------------------------------------------
/accessibility/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/accessibility/consumer-rules.pro
--------------------------------------------------------------------------------
/accessibility/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/accessibility/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/AsyncAccessibilityService.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.util.Log
5 | import android.view.accessibility.AccessibilityEvent
6 | import java.util.concurrent.Executors
7 |
8 | abstract class AsyncAccessibilityService : AccessibilityService() {
9 | private val TAG = this::class.java.simpleName
10 |
11 | private val executors = Executors.newSingleThreadExecutor()
12 |
13 | abstract fun targetPackageName(): String
14 |
15 | abstract fun asyncHandleAccessibilityEvent(event: AccessibilityEvent)
16 |
17 | override fun onAccessibilityEvent(event: AccessibilityEvent?) {
18 | val accessibilityEvent = event ?: return
19 | executors.run { asyncHandleAccessibilityEvent(accessibilityEvent) }
20 | }
21 |
22 | override fun onInterrupt() {
23 | Log.d(TAG, "onInterrupt: ")
24 | }
25 |
26 | override fun onServiceConnected() {
27 | Log.d(TAG, "onServiceConnected: ")
28 | }
29 |
30 | override fun onDestroy() {
31 | Log.d(TAG, "onDestroy: ")
32 | super.onDestroy()
33 | }
34 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.content.ComponentName
5 | import android.content.Context
6 | import android.content.Intent
7 | import android.os.Handler
8 | import android.os.Looper
9 | import android.provider.Settings
10 | import android.text.TextUtils
11 | import android.widget.Toast
12 |
13 | /**
14 | * 打开系统设置中辅助功能
15 | */
16 | fun Context.openAccessibilitySetting() {
17 | val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
18 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
19 | startActivity(intent)
20 | }
21 |
22 | fun Context.isAccessibilityOpened(serviceClass: Class): Boolean {
23 | val serviceName: String = this.applicationContext.packageName + "/" + serviceClass.canonicalName
24 | var accessibilityEnabled = 0
25 | try {
26 | accessibilityEnabled =
27 | Settings.Secure.getInt(this.contentResolver, Settings.Secure.ACCESSIBILITY_ENABLED)
28 | } catch (e: Settings.SettingNotFoundException) {
29 | e.printStackTrace()
30 | }
31 | val ms = TextUtils.SimpleStringSplitter(':')
32 | if (accessibilityEnabled == 1) {
33 | val settingValue = Settings.Secure.getString(
34 | this.contentResolver,
35 | Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES
36 | )
37 | if (settingValue != null) {
38 | ms.setString(settingValue)
39 | while (ms.hasNext()) {
40 | val accessibilityService = ms.next()
41 | if (accessibilityService.equals(serviceName, ignoreCase = true)) {
42 | return true
43 | }
44 | }
45 | }
46 | }
47 | return false
48 | }
49 |
50 | /**
51 | * 打开个人微信
52 | */
53 | fun Context.goToWx() = Intent(Intent.ACTION_MAIN)
54 | .apply {
55 | addCategory(Intent.CATEGORY_LAUNCHER)
56 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
57 | component = ComponentName(
58 | "com.tencent.mm",
59 | "com.tencent.mm.ui.LauncherUI",
60 | )
61 | }
62 | .apply(::startActivity)
63 |
64 | fun Context.toast(msg: String) {
65 | runOnUiThread {
66 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
67 | }
68 | }
69 |
70 | fun runOnUiThread(action: Runnable) {
71 | val uiThread = Looper.getMainLooper().thread
72 | val handler = Handler(Looper.getMainLooper())
73 | if (Thread.currentThread() !== uiThread) {
74 | handler.post(action)
75 | } else {
76 | action.run()
77 | }
78 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/FormatExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext
2 |
3 | fun String?.default(default: String = "") = if (this.isNullOrBlank()) default else this
4 |
5 | fun CharSequence?.default(default: String = "") =
6 | if (this.isNullOrBlank()) default else this.toString()
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/acc/AccessibilityEventExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.acc
2 |
3 | import android.view.accessibility.AccessibilityEvent
4 |
5 |
6 | /**
7 | * 窗口状态变化
8 | */
9 | fun AccessibilityEvent.isWindowStateChanged() =
10 | eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
11 |
12 | /**
13 | * 窗口内容变化
14 | */
15 | fun AccessibilityEvent.isWindowContentChanged() =
16 | eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
17 |
18 | /**
19 | * 通知变化
20 | */
21 | fun AccessibilityEvent.isNotificationStateChanged() =
22 | eventType == AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED
23 |
24 | /**
25 | * 是否是点击事件
26 | */
27 | fun AccessibilityEvent.isViewClicked(): Boolean =
28 | this.eventType == AccessibilityEvent.TYPE_VIEW_CLICKED
29 |
30 | /**
31 | * 是否是指定viewId的点击事件
32 | */
33 | fun AccessibilityEvent.onViewClickedById(viewId: String): Boolean {
34 | return isViewClicked() && source?.viewIdResourceName == viewId
35 | }
36 |
37 | /**
38 | * 是否是指定viewId的点击事件
39 | */
40 | fun AccessibilityEvent.onViewClickedByText(text: String): Boolean {
41 | return isViewClicked() && text.find { it.toString() == text } != null
42 | }
43 |
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/acc/AccessibilityNodeActionExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.acc
2 |
3 | import android.os.Bundle
4 | import android.view.accessibility.AccessibilityNodeInfo
5 |
6 | /**
7 | * 点击事件
8 | */
9 | fun AccessibilityNodeInfo?.click(): Boolean {
10 | this ?: return false
11 | return if (isClickable) {
12 | performAction(AccessibilityNodeInfo.ACTION_CLICK)
13 | } else {
14 | parent?.click() == true
15 | }
16 | }
17 |
18 | /**
19 | * 长按事件
20 | */
21 | fun AccessibilityNodeInfo.longClick(): Boolean {
22 | return if (isClickable) {
23 | performAction(AccessibilityNodeInfo.ACTION_LONG_CLICK)
24 | } else {
25 | parent.longClick()
26 | }
27 | }
28 |
29 | /**
30 | * 输入内容
31 | */
32 | fun AccessibilityNodeInfo.inputText(input: String): Boolean {
33 | val arguments = Bundle().apply {
34 | putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, input)
35 | }
36 | return performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
37 | }
38 |
39 | /**
40 | * 向下滚动
41 | */
42 | fun AccessibilityNodeInfo.scrollBackward() =
43 | performAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD)
44 |
45 | /**
46 | * 向上滚动
47 | */
48 | fun AccessibilityNodeInfo.scrollForward() =
49 | performAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD)
50 |
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/acc/AccessibilityNodeInfoExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.acc
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.util.Log
5 | import android.view.accessibility.AccessibilityNodeInfo
6 | import com.android.accessibility.ext.data.NodeWrapper
7 | import com.android.accessibility.ext.default
8 |
9 | /**
10 | * 简化findAccessibilityNodeInfosByViewId
11 | */
12 | fun AccessibilityNodeInfo.findNodesById(viewId: String): List =
13 | findAccessibilityNodeInfosByViewId(viewId)
14 |
15 | /**
16 | * 简化findAccessibilityNodeInfosByText
17 | */
18 | fun AccessibilityNodeInfo.findNodesByText(text: String): List =
19 | findAccessibilityNodeInfosByText(text)
20 |
21 | /**
22 | * 简化findAccessibilityNodeInfosByViewId(viewId).firstOrNull()
23 | */
24 | fun AccessibilityNodeInfo.findNodeById(viewId: String): AccessibilityNodeInfo? =
25 | findAccessibilityNodeInfosByViewId(viewId).firstOrNull()
26 |
27 | /**
28 | * 简化findAccessibilityNodeInfosByText(text).firstOrNull()
29 | */
30 | fun AccessibilityNodeInfo.findNodeByText(text: String): AccessibilityNodeInfo? =
31 | findAccessibilityNodeInfosByText(text).firstOrNull { it.text.default() == text }
32 |
33 | /**
34 | * 判断是否有viewId这个节点
35 | */
36 | fun AccessibilityNodeInfo.contains(viewId: String): Boolean =
37 | findNodesById(viewId).isNotEmpty()
38 |
39 |
40 | /**
41 | * 递归遍历结点的方法
42 | */
43 | private fun AccessibilityNodeInfo?.findNodeWrapper(
44 | isPrint: Boolean = true,
45 | prefix: String = "",
46 | isLast: Boolean = false,
47 | compare: (NodeWrapper) -> Boolean
48 | ): NodeWrapper? {
49 | val node = this ?: return null
50 | val nodeWrapper = NodeWrapper(
51 | className = node.className.default(),
52 | text = node.text.default(),
53 | id = node.viewIdResourceName.default(),
54 | description = node.contentDescription.default(),
55 | isClickable = node.isClickable,
56 | isScrollable = node.isScrollable,
57 | isEditable = node.isEditable,
58 | nodeInfo = node
59 | )
60 |
61 | if (compare(nodeWrapper)) {
62 | Log.d("findNodeWrapper", nodeWrapper.toString())
63 | return nodeWrapper
64 | }
65 | val marker = if (isLast) """\--- """ else "+--- "
66 | val currentPrefix = "$prefix$marker"
67 | if (isPrint) {
68 | Log.d("printNodeInfo", currentPrefix + nodeWrapper.toString())
69 | }
70 | val size = node.childCount
71 | if (size > 0) {
72 | val childPrefix = prefix + if (isLast) " " else "| "
73 | val lastChildIndex = size - 1
74 | for (index in 0 until size) {
75 | val isLastChild = index == lastChildIndex
76 | val find =
77 | node.getChild(index).findNodeWrapper(isPrint, childPrefix, isLastChild, compare)
78 | if (find != null) {
79 | return find
80 | }
81 | }
82 | }
83 | return null
84 | }
85 |
86 | fun AccessibilityNodeInfo?.findNodeWrapperById(id: String): NodeWrapper? {
87 | return findNodeWrapper { node -> node.id == id }
88 | }
89 |
90 | fun AccessibilityNodeInfo?.findNodeWrapperByText(text: String): NodeWrapper? {
91 | return findNodeWrapper { node -> node.text == text }
92 | }
93 |
94 | fun AccessibilityNodeInfo?.findNodeWrapperByIdAndText(id: String, text: String): NodeWrapper? {
95 | return findNodeWrapper { node -> node.text == text && node.id == id }
96 | }
97 |
98 | fun AccessibilityNodeInfo?.findNodeWrapperByContainsText(
99 | isPrint: Boolean = true,
100 | textList: List
101 | ): NodeWrapper? {
102 | return findNodeWrapper(isPrint) { node ->
103 | textList.find { node.text?.contains(it) == true } != null
104 | }
105 | }
106 |
107 | fun AccessibilityNodeInfo?.findNodeWithCustomRule(
108 | isPrint: Boolean = true,
109 | customRule: (AccessibilityNodeInfo) -> Boolean
110 | ): AccessibilityNodeInfo? {
111 | return findNodeWrapper(isPrint) { node ->
112 | val realNode = node.nodeInfo
113 | if (realNode == null) {
114 | false
115 | } else {
116 | customRule(realNode)
117 | }
118 | }?.nodeInfo
119 | }
120 |
121 | fun AccessibilityNodeInfo?.isTextView(): Boolean {
122 | this ?: return false
123 | return this.className.contains("TextView")
124 | }
125 |
126 | fun AccessibilityNodeInfo?.isEditText(): Boolean {
127 | this ?: return false
128 | return this.className.contains("EditText")
129 | }
130 |
131 | fun AccessibilityNodeInfo?.findParent(predicate: AccessibilityNodeInfo.() -> Boolean): AccessibilityNodeInfo? {
132 | this ?: return null
133 | return when {
134 | predicate(this) -> this
135 | else -> parent?.findParent(predicate)
136 | }
137 | }
138 |
139 | fun AccessibilityNodeInfo?.inListView(): Boolean {
140 | this ?: return false
141 | return this.findParent {
142 | className.contains(
143 | "ListView",
144 | true
145 | ) || className.contains("RecyclerView", true)
146 | } != null
147 | }
148 |
149 | fun AccessibilityNodeInfo?.clickById(
150 | id: String,
151 | gestureClick: Boolean = true,
152 | accessibilityService: AccessibilityService? = null
153 | ): Boolean {
154 | this ?: return false
155 | val find = this.findNodesById(id).firstOrNull() ?: return false
156 | return if (gestureClick && accessibilityService != null) {
157 | accessibilityService.gestureClick(find).takeIf { it } ?: find.click()
158 | } else {
159 | find.click().takeIf { it } ?: accessibilityService.gestureClick(find)
160 | }
161 | }
162 |
163 | fun AccessibilityNodeInfo?.clickByText(
164 | text: String,
165 | gestureClick: Boolean = true,
166 | accessibilityService: AccessibilityService? = null
167 | ): Boolean {
168 | this ?: return false
169 | val find = this.findNodesByText(text).firstOrNull() ?: return false
170 | return if (gestureClick && accessibilityService != null) {
171 | accessibilityService.gestureClick(find).takeIf { it } ?: find.click()
172 | } else {
173 | find.click().takeIf { it } ?: accessibilityService.gestureClick(find)
174 | }
175 | }
176 |
177 | fun AccessibilityNodeInfo?.clickByIdAndText(
178 | id: String,
179 | text: String,
180 | gestureClick: Boolean = true,
181 | accessibilityService: AccessibilityService? = null
182 | ): Boolean {
183 | this ?: return false
184 | this.findNodesById(id).firstOrNull { it.text.default() == text }?.let { find ->
185 | return if (gestureClick && accessibilityService != null) {
186 | accessibilityService.gestureClick(find).takeIf { it } ?: find.click()
187 | } else {
188 | find.click().takeIf { it } ?: accessibilityService.gestureClick(find)
189 | }
190 | }
191 | return false
192 | }
193 |
194 |
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/acc/GestureExt.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.acc
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.accessibilityservice.GestureDescription
5 | import android.graphics.Path
6 | import android.graphics.Rect
7 | import android.view.accessibility.AccessibilityNodeInfo
8 |
9 | /**
10 | * 利用手势模拟点击
11 | * @param node: 需要点击的节点
12 | * */
13 | fun AccessibilityService?.gestureClick(node: AccessibilityNodeInfo): Boolean {
14 | this ?: return false
15 | val nodeBounds = Rect().apply(node::getBoundsInScreen)
16 | val x = nodeBounds.centerX().toFloat()
17 | val y = nodeBounds.centerY().toFloat()
18 | return dispatchGesture(
19 | GestureDescription.Builder().apply {
20 | addStroke(
21 | GestureDescription.StrokeDescription(
22 | Path().apply { moveTo(x, y) },
23 | 0L,
24 | 200L
25 | )
26 | )
27 | }.build(),
28 | object : AccessibilityService.GestureResultCallback() {
29 | override fun onCompleted(gestureDescription: GestureDescription?) {
30 | super.onCompleted(gestureDescription)
31 | }
32 | },
33 | null
34 | )
35 | }
36 |
37 | /**
38 | * 向上滚动
39 | */
40 | fun AccessibilityService?.scrollUp(distance: Int = 500): Boolean {
41 | return gestureScroll(ScrollDirection.UP, distance)
42 | }
43 |
44 | /**
45 | * 向下滚动
46 | */
47 | fun AccessibilityService?.scrollDown(distance: Int = 500): Boolean {
48 | return gestureScroll(ScrollDirection.BOTTOM, distance)
49 | }
50 |
51 | /**
52 | * 向左滑动
53 | */
54 | fun AccessibilityService?.scrollLeft(distance: Int = 500): Boolean {
55 | return gestureScroll(ScrollDirection.LEFT, distance)
56 | }
57 |
58 | /**
59 | * 向右滑动
60 | */
61 | fun AccessibilityService?.scrollRight(distance: Int = 500): Boolean {
62 | return gestureScroll(ScrollDirection.RIGHT, distance)
63 | }
64 |
65 |
66 | enum class ScrollDirection {
67 | LEFT,
68 | RIGHT,
69 | UP,
70 | BOTTOM
71 | }
72 |
73 | /**
74 | * 利用手势模拟滑动
75 | * @param distance: 滑动距离占屏幕宽或高的百分比
76 | */
77 | private fun AccessibilityService?.gestureScroll(
78 | direction: ScrollDirection,
79 | distance: Int = 500,
80 | ): Boolean {
81 | val service = this ?: return false
82 | val node = service.rootInActiveWindow
83 | try {
84 | val nodeBounds = Rect().apply(node::getBoundsInScreen)
85 | val x = nodeBounds.centerX().toFloat()
86 | val y = nodeBounds.centerY().toFloat()
87 | val realYDistance = minOf(if (distance <= 0) 500 else distance, (y * 2).toInt())
88 | val realXDistance = minOf(if (distance <= 0) 500 else distance, (x * 2).toInt())
89 |
90 | val path = when (direction) {
91 | ScrollDirection.LEFT -> Path().apply {
92 | moveTo(x + realXDistance / 2, y)
93 | lineTo(x - realXDistance / 2, y)
94 | }
95 |
96 | ScrollDirection.RIGHT -> Path().apply {
97 | moveTo(x - realXDistance / 2, y)
98 | lineTo(x + realXDistance / 2, y)
99 | }
100 |
101 | ScrollDirection.UP -> Path().apply {
102 | moveTo(x, y + realYDistance / 2)
103 | lineTo(x, y - realYDistance / 2)
104 | }
105 |
106 | ScrollDirection.BOTTOM -> Path().apply {
107 | moveTo(x, y - realYDistance / 2)
108 | lineTo(x, y + realYDistance / 2)
109 | }
110 | }
111 | return dispatchGesture(
112 | GestureDescription.Builder().apply {
113 | addStroke(
114 | GestureDescription.StrokeDescription(path, 0L, 300)
115 | )
116 | }
117 | .build(),
118 | object : AccessibilityService.GestureResultCallback() {
119 | override fun onCompleted(gestureDescription: GestureDescription?) {
120 | super.onCompleted(gestureDescription)
121 | }
122 | },
123 | null
124 | )
125 | } catch (e: Exception) {
126 | e.printStackTrace()
127 | return false
128 | }
129 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/acc/PrintNodeInfo.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.acc
2 |
3 | import android.util.Log
4 | import android.view.accessibility.AccessibilityNodeInfo
5 | import com.android.accessibility.ext.data.NodeWrapper
6 | import com.android.accessibility.ext.default
7 |
8 | fun AccessibilityNodeInfo?.printNodeInfo(
9 | prefix: String = "",
10 | isLast: Boolean = false,
11 | printContent: StringBuilder = StringBuilder(),
12 | simplePrint: Boolean = false
13 | ): String {
14 | val node = this ?: return printContent.toString()
15 | val nodeWrapper = NodeWrapper(
16 | className = node.className.default(),
17 | text = node.text.default(),
18 | id = node.viewIdResourceName.default(),
19 | description = node.contentDescription.default(),
20 | isClickable = node.isClickable,
21 | isScrollable = node.isScrollable,
22 | isEditable = node.isEditable,
23 | isSelected = node.isSelected,
24 | isChecked = node.isChecked,
25 | nodeInfo = node
26 | )
27 | val marker = if (isLast) """\--- """ else "+--- "
28 | val currentPrefix = "$prefix$marker"
29 | val print =
30 | currentPrefix + if (simplePrint) nodeWrapper.toSimpleString() else nodeWrapper.toString()
31 | printContent.append("${print}\n")
32 | Log.d("printNodeInfo", print)
33 |
34 | val size = node.childCount
35 | if (size > 0) {
36 | val childPrefix = prefix + if (isLast) " " else "| "
37 | val lastChildIndex = size - 1
38 | for (index in 0 until size) {
39 | val isLastChild = index == lastChildIndex
40 | node.getChild(index).printNodeInfo(childPrefix, isLastChild, printContent, simplePrint)
41 | }
42 | }
43 | return printContent.toString()
44 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/data/NodeWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.data
2 |
3 | import android.view.accessibility.AccessibilityNodeInfo
4 |
5 | /**
6 | * 结点包装,方便查看打印
7 | */
8 | data class NodeWrapper(
9 | var className: String,
10 | var text: String? = null,
11 | var id: String? = null,
12 | var description: String? = null,
13 | var isClickable: Boolean = false,
14 | var isScrollable: Boolean = false,
15 | var isEditable: Boolean = false,
16 | var isSelected: Boolean = false,
17 | var isChecked: Boolean = false,
18 | var nodeInfo: AccessibilityNodeInfo? = null
19 | ) {
20 | override fun toString() =
21 | "className = $className → text = $text → id = $id → description = $description → isClickable = $isClickable → isScrollable = $isScrollable → isEditable = $isEditable"
22 |
23 | fun toSimpleString(): String {
24 | val ss = StringBuilder()
25 | ss.append("className = $className")
26 | if (text.isNullOrBlank().not()) {
27 | ss.append(" → text = $text")
28 | }
29 | if (id.isNullOrBlank().not()) {
30 | ss.append(" → id = $id")
31 | }
32 | if (description.isNullOrBlank().not()) {
33 | ss.append(" → description = $description")
34 | }
35 | if (isClickable) {
36 | ss.append(" → isClickable = $isClickable")
37 | }
38 | if (isScrollable) {
39 | ss.append(" → isScrollable = $isScrollable")
40 | }
41 | if (isEditable) {
42 | ss.append(" → isEditable = $isEditable")
43 | }
44 | if (isSelected) {
45 | ss.append(" → isSelected = $isSelected")
46 | }
47 | if (isChecked) {
48 | ss.append(" → isChecked = $isChecked")
49 | }
50 | return ss.toString()
51 | }
52 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/task/RetryTask.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.task
2 |
3 | import com.android.accessibility.ext.task.i.ITaskTracker
4 | import kotlinx.coroutines.CoroutineScope
5 | import kotlinx.coroutines.delay
6 | import kotlinx.coroutines.isActive
7 | import kotlinx.coroutines.withTimeout
8 |
9 |
10 | suspend fun retryCheckTask(
11 | timeOutMillis: Long = 5_000,
12 | periodMillis: Long = 500,
13 | tracker: ITaskTracker = ITaskTracker.empty(),
14 | predicate: suspend CoroutineScope.() -> Boolean
15 | ): Boolean {
16 | return try {
17 | val block: suspend CoroutineScope.() -> Unit? = { if (predicate()) Unit else null }
18 | retryTask(timeOutMillis, periodMillis, tracker, block)
19 | true
20 | } catch (e: Exception) {
21 | false
22 | }
23 | }
24 |
25 | suspend fun retryTask(
26 | timeOutMillis: Long = 5_000,
27 | periodMillis: Long = 500,
28 | tracker: ITaskTracker = ITaskTracker.empty(),
29 | block: suspend CoroutineScope.() -> T?
30 | ): T {
31 | val start = System.currentTimeMillis()
32 | var count = 0
33 | try {
34 | return withTimeout(timeOutMillis) {
35 | tracker.onStart()
36 | var result: T? = null
37 | while (isActive) {
38 | count++
39 | result = block()
40 | if (null != result) {
41 | val executeDuration = System.currentTimeMillis() - start
42 | tracker.onSuccess(result, executeDuration, count)
43 | break
44 | } else {
45 | tracker.onEach(count)
46 | delay(periodMillis)
47 | }
48 | }
49 | result!!
50 | }
51 | } catch (e: Exception) {
52 | val executeDuration = System.currentTimeMillis() - start
53 | tracker.onError(e, executeDuration, count)
54 | throw e
55 | }
56 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/task/RetryTaskWithLog.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.task
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.task.i.ITaskTracker
5 | import kotlinx.coroutines.CoroutineScope
6 |
7 | private const val TAG = "LogTracker"
8 |
9 |
10 | suspend fun retryTaskWithLog(
11 | taskName: String,
12 | timeOutMillis: Long = 10_000L,
13 | periodMillis: Long = 500L,
14 | predicate: suspend CoroutineScope.() -> T?
15 | ): T? {
16 | return try {
17 | retryTask(timeOutMillis, periodMillis, LogTracker(taskName), predicate)
18 | } catch (e: Exception) {
19 | null
20 | }
21 | }
22 |
23 |
24 | suspend fun retryCheckTaskWithLog(
25 | taskName: String,
26 | timeOutMillis: Long = 10_000,
27 | periodMillis: Long = 500,
28 | predicate: suspend CoroutineScope.() -> Boolean
29 | ): Boolean {
30 | return try {
31 | retryCheckTask(timeOutMillis, periodMillis, LogTracker(taskName), predicate)
32 | } catch (e: Exception) {
33 | e.printStackTrace()
34 | false
35 | }
36 | }
37 |
38 |
39 | class LogTracker(private val taskName: String) : ITaskTracker {
40 |
41 | override fun onStart() {
42 | Log.d(TAG, "【$taskName】开始执行")
43 | }
44 |
45 | override fun onEach(currentCount: Int) {
46 | Log.d(TAG, "【$taskName】第 $currentCount 次执行")
47 | }
48 |
49 | override fun onSuccess(result: T, executeDuration: Long, executeCount: Int) {
50 | Log.d(TAG, "【$taskName】任务执行成功,轮训总次数:${executeCount}, 耗时:$executeDuration ms")
51 | }
52 |
53 | override fun onError(error: Throwable, executeDuration: Long, executeCount: Int) {
54 | Log.d(TAG, "【$taskName】任务执行异常【${error.message}】,轮训总次数:${executeCount}, 耗时:$executeDuration ms")
55 | }
56 |
57 | }
--------------------------------------------------------------------------------
/accessibility/src/main/java/com/android/accessibility/ext/task/i/ITaskTracker.kt:
--------------------------------------------------------------------------------
1 | package com.android.accessibility.ext.task.i
2 |
3 |
4 | /**
5 | * 任务执行链路跟踪
6 | */
7 | interface ITaskTracker {
8 |
9 | /** 方法开始执行触发 */
10 | fun onStart() {}
11 |
12 | /** 方法执行成功触发 */
13 | fun onSuccess(result: T, executeDuration: Long, executeCount: Int) {}
14 |
15 | /** 方法执行异常触发 */
16 | fun onError(error: Throwable, executeDuration: Long, executeCount: Int) {}
17 |
18 | /** 每次重试触发 */
19 | fun onEach(currentCount: Int) {}
20 |
21 | private object EMPTY : ITaskTracker
22 |
23 | @Suppress("UNCHECKED_CAST")
24 | companion object {
25 | fun empty(): ITaskTracker = EMPTY as ITaskTracker
26 | }
27 | }
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | namespace 'com.lygttpod.android.auto'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | applicationId "com.lygttpod.android.auto"
13 | minSdk 26
14 | targetSdk 33
15 | versionCode 109
16 | versionName "1.0.9"
17 |
18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19 | }
20 |
21 | signingConfigs {
22 | release {
23 | keyAlias 'Allen'
24 | keyPassword '123456'
25 | storeFile file('../we_chat_tools.jks')
26 | storePassword '123456'
27 | }
28 | }
29 |
30 | buildTypes {
31 | debug {
32 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
33 | signingConfig signingConfigs.release
34 | ndk {
35 | abiFilters 'arm64-v8a'
36 | }
37 | }
38 | release {
39 | minifyEnabled true
40 | shrinkResources true
41 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
42 | signingConfig signingConfigs.release
43 | ndk {
44 | abiFilters 'arm64-v8a'
45 | }
46 | }
47 | }
48 |
49 | applicationVariants.configureEach { variant ->
50 | variant.outputs.configureEach {
51 | outputFileName = "Android自动化_${defaultConfig.versionName}.apk"
52 | }
53 | }
54 |
55 | compileOptions {
56 | sourceCompatibility JavaVersion.VERSION_17
57 | targetCompatibility JavaVersion.VERSION_17
58 | }
59 | kotlinOptions {
60 | jvmTarget = '17'
61 | }
62 | buildFeatures {
63 | viewBinding true
64 | buildConfig true
65 | }
66 | }
67 |
68 | dependencies {
69 |
70 | implementation(project(":accessibility"))
71 | implementation(project(":auto-wx"))
72 | implementation(project(":auto-tools"))
73 | implementation(project(":auto-ad"))
74 | implementation 'androidx.core:core-ktx:1.9.0'
75 | implementation 'androidx.appcompat:appcompat:1.6.1'
76 | implementation 'com.google.android.material:material:1.8.0'
77 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
78 | implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
79 | implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
80 | implementation 'com.github.lygttpod:ShapeView:1.0.2'
81 | implementation 'com.github.getActivity:EasyWindow:10.3'
82 | implementation 'com.pgyersdk:sdk:3.0.10'
83 | implementation 'io.github.lygttpod:activity-result-api:0.0.2'
84 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
23 | -dontwarn com.pgyersdk.**
24 | -keep class com.pgyersdk.** { *; }
25 | -keep class com.pgyersdk.**$* { *; }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
18 |
19 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/App.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto
2 |
3 | import android.app.Application
4 | import com.lygttpod.android.auto.ad.FuckAdManager
5 | import com.lygttpod.android.auto.tools.AutoTools
6 |
7 | class App : Application() {
8 |
9 | companion object {
10 | private var instance: Application? = null
11 | fun instance() = instance!!
12 | }
13 |
14 | override fun onCreate() {
15 | super.onCreate()
16 | instance = this
17 | AutoTools.init(this)
18 | FuckAdManager.init(this)
19 | }
20 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto
2 |
3 | import android.Manifest
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.navigation.findNavController
7 | import androidx.navigation.ui.AppBarConfiguration
8 | import androidx.navigation.ui.navigateUp
9 | import androidx.navigation.ui.setupActionBarWithNavController
10 | import com.lygttpod.android.activity.result.api.ktx.showToast
11 | import com.lygttpod.android.activity.result.api.observer.PermissionApi
12 | import com.lygttpod.android.auto.databinding.ActivityMainBinding
13 | import com.lygttpod.android.auto.update.UpdateApp
14 | import com.lygttpod.android.auto.wx.service.WXAccessibility
15 | import com.pgyersdk.update.PgyUpdateManager
16 |
17 | class MainActivity : AppCompatActivity() {
18 |
19 | private lateinit var appBarConfiguration: AppBarConfiguration
20 | private lateinit var binding: ActivityMainBinding
21 |
22 | private val permissionApi = PermissionApi(this)
23 |
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 |
27 | binding = ActivityMainBinding.inflate(layoutInflater)
28 | setContentView(binding.root)
29 |
30 | setSupportActionBar(binding.toolbar)
31 |
32 | val navController = findNavController(R.id.nav_host_fragment_content_main)
33 | appBarConfiguration = AppBarConfiguration(navController.graph)
34 | setupActionBarWithNavController(navController, appBarConfiguration)
35 |
36 | UpdateApp.checkVersion(this) { downloadUrl ->
37 | permissionApi.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) {
38 | if (it) {
39 | showToast("正在下载新版本")
40 | PgyUpdateManager.downLoadApk(downloadUrl)
41 | } else {
42 | showToast("未授权无法下载新版本哦")
43 | }
44 | }
45 | }
46 | }
47 |
48 | override fun onResume() {
49 | super.onResume()
50 | WXAccessibility.isInWXApp.set(false)
51 | }
52 |
53 | override fun onSupportNavigateUp(): Boolean {
54 | val navController = findNavController(R.id.nav_host_fragment_content_main)
55 | return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
56 | }
57 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/MainFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.navigation.fragment.findNavController
9 | import com.lygttpod.android.auto.databinding.FragmentMainBinding
10 |
11 |
12 | class MainFragment : Fragment() {
13 |
14 | private var _binding: FragmentMainBinding? = null
15 |
16 | private val binding get() = _binding!!
17 |
18 | override fun onCreateView(
19 | inflater: LayoutInflater, container: ViewGroup?,
20 | savedInstanceState: Bundle?
21 | ): View {
22 |
23 | _binding = FragmentMainBinding.inflate(inflater, container, false)
24 | return binding.root
25 |
26 | }
27 |
28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
29 | super.onViewCreated(view, savedInstanceState)
30 | initListener()
31 | }
32 |
33 | private fun initListener() {
34 | binding.btnPrintTools.setOnClickListener {
35 | findNavController().navigate(R.id.ToolsMainFragment)
36 | }
37 |
38 | binding.btnWxAuto.setOnClickListener {
39 | findNavController().navigate(R.id.WxMainFragment)
40 | }
41 |
42 | binding.btnFuckAd.setOnClickListener {
43 | findNavController().navigate(R.id.FuckAdMainFragment)
44 | }
45 | }
46 |
47 | override fun onDestroyView() {
48 | super.onDestroyView()
49 | _binding = null
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/ktx/ContextKtx.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ktx
2 |
3 | import android.content.Context
4 |
5 | fun Context.dp2px(dipValue: Float): Int {
6 | val scale = this.resources.displayMetrics.density
7 | return (dipValue * scale + 0.5f).toInt()
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/ktx/LogUtils.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ktx
2 | import android.util.Log
3 | import com.lygttpod.android.auto.BuildConfig
4 |
5 | object LogUtils {
6 | private const val TAG = "AndroidAuto"
7 | fun log(log: String) {
8 | if (BuildConfig.DEBUG) {
9 | Log.d(TAG, log)
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/update/UpdateApp.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.update
2 |
3 | import android.content.Context
4 | import android.widget.Toast
5 | import com.lygttpod.android.auto.ktx.LogUtils
6 | import com.pgyersdk.update.DownloadFileListener
7 | import com.pgyersdk.update.PgyUpdateManager
8 | import com.pgyersdk.update.UpdateManagerListener
9 | import com.pgyersdk.update.javabean.AppBean
10 | import java.io.File
11 |
12 | object UpdateApp {
13 | fun checkVersion(context: Context, downloadClick: ((String) -> Unit)?) {
14 | PgyUpdateManager
15 | .Builder()
16 | .setUserCanRetry(true)
17 | .setDeleteHistroyApk(false)
18 | .setUpdateManagerListener(object : UpdateManagerListener {
19 | override fun onNoUpdateAvailable() {
20 | LogUtils.log("there is no new version")
21 | }
22 |
23 | override fun onUpdateAvailable(appBean: AppBean) {
24 | LogUtils.log("there is new version can update, new versionCode is " + appBean.versionCode)
25 | VersionTipDialog.showDialog(context, appBean, downloadClick)
26 | }
27 |
28 | override fun checkUpdateFailed(e: Exception?) {
29 | LogUtils.log("check update failed : $e")
30 | }
31 |
32 | })
33 | .setDownloadFileListener(object : DownloadFileListener {
34 | override fun downloadFailed() {
35 | LogUtils.log("download apk failed")
36 | Toast.makeText(context, "下载失败,稍后再试", Toast.LENGTH_SHORT).show()
37 | }
38 |
39 | override fun downloadSuccessful(file: File) {
40 | LogUtils.log("download apk success: file = ${file.path}")
41 | PgyUpdateManager.installApk(file)
42 | }
43 |
44 | override fun onProgressUpdate(vararg args: Int?) {
45 | LogUtils.log("update download apk progress$args")
46 | }
47 |
48 | })
49 | .register()
50 | }
51 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/lygttpod/android/auto/update/VersionTipDialog.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.update
2 |
3 | import android.app.Dialog
4 | import android.content.Context
5 | import android.os.Bundle
6 | import android.view.View
7 | import android.view.WindowManager
8 | import com.lygttpod.android.auto.R
9 | import com.lygttpod.android.auto.databinding.DialogVersionTipBinding
10 | import com.lygttpod.android.auto.ktx.dp2px
11 | import com.pgyersdk.update.javabean.AppBean
12 |
13 |
14 | class VersionTipDialog(context: Context) : Dialog(context, R.style.loading_dialog) {
15 |
16 | companion object {
17 | private var appBean: AppBean? = null
18 | private var downloadClick: ((String) -> Unit)? = null
19 | fun showDialog(context: Context, appBean: AppBean, downloadClick: ((String) -> Unit)?) {
20 | Companion.appBean = appBean
21 | Companion.downloadClick = downloadClick
22 | VersionTipDialog(context).show()
23 | }
24 | }
25 |
26 | private lateinit var binding: DialogVersionTipBinding
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 | binding = DialogVersionTipBinding.inflate(layoutInflater)
31 | setContentView(binding.root)
32 | setCancelable(false)
33 | setCanceledOnTouchOutside(false)
34 | initWindow()
35 | binding.btnCancel.setOnClickListener { dismiss() }
36 | binding.btnDownload.setOnClickListener {
37 | downloadClick?.invoke(
38 | appBean?.downloadURL ?: return@setOnClickListener
39 | )
40 | if (appBean?.isShouldForceToUpdate != true) {
41 | dismiss()
42 | }
43 | }
44 | }
45 |
46 | private fun initWindow() {
47 | window?.setLayout(
48 | context.dp2px(250f),
49 | WindowManager.LayoutParams.WRAP_CONTENT
50 | )
51 | }
52 |
53 | override fun show() {
54 | super.show()
55 | showData()
56 | }
57 |
58 | private fun showData() {
59 | val content = appBean?.releaseNote
60 | val versionName = appBean?.versionName
61 | val forceToUpdate = appBean?.isShouldForceToUpdate ?: false
62 | binding.tvContent.text = if (content.isNullOrBlank()) "代码优化" else content
63 | binding.tvTitle.text = context.getString(R.string.version_update_title, versionName)
64 | binding.btnCancel.visibility = if (forceToUpdate) View.GONE else View.VISIBLE
65 | }
66 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_tip_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/content_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_version_tip.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
24 |
25 |
36 |
37 |
53 |
54 |
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
27 |
28 |
36 |
37 |
46 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/icon_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/app/src/main/res/mipmap-xxxhdpi/icon_logo.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
19 |
20 |
25 |
26 |
31 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF6200EE
7 | #FF6200EE
8 | #FF000000
9 | #FFFFFFFF
10 | #FF808080
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android自动化
3 | Android自动化工具
4 | 新版本来袭(%s)
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
22 |
23 |
24 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/auto-ad/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/auto-ad/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-parcelize'
5 | }
6 |
7 | android {
8 | namespace 'com.lygttpod.android.auto.ad'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | minSdk 24
13 |
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | consumerProguardFiles "consumer-rules.pro"
16 | }
17 |
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 | compileOptions {
25 | sourceCompatibility JavaVersion.VERSION_1_8
26 | targetCompatibility JavaVersion.VERSION_1_8
27 | }
28 | kotlinOptions {
29 | jvmTarget = '1.8'
30 | }
31 | buildFeatures {
32 | viewBinding true
33 | }
34 | }
35 |
36 | dependencies {
37 | implementation(project(':accessibility'))
38 | implementation 'androidx.core:core-ktx:1.9.0'
39 | implementation 'androidx.appcompat:appcompat:1.6.1'
40 | implementation 'com.google.android.material:material:1.9.0'
41 | implementation 'com.google.code.gson:gson:2.10.1'
42 | implementation 'com.tencent:mmkv:1.3.1'
43 | }
--------------------------------------------------------------------------------
/auto-ad/consumer-rules.pro:
--------------------------------------------------------------------------------
1 | -keep class com.lygttpod.android.auto.ad.data.** { *; }
--------------------------------------------------------------------------------
/auto-ad/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/auto-ad/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/FuckAdManager.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad
2 |
3 | import android.content.Context
4 | import com.tencent.mmkv.MMKV
5 |
6 |
7 | val AppContext = FuckAdManager.appContext ?: error("请先在application中调用FuckAdManager.init(this)初始化")
8 |
9 | object FuckAdManager {
10 |
11 | var appContext: Context? = null
12 | fun init(context: Context) {
13 | this.appContext = context
14 | MMKV.initialize(context)
15 | }
16 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/accessibility/FuckADAccessibility.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.accessibility
2 |
3 | import android.view.accessibility.AccessibilityEvent
4 | import com.android.accessibility.ext.AsyncAccessibilityService
5 | import com.lygttpod.android.auto.ad.task.FuckADTask
6 |
7 | class FuckADAccessibility : AsyncAccessibilityService() {
8 |
9 | companion object {
10 | var fuckADAccessibility: FuckADAccessibility? = null
11 | }
12 |
13 | override fun targetPackageName() = ""
14 |
15 | override fun onCreate() {
16 | super.onCreate()
17 | fuckADAccessibility = this
18 | }
19 |
20 | override fun onDestroy() {
21 | fuckADAccessibility = null
22 | super.onDestroy()
23 | }
24 |
25 | override fun asyncHandleAccessibilityEvent(event: AccessibilityEvent) {
26 | // Log.d("FuckADAccessibility", "asyncHandleAccessibilityEvent: ${event}")
27 | FuckADTask.fuckAD(event)
28 | }
29 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/data/FilterKeywordData.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class FilterKeywordData(val list: MutableList) : Parcelable
8 |
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/data/FuckAdAppsData.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.data
2 |
3 | import android.os.Parcelable
4 | import android.util.Log
5 | import kotlinx.parcelize.Parcelize
6 |
7 | @Parcelize
8 | data class FuckAdApps(val fuckAd: FuckAd) : Parcelable
9 |
10 | @Parcelize
11 | data class FuckAd(val apps: MutableList) : Parcelable
12 |
13 | @Parcelize
14 | data class AdApp(
15 | val appName: String,
16 | val packageName: String,
17 | val launcher: String,
18 | var lastSkipSuccessTime: Long = 0,
19 | var enableCheck: Boolean = true,//是否允许检测,默认允许
20 | var adNode: ADNode = ADNode()
21 | ) : Parcelable {
22 | fun skipSuccess() {
23 | lastSkipSuccessTime = System.currentTimeMillis()
24 | }
25 |
26 | /**
27 | * 五秒内成功跳过再次启动就不处理了,目的是为了减少检测次数
28 | */
29 | fun isSkipped(intervalTime: Long = 5_000): Boolean {
30 | val skipped = System.currentTimeMillis() - lastSkipSuccessTime <= intervalTime
31 | if (skipped) {
32 | Log.d(
33 | "FuckADTask",
34 | "isSkipped: ${intervalTime / 1000}秒内已经成功跳过一次广告,无需重复检测"
35 | )
36 | }
37 | return skipped
38 | }
39 |
40 | fun actionMaxLength(): Int = adNode.actionMaxLength
41 |
42 | fun getNodeId(): String = adNode.id
43 | fun getMaxLength(): Int = adNode.actionMaxLength
44 | }
45 |
46 | @Parcelize
47 | data class ADNode(
48 | var action: String = "跳过,关闭",
49 | var actionMaxLength: Int = 5,
50 | var id: String = ""
51 | ) : Parcelable
52 |
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/ktx/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.ktx
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.content.pm.ResolveInfo
6 | import android.util.Log
7 | import com.lygttpod.android.auto.ad.AppContext
8 | import com.lygttpod.android.auto.ad.data.AdApp
9 |
10 |
11 | fun Context.loadAsset(fileName: String): String? {
12 | try {
13 | return assets.open(fileName).bufferedReader().use { it.readText() }
14 | } catch (e: Exception) {
15 | e.printStackTrace()
16 | }
17 | return null
18 | }
19 |
20 | @Suppress("DEPRECATION")
21 | fun Context.queryAllInstallApp(): MutableList {
22 | val packageManager = this.packageManager
23 | // 创建一个 Intent,用于查询所有启动的应用程序
24 | val intent = Intent(Intent.ACTION_MAIN, null)
25 | intent.addCategory(Intent.CATEGORY_LAUNCHER)
26 | // 使用 queryIntentActivities 获取所有匹配的应用列表
27 | val appInfoList =
28 | packageManager.queryIntentActivities(intent, 0)
29 | .filterNot { isSystemApp(it) || isSelf(it) }
30 | .map {
31 | val appName = it.loadLabel(packageManager).toString()
32 | val packageName = it.activityInfo.packageName
33 | val className = it.activityInfo.name
34 | AdApp(appName, packageName, className)
35 | }
36 | .toMutableList()
37 |
38 | return appInfoList
39 | }
40 |
41 | // 检查应用是否为系统应用
42 | private fun isSystemApp(resolveInfo: ResolveInfo): Boolean {
43 | val regex = "com\\.android\\.[^.]+" // 匹配系统应用 com.android. 后面跟着任意不包含 . 的字符
44 | val result = resolveInfo.activityInfo.packageName.matches(Regex(regex))
45 | Log.d("isSystemApp", "isSystemApp: ${resolveInfo.activityInfo.packageName} : $result")
46 | return result
47 | }
48 |
49 | private fun isSelf(resolveInfo: ResolveInfo): Boolean {
50 | return resolveInfo.activityInfo.packageName == AppContext.packageName
51 | }
52 |
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/ktx/GsonExt.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.ktx
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.reflect.TypeToken
5 |
6 |
7 | private val gson = Gson()
8 |
9 | fun fromJson(json: String): T {
10 | return gson.fromJson(json, object : TypeToken() {}.type)
11 | }
12 |
13 | fun String?.toObject(): T? {
14 | val json = this ?: return null
15 | return fromJson(json)
16 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/task/FuckADTask.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.task
2 |
3 | import android.util.Log
4 | import android.view.accessibility.AccessibilityEvent
5 | import androidx.lifecycle.MutableLiveData
6 | import com.android.accessibility.ext.acc.clickByCustomRule
7 | import com.android.accessibility.ext.acc.inListView
8 | import com.android.accessibility.ext.acc.isTextView
9 | import com.android.accessibility.ext.default
10 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
11 | import com.android.accessibility.ext.toast
12 | import com.lygttpod.android.auto.ad.AppContext
13 | import com.lygttpod.android.auto.ad.accessibility.FuckADAccessibility
14 | import com.lygttpod.android.auto.ad.data.AdApp
15 | import com.lygttpod.android.auto.ad.data.FilterKeywordData
16 | import com.lygttpod.android.auto.ad.data.FuckAd
17 | import com.lygttpod.android.auto.ad.data.FuckAdApps
18 | import com.lygttpod.android.auto.ad.ktx.queryAllInstallApp
19 | import com.tencent.mmkv.MMKV
20 | import kotlinx.coroutines.CoroutineScope
21 | import kotlinx.coroutines.Dispatchers
22 | import kotlinx.coroutines.SupervisorJob
23 | import kotlinx.coroutines.launch
24 | import kotlinx.coroutines.sync.Mutex
25 | import kotlinx.coroutines.sync.withLock
26 | import kotlinx.coroutines.withContext
27 |
28 |
29 | object FuckADTask {
30 |
31 | private const val APP_CONFIG = "appConfig"
32 | private const val FILTER_KEYWORD = "filterKeyword"
33 |
34 | private val mutex = Mutex()
35 |
36 | private val fuckADTaskScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
37 |
38 | private var fuckAdApps: FuckAdApps? = null
39 | set(value) {
40 | field = value
41 | fuckAdAppsLiveData.postValue(value)
42 | }
43 |
44 | var fuckAdAppsLiveData = MutableLiveData()
45 |
46 | private var filterKeywordData: FilterKeywordData? = null
47 |
48 | fun updateKeywordList(keyword: String) {
49 | fuckADTaskScope.launch {
50 | getKeywordList()?.let {
51 | if (it.list.contains(keyword)) {
52 | withContext(Dispatchers.Main) {
53 | AppContext.toast("已经添加过啦,不要重复添加哦")
54 | }
55 | return@launch
56 | } else {
57 | it.list.add(keyword)
58 | MMKV.defaultMMKV().encode(FILTER_KEYWORD, it)
59 | withContext(Dispatchers.Main) {
60 | AppContext.toast("添加成功")
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | fun getKeywordList(): FilterKeywordData? {
68 | filterKeywordData =
69 | MMKV.defaultMMKV().decodeParcelable(FILTER_KEYWORD, FilterKeywordData::class.java)
70 | ?: FilterKeywordData(list = mutableListOf("关闭闹钟", "关闭提醒"))
71 | return filterKeywordData
72 | }
73 |
74 | fun updateData(ad: AdApp) {
75 | fuckAdApps?.fuckAd?.apps?.find { it.appName == ad.packageName }
76 | ?.apply { adNode = ad.adNode }
77 | save2File()
78 | }
79 |
80 | private fun loadAdConfig(): FuckAdApps? {
81 | return try {
82 | FuckAdApps(fuckAd = FuckAd(AppContext.queryAllInstallApp()))
83 | } catch (e: Exception) {
84 | e.printStackTrace()
85 | null
86 | }
87 | }
88 |
89 | fun analysisAppConfig() {
90 | fuckADTaskScope.launch {
91 | if (fuckAdApps == null) {
92 | fuckAdApps =
93 | MMKV.defaultMMKV().decodeParcelable(APP_CONFIG, FuckAdApps::class.java, null)
94 | }
95 | loadAdConfig()?.let {
96 | if (fuckAdApps == null) {
97 | fuckAdApps = it
98 | } else {
99 | it.fuckAd.apps.forEach { newData ->
100 | val oldData =
101 | fuckAdApps!!.fuckAd.apps.find { it.packageName == newData.packageName }
102 | if (oldData != null) {
103 | newData.adNode = oldData.adNode
104 | newData.enableCheck = oldData.enableCheck
105 | }
106 | }
107 | fuckAdApps = it
108 | }
109 | save2File()
110 | }
111 | }
112 | }
113 |
114 | private fun save2File() {
115 | fuckADTaskScope.launch {
116 | MMKV.defaultMMKV().remove(APP_CONFIG)
117 | MMKV.defaultMMKV().encode(APP_CONFIG, fuckAdApps)
118 | }
119 | }
120 |
121 | fun fuckAD(event: AccessibilityEvent) {
122 | if (mutex.isLocked) return
123 | val packageName = event.packageName.default()
124 | fuckAdApps?.fuckAd?.apps?.find { it.packageName == packageName && it.enableCheck }?.let {
125 | Log.d("FuckADTask", "打开了【${it.appName}】的【${it.launcher}】")
126 | if (it.isSkipped()) return@let
127 | fuckADTaskScope.launch {
128 | if (skipAd(it)) {
129 | it.skipSuccess()
130 | }
131 | }
132 | }
133 | }
134 |
135 | private suspend fun skipAd(adApp: AdApp): Boolean {
136 | return mutex.withLock {
137 | val acc = FuckADAccessibility.fuckADAccessibility ?: return false
138 | val actions = adApp.adNode.action.split(",")
139 | val id = adApp.adNode.id
140 | retryCheckTaskWithLog(
141 | "查找【${adApp.appName}】的【$actions】节点",
142 | timeOutMillis = 5000
143 | ) {
144 | val skipResult = acc.clickByCustomRule {
145 | if (id.isNotBlank()) {
146 | it.viewIdResourceName == id
147 | } else {
148 | if (it.isTextView()) {
149 | val text = it.text.default()
150 | //通过多重判断大大减少误触的概率
151 | filterKeywordData?.list?.find { it == text } == null
152 | && text.length <= adApp.actionMaxLength()
153 | && actions.find { action -> text.contains(action) } != null
154 | && it.inListView().not()
155 | } else {
156 | false
157 | }
158 | }
159 | }
160 | if (skipResult) {
161 | withContext(Dispatchers.Main) {
162 | Log.d("FuckADTask", "自动跳过【${adApp.appName}】的广告啦")
163 | }
164 | }
165 | skipResult
166 | }
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/ui/FuckAdMainFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.ui
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.appcompat.app.AlertDialog
8 | import androidx.fragment.app.Fragment
9 | import androidx.lifecycle.MutableLiveData
10 | import androidx.recyclerview.widget.LinearLayoutManager
11 | import com.android.accessibility.ext.default
12 | import com.android.accessibility.ext.isAccessibilityOpened
13 | import com.android.accessibility.ext.openAccessibilitySetting
14 | import com.android.accessibility.ext.toast
15 | import com.lygttpod.android.auto.ad.R
16 | import com.lygttpod.android.auto.ad.accessibility.FuckADAccessibility
17 | import com.lygttpod.android.auto.ad.data.AdApp
18 | import com.lygttpod.android.auto.ad.databinding.DialogAppConfigBinding
19 | import com.lygttpod.android.auto.ad.databinding.DialogFilterKeywordBinding
20 | import com.lygttpod.android.auto.ad.databinding.FragmentFuckAdMainBinding
21 | import com.lygttpod.android.auto.ad.task.FuckADTask
22 | import com.lygttpod.android.auto.ad.ui.adapter.AppConfigAdapter
23 |
24 |
25 | class FuckAdMainFragment : Fragment() {
26 |
27 | private var _binding: FragmentFuckAdMainBinding? = null
28 |
29 | private val binding get() = _binding!!
30 |
31 | private val toolsServiceLiveData = MutableLiveData()
32 |
33 | private var appConfigAdapter = AppConfigAdapter()
34 |
35 | override fun onCreateView(
36 | inflater: LayoutInflater, container: ViewGroup?,
37 | savedInstanceState: Bundle?
38 | ): View {
39 |
40 | _binding = FragmentFuckAdMainBinding.inflate(inflater, container, false)
41 | return binding.root
42 |
43 | }
44 |
45 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
46 | super.onViewCreated(view, savedInstanceState)
47 | initView()
48 | initListener()
49 | initObserver()
50 | FuckADTask.getKeywordList()
51 | }
52 |
53 | private fun initView() {
54 | appConfigAdapter.onItemClick = { adApp ->
55 | val view = layoutInflater.inflate(R.layout.dialog_app_config, null)
56 | val binding = DialogAppConfigBinding.bind(view)
57 | binding.enableCheck.isChecked = adApp.enableCheck
58 | binding.enableCheck.setOnCheckedChangeListener { buttonView, isChecked ->
59 | adApp.enableCheck = isChecked
60 | }
61 | binding.etNodeAction.setText(adApp.adNode.action)
62 | binding.etNodeActionMaxLength.setText("${adApp.getMaxLength()}")
63 | binding.etNodeId.setText(adApp.getNodeId())
64 | AlertDialog
65 | .Builder(requireContext())
66 | .setView(view)
67 | .setTitle(adApp.appName)
68 | .setNegativeButton("取消") { _, _ ->
69 | {
70 | }
71 | }
72 | .setPositiveButton("保存") { _, _ ->
73 | run {
74 | adApp.adNode.apply {
75 | action = binding.etNodeAction.text.toString()
76 | actionMaxLength =
77 | binding.etNodeActionMaxLength.text?.toString()?.toIntOrNull() ?: 5
78 | id = binding.etNodeId.text.default()
79 | }
80 | updateData(adApp)
81 | }
82 | }
83 | .show()
84 | }
85 | binding.recyclerView.apply {
86 | layoutManager = LinearLayoutManager(requireContext())
87 | adapter = appConfigAdapter
88 | }
89 | }
90 |
91 | private fun updateData(adApp: AdApp) {
92 | appConfigAdapter.updateData(adApp)
93 | FuckADTask.updateData(adApp)
94 | }
95 |
96 | override fun onResume() {
97 | super.onResume()
98 | toolsServiceLiveData.value =
99 | requireContext().isAccessibilityOpened(FuckADAccessibility::class.java)
100 | FuckADTask.analysisAppConfig()
101 | }
102 |
103 | private fun initObserver() {
104 | toolsServiceLiveData.observe(viewLifecycleOwner) { open ->
105 | binding.btnOpenService.text =
106 | if (open) "【自动跳过广告(FuckAD)】服务已开启" else "打开【自动跳过广告(FuckAD)】服务"
107 | }
108 | FuckADTask.fuckAdAppsLiveData.observe(viewLifecycleOwner) {
109 | it?.fuckAd?.apps?.let { appConfigAdapter.setData(it) }
110 | }
111 | }
112 |
113 | private fun initListener() {
114 | binding.btnOpenService.setOnClickListener {
115 | activity?.openAccessibilitySetting()
116 | }
117 | binding.btnFilterKeyword.setOnClickListener {
118 | val view = layoutInflater.inflate(R.layout.dialog_filter_keyword, null)
119 | val binding = DialogFilterKeywordBinding.bind(view)
120 | binding.tvFilterKeywordList.text = FuckADTask.getKeywordList()?.list.toString()
121 | AlertDialog
122 | .Builder(requireContext())
123 | .setView(view)
124 | .setTitle("过滤关键词")
125 | .setNegativeButton("取消") { _, _ ->
126 | {
127 | }
128 | }
129 | .setPositiveButton("添加") { _, _ ->
130 | run {
131 | val keyword = binding.etKeyword.text?.toString().default()
132 | if (keyword.isBlank()) {
133 | requireContext().toast("添加的关键词不能为空")
134 | } else {
135 | FuckADTask.updateKeywordList(keyword)
136 | }
137 | }
138 | }
139 | .show()
140 | }
141 | }
142 |
143 | override fun onDestroyView() {
144 | super.onDestroyView()
145 | _binding = null
146 | }
147 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/java/com/lygttpod/android/auto/ad/ui/adapter/AppConfigAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.ad.ui.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.lygttpod.android.auto.ad.R
8 | import com.lygttpod.android.auto.ad.data.AdApp
9 | import com.lygttpod.android.auto.ad.databinding.ItemAppBinding
10 |
11 | class AppConfigAdapter : RecyclerView.Adapter() {
12 |
13 | private var list: MutableList = mutableListOf()
14 |
15 | var onItemClick: ((AdApp) -> Unit)? = null
16 |
17 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppConfigViewHolder {
18 | return AppConfigViewHolder(
19 | LayoutInflater.from(parent.context).inflate(R.layout.item_app, parent, false)
20 | )
21 | }
22 |
23 | override fun getItemCount() = list.size
24 |
25 | override fun onBindViewHolder(holder: AppConfigViewHolder, position: Int) {
26 | holder.bindData(list[position])
27 | }
28 |
29 | fun setData(list: MutableList) {
30 | this.list = list
31 | notifyDataSetChanged()
32 | }
33 |
34 | fun updateData(app: AdApp) {
35 | val index = this.list.indexOfFirst { it.packageName == app.packageName }
36 | if (index > -1) {
37 | list[index] = app
38 | notifyItemChanged(index)
39 | }
40 | }
41 |
42 | inner class AppConfigViewHolder(view: View) : RecyclerView.ViewHolder(view) {
43 | private val binding = ItemAppBinding.bind(view)
44 | var data: AdApp? = null
45 |
46 | init {
47 | binding.root.setOnClickListener {
48 | onItemClick?.invoke(
49 | data ?: return@setOnClickListener
50 | )
51 | }
52 | }
53 |
54 | fun bindData(adApp: AdApp) {
55 | this.data = adApp
56 | binding.tvAppName.text = adApp.appName
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/auto-ad/src/main/res/layout/dialog_app_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
16 |
17 |
26 |
27 |
36 |
37 |
48 |
49 |
58 |
59 |
68 |
69 |
80 |
81 |
90 |
91 |
100 |
101 |
112 |
--------------------------------------------------------------------------------
/auto-ad/src/main/res/layout/dialog_filter_keyword.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
26 |
27 |
37 |
38 |
47 |
48 |
57 |
58 |
--------------------------------------------------------------------------------
/auto-ad/src/main/res/layout/fragment_fuck_ad_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
29 |
30 |
40 |
41 |
49 |
50 |
61 |
--------------------------------------------------------------------------------
/auto-ad/src/main/res/layout/item_app.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
19 |
20 |
27 |
28 |
--------------------------------------------------------------------------------
/auto-ad/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 自动跳过广告
4 |
5 | 【自动跳过广告(FuckAD)】
6 | 使用说明
7 | 使用说明\n\n在无障碍服务中找到【自动跳过广告(FuckAD)】并开启;\n\n自动跳过启动页带有【跳过】或【关闭】字样的广告\n
8 |
9 |
--------------------------------------------------------------------------------
/auto-ad/src/main/res/xml/fuck_ad_accessible_service_config.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/auto-tools/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/auto-tools/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 | namespace 'com.lygttpod.android.auto.tools'
9 | compileSdk 33
10 |
11 | defaultConfig {
12 | minSdk 26
13 | targetSdk 33
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_17
27 | targetCompatibility JavaVersion.VERSION_17
28 | }
29 | kotlinOptions {
30 | jvmTarget = '17'
31 | }
32 | buildFeatures {
33 | viewBinding true
34 | }
35 | }
36 |
37 | dependencies {
38 |
39 | implementation(project(':accessibility'))
40 | implementation 'androidx.core:core-ktx:1.9.0'
41 | implementation 'androidx.appcompat:appcompat:1.6.1'
42 | implementation 'com.google.android.material:material:1.9.0'
43 | implementation 'com.github.lygttpod:ShapeView:1.0.2'
44 | implementation 'com.github.getActivity:EasyWindow:10.3'
45 | implementation 'io.github.lygttpod.android-local-service:core:0.0.1'
46 | implementation 'io.github.lygttpod.android-local-service:annotation:0.0.1'
47 | kapt 'io.github.lygttpod.android-local-service:processor:0.0.1'
48 | }
--------------------------------------------------------------------------------
/auto-tools/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/auto-tools/consumer-rules.pro
--------------------------------------------------------------------------------
/auto-tools/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/auto-tools/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
14 |
15 |
16 |
17 |
18 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/AutoTools.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools
2 |
3 | import android.content.Context
4 |
5 |
6 | val AppContext = AutoTools.appContext ?: error("请先在application中调用AutoTools.init(this)初始化")
7 |
8 | object AutoTools {
9 |
10 | var appContext: Context? = null
11 | fun init(context: Context) {
12 | this.appContext = context
13 | }
14 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/accessibility/AutoToolsAccessibility.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.accessibility
2 |
3 | import android.view.accessibility.AccessibilityEvent
4 | import android.view.accessibility.AccessibilityNodeInfo
5 | import android.view.accessibility.AccessibilityWindowInfo.TYPE_SYSTEM
6 | import com.android.accessibility.ext.AsyncAccessibilityService
7 |
8 | class AutoToolsAccessibility : AsyncAccessibilityService() {
9 |
10 | companion object {
11 | var autoToolsAccessibility: AutoToolsAccessibility? = null
12 |
13 | var floatWindowPackageName: String? = null
14 | fun getRootWindowNodeInfo(): AccessibilityNodeInfo? {
15 | if (floatWindowPackageName.isNullOrBlank()) return autoToolsAccessibility?.rootInActiveWindow
16 | return autoToolsAccessibility?.windows?.filter { it.type == TYPE_SYSTEM }
17 | ?.lastOrNull { it.root?.packageName == floatWindowPackageName }?.root
18 | }
19 | }
20 |
21 | override fun targetPackageName() = ""
22 |
23 | override fun onCreate() {
24 | super.onCreate()
25 | autoToolsAccessibility = this
26 | }
27 |
28 | override fun onDestroy() {
29 | autoToolsAccessibility = null
30 | super.onDestroy()
31 | }
32 |
33 | override fun asyncHandleAccessibilityEvent(event: AccessibilityEvent) {
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/ktx/ContextExt.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.ktx
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
7 | import android.net.Uri
8 | import android.provider.Settings
9 | import com.lygttpod.android.auto.tools.manager.FloatManager
10 | import com.lygttpod.android.auto.tools.utils.SPUtils
11 |
12 | fun Context?.gotoOverlayPermission() {
13 | this ?: return
14 | val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
15 | intent.data = Uri.parse("package:" + this.applicationContext.packageName)
16 | intent.addFlags(FLAG_ACTIVITY_NEW_TASK)
17 | this.startActivity(intent)
18 | }
19 |
20 | fun Context?.canDrawOverlays(): Boolean {
21 | this ?: return false
22 | return Settings.canDrawOverlays(this)
23 | }
24 |
25 | fun Context?.showPrintFloat(application: Application) {
26 | this ?: return
27 | if (canDrawOverlays()) {
28 | FloatManager.showPrintFloat(application)
29 | } else {
30 | gotoOverlayPermission()
31 | }
32 | }
33 |
34 | fun Context.getSpValue(key: String): String {
35 | return SPUtils.getString(this, this.packageName, key) ?: ""
36 | }
37 |
38 | fun Context.setSpValue(key: String, value: Any) {
39 | SPUtils.saveValue(this, this.packageName, key, value)
40 | }
41 |
42 |
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/ktx/IpAddress.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.ktx
2 |
3 | import java.net.Inet4Address
4 | import java.net.NetworkInterface
5 | import java.net.SocketException
6 |
7 | fun getPhoneWifiIpAddress(): String? {
8 | try {
9 | val networkInterfaces = NetworkInterface.getNetworkInterfaces()
10 | while (networkInterfaces.hasMoreElements()) {
11 | val networkInterface = networkInterfaces.nextElement()
12 | val inetAddresses = networkInterface.inetAddresses
13 | while (inetAddresses.hasMoreElements()) {
14 | val inetAddress = inetAddresses.nextElement()
15 | if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
16 | return inetAddress.getHostAddress()
17 | }
18 | }
19 | }
20 | } catch (e: SocketException) {
21 | e.printStackTrace()
22 | return null
23 | }
24 |
25 | return null
26 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/manager/ContentManger.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.manager
2 |
3 | import androidx.lifecycle.MutableLiveData
4 |
5 | object ContentManger {
6 |
7 | var printContent: String? = null
8 | set(value) {
9 | field = value
10 | printContentLiveData.postValue(value)
11 | }
12 |
13 | var printContentLiveData: MutableLiveData = MutableLiveData()
14 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/manager/FloatManager.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.manager
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Application
5 | import com.android.accessibility.ext.acc.printNodeInfo
6 | import com.android.accessibility.ext.isAccessibilityOpened
7 | import com.android.accessibility.ext.toast
8 | import com.hjq.window.EasyWindow
9 | import com.lygttpod.android.auto.tools.accessibility.AutoToolsAccessibility
10 | import com.lygttpod.android.auto.tools.accessibility.AutoToolsAccessibility.Companion.autoToolsAccessibility
11 | import com.lygttpod.android.auto.tools.R
12 | import com.lygttpod.android.auto.tools.manager.ContentManger.printContent
13 | import com.lygttpod.android.auto.tools.ktx.canDrawOverlays
14 | import com.lygttpod.android.auto.tools.manager.ContentManger.printContentLiveData
15 |
16 | object FloatManager {
17 |
18 | private const val FLOAT_PRINT = "float_print"
19 |
20 | @SuppressLint("StaticFieldLeak")
21 | private var printFloat: EasyWindow<*>? = null
22 |
23 | fun showPrintFloat(context: Application) {
24 | if (!context.canDrawOverlays()) return
25 | if (printFloat?.isShowing == true) {
26 | printFloat?.cancel()
27 | return
28 | }
29 | if (printFloat == null) {
30 | printFloat = EasyWindow.with(context)
31 | .setTag(FLOAT_PRINT)
32 | .setContentView(R.layout.widget_float_print)
33 | .setWidth(150)
34 | .setHeight(150)
35 | .setOnClickListener(
36 | R.id.tv_print_node
37 | ) { window, view ->
38 | if (context.isAccessibilityOpened(AutoToolsAccessibility::class.java)) {
39 | printContent = autoToolsAccessibility?.printNodeInfo()
40 | printContentLiveData.value = printContent
41 | context.toast("成功获取页面元素")
42 | } else {
43 | context.toast("请先开启无障碍服务")
44 | }
45 | }
46 | .setDraggable()
47 | }
48 | printFloat?.show()
49 | }
50 |
51 |
52 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/service/AutoToolsService.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.service
2 |
3 | import com.android.accessibility.ext.acc.clickById
4 | import com.android.accessibility.ext.acc.clickByIdAndText
5 | import com.android.accessibility.ext.acc.clickByText
6 | import com.android.accessibility.ext.acc.findNodeById
7 | import com.android.accessibility.ext.acc.inputText
8 | import com.android.accessibility.ext.acc.pressBackButton
9 | import com.android.accessibility.ext.acc.printNodeInfo
10 | import com.android.accessibility.ext.acc.scrollDown
11 | import com.android.accessibility.ext.acc.scrollLeft
12 | import com.android.accessibility.ext.acc.scrollRight
13 | import com.android.accessibility.ext.acc.scrollUp
14 | import com.android.accessibility.ext.default
15 | import com.android.local.service.annotation.Get
16 | import com.android.local.service.annotation.Page
17 | import com.android.local.service.annotation.Service
18 | import com.lygttpod.android.auto.tools.AppContext
19 | import com.lygttpod.android.auto.tools.accessibility.AutoToolsAccessibility
20 | import com.lygttpod.android.auto.tools.ktx.getSpValue
21 | import com.lygttpod.android.auto.tools.ktx.setSpValue
22 | import com.lygttpod.android.auto.tools.manager.ContentManger
23 |
24 | @Service(port = 9527)
25 | abstract class AutoToolsService {
26 |
27 | @Page("")
28 | fun getIndexPage(): String {
29 | AutoToolsAccessibility.floatWindowPackageName = null
30 | return "auto_tools_index.html"
31 | }
32 |
33 | @Get("getPageSimpleNodeInfo")
34 | fun getPageSimpleNodeInfo(): String {
35 | ContentManger.printContent =
36 | AutoToolsAccessibility.getRootWindowNodeInfo().printNodeInfo(simplePrint = true)
37 | return ContentManger.printContent
38 | ?: "暂未获取到页面节点信息,请先确保APP已开启【布局节点速查器】无障碍功能"
39 | }
40 |
41 | @Get("click")
42 | fun click(nodeId: String? = null, nodeText: String? = null): Boolean {
43 | val accessibility = AutoToolsAccessibility.autoToolsAccessibility
44 | return if (nodeId.isNullOrBlank().not() && nodeText.isNullOrBlank().not()) {
45 | AutoToolsAccessibility.getRootWindowNodeInfo().clickByIdAndText(nodeId!!, nodeText!!, accessibilityService = accessibility)
46 | } else if (nodeId.isNullOrBlank().not()) {
47 | AutoToolsAccessibility.getRootWindowNodeInfo().clickById(nodeId!!, accessibilityService = accessibility)
48 | } else if (nodeText.isNullOrBlank().not()) {
49 | AutoToolsAccessibility.getRootWindowNodeInfo().clickByText(nodeText!!, accessibilityService = accessibility)
50 | } else {
51 | false
52 | }
53 | }
54 |
55 | @Get("back")
56 | fun pressBack(): Boolean {
57 | return AutoToolsAccessibility.autoToolsAccessibility?.pressBackButton() ?: false
58 | }
59 |
60 | @Get("scrollUp")
61 | fun scrollUp(distance: Int): Boolean {
62 | return AutoToolsAccessibility.autoToolsAccessibility.scrollUp(distance)
63 | }
64 |
65 | @Get("scrollDown")
66 | fun scrollDown(distance: Int): Boolean {
67 | return AutoToolsAccessibility.autoToolsAccessibility.scrollDown(distance)
68 | }
69 |
70 | @Get("scrollLeft")
71 | fun scrollLeft(distance: Int): Boolean {
72 | return AutoToolsAccessibility.autoToolsAccessibility.scrollLeft(distance)
73 | }
74 |
75 | @Get("scrollRight")
76 | fun scrollRight(distance: Int): Boolean {
77 | return AutoToolsAccessibility.autoToolsAccessibility.scrollRight(distance)
78 | }
79 |
80 | @Get("input")
81 | fun input(nodeId: String, content: String): Boolean {
82 | val rootNode =
83 | AutoToolsAccessibility.autoToolsAccessibility?.rootInActiveWindow ?: return false
84 | return rootNode.findNodeById(nodeId)?.inputText(content) ?: false
85 | }
86 |
87 | @Get("setConfig")
88 | fun setConfig(type: String, value: String? = null): Boolean {
89 | when(type) {
90 | "idColor" -> setIdColor(value)
91 | "textColor" -> setTextColor(value)
92 | "floatWindowPackageName" -> setFloatWindowPackageName(value)
93 | }
94 | return true
95 | }
96 |
97 | private fun setIdColor(value: String?): Boolean {
98 | val illegalityId = value.isNullOrBlank().not() && value!!.length != 6
99 | if (illegalityId) {
100 | return false
101 | }
102 | AppContext.setSpValue("idColor", value.default())
103 | return true
104 | }
105 |
106 | private fun setTextColor(value: String?): Boolean {
107 | val illegalityText = value.isNullOrBlank().not() && value!!.length != 6
108 | if (illegalityText) {
109 | return false
110 | }
111 | AppContext.setSpValue("textColor", value.default())
112 | return true
113 | }
114 |
115 | private fun setFloatWindowPackageName(value: String?) {
116 | AutoToolsAccessibility.floatWindowPackageName = value
117 | }
118 |
119 | @Get("getHighlight")
120 | fun getHighlight(): List {
121 | val idColor = AppContext.getSpValue("idColor")
122 | val textColor = AppContext.getSpValue("textColor")
123 | return listOf(idColor, textColor)
124 | }
125 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/ui/ToolsMainFragment.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.ui
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.fragment.app.Fragment
8 | import androidx.lifecycle.MutableLiveData
9 | import com.android.accessibility.ext.isAccessibilityOpened
10 | import com.android.accessibility.ext.openAccessibilitySetting
11 | import com.android.local.service.core.ALSHelper
12 | import com.android.local.service.core.data.ServiceConfig
13 | import com.lygttpod.android.auto.tools.accessibility.AutoToolsAccessibility
14 | import com.lygttpod.android.auto.tools.databinding.FragmentToolsMainBinding
15 | import com.lygttpod.android.auto.tools.ktx.getPhoneWifiIpAddress
16 | import com.lygttpod.android.auto.tools.ktx.showPrintFloat
17 | import com.lygttpod.android.auto.tools.manager.ContentManger
18 | import com.lygttpod.android.auto.tools.service.AutoToolsService
19 |
20 |
21 | class ToolsMainFragment : Fragment() {
22 |
23 | private var _binding: FragmentToolsMainBinding? = null
24 |
25 | private val binding get() = _binding!!
26 |
27 |
28 | private val toolsServiceLiveData = MutableLiveData()
29 |
30 | override fun onCreateView(
31 | inflater: LayoutInflater, container: ViewGroup?,
32 | savedInstanceState: Bundle?
33 | ): View {
34 |
35 | _binding = FragmentToolsMainBinding.inflate(inflater, container, false)
36 | return binding.root
37 |
38 | }
39 |
40 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
41 | super.onViewCreated(view, savedInstanceState)
42 | initListener()
43 | initObserver()
44 | initALS()
45 | "PC端浏览器输入下边地址有惊喜哦\n${getPhoneWifiIpAddress()}:${ALSHelper.serviceList.firstOrNull()?.port}".also { binding.tvIpAddress.text = it }
46 | ContentManger.printContent = ""
47 | }
48 |
49 | private fun initALS() {
50 | ALSHelper.init(requireContext().applicationContext)
51 | ALSHelper.startService(ServiceConfig(AutoToolsService::class.java))
52 | }
53 |
54 | override fun onResume() {
55 | super.onResume()
56 | toolsServiceLiveData.value =
57 | requireContext().isAccessibilityOpened(AutoToolsAccessibility::class.java)
58 | }
59 |
60 | private fun initObserver() {
61 | toolsServiceLiveData.observe(viewLifecycleOwner) { open ->
62 | binding.btnNodePrint.isEnabled = open
63 | binding.btnOpenService.text =
64 | if (open) "【布局节点速查器】服务已开启" else "打开【布局节点速查器】服务"
65 | binding.tvIpAddress.visibility = if (open) View.VISIBLE else View.GONE
66 | }
67 |
68 | ContentManger.printContentLiveData.observe(viewLifecycleOwner) {
69 | val content = it ?: ""
70 | binding.tvToolsDes.visibility = if (content.isBlank()) View.VISIBLE else View.GONE
71 | binding.tvContent.text = content
72 | }
73 | }
74 |
75 | private fun initListener() {
76 | binding.btnOpenService.setOnClickListener {
77 | activity?.openAccessibilitySetting()
78 | }
79 |
80 | binding.btnNodePrint.setOnClickListener {
81 | activity?.application?.let { it.showPrintFloat(it) }
82 | }
83 |
84 | }
85 |
86 | override fun onDestroyView() {
87 | super.onDestroyView()
88 | _binding = null
89 | }
90 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/java/com/lygttpod/android/auto/tools/utils/SPUtils.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.tools.utils
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | object SPUtils {
7 | private fun getSharedPreference(context: Context, fileName: String): SharedPreferences {
8 | return context.getSharedPreferences(fileName, Context.MODE_PRIVATE)
9 | }
10 |
11 | fun getEditor(context: Context, fileName: String): SharedPreferences.Editor {
12 | return getSharedPreference(context, fileName).edit()
13 | }
14 |
15 | fun saveValue(context: Context, filename: String, key: String, value: Any?) {
16 | when (value) {
17 | is Int -> saveInt(context, filename, key, value)
18 | is Long -> saveLong(context, filename, key, value)
19 | is Float -> saveFloat(context, filename, key, value)
20 | is Boolean -> saveBoolean(context, filename, key, value)
21 | else -> saveString(context, filename, key, value?.toString() ?: "")
22 | }
23 | }
24 |
25 | fun saveBoolean(context: Context, filename: String, key: String, value: Boolean) {
26 | getEditor(context, filename).putBoolean(key, value).commit()
27 | }
28 |
29 | fun getBoolean(context: Context, filename: String, key: String): Boolean {
30 | return getSharedPreference(context, filename).getBoolean(key, false)
31 | }
32 |
33 | fun getBoolean(
34 | context: Context,
35 | filename: String,
36 | key: String,
37 | defaultValue: Boolean
38 | ): Boolean {
39 | return getSharedPreference(context, filename).getBoolean(key, defaultValue)
40 | }
41 |
42 | fun saveLong(context: Context, filename: String, key: String, value: Long) {
43 | getEditor(context, filename).putLong(key, value).commit()
44 | }
45 |
46 | fun saveFloat(context: Context, filename: String, key: String, value: Float) {
47 | getEditor(context, filename).putFloat(key, value).commit()
48 | }
49 |
50 | fun getLong(context: Context, filename: String, key: String): Long {
51 | return getSharedPreference(context, filename).getLong(key, 0)
52 | }
53 |
54 | fun getLong(context: Context, filename: String, key: String, defaultValue: Long): Long {
55 | return getSharedPreference(context, filename).getLong(key, defaultValue)
56 | }
57 |
58 | fun saveInt(context: Context, filename: String, key: String, value: Int) {
59 | getEditor(context, filename).putInt(key, value).commit()
60 | }
61 |
62 | fun getInt(context: Context, filename: String, key: String): Int {
63 | return getSharedPreference(context, filename).getInt(key, 0)
64 | }
65 |
66 | fun getInt(context: Context, filename: String, key: String, defaultValue: Int): Int {
67 | return getSharedPreference(context, filename).getInt(key, defaultValue)
68 | }
69 |
70 | fun saveString(context: Context, filename: String, key: String, value: String) {
71 | getEditor(context, filename).putString(key, value).commit()
72 | }
73 |
74 | fun getString(context: Context, filename: String, key: String): String? {
75 | return getSharedPreference(context, filename).getString(key, "")
76 | }
77 |
78 | fun getString(context: Context, filename: String, key: String, defaultValue: String): String? {
79 | return getSharedPreference(context, filename).getString(key, defaultValue)
80 | }
81 | }
--------------------------------------------------------------------------------
/auto-tools/src/main/res/layout/fragment_tools_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
20 |
28 |
29 |
40 |
41 |
49 |
50 |
51 |
58 |
59 |
62 |
63 |
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/auto-tools/src/main/res/layout/widget_float_print.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
23 |
--------------------------------------------------------------------------------
/auto-tools/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 布局节点速查器
4 |
5 | 【布局节点速查器】
6 | 使用说明
7 | 在无障碍服务中找到【布局节点速查器】并开启;\n\n
8 | APP中查看节点信息:\n\n
9 | 开启悬浮窗权限\n
10 | 点击【页面节点速查器】按钮显示出悬浮窗\n
11 | 点击悬浮窗按钮\n
12 | 回到APP中即可查看\n\n
13 | 电脑端浏览器中查看节点信息:\n\n
14 | 确保手机端和电脑端在一个局域网内\n
15 | 电脑浏览器中输入页面提示的IP地址\n
16 | 点击输入IP地址显示的网页上的按钮即可查看\n
17 |
18 |
--------------------------------------------------------------------------------
/auto-tools/src/main/res/xml/tools_accessible_service_config.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/auto-wx/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/auto-wx/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.lygttpod.android.auto.wx'
8 | compileSdk 33
9 |
10 | defaultConfig {
11 |
12 | minSdk 26
13 | targetSdk 33
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | kotlinOptions {
30 | jvmTarget = '1.8'
31 | }
32 | buildFeatures {
33 | viewBinding true
34 | }
35 | }
36 |
37 | dependencies {
38 | implementation(project(":accessibility"))
39 | implementation 'androidx.core:core-ktx:1.9.0'
40 | implementation 'androidx.appcompat:appcompat:1.6.1'
41 | implementation 'com.google.android.material:material:1.8.0'
42 | implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
43 | implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
44 | implementation 'com.github.lygttpod:ShapeView:1.0.2'
45 | }
--------------------------------------------------------------------------------
/auto-wx/consumer-rules.pro:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/auto-wx/consumer-rules.pro
--------------------------------------------------------------------------------
/auto-wx/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/auto-wx/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/adapter/FriendInfoAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.core.content.ContextCompat
7 | import androidx.recyclerview.widget.RecyclerView
8 | import com.lygttpod.android.auto.wx.R
9 | import com.lygttpod.android.auto.wx.data.WxUserInfo
10 | import com.lygttpod.android.auto.wx.databinding.ItemFriendBinding
11 | import com.lygttpod.android.auto.wx.em.FriendStatus
12 |
13 |
14 | class FriendInfoAdapter : RecyclerView.Adapter() {
15 |
16 | private var list = mutableListOf()
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FriendInfoViewHolder {
19 | return FriendInfoViewHolder(
20 | LayoutInflater.from(parent.context).inflate(R.layout.item_friend, parent, false)
21 | )
22 | }
23 |
24 | override fun onBindViewHolder(holder: FriendInfoViewHolder, position: Int) {
25 | holder.bindData(list[position], position)
26 | }
27 |
28 | override fun getItemCount() = list.size
29 |
30 | fun clear() {
31 | list.clear()
32 | notifyDataSetChanged()
33 | }
34 |
35 | fun filterNotNormalData() {
36 | this.list =
37 | list.filterNot { it.status == FriendStatus.NORMAL || it.status == FriendStatus.UNKNOW }
38 | .toMutableList()
39 | notifyDataSetChanged()
40 | }
41 | fun filterUnCheckData() {
42 | this.list =
43 | list.filterNot { it.status == FriendStatus.NORMAL || it.status == FriendStatus.UNKNOW }
44 | .toMutableList()
45 | notifyDataSetChanged()
46 | }
47 | fun filterAllData() {
48 | this.list =
49 | list.filterNot { it.status == FriendStatus.NORMAL || it.status == FriendStatus.UNKNOW }
50 | .toMutableList()
51 | notifyDataSetChanged()
52 | }
53 |
54 | fun setData(list: MutableList) {
55 | this.list = list
56 | notifyDataSetChanged()
57 | }
58 |
59 | fun addData(data: WxUserInfo) {
60 | val index = list.indexOfFirst { it.nickName == data.nickName }
61 | if (index == -1) {
62 | this.list.add(data)
63 | notifyItemInserted(itemCount)
64 | } else {
65 | this.list[index] = data
66 | notifyItemChanged(index)
67 | }
68 | }
69 |
70 | fun addDatas(newData: MutableList) {
71 | this.list.addAll(newData)
72 | notifyItemRangeInserted(list.size - newData.size, newData.size)
73 | }
74 |
75 | class FriendInfoViewHolder(view: View) : RecyclerView.ViewHolder(view) {
76 | private val binding = ItemFriendBinding.bind(view)
77 |
78 | fun bindData(wxUserInfo: WxUserInfo, position: Int) {
79 | binding.tvIndex.text = "${position + 1}"
80 | binding.tvNickName.text = wxUserInfo.nickName
81 | binding.tvWxCode.text = wxUserInfo.wxCode
82 | binding.tvWxCode.visibility =
83 | if (wxUserInfo.wxCode.isNullOrBlank()) View.GONE else View.VISIBLE
84 | binding.tvStatus.text = wxUserInfo.status.status
85 | val color = when (wxUserInfo.status) {
86 | FriendStatus.BLACK -> R.color.friend_black
87 | FriendStatus.DELETE -> R.color.friend_delete
88 | FriendStatus.ACCOUNT_EXCEPTION -> R.color.friend_exc
89 | FriendStatus.NORMAL -> R.color.friend_normal
90 | FriendStatus.UNKNOW -> R.color.friend_unknow
91 | }
92 | binding.tvStatus.setTextColor(ContextCompat.getColor(itemView.context, color))
93 | }
94 | }
95 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/data/NodeInfo.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.data
2 |
3 | data class NodeInfo(val nodeText: String, val nodeId: String, val des: String)
4 |
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/data/WxUserInfo.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.data
2 |
3 | import com.lygttpod.android.auto.wx.em.FriendStatus
4 |
5 | data class WxUserInfo(
6 | val nickName: String,
7 | val wxCode: String? = null,
8 | var status: FriendStatus = FriendStatus.UNKNOW
9 | ) {
10 | override fun toString(): String {
11 | return "nickName: $nickName → wxCode: $wxCode → status: ${status.status}"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/em/CheckStatus.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.em
2 |
3 | enum class CheckStatus(val status: String, val mark: String) {
4 | BLACK("被拉黑", "请确认你和他(她)的好友关系是否正常"),
5 | DELETE("被删除", "你不是收款方好友,对方添加你为好友后才能发起转账"),
6 | ACCOUNT_EXCEPTION("帐号异常", "对方微信号已被限制登录,为保障你的资金安全,暂时无法完成交易"),
7 | NORMAL("正常", "付款方式"),
8 | PWD_PAY("正常", "请输入支付密码"),
9 | NO_AUTH("自己未实名认证", "实名认证"),
10 | }
11 |
12 | fun CheckStatus?.toFriendStatus(): FriendStatus {
13 | return when (this) {
14 | CheckStatus.BLACK -> FriendStatus.BLACK
15 | CheckStatus.DELETE -> FriendStatus.DELETE
16 | CheckStatus.ACCOUNT_EXCEPTION -> FriendStatus.ACCOUNT_EXCEPTION
17 | CheckStatus.NORMAL -> FriendStatus.NORMAL
18 | CheckStatus.PWD_PAY -> FriendStatus.NORMAL
19 | CheckStatus.NO_AUTH -> FriendStatus.NORMAL
20 | else -> FriendStatus.UNKNOW
21 | }
22 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/em/FriendStatus.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.em
2 |
3 | enum class FriendStatus(val status: String) {
4 | BLACK("被拉黑"),
5 | DELETE("被删除"),
6 | NORMAL("正常"),
7 | ACCOUNT_EXCEPTION("帐号异常"),
8 | UNKNOW("待检测"),
9 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/FriendStatusHelper.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.helper
2 |
3 | import android.util.Log
4 | import com.lygttpod.android.auto.wx.data.WxUserInfo
5 | import com.lygttpod.android.auto.wx.em.FriendStatus
6 |
7 | object FriendStatusHelper {
8 |
9 | interface TaskCallBack {
10 | fun onTaskStart(taskName: String)
11 | fun onTaskEnd(totalTime: Long)
12 | }
13 |
14 | var taskCallBack: TaskCallBack? = null
15 |
16 | //好友状态检测结果
17 | private val userResultList = mutableListOf()
18 |
19 |
20 | //上次检测到的好友,【假转账方法中「续捡」要用】
21 | var lastCheckUser: WxUserInfo? = null
22 |
23 | fun reset() {
24 | lastCheckUser = null
25 | clearFriendsStatusList()
26 | }
27 |
28 | fun addCheckResult(data: WxUserInfo) {
29 | Log.d("检查结果", "$data")
30 | val find = userResultList.indexOfFirst { it.nickName == data.nickName }
31 | if (find == -1) {
32 | userResultList.add(data)
33 | } else {
34 | userResultList[find] = data
35 | }
36 | }
37 |
38 | fun addCheckResults(list: MutableList?) {
39 | list ?: return
40 | Log.d("检查结果", "${list.map { it.toString() + "\n" }}")
41 | userResultList.addAll(list)
42 | }
43 |
44 | fun addFriends(list: List) {
45 | userResultList.clear()
46 | userResultList.addAll(list.map { WxUserInfo(it) })
47 | }
48 |
49 | fun clearFriendsStatusList() = userResultList.clear()
50 |
51 | fun getUserResultList() = userResultList
52 |
53 | fun filterNotNormalData(): MutableList {
54 | return userResultList.filterNot { it.status == FriendStatus.NORMAL || it.status == FriendStatus.UNKNOW }
55 | .toMutableList()
56 | }
57 |
58 | fun filterUnCheckData(): MutableList {
59 | return userResultList.filter { it.status == FriendStatus.UNKNOW }
60 | .toMutableList()
61 | }
62 |
63 | fun filterAllData(): MutableList {
64 | return userResultList
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/HBTaskHelper.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.helper
2 |
3 | import android.app.Notification
4 | import android.util.Log
5 | import android.view.accessibility.AccessibilityEvent
6 | import com.android.accessibility.ext.acc.isNotificationStateChanged
7 | import com.lygttpod.android.auto.wx.page.WXHBPage
8 | import kotlinx.coroutines.CoroutineScope
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.SupervisorJob
11 | import kotlinx.coroutines.cancel
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.buffer
14 | import kotlinx.coroutines.launch
15 | import kotlinx.coroutines.sync.Mutex
16 | import kotlinx.coroutines.sync.withLock
17 |
18 | object HBTaskHelper {
19 |
20 | var enableFuckMoney = false
21 |
22 | private val mutex = Mutex()
23 | private val eventFlow = MutableStateFlow(null)
24 |
25 | private val hbTaskScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
26 |
27 | fun autoFuckMoney(enable: Boolean) {
28 | enableFuckMoney = enable
29 | if (enableFuckMoney) {
30 | hbTaskScope.launch {
31 | eventFlow.buffer().collect {
32 | mutex.withLock {
33 | WXHBPage.fuckMoney()
34 | }
35 | }
36 | }
37 | } else {
38 | hbTaskScope.cancel("关闭自动抢红包功能")
39 | }
40 | }
41 |
42 | fun hbTask(event: AccessibilityEvent) {
43 | if (!enableFuckMoney) return
44 | if (event.isNotificationStateChanged()) {
45 | if (event.isHBEvent()) {
46 | (event.parcelableData as? Notification)?.let {
47 | try {
48 | it.contentIntent.send()
49 | Log.d("WxHbAccessibility", "点击了通知")
50 | } catch (e: Exception) {
51 | Log.d("WxHbAccessibility", "点击通知异常:${e.message}")
52 | }
53 | }
54 | }
55 | } else {
56 | hbTaskScope.launch {
57 | if (mutex.isLocked) return@launch
58 | eventFlow.emit(event)
59 | }
60 | }
61 | }
62 |
63 | private fun AccessibilityEvent.isHBEvent() =
64 | text.firstOrNull()?.contains(": [微信红包]恭喜发财,大吉大利") == true
65 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/helper/TaskByGroupHelper.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.helper
2 |
3 | import android.content.Context
4 | import com.android.accessibility.ext.acc.pressBackButton
5 | import com.android.accessibility.ext.goToWx
6 | import com.android.accessibility.ext.toast
7 | import com.lygttpod.android.auto.wx.data.WxUserInfo
8 | import com.lygttpod.android.auto.wx.page.WXHomePage
9 | import com.lygttpod.android.auto.wx.page.group.WXCreateGroupPage
10 | import com.lygttpod.android.auto.wx.page.group.WXGroupChatPage
11 | import com.lygttpod.android.auto.wx.page.group.WXGroupInfoPage
12 | import com.lygttpod.android.auto.wx.page.group.WXGroupManagerPage
13 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
14 | import kotlinx.coroutines.CoroutineScope
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.SupervisorJob
17 | import kotlinx.coroutines.delay
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.sync.Mutex
20 | import kotlinx.coroutines.sync.withLock
21 |
22 | /**
23 | * 拉群检测好友状态任务
24 | */
25 | object TaskByGroupHelper {
26 |
27 | private val taskScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
28 |
29 | //微信规则:1、同时最多邀请40人创建群;2、超过30人的群会出现单独向用户发送群聊邀请的消息
30 | //所以就设为一个稍微安全一点的值
31 | const val GROUP_USER_MAX_COUNT = 28
32 |
33 | //已检测过的用户列表
34 | val alreadyCheckedUsers = mutableListOf()
35 |
36 | private val mutex = Mutex()
37 |
38 | fun startTask(context: Context) {
39 | taskScope.launch {
40 | if (mutex.isLocked) {
41 | context.toast("上次任务还没结束哦(有重试机制),请稍等再试")
42 | return@launch
43 | }
44 | mutex.withLock {
45 | FriendStatusHelper.taskCallBack?.onTaskStart("正在执行【拉群检测】任务")
46 | val start = System.currentTimeMillis()
47 | startCheckByCreateGroup(context)
48 | val end = System.currentTimeMillis()
49 | FriendStatusHelper.taskCallBack?.onTaskEnd(end - start)
50 | }
51 | }
52 | }
53 |
54 | private suspend fun startCheckByCreateGroup(context: Context) {
55 | alreadyCheckedUsers.clear()
56 | FriendStatusHelper.clearFriendsStatusList()
57 | context.goToWx()
58 | //判断当前是否进入到微信
59 | val inWxApp = WXHomePage.waitEnterWxApp()
60 | if (!inWxApp) return
61 | singleTask()
62 | //任务结束返回APP
63 | val isHome = WXHomePage.backToHome()
64 | if (isHome) {
65 | wxAccessibilityService?.pressBackButton()
66 | }
67 | }
68 |
69 | private suspend fun singleTask() {
70 | val isHome = WXHomePage.backToHome()
71 | if (!isHome) return
72 | //微信首页添加好友的图标
73 | val isClickPlusBtn = WXHomePage.clickRightTopPlusBtn()
74 | if (!isClickPlusBtn) return
75 | //点击发起群聊按钮
76 | val isClickCreateGroup = WXHomePage.clickCreateGroupBtn()
77 | if (!isClickCreateGroup) return
78 | //判断当前是在发起群聊页
79 | val isCreateGroupPage = WXCreateGroupPage.inPage()
80 | if (!isCreateGroupPage) return
81 | //选择这次选择入群的好友列表
82 | val selectUser = WXCreateGroupPage.selectUser()
83 | //当发现选择建群的用户少于2个人的时候,无法建群(即选择一个人是无法拉群的)
84 | if (selectUser.size <= 1) {
85 | if (selectUser.size == 1) {
86 | FriendStatusHelper.addCheckResults(mutableListOf(WxUserInfo(selectUser.first())))
87 | }
88 | WXHomePage.backToHome()
89 | return
90 | }
91 | //点击完成,开始建群
92 | val isClickComplete = WXCreateGroupPage.clickCompleteBtn()
93 | if (!isClickComplete) return
94 | //判断是否进入到群聊页,因为创建群需要一定的时间
95 | val inGroupCHatPage = WXGroupChatPage.inPage()
96 | if (!inGroupCHatPage) return
97 | //在群里判断好友状态
98 | val result = WXGroupChatPage.checkUserStatus()
99 | FriendStatusHelper.addCheckResults(result)
100 |
101 | //=======检测完毕开始解散群和删除群=======
102 |
103 | //点击群聊右上角按钮
104 | val isClickMore = WXGroupChatPage.clickMoreBtn()
105 | if (!isClickMore) return
106 | //是否在群聊信息页
107 | val groupManagerBtnShow = WXGroupInfoPage.groupManagerBtnShow()
108 | if (!groupManagerBtnShow) return
109 | //点击群管理按钮
110 | val clickGroupManager = WXGroupInfoPage.clickGroupManager()
111 | if (!clickGroupManager) return
112 | //是否在群管页面
113 | val inGroupManagerPage = WXGroupManagerPage.inPage()
114 | if (!inGroupManagerPage) return
115 | //点击解散该群聊按钮
116 | val clickDisbandGroup = WXGroupManagerPage.clickDisbandGroup()
117 | if (!clickDisbandGroup) return
118 | //点击弹框确定解散按钮
119 | val clickDisbandGroupDialog = WXGroupManagerPage.clickDisbandGroupDialog()
120 | if (!clickDisbandGroupDialog) return
121 | //再次点击群聊右上角按钮
122 | val clickMoreAgain = WXGroupChatPage.clickMoreBtn()
123 | if (!clickMoreAgain) return
124 | //是否出现删除按钮
125 | val deleteShow = WXGroupInfoPage.deleteShow()
126 | if (!deleteShow) return
127 | //点击删除按钮
128 | val clickDeleteGroup = WXGroupInfoPage.clickDeleteGroup()
129 | if (!clickDeleteGroup) return
130 | //点击弹框确定删除按钮
131 | val clickDeleteGroupDialog = WXGroupInfoPage.clickDeleteGroupDialog()
132 | if (!clickDeleteGroupDialog) return
133 |
134 | delay(2000)
135 | //循环执行以上步骤
136 | singleTask()
137 | }
138 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/ktx/FormatExt.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.ktx
2 |
3 | fun Long.formatTime(): String {
4 | val m = (this / 1000 / 60 % 60).toInt()
5 | val s = (this / 1000 % 60).toInt()
6 | val minutes = if (m < 10) "$m" else m.toString()
7 | val seconds = if (s < 10) "$s" else s.toString()
8 | return "${minutes}分${seconds}秒"
9 | }
10 |
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/IPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import kotlinx.coroutines.delay
4 |
5 | interface IPage {
6 |
7 | fun delayTime() = 500L
8 |
9 | fun pageClassName(): String
10 |
11 | fun pageTitleName(): String
12 |
13 | fun isMe(): Boolean
14 |
15 | suspend fun delayAction(delayMillis: Long = delayTime(), block: suspend () -> T): T {
16 | delay(delayMillis)
17 | return block()
18 | }
19 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXChattingPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.acc.clickById
5 | import com.android.accessibility.ext.acc.clickByIdAndText
6 | import com.android.accessibility.ext.acc.findById
7 | import com.android.accessibility.ext.acc.findNodesById
8 | import com.android.accessibility.ext.acc.inputText
9 | import com.android.accessibility.ext.acc.isEditText
10 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
11 | import com.lygttpod.android.auto.wx.data.NodeInfo
12 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
13 | import com.lygttpod.android.auto.wx.version.nodeProxy
14 |
15 | object WXChattingPage : IPage {
16 |
17 | interface Nodes {
18 | val chattingBottomRootNode: NodeInfo
19 | val chattingBottomPlusNode: NodeInfo
20 | val chattingTransferMoneyNode: NodeInfo
21 | val chattingSendMsgNode: NodeInfo
22 | val chattingEditTextNode: NodeInfo
23 |
24 | companion object : Nodes by nodeProxy()
25 | }
26 |
27 | override fun pageClassName() = "com.tencent.mm.ui.chatting.ChattingUI"
28 |
29 | override fun pageTitleName() = "微信聊天页"
30 |
31 | override fun isMe(): Boolean {
32 | return wxAccessibilityService?.findById(Nodes.chattingBottomRootNode.nodeId) != null
33 | }
34 |
35 | suspend fun checkInPage(): Boolean {
36 | return delayAction {
37 | retryCheckTaskWithLog("检查当前是是否打开聊天页") {
38 | isMe()
39 | }
40 | }
41 | }
42 |
43 | /**
44 | * 点击更多功能按钮
45 | */
46 | suspend fun clickMoreOption(): Boolean {
47 | return delayAction(delayMillis = 1000) {
48 | retryCheckTaskWithLog("点击聊天页的功能区【+】按钮") {
49 | val clickMoreOption =
50 | wxAccessibilityService.clickById(Nodes.chattingBottomPlusNode.nodeId)
51 | if (!clickMoreOption) {
52 | val findSendBtn =
53 | wxAccessibilityService?.findById(Nodes.chattingSendMsgNode.nodeId) != null
54 | if (findSendBtn) {
55 | Log.d("LogTracker", "发现功能区是【发送】按钮,需要先去清空输入框的的内容")
56 | val clear = wxAccessibilityService
57 | ?.findNodesById(Nodes.chattingEditTextNode.nodeId)
58 | ?.lastOrNull { it.isEditText() }
59 | ?.inputText("")
60 | ?: false
61 | if (clear) {
62 | Log.d("LogTracker", "已清空输入的草本文本")
63 | }
64 | }
65 | }
66 | clickMoreOption
67 | }
68 | }
69 | }
70 |
71 | /**
72 | * 点击转账
73 | */
74 | suspend fun clickTransferMoney(): Boolean {
75 | return delayAction(delayMillis = 1000) {
76 | retryCheckTaskWithLog("点击聊天页功能区的【转账】按钮") {
77 | // wxAccessibilityService?.printNodeInfo()
78 | // wxAccessibilityService.findWithClickByText("转账")
79 | wxAccessibilityService.clickByIdAndText(
80 | Nodes.chattingTransferMoneyNode.nodeId,
81 | Nodes.chattingTransferMoneyNode.nodeText
82 | )
83 | }
84 | }
85 | }
86 |
87 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXContactInfoPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import com.android.accessibility.ext.acc.clickByText
4 | import com.android.accessibility.ext.acc.findById
5 | import com.android.accessibility.ext.acc.findByText
6 | import com.android.accessibility.ext.default
7 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
8 | import com.android.accessibility.ext.task.retryTaskWithLog
9 | import com.lygttpod.android.auto.wx.data.NodeInfo
10 | import com.lygttpod.android.auto.wx.data.WxUserInfo
11 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
12 | import com.lygttpod.android.auto.wx.version.nodeProxy
13 |
14 | object WXContactInfoPage : IPage {
15 |
16 | interface Nodes {
17 | val contactInfoSendMsgNode: NodeInfo
18 | val contactInfoNickNameNode: NodeInfo
19 | val contactInfoWxCodeNode: NodeInfo
20 |
21 | companion object : Nodes by nodeProxy()
22 | }
23 |
24 | override fun pageClassName() = "com.tencent.mm.plugin.profile.ui.ContactInfoUI"
25 |
26 | override fun pageTitleName() = "通讯录用户信息页"
27 |
28 | override fun isMe(): Boolean {
29 | return wxAccessibilityService?.findByText(Nodes.contactInfoSendMsgNode.nodeText) != null
30 | }
31 |
32 | suspend fun inPage(): Boolean {
33 | return delayAction {
34 | retryCheckTaskWithLog("判断是否进入到【通讯录用户信息页】") { isMe() }
35 | }
36 | }
37 |
38 | /**
39 | * 点击发消息按钮
40 | */
41 | suspend fun clickSendMsg(): Boolean {
42 | return delayAction(delayMillis = 1000) {
43 | retryCheckTaskWithLog("点击【发消息】按钮") {
44 | //【发消息】的按钮ID和【音视频通话】的ID一样,所以用文本区分
45 | wxAccessibilityService.clickByText(Nodes.contactInfoSendMsgNode.nodeText)
46 | }
47 | }
48 | }
49 |
50 | suspend fun getUserInfo(): WxUserInfo? {
51 | //android.widget.TextView → text = Allen → id = com.tencent.mm:id/bq1
52 | //android.widget.TextView → text = 微信号: lygttpod → id = com.tencent.mm:id/bq9
53 | return retryTaskWithLog("获取用户信息") {
54 | val nickName = wxAccessibilityService
55 | ?.findById(Nodes.contactInfoNickNameNode.nodeId)
56 | ?.text
57 | .default()
58 | //android.widget.TextView → 微信号: wxid_xxx → com.tencent.mm:id/ini
59 | val wxCode = wxAccessibilityService
60 | ?.findById(Nodes.contactInfoWxCodeNode.nodeId)
61 | ?.text
62 | ?.split("微信号:")
63 | ?.getOrNull(1)
64 | .default()
65 | .trim()
66 | //测试中发现当好友注销账号时候是没有微信号的,昵称下边显示的是【对方已注销账号】
67 | //所以这里去掉微信号不能为空的判断
68 | if (nickName.isNotBlank()) {
69 | WxUserInfo(nickName, wxCode)
70 | } else {
71 | null
72 | }
73 | }
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXContactPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.acc.*
5 | import com.android.accessibility.ext.default
6 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
7 | import com.android.accessibility.ext.task.retryTaskWithLog
8 | import com.lygttpod.android.auto.wx.data.NodeInfo
9 | import com.lygttpod.android.auto.wx.helper.TaskHelper
10 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
11 | import com.lygttpod.android.auto.wx.version.nodeProxy
12 | import kotlin.streams.toList
13 |
14 | object WXContactPage : IPage {
15 |
16 | interface Nodes {
17 | val contactTitleNode: NodeInfo
18 | val contactListNode: NodeInfo
19 | val contactUserNode: NodeInfo
20 |
21 | companion object : Nodes by nodeProxy()
22 | }
23 |
24 | override fun pageClassName() = "com.tencent.mm.plugin.profile.ui.ContactInfoUI"
25 |
26 | override fun pageTitleName() = "通讯录tab页"
27 |
28 | override fun isMe(): Boolean {
29 | return wxAccessibilityService?.findByIdAndText(
30 | Nodes.contactTitleNode.nodeId,
31 | Nodes.contactTitleNode.nodeText
32 | ) != null
33 | }
34 |
35 | suspend fun inPage(): Boolean {
36 | return retryCheckTaskWithLog("判断是否在通讯录列表并且获得了用户列表") {
37 | isMe() && isShowUserList()
38 | }
39 | }
40 |
41 | private fun isShowUserList(): Boolean {
42 | return wxAccessibilityService?.findChildNodes(
43 | Nodes.contactListNode.nodeId,
44 | Nodes.contactUserNode.nodeId
45 | )?.isNotEmpty() ?: false
46 | }
47 |
48 | /**
49 | * 通过字段滑动列表去找到需要点击的用户
50 | */
51 | suspend fun scrollToFindUserThenClick(userName: String): Boolean {
52 | return delayAction {
53 | retryCheckTaskWithLog("点击【$userName】用户", timeOutMillis = 60_000) {
54 | wxAccessibilityService.scrollToClickByText(
55 | Nodes.contactListNode.nodeId,
56 | userName
57 | )
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * 通过字段滑动列表去找到上次已经检测过的好友的位置,从他后边继续检测
64 | */
65 | suspend fun scrollToLastUserPosition(userName: String?): Boolean {
66 | userName ?: return false
67 | return delayAction {
68 | retryCheckTaskWithLog("【继续上次检测】先定位到【$userName】的位置", timeOutMillis = 60_000) {
69 | wxAccessibilityService.scrollToFindByText(
70 | Nodes.contactListNode.nodeId,
71 | userName
72 | ) != null
73 | }
74 | }
75 | }
76 |
77 | /**
78 | * 通过上次点击昵称去找下一个需要点击的节点
79 | */
80 | suspend fun scrollToClickNextNodeByCurrentText(lastUser: String?): String? {
81 | return delayAction {
82 | val taskName =
83 | if (lastUser.isNullOrBlank()) "寻找并点击第一个好友" else "寻找并点击【$lastUser】的下一个好友"
84 | retryTaskWithLog(taskName, timeOutMillis = 5 * 60_000) {
85 | val find = wxAccessibilityService.scrollToFindNextNodeByCurrentText(
86 | Nodes.contactListNode.nodeId,
87 | Nodes.contactUserNode.nodeId,
88 | lastUser,
89 | filterTexts = mutableListOf("微信团队", "文件传输助手").apply {
90 | TaskHelper.myWxInfo?.nickName?.let { this.add(it) }
91 | }
92 | )
93 | find.click()
94 | find?.text.default()
95 | }
96 | }
97 | }
98 |
99 | suspend fun getAllFriends(): List {
100 | return delayAction {
101 | retryTaskWithLog("获取通讯录好友列表", timeOutMillis = 2 * 60_000) {
102 | val friends = wxAccessibilityService.findAllChildByScroll(
103 | Nodes.contactListNode.nodeId,
104 | Nodes.contactUserNode.nodeId
105 | )
106 | val result = friends.stream().map { it.text.default() }
107 | .filter { it != "微信团队" && it != "文件传输助手" }.toList()
108 | Log.d("printNodeInfo", "好友个数: ${result.size} ----> 好友列表:${result}")
109 | result
110 | } ?: listOf()
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXHBPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import android.view.accessibility.AccessibilityNodeInfo
4 | import com.android.accessibility.ext.acc.*
5 | import com.android.accessibility.ext.default
6 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
7 | import com.android.accessibility.ext.task.retryTaskWithLog
8 | import com.lygttpod.android.auto.wx.data.NodeInfo
9 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
10 | import com.lygttpod.android.auto.wx.version.nodeProxy
11 |
12 | object WXHBPage : IPage {
13 |
14 | interface Nodes {
15 | val chatPageTitleNode: NodeInfo
16 | val hbPatentNode: NodeInfo
17 | val hbReceiveNode: NodeInfo
18 | val hbOpenNode: NodeInfo
19 | val hbBackNode: NodeInfo
20 | val hbSenderNode: NodeInfo
21 | val hbNumNode: NodeInfo
22 | val hbMissNode: NodeInfo
23 | val hbMissCloseNode: NodeInfo
24 |
25 | companion object : Nodes by nodeProxy()
26 | }
27 |
28 | enum class HBStatus {
29 | ENABLE,//可抢
30 | MISS,//错失
31 | UNKNOW//未知
32 | }
33 |
34 | override fun pageClassName() = ""
35 |
36 | override fun pageTitleName() = "微信抢红包"
37 |
38 | override fun isMe(): Boolean {
39 | return wxAccessibilityService?.findById(Nodes.chatPageTitleNode.nodeId) != null
40 | }
41 |
42 | private suspend fun inPage() = retryCheckTaskWithLog("判断是否在聊天页面") { isMe() }
43 |
44 | suspend fun fuckMoney() {
45 | val inPage = inPage()
46 | if (!inPage) return
47 | findHBNodes().forEach { openHB(it) }
48 | }
49 |
50 | private fun findHBNodes(): List {
51 | return wxAccessibilityService
52 | .findNodesById(Nodes.hbPatentNode.nodeId)
53 | .filter {
54 | it.findNodeById(Nodes.hbReceiveNode.nodeId) == null
55 | }
56 | }
57 |
58 | private suspend fun openHB(nodeInfo: AccessibilityNodeInfo) {
59 | val inPage = inPage()
60 | if (!inPage) return
61 | val click = clickHb(nodeInfo)
62 | if (!click) return
63 | when(checkClickStatus()) {
64 | HBStatus.ENABLE -> {
65 | val open = clickOpenHb()
66 | if (!open) return
67 | val inHbDetails = inHbDetails()
68 | if (!inHbDetails) return
69 | val hbInfo = getHbInfo()
70 | backFromHBDetail(hbInfo)
71 | }
72 | HBStatus.MISS -> {
73 | clickCloseMissHb()
74 | }
75 | else -> {}
76 | }
77 | }
78 |
79 | private suspend fun clickHb(nodeInfo: AccessibilityNodeInfo): Boolean {
80 | return retryCheckTaskWithLog("点击红包") { nodeInfo.click() }
81 | }
82 |
83 | private suspend fun checkClickStatus(): HBStatus {
84 | //1、可能被抢完了
85 | //2、可以正常抢
86 | return retryTaskWithLog("检查点击红包状态") {
87 | val miss = wxAccessibilityService?.findById(Nodes.hbMissNode.nodeId) != null
88 | val canOpen = wxAccessibilityService?.findById(Nodes.hbOpenNode.nodeId) != null
89 | when{
90 | miss -> HBStatus.MISS
91 | canOpen -> HBStatus.ENABLE
92 | else -> null
93 | }
94 | } ?: HBStatus.UNKNOW
95 | }
96 |
97 | private suspend fun clickCloseMissHb(): Boolean {
98 | return retryCheckTaskWithLog("点击关闭错失的红包") {
99 | wxAccessibilityService.clickById(Nodes.hbMissCloseNode.nodeId)
100 | }
101 | }
102 |
103 | private suspend fun clickOpenHb(): Boolean {
104 | return retryCheckTaskWithLog("点击开红包") {
105 | wxAccessibilityService.clickById(Nodes.hbOpenNode.nodeId)
106 | }
107 | }
108 |
109 | private suspend fun inHbDetails(): Boolean {
110 | return delayAction {
111 | retryCheckTaskWithLog("判断当前是否在红包详情页") {
112 | wxAccessibilityService?.findById(Nodes.hbSenderNode.nodeId) != null
113 | }
114 | }
115 | }
116 |
117 | private suspend fun getHbInfo(): String? {
118 | return retryTaskWithLog("获取红包信息") {
119 | val hbSender =
120 | wxAccessibilityService?.findById(Nodes.hbSenderNode.nodeId)?.text.default()
121 | val getHbNum =
122 | wxAccessibilityService?.findById(Nodes.hbNumNode.nodeId)?.text.default()
123 |
124 | "${hbSender},抢到:$getHbNum 元"
125 | }
126 | }
127 |
128 | private suspend fun backFromHBDetail(hbInfo: String?): Boolean {
129 | return delayAction {
130 | retryCheckTaskWithLog("$hbInfo 点击返回") {
131 | wxAccessibilityService.clickById(Nodes.hbBackNode.nodeId)
132 | }
133 | }
134 | }
135 |
136 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXHomePage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.acc.*
5 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
6 | import com.lygttpod.android.auto.wx.data.NodeInfo
7 | import com.lygttpod.android.auto.wx.service.WXAccessibility
8 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
9 | import com.lygttpod.android.auto.wx.version.nodeProxy
10 | import kotlinx.coroutines.delay
11 |
12 | object WXHomePage : IPage {
13 |
14 | interface Nodes {
15 | val homeBottomNavNode: NodeInfo
16 | val bottomNavContactsTabNode: NodeInfo
17 | val bottomNavMineTabNode: NodeInfo
18 | val homeRightTopPlusNode: NodeInfo
19 | val createGroupNode: NodeInfo
20 |
21 | companion object : Nodes by nodeProxy()
22 | }
23 |
24 | override fun pageClassName() = "com.tencent.mm.ui.LauncherUI"
25 |
26 | override fun pageTitleName() = "微信首页"
27 |
28 | /**
29 | * com.tencent.mm:id/fj3是微信首页底导布局id
30 | * 找到这个节点就可以说明当前在微信首页
31 | */
32 | override fun isMe(): Boolean {
33 | return wxAccessibilityService?.findById(Nodes.homeBottomNavNode.nodeId) != null
34 | }
35 |
36 | /**
37 | * 等待打开微信APP
38 | */
39 | suspend fun waitEnterWxApp(): Boolean {
40 | return delayAction {
41 | retryCheckTaskWithLog("等待打开微信APP") {
42 | val inApp = isMe()
43 | WXAccessibility.isInWXApp.set(inApp)
44 | inApp
45 | }
46 | }
47 | }
48 |
49 | /**
50 | * 回到微信首页
51 | */
52 | suspend fun backToHome(): Boolean {
53 | return retryCheckTaskWithLog("打开[微信首页]") {
54 | if (isMe()) {
55 | true
56 | } else {
57 | // TODO: 判断不准确,待完善
58 | if (WXAccessibility.isInWXApp.get()) {
59 | wxAccessibilityService?.pressBackButton()
60 | } else {
61 | throw RuntimeException("检测到不再微信首页了,终止自动化程序")
62 | }
63 | false
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * 点击通讯录tab
70 | */
71 | suspend fun clickContactsTab(doubleClick: Boolean = false): Boolean {
72 | return delayAction {
73 | retryCheckTaskWithLog("点击【通讯录】tab") {
74 | val tabNode = wxAccessibilityService?.findByIdAndText(Nodes.bottomNavContactsTabNode.nodeId, Nodes.bottomNavContactsTabNode.nodeText)
75 | if (doubleClick) {
76 | Log.d("clickContactsTab", "双击 通讯录 tab 第一次")
77 | tabNode.click()
78 | delay(300)
79 | Log.d("clickContactsTab", "双击 通讯录 tab 第二次")
80 | tabNode.click()
81 | } else {
82 | if (tabNode?.isSelected == true) {
83 | Log.d("clickContactsTab", "已经在 通讯录 页面了 无需再点击")
84 | true
85 | } else {
86 | Log.d("clickContactsTab", "单击 通讯录 tab")
87 | tabNode.click()
88 | }
89 | }
90 | }
91 | }
92 | }
93 |
94 | /**
95 | * 点击通讯录tab
96 | */
97 | suspend fun clickMineTab(): Boolean {
98 | return delayAction {
99 | retryCheckTaskWithLog("点击【我的】tab") {
100 | wxAccessibilityService?.findByIdAndText(
101 | Nodes.bottomNavMineTabNode.nodeId,
102 | Nodes.bottomNavMineTabNode.nodeText
103 | ).click()
104 | }
105 | }
106 | }
107 |
108 | /**
109 | * 点击微信首页右上角的➕按钮
110 | */
111 | suspend fun clickRightTopPlusBtn(): Boolean {
112 | return delayAction {
113 | retryCheckTaskWithLog("点击首页右上角的【+】按钮") {
114 | //android.widget.RelativeLayout → text = → id = com.tencent.mm:id/grs → description = 更多功能按钮
115 | wxAccessibilityService.clickById(Nodes.homeRightTopPlusNode.nodeId)
116 | // wxAccessibilityService?.findById("com.tencent.mm:id/grs")?.click() == true
117 | }
118 | }
119 | }
120 |
121 | /**
122 | * 点击发起群聊按钮
123 | */
124 | suspend fun clickCreateGroupBtn(): Boolean {
125 | return delayAction {
126 | retryCheckTaskWithLog("点击【发起群聊】按钮") {
127 | //android.widget.TextView → text = 发起群聊 → id = com.tencent.mm:id/knx
128 | wxAccessibilityService.clickById(Nodes.createGroupNode.nodeId)
129 | // wxAccessibilityService?.findById("com.tencent.mm:id/knx")?.click() == true
130 | }
131 | }
132 | }
133 |
134 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXMinePage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.acc.findById
5 | import com.android.accessibility.ext.default
6 | import com.android.accessibility.ext.task.retryTaskWithLog
7 | import com.lygttpod.android.auto.wx.data.NodeInfo
8 | import com.lygttpod.android.auto.wx.data.WxUserInfo
9 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
10 | import com.lygttpod.android.auto.wx.version.nodeProxy
11 |
12 |
13 | object WXMinePage : IPage {
14 |
15 | private val TAG = WXMinePage::class.java.simpleName
16 |
17 | interface Nodes {
18 | val mineNickNameNode: NodeInfo
19 | val mineWxCodeNode: NodeInfo
20 |
21 | companion object : Nodes by nodeProxy()
22 | }
23 |
24 | override fun pageClassName() = ""
25 |
26 | override fun pageTitleName() = "我的页面"
27 |
28 | override fun isMe(): Boolean {
29 | return wxAccessibilityService?.findById(Nodes.mineNickNameNode.nodeId)?.text.default()
30 | .isNotBlank()
31 | }
32 |
33 | suspend fun getMyWxInfo(): WxUserInfo? {
34 | return retryTaskWithLog("获取【我的】页面的微信昵称和微信号") {
35 | val nickName =
36 | wxAccessibilityService?.findById(Nodes.mineNickNameNode.nodeId)?.text.default()
37 | val wxCode =
38 | wxAccessibilityService?.findById(Nodes.mineWxCodeNode.nodeId)?.text?.split("微信号:")
39 | ?.getOrNull(1).default().trim()
40 |
41 | if (nickName.isNotBlank() && wxCode.isNotBlank()) {
42 | Log.d(TAG, "我的微信昵称: $nickName 我的微信号:$wxCode")
43 | WxUserInfo(nickName, wxCode)
44 | } else {
45 | null
46 | }
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/WXRemittancePage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page
2 |
3 | import com.android.accessibility.ext.acc.*
4 | import com.android.accessibility.ext.default
5 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
6 | import com.android.accessibility.ext.task.retryTaskWithLog
7 | import com.lygttpod.android.auto.wx.data.NodeInfo
8 | import com.lygttpod.android.auto.wx.data.WxUserInfo
9 | import com.lygttpod.android.auto.wx.em.CheckStatus
10 | import com.lygttpod.android.auto.wx.em.FriendStatus
11 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
12 | import com.lygttpod.android.auto.wx.version.nodeProxy
13 |
14 | object WXRemittancePage : IPage {
15 |
16 | interface Nodes {
17 | val remittanceUserNode: NodeInfo
18 | val remittanceWxCodeNode: NodeInfo
19 | val remittanceInputMoneyNode: NodeInfo
20 | val remittanceTransferMoneyNode: NodeInfo
21 | val remittanceDialogConfirmNode: NodeInfo
22 |
23 | companion object : Nodes by nodeProxy()
24 | }
25 |
26 | override fun pageClassName() = "com.tencent.mm.plugin.remittance.ui.RemittanceUI"
27 |
28 | override fun pageTitleName() = "转账页"
29 |
30 | override fun isMe(): Boolean {
31 | return false
32 | }
33 |
34 | /**
35 | * 在转账页先判断一次好友状态是否正常
36 | * 判断标准是【如果出现 [转账给 测试(**名)] 及昵称后边有加星的真名 就说明好友正常,否则还需要后边进一步验证 】
37 | */
38 | suspend fun checkIsNormalFriend(): WxUserInfo? {
39 | return delayAction {
40 | var wxUserInfo: WxUserInfo? = null
41 | wxUserInfo = retryTaskWithLog("获取转账页的用户名和微信号") {
42 | //android.widget.TextView → 转账给 测试(**名) → com.tencent.mm:id/inh
43 | val friendName = wxAccessibilityService
44 | ?.findById(Nodes.remittanceUserNode.nodeId)
45 | ?.text
46 | .default()
47 | .split("转账给")
48 | .getOrNull(1)
49 | .default()
50 | .trim()
51 | //android.widget.TextView → 微信号: wxid_xxx → com.tencent.mm:id/ini
52 | val wxCode = wxAccessibilityService
53 | ?.findById(Nodes.remittanceWxCodeNode.nodeId)
54 | ?.text
55 | ?.split("微信号:")
56 | ?.getOrNull(1)
57 | .default()
58 | .trim()
59 | if (friendName.isNotBlank()) {
60 | val isNormal = friendName.contains("(*") && friendName.endsWith(")")
61 | if (isNormal) {
62 | WxUserInfo(friendName, wxCode, FriendStatus.NORMAL)
63 | } else {
64 | WxUserInfo(friendName, wxCode)
65 | }
66 | } else {
67 | null
68 | }
69 | }
70 | wxUserInfo
71 | }
72 | }
73 |
74 | /**
75 | * 输入测试金额
76 | */
77 | suspend fun inputMoney(money: String = "0.01"): Boolean {
78 | return delayAction {
79 | retryCheckTaskWithLog("自动【输入转账金额】操作") {
80 | wxAccessibilityService?.findById(Nodes.remittanceInputMoneyNode.nodeId)
81 | ?.inputText(money) == true
82 | }
83 | }
84 | }
85 |
86 | /**
87 | * 点击转账按钮
88 | */
89 | suspend fun clickTransferMoney(): Boolean {
90 | return delayAction(delayMillis = 1000) {
91 | retryCheckTaskWithLog("点击转账页的【转账】按钮") {
92 | wxAccessibilityService.clickById(Nodes.remittanceTransferMoneyNode.nodeId)
93 | }
94 | }
95 | }
96 |
97 | /**
98 | * 根据转账后的提示去判断好友状态
99 | */
100 | suspend fun checkStatus(): CheckStatus? {
101 | return delayAction {
102 | retryTaskWithLog("查询支付状态", 20_000) {
103 | val find =
104 | wxAccessibilityService?.findByContainsText(
105 | false,
106 | CheckStatus.values().map { it.mark })
107 | if (find == null) {
108 | null
109 | } else {
110 | val status = CheckStatus.values().find { find.text.contains(it.mark) }
111 | status
112 | }
113 | }
114 | }
115 | }
116 |
117 | /**
118 | * 转账异常时候会有弹框提醒,点击【我知道了】关闭弹框
119 | */
120 | suspend fun clickIKnow() {
121 | delayAction {
122 | retryCheckTaskWithLog("点击【我知道了】按钮") {
123 | //android.widget.Button → 我知道了 → com.tencent.mm:id/guw
124 | wxAccessibilityService.clickByText(Nodes.remittanceDialogConfirmNode.nodeText)
125 | }
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/group/WXCreateGroupPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page.group
2 |
3 | import com.android.accessibility.ext.acc.clickById
4 | import com.android.accessibility.ext.acc.findById
5 | import com.android.accessibility.ext.acc.findByText
6 | import com.android.accessibility.ext.acc.selectChildByScroll
7 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
8 | import com.android.accessibility.ext.task.retryTaskWithLog
9 | import com.lygttpod.android.auto.wx.data.NodeInfo
10 | import com.lygttpod.android.auto.wx.helper.TaskByGroupHelper
11 | import com.lygttpod.android.auto.wx.page.IPage
12 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
13 | import com.lygttpod.android.auto.wx.version.nodeProxy
14 |
15 | object WXCreateGroupPage : IPage {
16 |
17 | interface Nodes {
18 | val createGroupPageTitleNode: NodeInfo
19 | val createGroupUserListNode: NodeInfo
20 | val createGroupUserNode: NodeInfo
21 | val createGroupCompleteNode: NodeInfo
22 |
23 | companion object : Nodes by nodeProxy()
24 | }
25 |
26 | override fun pageClassName(): String = ""
27 |
28 | override fun pageTitleName() = "发起群聊"
29 |
30 | override fun isMe(): Boolean {
31 | //页面 android.widget.TextView → text = 发起群聊 → id = android:id/text1
32 | return wxAccessibilityService?.findByText(Nodes.createGroupPageTitleNode.nodeText) != null
33 | }
34 |
35 | /**
36 | * 是否在聊天页获取到用户列表数据
37 | */
38 | suspend fun inPage(): Boolean {
39 | return retryCheckTaskWithLog(
40 | "判断是否是在发起群聊页",
41 | timeOutMillis = 30_000
42 | ) { isMe() && isShowUserList() }
43 | }
44 |
45 | private fun isShowUserList(): Boolean {
46 | return wxAccessibilityService?.findById(Nodes.createGroupUserNode.nodeId) != null
47 | }
48 |
49 | suspend fun selectUser(): List {
50 | val lastUser = TaskByGroupHelper.alreadyCheckedUsers.lastOrNull()
51 | val selectCount = TaskByGroupHelper.GROUP_USER_MAX_COUNT
52 | val log = if (lastUser.isNullOrBlank()) {
53 | "通过滑动去选中${selectCount}个好友"
54 | } else {
55 | "通过滑动去选中${lastUser}之后的${selectCount}个好友"
56 | }
57 | return retryTaskWithLog(log, timeOutMillis = 5 * 60_000) {
58 | val findNodes = wxAccessibilityService.selectChildByScroll(
59 | Nodes.createGroupUserListNode.nodeId,
60 | Nodes.createGroupUserNode.nodeId,
61 | TaskByGroupHelper.GROUP_USER_MAX_COUNT,
62 | lastText = TaskByGroupHelper.alreadyCheckedUsers.lastOrNull()
63 | )
64 | //TODO 当 findNodes.size == 1 说明最后恰好剩一个待验证的好友了,一个人是无法拉群的,那就自行验证吧
65 | TaskByGroupHelper.alreadyCheckedUsers.addAll(findNodes)
66 | findNodes
67 | } ?: listOf()
68 | }
69 |
70 | /**
71 | * 点击完成按钮
72 | */
73 | suspend fun clickCompleteBtn(): Boolean {
74 | return delayAction {
75 | retryCheckTaskWithLog("点击【完成】按钮") {
76 | //android.widget.Button → text = 完成 → id = com.tencent.mm:id/e9s
77 | wxAccessibilityService.clickById(Nodes.createGroupCompleteNode.nodeId)
78 | }
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/group/WXGroupChatPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page.group
2 |
3 | import android.util.Log
4 | import com.android.accessibility.ext.acc.clickById
5 | import com.android.accessibility.ext.acc.findById
6 | import com.android.accessibility.ext.acc.findChildNodes
7 | import com.android.accessibility.ext.default
8 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
9 | import com.android.accessibility.ext.task.retryTaskWithLog
10 | import com.lygttpod.android.auto.wx.data.NodeInfo
11 | import com.lygttpod.android.auto.wx.data.WxUserInfo
12 | import com.lygttpod.android.auto.wx.em.FriendStatus
13 | import com.lygttpod.android.auto.wx.page.IPage
14 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
15 | import com.lygttpod.android.auto.wx.version.nodeProxy
16 |
17 |
18 | object WXGroupChatPage : IPage {
19 |
20 | interface Nodes {
21 | val groupChatPageTitleNode: NodeInfo
22 | val groupChatContentListNode: NodeInfo
23 | val groupChatMsgNode: NodeInfo
24 | val groupChatRightTopNode: NodeInfo
25 |
26 | companion object : Nodes by nodeProxy()
27 | }
28 |
29 | override fun pageClassName() = "群聊页"
30 |
31 | override fun pageTitleName() = ""
32 |
33 | override fun isMe(): Boolean {
34 | //android.widget.TextView → text = 群聊(3) → id = com.tencent.mm:id/ko4
35 | return wxAccessibilityService?.findById(Nodes.groupChatPageTitleNode.nodeId) != null
36 | }
37 |
38 | suspend fun inPage(): Boolean {
39 | return delayAction {
40 | retryCheckTaskWithLog("判断当前是否在群聊页", timeOutMillis = 30_000) { isMe() && showContent() }
41 | }
42 | }
43 |
44 | private fun showContent(): Boolean {
45 | return wxAccessibilityService.findChildNodes(
46 | Nodes.groupChatContentListNode.nodeId,
47 | Nodes.groupChatMsgNode.nodeId
48 | ).isNotEmpty()
49 | }
50 |
51 | /**
52 | * 检测当前群聊里用户的状态
53 | */
54 | suspend fun checkUserStatus(): MutableList? {
55 | return delayAction {
56 | retryTaskWithLog("开始判断好友状态", timeOutMillis = 20_000) {
57 | //androidx.recyclerview.widget.RecyclerView → text = → id = com.tencent.mm:id/b79
58 |
59 | //android.widget.TextView → text = 你邀请XXX、XXX加入了群聊 → id = com.tencent.mm:id/b4b → description = → clickable = true → scrollable = false → editable = false
60 | //android.widget.TextView → text = 你无法邀请未添加你为好友的用户进去群聊,请先向XXX发送朋友验证申请。对方通过验证后,才能加入群聊。 → id = com.tencent.mm:id/b4b → description = → clickable = true → scrollable = false → editable = false
61 | //android.widget.TextView → text = XXX、XXX拒绝加入群聊 → id = com.tencent.mm:id/b4b → description = → clickable = true → scrollable = false → editable = false
62 | //android.widget.TextView → text = 由于账号安全原因,XXX、XXX无法加入当前群聊。 → id = com.tencent.mm:id/b4b → description = → clickable = true → scrollable = false → editable = false
63 |
64 | val list = mutableListOf()
65 | wxAccessibilityService.findChildNodes(
66 | Nodes.groupChatContentListNode.nodeId,
67 | Nodes.groupChatMsgNode.nodeId
68 | ).forEach {
69 | val resultText = it.text.default()
70 | val result = when {
71 | resultText.startsWith("你邀请") -> analyzeNormal(resultText)
72 | resultText.startsWith("你无法邀请未添加你为好友的用户进去群聊") -> analyzeDelete(
73 | resultText
74 | )
75 |
76 | resultText.startsWith("由于账号安全原因") -> analyzeException(resultText)
77 | resultText.endsWith("拒绝加入群聊") -> analyzeBlack(resultText)
78 | else -> listOf()
79 | }
80 | list.addAll(result)
81 | }
82 | list
83 | }
84 | }
85 | }
86 |
87 | //你邀请AAA、BBB加入了群聊 → id = com.tencent.mm:id/b4b
88 | private fun analyzeNormal(text: String): MutableList {
89 | val result = mutableListOf()
90 | val start = "你邀请"
91 | val end = "加入了群聊"
92 | val users = text.substring(start.length, text.length - end.length)
93 | val split = users.split("、")
94 | if (split.isEmpty()) {
95 | result.add(WxUserInfo(users, "", FriendStatus.NORMAL))
96 | } else {
97 | result.addAll(split.map { WxUserInfo(it, "", FriendStatus.NORMAL) }.toMutableList())
98 | }
99 | Log.d("检测结果", "analyzeNormal: $result")
100 | return result
101 | }
102 |
103 | //你无法邀请未添加你为好友的用户进去群聊,请先向XXX发送朋友验证申请。对方通过验证后,才能加入群聊。 → id = com.tencent.mm:id/b4b
104 | private fun analyzeDelete(text: String): MutableList {
105 | val result = mutableListOf()
106 | val start = "你无法邀请未添加你为好友的用户进去群聊,请先向"
107 | val end = "发送朋友验证申请。对方通过验证后,才能加入群聊。"
108 | val users = text.substring(start.length, text.length - end.length)
109 | val split = users.split("、")
110 | if (split.isEmpty()) {
111 | result.add(WxUserInfo(users, "", FriendStatus.DELETE))
112 | } else {
113 | result.addAll(split.map { WxUserInfo(it, "", FriendStatus.DELETE) }.toMutableList())
114 | }
115 | Log.d("检测结果", "analyzeDelete: $result")
116 | return result
117 | }
118 |
119 | //XXX拒绝加入群聊
120 | private fun analyzeBlack(text: String): MutableList {
121 | val result = mutableListOf()
122 | val end = "拒绝加入群聊"
123 | val users = text.substring(0, text.length - end.length)
124 | val split = users.split("、")
125 | if (split.isEmpty()) {
126 | result.add(WxUserInfo(users, "", FriendStatus.BLACK))
127 | } else {
128 | result.addAll(split.map { WxUserInfo(it, "", FriendStatus.BLACK) }.toMutableList())
129 | }
130 | Log.d("检测结果", "analyzeBlack: $result")
131 | return result
132 | }
133 |
134 | //由于账号安全原因,XXX无法加入当前群聊。
135 | private fun analyzeException(text: String): MutableList {
136 | val result = mutableListOf()
137 | val start = "由于账号安全原因,"
138 | val end = "无法加入当前群聊。"
139 | val users = text.substring(start.length, text.length - end.length)
140 | val split = users.split("、")
141 | if (split.isEmpty()) {
142 | result.add(WxUserInfo(users, "", FriendStatus.ACCOUNT_EXCEPTION))
143 | } else {
144 | result.addAll(split.map { WxUserInfo(it, "", FriendStatus.ACCOUNT_EXCEPTION) }
145 | .toMutableList())
146 | }
147 | Log.d("检测结果", "analyzeException: $result")
148 | return result
149 | }
150 |
151 | suspend fun clickMoreBtn(): Boolean {
152 | return delayAction {
153 | retryCheckTaskWithLog("点击【群聊页右上角】按钮") {
154 | //android.widget.ImageView → text = → id = com.tencent.mm:id/eo → description = 聊天信息
155 | wxAccessibilityService.clickById(Nodes.groupChatRightTopNode.nodeId, false)
156 | }
157 | }
158 | }
159 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/group/WXGroupInfoPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page.group
2 |
3 | import com.android.accessibility.ext.acc.clickByText
4 | import com.android.accessibility.ext.acc.findByText
5 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
6 | import com.lygttpod.android.auto.wx.data.NodeInfo
7 | import com.lygttpod.android.auto.wx.page.IPage
8 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
9 | import com.lygttpod.android.auto.wx.version.nodeProxy
10 |
11 |
12 | object WXGroupInfoPage : IPage {
13 |
14 | interface Nodes {
15 | val groupManagerNode: NodeInfo
16 | val groupDeleteNode: NodeInfo
17 | val groupDeleteDialogConfirmNode: NodeInfo
18 |
19 | companion object : Nodes by nodeProxy()
20 | }
21 |
22 | override fun pageClassName() = ""
23 |
24 | override fun pageTitleName() = "群聊天信息页"
25 |
26 | override fun isMe(): Boolean {
27 | // 群管理 → id = android:id/text1
28 | return wxAccessibilityService?.findByText(Nodes.groupManagerNode.nodeText) != null
29 | }
30 |
31 | suspend fun groupManagerBtnShow(): Boolean {
32 | return delayAction {
33 | retryCheckTaskWithLog("判断是否在【群聊信息页】") {
34 | isMe()
35 | }
36 | }
37 | }
38 |
39 | suspend fun clickGroupManager(): Boolean {
40 | return delayAction {
41 | retryCheckTaskWithLog("点击【群管理】按钮") {
42 | wxAccessibilityService.clickByText(Nodes.groupManagerNode.nodeText)
43 | }
44 | }
45 | }
46 |
47 | suspend fun deleteShow(): Boolean {
48 | return delayAction {
49 | retryCheckTaskWithLog("判断【删除】按钮是否显示") {
50 | wxAccessibilityService?.findByText(Nodes.groupDeleteNode.nodeText) != null
51 | }
52 | }
53 | }
54 |
55 | suspend fun clickDeleteGroup(): Boolean {
56 | return delayAction {
57 | //android.widget.TextView → text = 删除 → id = com.tencent.mm:id/khj
58 | retryCheckTaskWithLog("点击【删除】按钮") {
59 | wxAccessibilityService.clickByText(Nodes.groupDeleteNode.nodeText)
60 | }
61 | }
62 | }
63 |
64 | suspend fun clickDeleteGroupDialog(): Boolean {
65 | return delayAction {
66 | //text = 清空聊天记录,并在聊天列表中删除。 → id = com.tencent.mm:id/kpi
67 | //确定 → id = com.tencent.mm:id/knx → description
68 | retryCheckTaskWithLog("点击【确定】按钮") {
69 | wxAccessibilityService.clickByText(
70 | Nodes.groupDeleteDialogConfirmNode.nodeText,
71 | false
72 | )
73 | }
74 | }
75 | }
76 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/page/group/WXGroupManagerPage.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.page.group
2 |
3 | import com.android.accessibility.ext.acc.clickById
4 | import com.android.accessibility.ext.acc.clickByText
5 | import com.android.accessibility.ext.acc.findByText
6 | import com.android.accessibility.ext.task.retryCheckTaskWithLog
7 | import com.lygttpod.android.auto.wx.data.NodeInfo
8 | import com.lygttpod.android.auto.wx.page.IPage
9 | import com.lygttpod.android.auto.wx.service.wxAccessibilityService
10 | import com.lygttpod.android.auto.wx.version.nodeProxy
11 |
12 |
13 | object WXGroupManagerPage : IPage {
14 |
15 | interface Nodes {
16 | val groupManagerDisbandNode: NodeInfo
17 | val groupManagerDialogConfirmNode: NodeInfo
18 |
19 | companion object : Nodes by nodeProxy()
20 | }
21 |
22 | override fun pageClassName() = ""
23 |
24 | override fun pageTitleName() = "群管理"
25 |
26 | override fun isMe(): Boolean {
27 | //解散该群聊 → id = com.tencent.mm:id/khj
28 | return wxAccessibilityService?.findByText(Nodes.groupManagerDisbandNode.nodeText) != null
29 | }
30 |
31 | suspend fun inPage(): Boolean {
32 | return delayAction {
33 | retryCheckTaskWithLog("检测是否在【群管理】页面") {
34 | isMe()
35 | }
36 | }
37 | }
38 |
39 | suspend fun clickDisbandGroup(): Boolean {
40 | return delayAction {
41 | retryCheckTaskWithLog("点击【解散该群聊】按钮") {
42 | wxAccessibilityService.clickByText(Nodes.groupManagerDisbandNode.nodeText)
43 | }
44 | }
45 | }
46 |
47 |
48 | suspend fun clickDisbandGroupDialog(): Boolean {
49 | return delayAction {
50 | //text = 解散群聊后,群成员和群主都将被移出群聊。 → id = com.tencent.mm:id/kpi
51 | //text = 解散 → id = com.tencent.mm:id/knx
52 | retryCheckTaskWithLog("点击【解散】按钮") {
53 | wxAccessibilityService.clickById(Nodes.groupManagerDialogConfirmNode.nodeId, false)
54 | }
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/service/WXAccessibility.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.service
2 |
3 | import android.accessibilityservice.AccessibilityService
4 | import android.view.accessibility.AccessibilityEvent
5 | import androidx.lifecycle.MutableLiveData
6 | import com.android.accessibility.ext.AsyncAccessibilityService
7 | import com.lygttpod.android.auto.wx.helper.HBTaskHelper
8 | import java.util.concurrent.atomic.AtomicBoolean
9 |
10 | val wxAccessibilityServiceLiveData = MutableLiveData(null)
11 | val wxAccessibilityService: AccessibilityService? get() = wxAccessibilityServiceLiveData.value
12 |
13 | class WXAccessibility : AsyncAccessibilityService() {
14 |
15 | companion object {
16 | var isInWXApp = AtomicBoolean(false)
17 | }
18 |
19 | override fun targetPackageName() = "com.tencent.mm"
20 |
21 | override fun onCreate() {
22 | super.onCreate()
23 | wxAccessibilityServiceLiveData.value = this
24 | }
25 |
26 | override fun onDestroy() {
27 | wxAccessibilityServiceLiveData.value = null
28 | super.onDestroy()
29 | }
30 |
31 | override fun asyncHandleAccessibilityEvent(event: AccessibilityEvent) {
32 | HBTaskHelper.hbTask(event)
33 | }
34 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/java/com/lygttpod/android/auto/wx/version/WeChatVersionSupport.kt:
--------------------------------------------------------------------------------
1 | package com.lygttpod.android.auto.wx.version
2 |
3 | import java.lang.reflect.Proxy
4 |
5 | val wechatVersionArray by lazy { WeChatNodesImpl.values().map { it.version }.toList() }
6 |
7 | var currentWXVersion = wechatVersionArray[0]
8 |
9 | private val wechatNodesImplMap by lazy { WeChatNodesImpl.values().associateBy { it.version } }
10 |
11 | val currentWechatNodes: WeChatNodesImpl
12 | get() = wechatNodesImplMap[currentWXVersion] ?: error("未适配的微信版本, currentWXVersion: $currentWXVersion")
13 |
14 | inline fun nodeProxy(): Nodes {
15 | val nodesClass = Nodes::class.java
16 | val proxy =
17 | Proxy.newProxyInstance(nodesClass.classLoader, arrayOf(nodesClass)) { _, method, args ->
18 | val impl = currentWechatNodes
19 | method.invoke(impl, *args.orEmpty())
20 | }
21 | return proxy as Nodes
22 | }
--------------------------------------------------------------------------------
/auto-wx/src/main/res/layout/fragment_wx_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
30 |
31 |
39 |
40 |
41 |
50 |
51 |
59 |
60 |
68 |
69 |
77 |
78 |
87 |
88 |
95 |
96 |
114 |
115 |
129 |
130 |
149 |
150 |
151 |
161 |
162 |
--------------------------------------------------------------------------------
/auto-wx/src/main/res/layout/item_friend.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
30 |
31 |
44 |
45 |
55 |
56 |
63 |
--------------------------------------------------------------------------------
/auto-wx/src/main/res/layout/item_version.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
--------------------------------------------------------------------------------
/auto-wx/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | #FF6200EE
5 | #FF000000
6 | #FFFFFFFF
7 |
8 | #ff3030
9 | #00ff00
10 | #ee00ee
11 | #FF6200EE
12 | #FF000000
13 |
--------------------------------------------------------------------------------
/auto-wx/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 微信自动化
4 |
5 | 【微信自动化】
6 | 1、自动化检测好友关系,甄别是否被好友 拉黑、删除,或者好友账号异常等状态;\n2、微信自动抢红包;\n3、自动化过程不涉个人信息,请放心使用
7 |
--------------------------------------------------------------------------------
/auto-wx/src/main/res/xml/wx_accessible_service_config.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | id 'com.android.application' version '8.0.1' apply false
4 | id 'com.android.library' version '8.0.1' apply false
5 | id 'org.jetbrains.kotlin.android' version '1.8.20' apply false
6 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
24 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Aug 05 09:48:33 CST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | jcenter()
14 | maven { url 'https://jitpack.io' }
15 | maven { url "https://raw.githubusercontent.com/Pgyer/mvn_repo_pgyer/master" }
16 | }
17 | }
18 | rootProject.name = "AndroidAuto"
19 | include ':app'
20 | include ':accessibility'
21 | include ':auto-wx'
22 | include ':auto-tools'
23 | include ':auto-ad'
24 |
--------------------------------------------------------------------------------
/we_chat_tools.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lygttpod/AndroidAuto/e5f9ada63722cc7671cec2f4176be199668f1365/we_chat_tools.jks
--------------------------------------------------------------------------------