├── .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 | ![PC端展示效果](https://github.com/lygttpod/AndroidAuto/assets/11826777/f76e782b-4f9e-4de0-ba1d-0670f586ff5d) 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 |