init
1
app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/build
|
76
app/build.gradle
Normal file
|
@ -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")
|
||||||
|
|
||||||
|
}
|
21
app/proguard-rules.pro
vendored
Normal file
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
49
app/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="li.songe.ad_closer">
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
<application
|
||||||
|
android:allowBackup="false"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:name=".App"
|
||||||
|
android:theme="@style/Theme.AdCloser">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:excludeFromRecents="true"
|
||||||
|
android:theme="@style/Theme.AdCloser.NoActionBar">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".service.AdCloserService"
|
||||||
|
android:exported="false"
|
||||||
|
android:label="@string/accessibility_service_label"
|
||||||
|
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accessibilityservice.AccessibilityService" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accessibilityservice"
|
||||||
|
android:resource="@xml/accessibility_service_description"/>
|
||||||
|
</service>
|
||||||
|
<!-- This provider is required by Shizuku, remove this if your app only supports Sui -->
|
||||||
|
<provider
|
||||||
|
android:name="rikka.shizuku.ShizukuProvider"
|
||||||
|
android:authorities="${applicationId}.shizuku"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="true"
|
||||||
|
android:multiprocess="false"
|
||||||
|
android:permission="android.permission.INTERACT_ACROSS_USERS_FULL" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
10
app/src/main/aidl/li/songe/ad_closer/IUserService.aidl
Normal file
|
@ -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;
|
||||||
|
}
|
11
app/src/main/java/li/songe/ad_closer/App.kt
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
72
app/src/main/java/li/songe/ad_closer/MainActivity.kt
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
130
app/src/main/java/li/songe/ad_closer/data/Rule.kt
Normal file
|
@ -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>(
|
||||||
|
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]"
|
||||||
|
// )
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/java/li/songe/ad_closer/data/RuleGroup.kt
Normal file
|
@ -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<String>
|
||||||
|
)
|
||||||
|
// 从网址导入时, 会显示 规则描述 目标应用 目标活动界面, 此界面可点击打开
|
|
@ -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)
|
153
app/src/main/java/li/songe/ad_closer/service/AdCloserService.kt
Normal file
|
@ -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<String, MutableList<MatchRule>>()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
15
app/src/main/java/li/songe/ad_closer/service/UserService.kt
Normal file
|
@ -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 ""
|
||||||
|
}
|
||||||
|
}
|
8
app/src/main/java/li/songe/ad_closer/ui/theme/Color.kt
Normal file
|
@ -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)
|
11
app/src/main/java/li/songe/ad_closer/ui/theme/Shape.kt
Normal file
|
@ -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)
|
||||||
|
)
|
44
app/src/main/java/li/songe/ad_closer/ui/theme/Theme.kt
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
28
app/src/main/java/li/songe/ad_closer/ui/theme/Type.kt
Normal file
|
@ -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
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
)
|
153
app/src/main/java/li/songe/ad_closer/util/AttributeSelector.kt
Normal file
|
@ -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<Char>()
|
||||||
|
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<AttributeSelector> {
|
||||||
|
var startIndex = -1
|
||||||
|
var endIndex = -1
|
||||||
|
val attrRawList = mutableListOf<String>()
|
||||||
|
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<AttributeSelector>): String {
|
||||||
|
return selectorList.joinToString("") { stringify(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
170
app/src/main/java/li/songe/ad_closer/util/Extension.kt
Normal file
|
@ -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<Int>
|
||||||
|
): 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<Int>,
|
||||||
|
): 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
|
||||||
|
}
|
||||||
|
}
|
42
app/src/main/java/li/songe/ad_closer/util/MatchRule.kt
Normal file
|
@ -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<MatchUnit>()
|
||||||
|
val operatorList = mutableListOf<RelationUnit.Operator>()
|
||||||
|
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<T>(open val data: T)
|
||||||
|
// class MatchRuleUnit(override val data: MatchUnit) : RuleUnit<MatchUnit>(data)
|
||||||
|
// class RelationRuleUnit(override val data: RelationUnit) : RuleUnit<RelationUnit>(data)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
// example
|
||||||
|
root >> LinearLayout -4 WebView > TextView +3 ImageView[text*=x]
|
||||||
|
*/
|
38
app/src/main/java/li/songe/ad_closer/util/MatchUnit.kt
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
package li.songe.ad_closer.util
|
||||||
|
|
||||||
|
data class MatchUnit(
|
||||||
|
val className: String,
|
||||||
|
val attributeSelectorList: List<AttributeSelector>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
app/src/main/java/li/songe/ad_closer/util/RelationUnit.kt
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
170
app/src/main/res/drawable/ic_launcher_background.xml
Normal file
|
@ -0,0 +1,170 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@drawable/ic_launcher_background" />
|
||||||
|
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||||
|
</adaptive-icon>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 982 B |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file
After Width: | Height: | Size: 7.6 KiB |
16
app/src/main/res/values-night/themes.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.AdCloser" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_200</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/black</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_200</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
10
app/src/main/res/values/colors.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="purple_200">#FFBB86FC</color>
|
||||||
|
<color name="purple_500">#FF6200EE</color>
|
||||||
|
<color name="purple_700">#FF3700B3</color>
|
||||||
|
<color name="teal_200">#FF03DAC5</color>
|
||||||
|
<color name="teal_700">#FF018786</color>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
5
app/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">AdCloser</string>
|
||||||
|
<string name="accessibility_service_label">accessibility_service_label</string>
|
||||||
|
<string name="accessibility_service_description">accessibility_service_description</string>
|
||||||
|
</resources>
|
25
app/src/main/res/values/themes.xml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.AdCloser" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||||
|
<!-- Primary brand color. -->
|
||||||
|
<item name="colorPrimary">@color/purple_500</item>
|
||||||
|
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||||
|
<item name="colorOnPrimary">@color/white</item>
|
||||||
|
<!-- Secondary brand color. -->
|
||||||
|
<item name="colorSecondary">@color/teal_200</item>
|
||||||
|
<item name="colorSecondaryVariant">@color/teal_700</item>
|
||||||
|
<item name="colorOnSecondary">@color/black</item>
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.AdCloser.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Theme.AdCloser.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
|
||||||
|
|
||||||
|
<style name="Theme.AdCloser.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
|
||||||
|
</resources>
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:accessibilityEventTypes="typeAllMask"
|
||||||
|
android:accessibilityFeedbackType="feedbackGeneric"
|
||||||
|
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews"
|
||||||
|
android:canRetrieveWindowContent="true"
|
||||||
|
android:description="@string/accessibility_service_description"
|
||||||
|
android:notificationTimeout="100" />
|
19
app/src/test/java/li/songe/ad_closer/ExampleUnitTest.kt
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package li.songe.ad_closer
|
||||||
|
|
||||||
|
import li.songe.ad_closer.util.MatchRule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
import org.junit.Assert.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* See [testing documentation](http://d.android.com/tools/testing).
|
||||||
|
*/
|
||||||
|
class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
fun addition_isCorrect() {
|
||||||
|
assertEquals(4, 4L)
|
||||||
|
println(MatchRule.parse("ImageView[text=hi][id=hi] >> WebView[text=hi] - TextView"))
|
||||||
|
}
|
||||||
|
}
|
22
build.gradle
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
buildscript {
|
||||||
|
ext {
|
||||||
|
compose_version = "1.0.5"
|
||||||
|
kotlin_version = "1.5.30"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath "com.android.tools.build:gradle:7.0.3"
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
21
gradle.properties
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Project-wide Gradle settings.
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app"s APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
|
android.enableJetifier=true
|
||||||
|
# Kotlin code style for this project: "official" or "obsolete":
|
||||||
|
kotlin.code.style=official
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
#Wed Oct 13 10:13:24 CST 2021
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
185
gradlew
vendored
Normal file
|
@ -0,0 +1,185 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright 2015 the original author or authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
##
|
||||||
|
## Gradle start up script for UN*X
|
||||||
|
##
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
PRG="$0"
|
||||||
|
# Need this for relative symlinks.
|
||||||
|
while [ -h "$PRG" ] ; do
|
||||||
|
ls=`ls -ld "$PRG"`
|
||||||
|
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||||
|
if expr "$link" : '/.*' > /dev/null; then
|
||||||
|
PRG="$link"
|
||||||
|
else
|
||||||
|
PRG=`dirname "$PRG"`"/$link"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
SAVED="`pwd`"
|
||||||
|
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||||
|
APP_HOME="`pwd -P`"
|
||||||
|
cd "$SAVED" >/dev/null
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=`basename "$0"`
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD="maximum"
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "`uname`" in
|
||||||
|
CYGWIN* )
|
||||||
|
cygwin=true
|
||||||
|
;;
|
||||||
|
Darwin* )
|
||||||
|
darwin=true
|
||||||
|
;;
|
||||||
|
MINGW* )
|
||||||
|
msys=true
|
||||||
|
;;
|
||||||
|
NONSTOP* )
|
||||||
|
nonstop=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="java"
|
||||||
|
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||||
|
MAX_FD_LIMIT=`ulimit -H -n`
|
||||||
|
if [ $? -eq 0 ] ; then
|
||||||
|
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||||
|
MAX_FD="$MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
ulimit -n $MAX_FD
|
||||||
|
if [ $? -ne 0 ] ; then
|
||||||
|
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Darwin, add options to specify how the application appears in the dock
|
||||||
|
if $darwin; then
|
||||||
|
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
|
||||||
|
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||||
|
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||||
|
|
||||||
|
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||||
|
|
||||||
|
# We build the pattern for arguments to be converted via cygpath
|
||||||
|
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||||
|
SEP=""
|
||||||
|
for dir in $ROOTDIRSRAW ; do
|
||||||
|
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||||
|
SEP="|"
|
||||||
|
done
|
||||||
|
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||||
|
# Add a user-defined pattern to the cygpath arguments
|
||||||
|
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||||
|
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||||
|
fi
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
i=0
|
||||||
|
for arg in "$@" ; do
|
||||||
|
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||||
|
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||||
|
|
||||||
|
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||||
|
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||||
|
else
|
||||||
|
eval `echo args$i`="\"$arg\""
|
||||||
|
fi
|
||||||
|
i=`expr $i + 1`
|
||||||
|
done
|
||||||
|
case $i in
|
||||||
|
0) set -- ;;
|
||||||
|
1) set -- "$args0" ;;
|
||||||
|
2) set -- "$args0" "$args1" ;;
|
||||||
|
3) set -- "$args0" "$args1" "$args2" ;;
|
||||||
|
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||||
|
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||||
|
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||||
|
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||||
|
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||||
|
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Escape application args
|
||||||
|
save () {
|
||||||
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
|
echo " "
|
||||||
|
}
|
||||||
|
APP_ARGS=`save "$@"`
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||||
|
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
89
gradlew.bat
vendored
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if "%ERRORLEVEL%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
10
settings.gradle
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
dependencyResolutionManagement {
|
||||||
|
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
jcenter() // Warning: this repository is going to shut down soon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = "AdCloser"
|
||||||
|
include ':app'
|