diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..6f7fb4f --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,76 @@ +plugins { + id "com.android.application" + id "kotlin-android" +} + +android { + compileSdk 31 + buildToolsVersion "31.0.0" + + defaultConfig { + applicationId "li.songe.ad_closer" + minSdk 26 + targetSdk 31 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary true + } + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + useIR = true + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } + packagingOptions { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation "androidx.core:core-ktx:1.7.0" + implementation "androidx.appcompat:appcompat:1.3.1" + implementation "com.google.android.material:material:1.4.0" + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0" + implementation "androidx.activity:activity-compose:1.4.0" + testImplementation "junit:junit:4.13.2" + androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + +// https://github.com/RikkaApps/Shizuku-API + def shizuku_version = '12.1.0' + implementation "dev.rikka.shizuku:api:$shizuku_version" + // Add this line if you want to support Shizuku + implementation "dev.rikka.shizuku:provider:$shizuku_version" + + // 工具集合类 + // https://github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/README-CN.md + implementation("com.blankj:utilcodex:1.30.6") + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/li/songe/ad_closer/ExampleInstrumentedTest.kt b/app/src/androidTest/java/li/songe/ad_closer/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..3b271f3 --- /dev/null +++ b/app/src/androidTest/java/li/songe/ad_closer/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package li.songe.ad_closer + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("li.songe.ad_closer", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8ace3cc --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/aidl/li/songe/ad_closer/IUserService.aidl b/app/src/main/aidl/li/songe/ad_closer/IUserService.aidl new file mode 100644 index 0000000..4495573 --- /dev/null +++ b/app/src/main/aidl/li/songe/ad_closer/IUserService.aidl @@ -0,0 +1,10 @@ +// IUserService.aidl +package li.songe.ad_closer; + +interface IUserService { + void destroy() = 16777114; // Destroy method defined by Shizuku server + + void exit() = 1; // Exit method defined by user + + String doSomething() = 2; +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/App.kt b/app/src/main/java/li/songe/ad_closer/App.kt new file mode 100644 index 0000000..d23f010 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/App.kt @@ -0,0 +1,11 @@ +package li.songe.ad_closer + +import android.app.Application +import com.blankj.utilcode.util.LogUtils + +class App:Application() { + override fun onCreate() { + super.onCreate() + LogUtils.d("onCreate") + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/MainActivity.kt b/app/src/main/java/li/songe/ad_closer/MainActivity.kt new file mode 100644 index 0000000..ae18899 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/MainActivity.kt @@ -0,0 +1,72 @@ +package li.songe.ad_closer + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.blankj.utilcode.util.LogUtils +import li.songe.ad_closer.ui.theme.AdCloserTheme +import rikka.shizuku.Shizuku +import rikka.shizuku.Shizuku.OnRequestPermissionResultListener +import android.content.pm.PackageManager + + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AdCloserTheme { + // A surface container using the 'background' color from the theme + Surface(color = MaterialTheme.colors.background) { + Greeting("Android2") + } + } + LogUtils.d(this) + packageName + } + checkPermission(0) + Shizuku.addRequestPermissionResultListener { requestCode, grantResult -> + LogUtils.d(requestCode, grantResult) + } +// Shizuku.bindUserService() + + } + private fun checkPermission(code: Int): Boolean { + if (Shizuku.isPreV11()) { + // Pre-v11 is unsupported + return false + } + return when { + Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED -> { + // Granted + true + } + Shizuku.shouldShowRequestPermissionRationale() -> { + // Users choose "Deny and don't ask again" + false + } + else -> { + // Request the permission + Shizuku.requestPermission(code) + false + } + } + } +} + +@Composable +fun Greeting(name: String) { + Text(text = "Hello $name 4399") +} + +@Preview(showBackground = true) +@Composable +fun DefaultPreview() { + AdCloserTheme { + Greeting("React") + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/data/Rule.kt b/app/src/main/java/li/songe/ad_closer/data/Rule.kt new file mode 100644 index 0000000..3ad6f25 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/data/Rule.kt @@ -0,0 +1,130 @@ +package li.songe.ad_closer.data + +data class Rule( + val packageName: String, + val className: String, + val selector: String +) { + companion object { + val defaultRuleList = listOf( + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text=查看详情] + View[text=×]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text$=的广告] - View[text=×]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "TextView[text*=的广告] - Image" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text$=的广告] + View[text=×]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text$=的广告] +2 View[text=×]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text$=关注][text*=回答] + View[text=×]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.mix.activity.ContentMixProfileActivity", + "View[text*=的回答][text*=点赞][text$=评论] + TextView + Image" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.ContentActivity", + "TextView[id=com.zhihu.android:id/confirm_uninterest]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.ContentActivity", + "TextView[id=com.zhihu.android:id/uninterest_reason]" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.ContentActivity", + "ViewGroup > TextView[text*=广告] +4 ImageView" + ), + Rule( + "com.zhihu.android", + "com.zhihu.android.ContentActivity", + "ViewGroup > TextView[text*=广告] +2 ImageView" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.tblauncher.MainTabActivity", + "FrameLayout[id=com.baidu.tieba:id/ad_close_view]" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.tblauncher.MainTabActivity", + "View[id=com.baidu.tieba:id/forbid_thread_btn]" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.tblauncher.MainTabActivity", + "ImageView[id=com.baidu.tieba:id/float_layer_feedback_picture]" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.tblauncher.MainTabActivity", + "RelativeLayout[id=com.baidu.tieba:id/close_layout]" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.pb.pb.main.PbActivity", + "View[id=com.baidu.tieba:id/forbid_thread_btn]" + ), + Rule( + "com.baidu.tieba", + "com.baidu.tieba.pb.pb.main.PbActivity", + "FrameLayout[id=com.baidu.tieba:id/ad_close_view]" + ), + Rule( + "tv.danmaku.bili", + "tv.danmaku.bili.MainActivityV2", + "TextView[id=tv.danmaku.bili:id/count_down]" + ), + Rule( + "tv.danmaku.bili", + "tv.danmaku.bili.ui.video.VideoDetailsActivity", + "ImageView[id=tv.danmaku.bili:id/close]" + ) +// TODO NAF node 的父亲节点属性无法查询 + , + + Rule( + "com.duokan.phone.remotecontroller", + "com.xiaomi.mitv.phone.remotecontroller.HoriWidgetMainActivityV2", + "ImageView[id=com.duokan.phone.remotecontroller:id/image_close_banner]" + ) +// Rule( +// "com.coolapk.market", +// "com.coolapk.market.view.main.MainActivity", +// "ViewGroup > TextView[text=疑似抄袭]" +// ), +// Rule( +// "com.coolapk.market", +// "com.coolapk.market.view.main.MainActivity", +// "Button[text^=看视频][text$=免广告] - Button[text=不感兴趣]" +// ), +// Rule( +// "com.coolapk.market", +// "com.coolapk.market.view.main.MainActivity", +// "TextView[id=com.coolapk.market:id/ad_text_view] + ImageView[id=com.coolapk.market:id/close_view]" +// ) + ) + } +} diff --git a/app/src/main/java/li/songe/ad_closer/data/RuleGroup.kt b/app/src/main/java/li/songe/ad_closer/data/RuleGroup.kt new file mode 100644 index 0000000..618462a --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/data/RuleGroup.kt @@ -0,0 +1,11 @@ +package li.songe.ad_closer.data + + +data class RuleGroup( + val id: Long, + val description: String, + val packageName: String, + val className: String, + val ruleList: List +) +// 从网址导入时, 会显示 规则描述 目标应用 目标活动界面, 此界面可点击打开 diff --git a/app/src/main/java/li/songe/ad_closer/log/OperationRecord.kt b/app/src/main/java/li/songe/ad_closer/log/OperationRecord.kt new file mode 100644 index 0000000..35b6a98 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/log/OperationRecord.kt @@ -0,0 +1,7 @@ +package li.songe.ad_closer.log + +data class OperationRecord( + val timestamp: Long, + val packageName: String, + val classNme: String, + val ruleId: String) diff --git a/app/src/main/java/li/songe/ad_closer/service/AdCloserService.kt b/app/src/main/java/li/songe/ad_closer/service/AdCloserService.kt new file mode 100644 index 0000000..f3a09d7 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/service/AdCloserService.kt @@ -0,0 +1,153 @@ +package li.songe.ad_closer.service + +import android.accessibilityservice.AccessibilityService +import android.content.ComponentName +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager.NameNotFoundException +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import com.blankj.utilcode.util.LogUtils +import kotlinx.coroutines.* +import li.songe.ad_closer.data.Rule +import li.songe.ad_closer.util.MatchRule +import li.songe.ad_closer.util.findNodeInfo + + +/** + * demo: https://juejin.cn/post/6844903589127651335 + */ +class AdCloserService : AccessibilityService() { + + private fun getActivityInfo(componentName: ComponentName): ActivityInfo? { + return try { + packageManager.getActivityInfo(componentName, 0) + } catch (e: NameNotFoundException) { + null + } + } + + private val scope = CoroutineScope(Dispatchers.Default + Job()) +// override fun onDestroy() { +// super.onDestroy() +// } + + private var currentPackageName: String? = null +// 考虑n种模式 +// 默认情况 正常获取 activity class name +// 不使用 activity class name, 直接用 package class name, 会导致 匹配规则 变多 +// 借助 shizuku 使用 adb 获取 activity class name + + override fun onServiceConnected() { + super.onServiceConnected() + scope.launch { + while (true) { + if (!this.isActive) { + break + } + val window = rootInActiveWindow + +// val packageName = window?.packageName?.toString() +// if (packageName != null && currentPackageName != packageName) { +// currentPackageName = packageName +// LogUtils.d(currentPackageName, currentActivityClassName) +// } + if (window != null && ruleListMap.containsKey(currentActivityClassName)) { + run loop@{ + ruleListMap[currentActivityClassName]!!.forEachIndexed { _, rule -> + val nodeInfo = + findNodeInfo(window, rule.matchUnit, listOf(0)) + if (nodeInfo != null) { + LogUtils.dTag("click", rule.rawText) + nodeInfo.performAction(AccessibilityNodeInfo.ACTION_CLICK) + nodeInfo.recycle() + return@loop + } + } + } + } + delay(200) + } + } + } + + + private val ruleListMap by lazy { + val h = HashMap>() + Rule.defaultRuleList.forEach { + val key = it.className + if (!h.containsKey(key)) { + h[key] = mutableListOf() + } + try { + h[key]?.add(MatchRule.parse(it.selector)) + } catch (e: Exception) { + LogUtils.d(e) + } + } + return@lazy h + } + + override fun onAccessibilityEvent(event: AccessibilityEvent?) { + if (event == null) { + return + } + when (event.eventType) { + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> { +// LogUtils.d(rootInActiveWindow?.packageName, event.packageName, event.className) +// if (event.packageName == rootInActiveWindow?.packageName && event.className != null && event.className.startsWith( +// event.packageName +// ) +// ) { +// +// } + val className = event.className.toString() +// val packageName = event.packageName.toString() +// 在桌面和应用之间来回切换, 大概率导致识别失败 + if (!className.startsWith("android.") && !className.startsWith("androidx.")) { +// className.startsWith(packageName) + currentActivityClassName = className + } + } + else -> { + } + } +// val componentName = +// ComponentName( +// event.packageName?.toString() ?: "", +// event.className?.toString() ?: "" +// ) +// val activityInfo = getActivityInfo(componentName) +// if (activityInfo != null) { +// val newClassName = event.className.toString() +// if (currentActivityClassName != newClassName) { +// currentActivityClassName = newClassName +//// LogUtils.dTag("newClassName", newClassName, rootInActiveWindow?.packageName) +// } +// } +// when (event.eventType) { +// AccessibilityEvent.TYPE_WINDOWS_CHANGED -> { +// } +// AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED -> { +// } +// AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED -> { +// +// } +// else -> { +// } +// } + } + + private var currentActivityClassName = "" + set(value) { + if (field != value) { + field = value + LogUtils.dTag("updateClassName", field, rootInActiveWindow?.packageName) + } + } + + override fun onInterrupt() { + scope.cancel() + } + + +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/service/UserService.kt b/app/src/main/java/li/songe/ad_closer/service/UserService.kt new file mode 100644 index 0000000..1f65119 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/service/UserService.kt @@ -0,0 +1,15 @@ +package li.songe.ad_closer.service + +import li.songe.ad_closer.IUserService + +class UserService: IUserService.Stub() { + override fun destroy() { + } + + override fun exit() { + } + + override fun doSomething(): String { + return "" + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/ui/theme/Color.kt b/app/src/main/java/li/songe/ad_closer/ui/theme/Color.kt new file mode 100644 index 0000000..0211bd5 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package li.songe.ad_closer.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple200 = Color(0xFFBB86FC) +val Purple500 = Color(0xFF6200EE) +val Purple700 = Color(0xFF3700B3) +val Teal200 = Color(0xFF03DAC5) \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/ui/theme/Shape.kt b/app/src/main/java/li/songe/ad_closer/ui/theme/Shape.kt new file mode 100644 index 0000000..0912a14 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package li.songe.ad_closer.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(4.dp), + medium = RoundedCornerShape(4.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/ui/theme/Theme.kt b/app/src/main/java/li/songe/ad_closer/ui/theme/Theme.kt new file mode 100644 index 0000000..e4e6235 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/ui/theme/Theme.kt @@ -0,0 +1,44 @@ +package li.songe.ad_closer.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple200, + primaryVariant = Purple700, + secondary = Teal200 +) + +private val LightColorPalette = lightColors( + primary = Purple500, + primaryVariant = Purple700, + secondary = Teal200 + + /* Other default colors to override + background = Color.White, + surface = Color.White, + onPrimary = Color.White, + onSecondary = Color.Black, + onBackground = Color.Black, + onSurface = Color.Black, + */ +) + +@Composable +fun AdCloserTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/ui/theme/Type.kt b/app/src/main/java/li/songe/ad_closer/ui/theme/Type.kt new file mode 100644 index 0000000..4a44331 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/ui/theme/Type.kt @@ -0,0 +1,28 @@ +package li.songe.ad_closer.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp + ) + /* Other default text styles to override + button = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.W500, + fontSize = 14.sp + ), + caption = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 12.sp + ) + */ +) \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/util/AttributeSelector.kt b/app/src/main/java/li/songe/ad_closer/util/AttributeSelector.kt new file mode 100644 index 0000000..360ca6d --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/util/AttributeSelector.kt @@ -0,0 +1,153 @@ +package li.songe.ad_closer.util + +import java.lang.Error +import java.util.* + +data class AttributeSelector(val attr: Attribute, val operator: Operator, val value: String) { + sealed class Operator { + object Equal : Operator() + object Include : Operator() + object Start : Operator() + object End : Operator() + object Less : Operator() + object More : Operator() + + override fun toString(): String { + return when (this) { + End -> "$" + Equal -> "" + Include -> "*" + Less -> "<" + More -> ">" + Start -> "^" + else-> TODO() + } + "=" + } + } + + sealed class Attribute { + object Text : Attribute() + object ChildCount : Attribute() + object Id : Attribute() + + override fun toString(): String { + return when (this) { + ChildCount -> "childCount" + Id -> "id" + Text -> "text" + else -> TODO() + } + } + } + + companion object { + private val markCharList = listOf('*', '^', '$', '>', '<') + fun parse(text: String): AttributeSelector { + val stack = Stack() + val attrSb = StringBuilder() + val operatorSb = StringBuilder() + val valueSb = StringBuilder() + run loop@{ + text.forEach { c -> + when (c) { + '[' -> { + assert(stack.empty()) + stack.push(c) + } + '=' -> { + assert(stack.peek() == '[' || markCharList.contains(stack.peek())) + stack.push(c) + operatorSb.append(c) + } + in markCharList -> { + assert(stack.peek() == '[') + stack.push(c) + operatorSb.append(c) + } + ']' -> { + assert(stack.peek() == '[' || stack.peek() == '=') + stack.push(c) + return@loop + } + else -> { + when (stack.peek()) { + '[' -> { + attrSb.append(c) + } + '=' -> { + valueSb.append(c) + } + } + } + } + } + } + assert(attrSb.isNotEmpty()) + assert(operatorSb.isNotEmpty()) + assert(valueSb.isNotEmpty()) + val attr = when (attrSb.toString()) { + "text" -> Attribute.Text + "id" -> Attribute.Id + "childCount" -> Attribute.ChildCount + else -> throw Error("invalid attr") + } + val operator = when (operatorSb.toString()) { + "=" -> Operator.Equal + "*=" -> Operator.Include + "^=" -> Operator.Start + "$=" -> Operator.End + ">=" -> Operator.More + "<=" -> Operator.Less + else -> throw Error("invalid operator") + } + val value = valueSb.toString() +// TODO("转义字符处理") + return AttributeSelector(attr, operator, value) + } + + fun parseMulti(text: String): List { + var startIndex = -1 + var endIndex = -1 + val attrRawList = mutableListOf() + text.forEachIndexed { index, c -> + when (c) { + '[' -> { + startIndex = index + } + ']' -> { + endIndex = index + assert(startIndex in 0 until endIndex) + attrRawList.add(text.substring(startIndex, endIndex + 1)) + startIndex = -1 + endIndex = -1 + } + } + } + return attrRawList.map { parse(it) } + } + + fun stringify(selector: AttributeSelector): String { + val attr = when (selector.attr) { + Attribute.ChildCount -> "childCount" + Attribute.Id -> "id" + Attribute.Text -> "text" + else-> TODO() + } + val operator = (when (selector.operator) { + Operator.End -> "$" + Operator.Equal -> "" + Operator.Include -> "*" + Operator.Less -> "<" + Operator.More -> ">" + Operator.Start -> "^" + else-> TODO() + }) + return "[$attr$operator=${selector.value}]" + } + + fun stringifyMulti(selectorList: List): String { + return selectorList.joinToString("") { stringify(it) } + } + } + +} diff --git a/app/src/main/java/li/songe/ad_closer/util/Extension.kt b/app/src/main/java/li/songe/ad_closer/util/Extension.kt new file mode 100644 index 0000000..b4b2c96 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/util/Extension.kt @@ -0,0 +1,170 @@ +package li.songe.ad_closer.util + +import android.graphics.Rect +import android.view.accessibility.AccessibilityNodeInfo +import com.blankj.utilcode.util.LogUtils + +/** + * @param pathIndexList 当前节点在节点树的路径, 最后一项代表节点是父节点第几个元素, 第一项是 0 + */ +private fun match( + nodeInfo: AccessibilityNodeInfo, + matchUnit: MatchUnit, + pathIndexList: List +): Boolean { +// val react = Rect() +// nodeInfo.getBoundsInScreen(react) +// val debug = nodeInfo.viewIdResourceName==null +// && nodeInfo.className.endsWith("android.widget.Image") +// && react.left==942 +// && matchUnit.className== "Image" +// 匹配最小节点深度 + var miniDepth = 1 + var r = matchUnit.relationUnit + while (r != null) { + if (r.operator !is RelationUnit.Operator.Brother) { + miniDepth += 1 + } + r = r.to.relationUnit + } + if (pathIndexList.size <= miniDepth) { + return false + } +// 匹配类名 + if (!nodeInfo.className.endsWith(matchUnit.className)) { + return false + } + val childCount = nodeInfo.childCount + val text:CharSequence? = nodeInfo.text + val id:String? = nodeInfo.viewIdResourceName + +// 在属性匹配列表不空的情况下, 列表所有项都匹配 + if (matchUnit.attributeSelectorList.isNotEmpty()) { + val condition2 = matchUnit.attributeSelectorList.all { + when (it.attr) { + AttributeSelector.Attribute.ChildCount -> when (it.operator) { + AttributeSelector.Operator.End -> false + AttributeSelector.Operator.Equal -> childCount == it.value.toInt() + AttributeSelector.Operator.Include -> false + AttributeSelector.Operator.Less -> childCount < it.value.toInt() + AttributeSelector.Operator.More -> childCount > it.value.toInt() + AttributeSelector.Operator.Start -> false + else-> TODO() + } + AttributeSelector.Attribute.Id -> { + when (it.operator) { + AttributeSelector.Operator.End -> false + AttributeSelector.Operator.Equal -> id == it.value + AttributeSelector.Operator.Include -> false + AttributeSelector.Operator.Less -> false + AttributeSelector.Operator.More -> false + AttributeSelector.Operator.Start -> false + else-> TODO() + } + } + AttributeSelector.Attribute.Text -> text!=null &&when (it.operator) { + AttributeSelector.Operator.End -> text.endsWith(it.value) + AttributeSelector.Operator.Equal -> text == it.value + AttributeSelector.Operator.Include -> text.contains(it.value) + AttributeSelector.Operator.Less -> false + AttributeSelector.Operator.More -> false + AttributeSelector.Operator.Start -> text.startsWith(it.value) + else-> TODO() + } + else-> TODO() + } + } + if (!condition2) { + return false + } + } + + val relationUnit = matchUnit.relationUnit ?: return true +// 目前有 父亲/祖先/兄弟 关系 此节点必须有 父节点 + val parent: AccessibilityNodeInfo? = nodeInfo.parent + if (parent != null) { + when (relationUnit.operator) { + RelationUnit.Operator.Ancestor -> { + var p = parent + var pl = pathIndexList.subList(0, pathIndexList.size - 1) + while (p != null) { + if (match(p, relationUnit.to, pl)) { + return true + } + p = p.parent + pl = pl.subList(0, pl.size - 1) + } + } + is RelationUnit.Operator.Brother -> { + val brotherIndex = pathIndexList.last() - relationUnit.operator.offset + return if (brotherIndex !in 0 until parent.childCount) { + false + } else { + val brother = parent.getChild(brotherIndex) + if (brother == null) { + false + } else { + val pl = pathIndexList.toMutableList() + pl[pl.size - 1] = brotherIndex + match(brother, relationUnit.to, pl.toList()) + } + + } + } + RelationUnit.Operator.Parent -> { + return match( + parent, + relationUnit.to, + pathIndexList.subList(0, pathIndexList.size - 1) + ) + } + } + } + return false +} + + fun findNodeInfo( + nodeInfo: AccessibilityNodeInfo?, + matchUnit: MatchUnit, + pathIndexList: List, +): AccessibilityNodeInfo? { + if (nodeInfo == null) { + return null + } + if (match(nodeInfo, matchUnit, pathIndexList)) { + return nodeInfo + } + var nodeInfo1: AccessibilityNodeInfo? = null + run loop@{ + nodeInfo.forEachIndexed { index, child -> + val nowPathList = pathIndexList.toMutableList().apply { add(index) } + nodeInfo1 = findNodeInfo(child, matchUnit, nowPathList) + if (nodeInfo1 != null) { + return@loop + } + } + } + return nodeInfo1 +} + +//inline fun AccessibilityNodeInfo.forEach(action: (AccessibilityNodeInfo) -> Unit): Unit { +// var index = 0 +// while (index < childCount) { +// val child: AccessibilityNodeInfo? = getChild(index) +// if (child != null) { +// action(child) +// } +// index += 1 +// } +//} + +inline fun AccessibilityNodeInfo.forEachIndexed(action: (index: Int, AccessibilityNodeInfo) -> Unit) { + var index = 0 + while (index < childCount) { + val child: AccessibilityNodeInfo? = getChild(index) + if (child != null) { + action(index, child) + } + index += 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/li/songe/ad_closer/util/MatchRule.kt b/app/src/main/java/li/songe/ad_closer/util/MatchRule.kt new file mode 100644 index 0000000..23f6a74 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/util/MatchRule.kt @@ -0,0 +1,42 @@ +package li.songe.ad_closer.util + +data class MatchRule(val matchUnit: MatchUnit, val rawText: String) { + companion object { + fun parse(text: String): MatchRule { + val unitStrList = text.split("\u0020").filter { it.isNotEmpty() }.reversed() + assert(unitStrList.isNotEmpty()) + val matchUnitList = mutableListOf() + val operatorList = mutableListOf() + unitStrList.forEachIndexed { index, s -> + when (index % 2) { + 0 -> { + matchUnitList.add(MatchUnit.parse(s)) + } + 1 -> { + operatorList.add(RelationUnit.parse(s)) + } + } + } + matchUnitList.forEachIndexed { index, unit -> + if (index < matchUnitList.size - 1) { + unit.relationUnit = RelationUnit(matchUnitList[index + 1], operatorList[index]) + } + } + return MatchRule(matchUnitList.first(), text) + } + + fun stringify(selector: MatchRule): String { + return selector.rawText + } + } + +// sealed class RuleUnit(open val data: T) +// class MatchRuleUnit(override val data: MatchUnit) : RuleUnit(data) +// class RelationRuleUnit(override val data: RelationUnit) : RuleUnit(data) + +} + +/** +// example +root >> LinearLayout -4 WebView > TextView +3 ImageView[text*=x] + */ diff --git a/app/src/main/java/li/songe/ad_closer/util/MatchUnit.kt b/app/src/main/java/li/songe/ad_closer/util/MatchUnit.kt new file mode 100644 index 0000000..2d7414c --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/util/MatchUnit.kt @@ -0,0 +1,38 @@ +package li.songe.ad_closer.util + +data class MatchUnit( + val className: String, + val attributeSelectorList: List, + var relationUnit: RelationUnit?, +) { + companion object { + fun parse(text: String): MatchUnit { + val sb = StringBuilder() + var markIndex = -1 + run loop@{ + text.forEachIndexed { index, c -> + if (c == '[') { + markIndex = index + return@loop + } else { + sb.append(c) + } + } + } +// if (markIndex <= 0) { +// throw Error("markIndex: expect it>0, got $markIndex") +// } + val className = sb.toString() + + return MatchUnit( + className, + AttributeSelector.parseMulti(if (markIndex > 0) text.substring(markIndex) else ""), + null + ) + } + + fun stringify(matchUnit: MatchUnit): String { + TODO() + } + } +} diff --git a/app/src/main/java/li/songe/ad_closer/util/RelationUnit.kt b/app/src/main/java/li/songe/ad_closer/util/RelationUnit.kt new file mode 100644 index 0000000..9d53e17 --- /dev/null +++ b/app/src/main/java/li/songe/ad_closer/util/RelationUnit.kt @@ -0,0 +1,63 @@ +package li.songe.ad_closer.util + +data class RelationUnit(val to: MatchUnit, val operator: Operator) { + sealed class Operator { + object Parent : Operator() + object Ancestor : Operator() + data class Brother(val offset: Int) : Operator() + + override fun toString(): String { + return when (this) { + Ancestor -> ">>" + is Brother -> { + assert(offset != 0) + return offset.toString() + } + Parent -> ">" + } + } + } + + companion object { + fun parse(text: String): Operator { + if (text == ">>") { + return Operator.Ancestor + } else if (text == ">") { + return Operator.Parent + } + val i = when (text) { + "+" -> { + 1 + } + "-" -> { + -1 + } + else -> { + text.toIntOrNull() + } + } + if (i != null) { + return Operator.Brother(i) + } + throw Error("invalid operator: $text") + } + + fun stringify(operator: Operator): String { + return when (operator) { + Operator.Parent -> ">" + Operator.Ancestor -> "\u0020" + is Operator.Brother -> when { + operator.offset > 0 -> { + "+" + operator.offset.toString() + } + operator.offset < 0 -> { + operator.offset.toString() + } + else -> { + throw Error("operator.offset: expect no-zero, got 0") + } + } + } + } + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..0dae144 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..4fb74aa --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + AdCloser + accessibility_service_label + accessibility_service_description + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..8f8fb4b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,25 @@ + + + + + + +