├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── cn.ac.lz233.tarnhelm.logic.AppDatabase │ │ ├── 3.json │ │ └── 4.json └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── ac │ │ └── lz233 │ │ └── tarnhelm │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── aidl │ │ └── cn │ │ │ └── ac │ │ │ └── lz233 │ │ │ └── tarnhelm │ │ │ └── xposed │ │ │ └── ModuleDataBridge.aidl │ ├── assets │ │ └── xposed_init │ ├── ic_launcher-playstore.png │ ├── java │ │ └── cn │ │ │ └── ac │ │ │ └── lz233 │ │ │ └── tarnhelm │ │ │ ├── App.kt │ │ │ ├── extension │ │ │ ├── ExtensionClassLoader.kt │ │ │ ├── ExtensionManager.kt │ │ │ ├── ExtensionManagerService.kt │ │ │ ├── ExtensionRecord.kt │ │ │ ├── ExtensionRecordAdapter.kt │ │ │ ├── MemoryDexLoader.java │ │ │ ├── api │ │ │ │ ├── ExtContext.java │ │ │ │ ├── ExtService.java │ │ │ │ ├── ExtSharedPreferences.java │ │ │ │ ├── IExtConfigurationPanel.java │ │ │ │ └── ITarnhelmExt.java │ │ │ ├── exception │ │ │ │ ├── ConfigurationPanelException.kt │ │ │ │ └── InvalidExtensionException.kt │ │ │ └── storage │ │ │ │ ├── ExtensionOwnStorage.kt │ │ │ │ └── ExtensionRecordStorage.kt │ │ │ ├── logic │ │ │ ├── AppDatabase.kt │ │ │ ├── Network.kt │ │ │ ├── dao │ │ │ │ ├── ConfigDao.kt │ │ │ │ ├── ExtensionDao.kt │ │ │ │ ├── ParameterRuleDao.kt │ │ │ │ ├── RedirectRuleDao.kt │ │ │ │ ├── RegexRuleDao.kt │ │ │ │ └── SettingsDao.kt │ │ │ └── module │ │ │ │ └── meta │ │ │ │ ├── Extension.kt │ │ │ │ ├── ParameterRule.kt │ │ │ │ ├── RedirectRule.kt │ │ │ │ └── RegexRule.kt │ │ │ ├── receiver │ │ │ └── BootBroadcast.kt │ │ │ ├── service │ │ │ ├── ClipboardService.kt │ │ │ └── ModuleDataBridgeService.kt │ │ │ ├── ui │ │ │ ├── BaseActivity.kt │ │ │ ├── SecondaryBaseActivity.kt │ │ │ ├── extensions │ │ │ │ └── ExtensionsActivity.kt │ │ │ ├── main │ │ │ │ ├── MainActivity.kt │ │ │ │ └── PlaceHolderActivity.kt │ │ │ ├── process │ │ │ │ ├── ProcessCopyActivity.kt │ │ │ │ ├── ProcessEditTextActivity.kt │ │ │ │ ├── ProcessOverlayActivity.kt │ │ │ │ ├── ProcessRulesActivity.kt │ │ │ │ ├── ProcessServiceActivity.kt │ │ │ │ ├── ProcessShareActivity.kt │ │ │ │ └── ProcessShortcutActivity.kt │ │ │ ├── rules │ │ │ │ ├── DragSwipeCallback.kt │ │ │ │ ├── IDragSwipe.kt │ │ │ │ ├── RulesActivity.kt │ │ │ │ ├── parameter │ │ │ │ │ ├── ParameterRulesAdapter.kt │ │ │ │ │ └── ParameterRulesFragment.kt │ │ │ │ ├── redirect │ │ │ │ │ ├── RedirectRulesAdapter.kt │ │ │ │ │ └── RedirectRulesFragment.kt │ │ │ │ └── regex │ │ │ │ │ ├── RegexRulesAdapter.kt │ │ │ │ │ └── RegexRulesFragment.kt │ │ │ └── settings │ │ │ │ ├── SettingsActivity.kt │ │ │ │ ├── SettingsFragment.kt │ │ │ │ └── backup │ │ │ │ ├── BackupBottomSheetFragment.kt │ │ │ │ └── SaveViaSAFActivity.kt │ │ │ ├── util │ │ │ ├── LogUtil.kt │ │ │ └── ktx │ │ │ │ ├── Extension.kt │ │ │ │ ├── Float.kt │ │ │ │ ├── IO.kt │ │ │ │ ├── Int.kt │ │ │ │ ├── Intent.kt │ │ │ │ ├── JSONArray.kt │ │ │ │ ├── JSONObject.kt │ │ │ │ ├── List.kt │ │ │ │ ├── OkHttp.kt │ │ │ │ ├── Rule.kt │ │ │ │ └── String.kt │ │ │ ├── view │ │ │ └── EditText.kt │ │ │ └── xposed │ │ │ ├── Config.kt │ │ │ ├── XposedEntry.kt │ │ │ ├── module │ │ │ ├── Android.kt │ │ │ ├── Self.kt │ │ │ └── SystemUI.kt │ │ │ └── util │ │ │ ├── AMSHelper.kt │ │ │ ├── KotlinXposedHelper.kt │ │ │ └── ModuleBridgeHelper.kt │ └── res │ │ ├── drawable │ │ ├── ic_add.xml │ │ ├── ic_arrow_back.xml │ │ ├── ic_check_circle.xml │ │ ├── ic_copy.xml │ │ ├── ic_delete.xml │ │ ├── ic_error.xml │ │ ├── ic_extension.xml │ │ ├── ic_icon.xml │ │ ├── ic_info.xml │ │ ├── ic_launcher.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_open.xml │ │ ├── ic_paste.xml │ │ ├── ic_rule_folder.xml │ │ ├── ic_save.xml │ │ ├── ic_settings.xml │ │ └── ic_share.xml │ │ ├── layout │ │ ├── activity_extensions.xml │ │ ├── activity_main.xml │ │ ├── activity_rules.xml │ │ ├── activity_settings.xml │ │ ├── dialog_about.xml │ │ ├── dialog_parameter_rule_add.xml │ │ ├── dialog_parameter_rule_edit.xml │ │ ├── dialog_redirect_rule_add.xml │ │ ├── dialog_redirect_rule_edit.xml │ │ ├── dialog_regex_rule_add.xml │ │ ├── dialog_regex_rule_edit.xml │ │ ├── fragment_backup.xml │ │ ├── fragment_parameter_rules.xml │ │ ├── fragment_redirect_rules.xml │ │ ├── fragment_regex_rules.xml │ │ ├── item_extension.xml │ │ ├── item_parameter_rule.xml │ │ ├── item_redirect_rule.xml │ │ ├── item_regex_rule.xml │ │ ├── m3_icon_frame_open.xml │ │ ├── m3_preference.xml │ │ └── m3_preference_category.xml │ │ ├── values-ja-rJP │ │ └── strings.xml │ │ ├── values-ja │ │ └── strings.xml │ │ ├── values-night-v32 │ │ └── themes.xml │ │ ├── values-night │ │ └── colors.xml │ │ ├── values-v29 │ │ └── themes.xml │ │ ├── values-v32 │ │ └── themes.xml │ │ ├── values-yue-rCN │ │ └── strings.xml │ │ ├── values-zh-rCN │ │ └── strings.xml │ │ ├── values-zh-rHK │ │ └── strings.xml │ │ ├── values-zh-rTW │ │ └── strings.xml │ │ ├── values-zh │ │ └── strings.xml │ │ ├── values │ │ ├── array.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── file_paths.xml │ │ ├── locale_config.xml │ │ ├── preferences.xml │ │ └── shortcuts.xml │ └── test │ └── java │ └── cn │ └── ac │ └── lz233 │ └── tarnhelm │ └── ExampleUnitTest.kt ├── art ├── animation.pptx ├── animation.webp ├── banner.png ├── banner.psd ├── fdroid-badge.png ├── google-play-badge.png ├── icon-color-coolapk.png ├── icon-color.png └── icon.svg ├── base ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── ac │ │ └── lz233 │ │ └── tarnhelm │ │ └── ExampleInstrumentedTest.kt │ ├── main │ └── AndroidManifest.xml │ └── test │ └── java │ └── cn │ └── ac │ └── lz233 │ └── tarnhelm │ └── ExampleUnitTest.kt ├── bin ├── test_cloud_backup.sh └── test_d2d_backup.sh ├── build.gradle ├── fastlane └── metadata │ └── android │ ├── en-US │ ├── full_description.txt │ ├── images │ │ ├── icon.png │ │ └── phoneScreenshots │ │ │ ├── 1.png │ │ │ └── 2.png │ └── short_description.txt │ └── zh-CN │ ├── full_description.txt │ └── short_description.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── hidden-api ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── cn │ │ └── ac │ │ └── lz233 │ │ └── tarnhelm │ │ └── hidden_api │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── android │ │ └── app │ │ └── AppOpsManagerHidden.java │ └── test │ └── java │ └── cn │ └── ac │ └── lz233 │ └── tarnhelm │ └── hidden_api │ └── ExampleUnitTest.kt ├── license.md ├── settings.gradle └── shizuku-service ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── cn │ └── ac │ └── lz233 │ └── tarnhelm │ └── shizuku_service │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml ├── aidl │ └── cn │ │ └── ac │ │ └── lz233 │ │ └── tarnhelm │ │ └── service │ │ ├── IClipboardShizukuService.aidl │ │ └── ShizukuCallback.aidl └── java │ └── cn │ └── ac │ └── lz233 │ └── tarnhelm │ └── service │ └── ClipboardShizukuService.kt └── test └── java └── cn └── ac └── lz233 └── tarnhelm └── shizuku_service └── ExampleUnitTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | *.aab 2 | *.apk 3 | *.iml 4 | .gradle 5 | .idea/ 6 | .DS_Store 7 | build 8 | captures 9 | .externalNativeBuild 10 | .cxx 11 | local.properties 12 | app/github 13 | app/fdroid 14 | app/google 15 | app/coolapk 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 120 | 121 | 124 | 125 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](./art/animation.webp) 2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | 更多信息,参见:[tarnhelm.project.ac.cn](https://tarnhelm.project.ac.cn/) 13 | 14 | 从 Play Store 获取:https://play.google.com/store/apps/details?id=cn.ac.lz233.tarnhelm 15 | 16 | 从 F-Droid 获取:https://f-droid.org/packages/cn.ac.lz233.tarnhelm 17 | 18 | 从 LSPosed Repository 获取:https://github.com/Xposed-Modules-Repo/cn.ac.lz233.tarnhelm 19 | 20 | [![Star History Chart](https://api.star-history.com/svg?repos=lz233/Tarnhelm&type=Date)](https://star-history.com/#lz233/Tarnhelm&Date) 21 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release/* 3 | /*/release/* 4 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'com.google.devtools.ksp' 5 | } 6 | 7 | android { 8 | namespace 'cn.ac.lz233.tarnhelm' 9 | compileSdk 35 10 | 11 | defaultConfig { 12 | Date date = new Date() 13 | applicationId "cn.ac.lz233.tarnhelm" 14 | minSdk 27 15 | targetSdk 35 16 | versionCode = date.format("yyyyMMdd").toInteger() 17 | versionName "1.8.0" 18 | 19 | ksp { 20 | arg("room.schemaLocation", "$projectDir/schemas") 21 | } 22 | 23 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 24 | } 25 | 26 | buildTypes { 27 | release { 28 | minifyEnabled true 29 | shrinkResources true 30 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 31 | } 32 | } 33 | flavorDimensions = ["version"] 34 | productFlavors { 35 | github { 36 | buildConfigField 'String', 'FLAVOR', '"github"' 37 | } 38 | google { 39 | buildConfigField 'String', 'FLAVOR', '"google"' 40 | } 41 | fdroid { 42 | buildConfigField 'String', 'FLAVOR', '"fdroid"' 43 | } 44 | coolapk { 45 | buildConfigField 'String', 'FLAVOR', '"coolapk"' 46 | } 47 | } 48 | 49 | compileOptions { 50 | sourceCompatibility JavaVersion.VERSION_17 51 | targetCompatibility JavaVersion.VERSION_17 52 | } 53 | kotlinOptions { 54 | jvmTarget = '17' 55 | } 56 | buildFeatures { 57 | viewBinding true 58 | buildConfig true 59 | aidl true 60 | } 61 | packagingOptions { 62 | jniLibs { 63 | excludes += [] 64 | } 65 | resources { 66 | excludes += ['META-INF/**', 'kotlin/**', 'okhttp3/**', 'org/**', '**.properties', '**.bin', '**kotlin**'] 67 | } 68 | } 69 | } 70 | 71 | configurations.configureEach { 72 | exclude group: 'dev.rikka.rikkax.appcompat', module: 'appcompat' 73 | } 74 | 75 | dependencies { 76 | implementation fileTree(dir: 'libs', include: ['*.jar']) 77 | implementation project(":shizuku-service") 78 | 79 | implementation 'androidx.appcompat:appcompat:1.7.0' 80 | implementation 'androidx.core:core-ktx:1.15.0' 81 | implementation 'androidx.room:room-runtime:2.6.1' 82 | implementation 'androidx.viewpager2:viewpager2:1.1.0' 83 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0' 84 | implementation 'androidx.preference:preference-ktx:1.2.1' 85 | implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.7' 86 | implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' 87 | implementation 'androidx.window:window:1.3.0' 88 | ksp 'androidx.room:room-compiler:2.6.1' 89 | 90 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' 91 | 92 | // Visual Studio App Center Retirement: https://learn.microsoft.com/en-us/appcenter/retirement 93 | // githubImplementation 'com.microsoft.appcenter:appcenter-analytics:5.0.5' 94 | // githubImplementation 'com.microsoft.appcenter:appcenter-crashes:5.0.5' 95 | // googleImplementation 'com.microsoft.appcenter:appcenter-analytics:5.0.5' 96 | // googleImplementation 'com.microsoft.appcenter:appcenter-crashes:5.0.5' 97 | // coolapkImplementation 'com.microsoft.appcenter:appcenter-analytics:5.0.5' 98 | // coolapkImplementation 'com.microsoft.appcenter:appcenter-crashes:5.0.5' 99 | 100 | implementation 'com.google.android.material:material:1.12.0' 101 | implementation "dev.rikka.rikkax.material:material-preference:2.0.0" 102 | 103 | implementation 'com.guolindev.permissionx:permissionx:1.8.1' 104 | 105 | implementation 'com.squareup.okhttp3:okhttp:5.0.0-alpha.14' 106 | implementation 'com.squareup.retrofit2:retrofit:2.11.0' 107 | implementation 'com.squareup.retrofit2:converter-gson:2.11.0' 108 | 109 | compileOnly 'de.robv.android.xposed:api:82' 110 | 111 | def shizukuVersion = "13.1.5" 112 | implementation "dev.rikka.shizuku:api:$shizukuVersion" 113 | implementation "dev.rikka.shizuku:provider:$shizukuVersion" 114 | 115 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:5.1' 116 | 117 | implementation 'io.github.billywei01:fastkv:2.6.0' 118 | implementation 'io.github.billywei01:packable-kotlin:2.1.3' 119 | 120 | testImplementation 'junit:junit:4.13.2' 121 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 122 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 123 | } 124 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep public class cn.ac.lz233.tarnhelm.xposed.XposedEntry 24 | 25 | -keep class cn.ac.lz233.tarnhelm.App$Companion { 26 | isXposedActive(); 27 | } 28 | 29 | -assumenosideeffects class cn.ac.lz233.tarnhelm.util.LogUtil { 30 | public static void _d*(...); 31 | } 32 | 33 | -keep class cn.ac.lz233.tarnhelm.service.** { 34 | *; 35 | } 36 | 37 | -keep class cn.ac.lz233.tarnhelm.extension.api.** { 38 | *; 39 | } 40 | 41 | -dontwarn android.app.AppOpsManager$OnOpNotedListener -------------------------------------------------------------------------------- /app/src/androidTest/java/cn/ac/lz233/tarnhelm/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("cn.ac.lz233.tarnhelm", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/aidl/cn/ac/lz233/tarnhelm/xposed/ModuleDataBridge.aidl: -------------------------------------------------------------------------------- 1 | // ModuleDataBridge.aidl 2 | package cn.ac.lz233.tarnhelm.xposed; 3 | 4 | // Declare any non-default types here with import statements 5 | 6 | interface ModuleDataBridge { 7 | 8 | String doTarnhelms(String string); 9 | int ping(); 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/assets/xposed_init: -------------------------------------------------------------------------------- 1 | cn.ac.lz233.tarnhelm.xposed.XposedEntry -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/ExtensionClassLoader.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension 2 | 3 | import android.content.Context 4 | 5 | class ExtensionClassLoader: ClassLoader(sBootClassLoader) { 6 | 7 | companion object { 8 | private val sBootClassLoader = Context::class.java.classLoader 9 | private val sHostClassLoader = ExtensionClassLoader::class.java.classLoader 10 | private val whitelist = arrayOf( 11 | "cn.ac.lz233.tarnhelm.extension.api.", 12 | "cn.ac.lz233.tarnhelm.extension.storage.ExtensionOwnStorage" 13 | ) 14 | } 15 | 16 | override fun findLibrary(name: String?): String { 17 | throw RuntimeException("findLibrary is not supported") 18 | } 19 | 20 | override fun findClass(name: String?): Class<*> { 21 | try { 22 | return sBootClassLoader.loadClass(name) 23 | } catch (ignored: ClassNotFoundException) {} 24 | if (whitelist.any { name?.startsWith(it) == true }) { 25 | return sHostClassLoader.loadClass(name) 26 | } 27 | return super.findClass(name) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/ExtensionRecord.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension 2 | 3 | import cn.ac.lz233.tarnhelm.extension.api.ITarnhelmExt.ExtInfo 4 | 5 | data class ExtensionRecord( 6 | var enabled: Boolean = false, 7 | val entryClassName: String, 8 | val id: String, 9 | val author: String?, 10 | val name: String, 11 | val description: String?, 12 | val extensionURL: String?, 13 | val versionCode: Int, 14 | val versionName: String?, 15 | val hasConfigurationPanel: Boolean, 16 | val minTarnhelmSdkVersion: Int, 17 | val minAndroidSdkVersion: Int, 18 | val regexes: List 19 | ) { 20 | 21 | fun toExtInfo(): ExtInfo { 22 | return object : ExtInfo { 23 | override fun id(): String = id 24 | override fun author(): String? = author 25 | override fun name(): String = name 26 | override fun description(): String? = description 27 | override fun extensionURL(): String? = extensionURL 28 | override fun versionCode(): Int = versionCode 29 | override fun versionName(): String? = versionName 30 | override fun hasConfigurationPanel(): Boolean = hasConfigurationPanel 31 | override fun minTarnhelmSdkVersion(): Int = minTarnhelmSdkVersion 32 | override fun minAndroidSdkVersion(): Int = minAndroidSdkVersion 33 | override fun regexes(): Array = regexes.toTypedArray() 34 | } 35 | } 36 | 37 | companion object { 38 | 39 | fun fromExtInfo(extInfo: ExtInfo, entryClassName: String): ExtensionRecord { 40 | return ExtensionRecord( 41 | id = extInfo.id(), 42 | entryClassName = entryClassName, 43 | author = extInfo.author(), 44 | name = extInfo.name(), 45 | description = extInfo.description(), 46 | extensionURL = extInfo.extensionURL(), 47 | versionCode = extInfo.versionCode(), 48 | versionName = extInfo.versionName(), 49 | hasConfigurationPanel = extInfo.hasConfigurationPanel(), 50 | minTarnhelmSdkVersion = extInfo.minTarnhelmSdkVersion(), 51 | minAndroidSdkVersion = extInfo.minAndroidSdkVersion(), 52 | regexes = extInfo.regexes().toList() 53 | ) 54 | } 55 | 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/ExtensionRecordAdapter.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension 2 | 3 | import io.packable.PackDecoder 4 | import io.packable.PackEncoder 5 | import io.packable.TypeAdapter 6 | 7 | object ExtensionRecordAdapter : TypeAdapter { 8 | 9 | override fun decode(decoder: PackDecoder): ExtensionRecord { 10 | return ExtensionRecord( 11 | enabled = decoder.getBoolean(0), 12 | entryClassName = decoder.getString(1), 13 | id = decoder.getString(2), 14 | author = decoder.getString(3), 15 | name = decoder.getString(4), 16 | description = decoder.getString(5), 17 | extensionURL = decoder.getString(6), 18 | versionCode = decoder.getInt(7), 19 | versionName = decoder.getString(8), 20 | hasConfigurationPanel = decoder.getBoolean(9), 21 | minTarnhelmSdkVersion = decoder.getInt(10), 22 | minAndroidSdkVersion = decoder.getInt(11), 23 | regexes = decoder.getStringList(12)!! 24 | ) 25 | } 26 | 27 | override fun encode(encoder: PackEncoder, target: ExtensionRecord) { 28 | encoder.putBoolean(0, target.enabled) 29 | encoder.putString(1, target.entryClassName) 30 | encoder.putString(2, target.id) 31 | encoder.putString(3, target.author) 32 | encoder.putString(4, target.name) 33 | encoder.putString(5, target.description) 34 | encoder.putString(6, target.extensionURL) 35 | encoder.putInt(7, target.versionCode) 36 | encoder.putString(8, target.versionName) 37 | encoder.putBoolean(9, target.hasConfigurationPanel) 38 | encoder.putInt(10, target.minTarnhelmSdkVersion) 39 | encoder.putInt(11, target.minAndroidSdkVersion) 40 | encoder.putStringList(12, target.regexes) 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/MemoryDexLoader.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.annotation.RequiresApi; 8 | 9 | import java.lang.reflect.Constructor; 10 | import java.nio.ByteBuffer; 11 | import java.util.Objects; 12 | 13 | import dalvik.system.DexFile; 14 | import dalvik.system.InMemoryDexClassLoader; 15 | 16 | public class MemoryDexLoader { 17 | 18 | private MemoryDexLoader() { 19 | throw new AssertionError("No instance for you!"); 20 | } 21 | 22 | @NonNull 23 | public static ClassLoader createClassLoaderWithDex(@NonNull byte[] dexFile, @Nullable ClassLoader parent) { 24 | if (dexFile.length == 0) { 25 | throw new IllegalArgumentException("dexFile is empty"); 26 | } 27 | if (parent == null) { 28 | parent = Runtime.class.getClassLoader(); 29 | } 30 | return createClassLoaderWithDexAboveOreo(dexFile, parent); 31 | } 32 | 33 | @RequiresApi(26) 34 | @NonNull 35 | private static ClassLoader createClassLoaderWithDexAboveOreo(@NonNull byte[] dexFile, @NonNull ClassLoader parent) { 36 | ByteBuffer byteBuffer = ByteBuffer.wrap(dexFile); 37 | return new InMemoryDexClassLoader(byteBuffer, parent); 38 | } 39 | 40 | /** 41 | * Create a DexFile instance from a byte array. Applications generally should not create a DexFile directly. 42 | *

43 | * It will hurt performance in most cases and will lead to incorrect execution of bytecode in the worst case. 44 | * 45 | * @param dexBytes dex file data 46 | * @param definingContext the class loader where the dex file will be attached to 47 | * @param name optional name for the dex file, may be null 48 | * @return a DexFile instance 49 | */ 50 | @NonNull 51 | public static DexFile createDexFileFormBytes(@NonNull byte[] dexBytes, @NonNull ClassLoader definingContext, @Nullable String name) { 52 | Objects.requireNonNull(dexBytes, "dexBytes is null"); 53 | Objects.requireNonNull(definingContext, "definingContext is null"); 54 | if (dexBytes.length < 20) { 55 | throw new IllegalArgumentException("dexBytes is too short"); 56 | } 57 | return createDexFileFormBytesAboveOreo(dexBytes, definingContext, name); 58 | } 59 | 60 | @RequiresApi(26) 61 | @NonNull 62 | private static DexFile createDexFileFormBytesAboveOreo(@NonNull byte[] dexBytes, @NonNull ClassLoader definingContext, @Nullable String name) { 63 | // Android 8.0 - 10: DexFile(ByteBuffer buf) throws IOException; 64 | // Android 10+: DexFile(ByteBuffer[] bufs, ClassLoader loader, DexPathList.Element[] elements); 65 | Constructor constructor1 = null; 66 | Constructor constructor3 = null; 67 | Class kElementArray; 68 | try { 69 | kElementArray = Class.forName("[Ldalvik.system.DexPathList$Element;"); 70 | } catch (ClassNotFoundException e) { 71 | throw new IllegalStateException("Class.forName(\"[Ldalvik.system.DexPathList$Element;\"); fail", e); 72 | } 73 | try { 74 | constructor1 = DexFile.class.getDeclaredConstructor(ByteBuffer.class); 75 | constructor1.setAccessible(true); 76 | } catch (NoSuchMethodException ignored) { 77 | } 78 | try { 79 | constructor3 = DexFile.class.getDeclaredConstructor(ByteBuffer[].class, ClassLoader.class, kElementArray); 80 | constructor3.setAccessible(true); 81 | } catch (NoSuchMethodException ignored) { 82 | } 83 | ByteBuffer byteBuffer = ByteBuffer.wrap(dexBytes); 84 | if (constructor3 != null) { 85 | ByteBuffer[] byteBuffers = new ByteBuffer[]{byteBuffer}; 86 | try { 87 | return constructor3.newInstance(byteBuffers, definingContext, null); 88 | } catch (ReflectiveOperationException e) { 89 | throw new IllegalStateException(e); 90 | } 91 | } else if (constructor1 != null) { 92 | try { 93 | return constructor1.newInstance(byteBuffer); 94 | } catch (ReflectiveOperationException e) { 95 | throw new IllegalStateException(e); 96 | } 97 | } else { 98 | throw new IllegalStateException("DexFile constructor not found, SDK_INT=" + Build.VERSION.SDK_INT); 99 | } 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/api/ExtContext.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.api; 2 | 3 | import android.os.Build; 4 | 5 | public interface ExtContext { 6 | 7 | int tarnhelmSdkVersion(); 8 | 9 | default int androidSdkVersion() { 10 | return Build.VERSION.SDK_INT; 11 | } 12 | 13 | ExtSharedPreferences getSharedPreferences(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/api/ExtService.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.api; 2 | 3 | public abstract class ExtService { 4 | 5 | private ExtService() {} 6 | 7 | public ExtService(ExtContext extContext) {} 8 | 9 | public abstract void onExtInstall(); 10 | 11 | public abstract String handleLoadString(CharSequence charSequence); 12 | 13 | public abstract void onExtUninstall(); 14 | 15 | public abstract String checkUpdate(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/api/ExtSharedPreferences.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.api; 2 | 3 | import org.jetbrains.annotations.Nullable; 4 | 5 | import java.util.Map; 6 | import java.util.Set; 7 | 8 | public interface ExtSharedPreferences { 9 | 10 | Map getAll(); 11 | 12 | @Nullable 13 | String getString(String var1, @Nullable String var2); 14 | 15 | @Nullable 16 | Set getStringSet(String var1, @Nullable Set var2); 17 | 18 | int getInt(String var1, int var2); 19 | 20 | long getLong(String var1, long var2); 21 | 22 | float getFloat(String var1, float var2); 23 | 24 | boolean getBoolean(String var1, boolean var2); 25 | 26 | boolean contains(String var1); 27 | 28 | Editor edit(); 29 | 30 | interface Editor { 31 | Editor putString(String var1, @Nullable String var2); 32 | 33 | Editor putStringSet(String var1, @Nullable Set var2); 34 | 35 | Editor putInt(String var1, int var2); 36 | 37 | Editor putLong(String var1, long var2); 38 | 39 | Editor putFloat(String var1, float var2); 40 | 41 | Editor putBoolean(String var1, boolean var2); 42 | 43 | Editor remove(String var1); 44 | 45 | Editor clear(); 46 | 47 | boolean commit(); 48 | 49 | void apply(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/api/IExtConfigurationPanel.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.api; 2 | 3 | import android.content.Context; 4 | import android.view.View; 5 | 6 | public interface IExtConfigurationPanel { 7 | 8 | View onRequestConfigurationPanel(Context context, ExtSharedPreferences sharedPreferences); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/api/ITarnhelmExt.java: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.api; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | import org.jetbrains.annotations.Nullable; 5 | 6 | public interface ITarnhelmExt { 7 | 8 | ExtInfo extensionInfo(); 9 | ExtService createExtensionService(ExtContext extContext); 10 | 11 | interface ExtInfo { 12 | 13 | @NotNull 14 | String id(); 15 | 16 | @Nullable 17 | String author(); 18 | 19 | @NotNull 20 | String name(); 21 | 22 | @Nullable 23 | String description(); 24 | 25 | @Nullable 26 | String extensionURL(); 27 | 28 | int versionCode(); 29 | 30 | @Nullable 31 | String versionName(); 32 | 33 | boolean hasConfigurationPanel(); 34 | 35 | int minTarnhelmSdkVersion(); 36 | 37 | int minAndroidSdkVersion(); 38 | 39 | @NotNull 40 | String[] regexes(); 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/exception/ConfigurationPanelException.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.exception 2 | 3 | class ConfigurationPanelException : Exception { 4 | constructor() : super() 5 | constructor(message: String?) : super(message) 6 | constructor(message: String?, cause: Throwable?) : super(message, cause) 7 | constructor(cause: Throwable?) : super(cause) 8 | constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(message, cause, enableSuppression, writableStackTrace) 9 | 10 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/exception/InvalidExtensionException.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.exception 2 | 3 | class InvalidExtensionException : Exception { 4 | constructor() : super() 5 | constructor(message: String?) : super(message) 6 | constructor(message: String?, cause: Throwable?) : super(message, cause) 7 | constructor(cause: Throwable?) : super(cause) 8 | constructor(message: String?, cause: Throwable?, enableSuppression: Boolean, writableStackTrace: Boolean) : super(message, cause, enableSuppression, writableStackTrace) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/storage/ExtensionOwnStorage.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.storage 2 | 3 | import cn.ac.lz233.tarnhelm.extension.api.ExtSharedPreferences 4 | import io.fastkv.FastKV 5 | 6 | class ExtensionOwnStorage(dirPath: String) : ExtSharedPreferences { 7 | 8 | private val kv: FastKV = FastKV.Builder(dirPath, "sp.fastkv").build() 9 | 10 | override fun getAll(): MutableMap = kv.all 11 | 12 | override fun getString(var1: String?, var2: String?): String? = kv.getString(var1, var2) 13 | 14 | override fun getStringSet(var1: String?, var2: MutableSet?): MutableSet? = kv.getStringSet(var1, var2) 15 | 16 | override fun getInt(var1: String?, var2: Int): Int = kv.getInt(var1, var2) 17 | 18 | override fun getLong(var1: String?, var2: Long): Long = kv.getLong(var1, var2) 19 | 20 | override fun getFloat(var1: String?, var2: Float): Float = kv.getFloat(var1, var2) 21 | 22 | override fun getBoolean(var1: String?, var2: Boolean): Boolean = kv.getBoolean(var1, var2) 23 | 24 | override fun contains(var1: String?): Boolean = kv.contains(var1) 25 | 26 | override fun edit(): ExtSharedPreferences.Editor = object : ExtSharedPreferences.Editor { 27 | 28 | override fun putString(var1: String?, var2: String?): ExtSharedPreferences.Editor { 29 | kv.putString(var1, var2) 30 | return this 31 | } 32 | 33 | override fun putStringSet(var1: String?, var2: MutableSet?): ExtSharedPreferences.Editor { 34 | kv.putStringSet(var1, var2) 35 | return this 36 | } 37 | 38 | override fun putInt(var1: String?, var2: Int): ExtSharedPreferences.Editor { 39 | kv.putInt(var1, var2) 40 | return this 41 | } 42 | 43 | override fun putLong(var1: String?, var2: Long): ExtSharedPreferences.Editor { 44 | kv.putLong(var1, var2) 45 | return this 46 | } 47 | 48 | override fun putFloat(var1: String?, var2: Float): ExtSharedPreferences.Editor { 49 | kv.putFloat(var1, var2) 50 | return this 51 | } 52 | 53 | override fun putBoolean(var1: String?, var2: Boolean): ExtSharedPreferences.Editor { 54 | kv.putBoolean(var1, var2) 55 | return this 56 | } 57 | 58 | override fun remove(var1: String?): ExtSharedPreferences.Editor { 59 | kv.remove(var1) 60 | return this 61 | } 62 | 63 | override fun clear(): ExtSharedPreferences.Editor { 64 | kv.clear() 65 | return this 66 | } 67 | 68 | override fun commit(): Boolean { 69 | return kv.commit() 70 | } 71 | 72 | override fun apply() { 73 | kv.apply() 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/extension/storage/ExtensionRecordStorage.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.extension.storage 2 | 3 | import android.content.Context 4 | import cn.ac.lz233.tarnhelm.extension.ExtensionRecord 5 | import cn.ac.lz233.tarnhelm.extension.ExtensionRecordAdapter 6 | import cn.ac.lz233.tarnhelm.extension.api.ITarnhelmExt.ExtInfo 7 | import io.fastkv.FastKV 8 | import io.fastkv.interfaces.FastEncoder 9 | import io.packable.PackDecoder 10 | import io.packable.PackEncoder 11 | 12 | class ExtensionRecordStorage(context: Context) { 13 | 14 | companion object { 15 | 16 | const val KEY = "extensions" 17 | 18 | private val extensionRecordListEncoder = object : FastEncoder> { 19 | 20 | override fun tag(): String = "ExtInfoMap" 21 | 22 | override fun decode(bytes: ByteArray, offset: Int, length: Int): List { 23 | return PackDecoder(bytes, offset, length).getObjectList(0, ExtensionRecordAdapter) ?: emptyList() 24 | } 25 | 26 | override fun encode(obj: List): ByteArray { 27 | return PackEncoder().putObjectList(0, obj, ExtensionRecordAdapter).toBytes() 28 | } 29 | 30 | } 31 | 32 | } 33 | 34 | private val theList = arrayListOf() 35 | 36 | private val kv: FastKV 37 | 38 | init { 39 | kv = FastKV 40 | .Builder("${context.filesDir.absolutePath}/extensions", "config.fastkv") 41 | .encoder(arrayOf(extensionRecordListEncoder)) 42 | .build() 43 | val data : List = kv.getObject(KEY) ?: emptyList() 44 | theList.addAll(data) 45 | } 46 | 47 | val size: Int 48 | get() { 49 | synchronized(theList) { 50 | return theList.size 51 | } 52 | } 53 | 54 | fun add(extensionRecord: ExtensionRecord) { 55 | synchronized(theList) { 56 | theList.add(extensionRecord) 57 | kv.putObject(KEY, theList, extensionRecordListEncoder) 58 | } 59 | } 60 | 61 | fun add(extInfo: ExtInfo, entryClassName: String) { 62 | synchronized(theList) { 63 | theList.add(ExtensionRecord.fromExtInfo(extInfo, entryClassName)) 64 | kv.putObject(KEY, theList, extensionRecordListEncoder) 65 | } 66 | } 67 | 68 | fun remove(extensionRecord: ExtensionRecord) { 69 | synchronized(theList) { 70 | theList.remove(extensionRecord) 71 | kv.putObject(KEY, theList, extensionRecordListEncoder) 72 | } 73 | } 74 | 75 | fun remove(id: String) { 76 | synchronized(theList) { 77 | theList.removeIf { it.id == id } 78 | kv.putObject(KEY, theList, extensionRecordListEncoder) 79 | } 80 | } 81 | 82 | fun contains(extensionRecord: ExtensionRecord): Boolean { 83 | synchronized(theList) { 84 | return theList.contains(extensionRecord) 85 | } 86 | } 87 | 88 | fun contains(id: String): Boolean { 89 | synchronized(theList) { 90 | return theList.any { it.id == id } 91 | } 92 | } 93 | 94 | fun clear() { 95 | synchronized(theList) { 96 | theList.clear() 97 | kv.putObject(KEY, theList, extensionRecordListEncoder) 98 | } 99 | } 100 | 101 | fun modify(id: String, extensionRecord: ExtensionRecord) { 102 | synchronized(theList) { 103 | theList.removeIf { it.id == id } 104 | theList.add(extensionRecord) 105 | kv.putObject(KEY, theList, extensionRecordListEncoder) 106 | } 107 | } 108 | 109 | fun getAll(): List { 110 | synchronized(theList) { 111 | return theList.toList() 112 | } 113 | } 114 | 115 | fun get(id: String): ExtensionRecord? { 116 | synchronized(theList) { 117 | return theList.firstOrNull { it.id == id } 118 | } 119 | } 120 | 121 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/AppDatabase.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic 2 | 3 | import androidx.room.AutoMigration 4 | import androidx.room.Database 5 | import androidx.room.RoomDatabase 6 | import cn.ac.lz233.tarnhelm.logic.dao.ExtensionDao 7 | import cn.ac.lz233.tarnhelm.logic.dao.ParameterRuleDao 8 | import cn.ac.lz233.tarnhelm.logic.dao.RedirectRuleDao 9 | import cn.ac.lz233.tarnhelm.logic.dao.RegexRuleDao 10 | import cn.ac.lz233.tarnhelm.logic.module.meta.Extension 11 | import cn.ac.lz233.tarnhelm.logic.module.meta.ParameterRule 12 | import cn.ac.lz233.tarnhelm.logic.module.meta.RedirectRule 13 | import cn.ac.lz233.tarnhelm.logic.module.meta.RegexRule 14 | 15 | @Database( 16 | entities = [RegexRule::class, ParameterRule::class, RedirectRule::class, Extension::class], 17 | version = 5, 18 | exportSchema = true, 19 | autoMigrations = [ 20 | AutoMigration(from = 3, to = 4), 21 | AutoMigration(from = 4, to = 5) 22 | ] 23 | ) 24 | abstract class AppDatabase : RoomDatabase() { 25 | abstract fun regexRuleDao(): RegexRuleDao 26 | abstract fun parameterRuleDao(): ParameterRuleDao 27 | abstract fun redirectRuleDao(): RedirectRuleDao 28 | abstract fun extensionDao(): ExtensionDao 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/Network.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic 2 | 3 | import okhttp3.OkHttpClient 4 | 5 | object Network { 6 | val okHttpClient = OkHttpClient.Builder() 7 | .retryOnConnectionFailure(true) 8 | .build() 9 | val okHttpClientNoRedirect = OkHttpClient.Builder() 10 | .retryOnConnectionFailure(true) 11 | .followRedirects(false) 12 | .build() 13 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/ConfigDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import cn.ac.lz233.tarnhelm.App 4 | 5 | object ConfigDao { 6 | var agreedPrivacyPolicy: Boolean 7 | get() = App.sp.getBoolean("agreedPrivacyPolicy", false) 8 | set(value) = App.editor.putBoolean("agreedPrivacyPolicy", value).apply() 9 | 10 | var ranked: Boolean 11 | get() = App.sp.getBoolean("ranked", false) 12 | set(value) = App.editor.putBoolean("ranked", value).apply() 13 | 14 | var openTimes: Int 15 | get() = App.sp.getInt("openTimes", 0) 16 | set(value) = App.editor.putInt("openTimes", if (value >= Int.MAX_VALUE) 0 else value).apply() 17 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/ExtensionDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import androidx.room.* 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.Extension 5 | 6 | @Dao 7 | interface ExtensionDao { 8 | @Insert(onConflict = OnConflictStrategy.REPLACE) 9 | fun insert(vararg extensions: Extension) 10 | 11 | @Query("SELECT * FROM extension") 12 | fun getAll(): MutableList 13 | 14 | @Query("SELECT count(*) FROM extension") 15 | fun getCount(): Int 16 | 17 | @Delete 18 | fun delete(extension: Extension) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/ParameterRuleDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import androidx.room.* 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.ParameterRule 5 | 6 | @Dao 7 | interface ParameterRuleDao { 8 | @Insert(onConflict = OnConflictStrategy.REPLACE) 9 | fun insert(vararg parameterRule: ParameterRule) 10 | 11 | @Query("SELECT * FROM parameterrule") 12 | fun getAll(): MutableList 13 | 14 | @Query("SELECT count(*) FROM parameterrule") 15 | fun getCount(): Int 16 | 17 | @Query("SELECT max(id) FROM parameterrule") 18 | fun getMaxId(): Int 19 | 20 | @Delete 21 | fun delete(parameterRule: ParameterRule) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/RedirectRuleDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import androidx.room.* 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.RedirectRule 5 | 6 | @Dao 7 | interface RedirectRuleDao { 8 | @Insert(onConflict = OnConflictStrategy.REPLACE) 9 | fun insert(vararg redirectRule: RedirectRule) 10 | 11 | @Query("SELECT * FROM redirectrule") 12 | fun getAll(): MutableList 13 | 14 | @Query("SELECT count(*) FROM redirectrule") 15 | fun getCount(): Int 16 | 17 | @Query("SELECT max(id) FROM redirectrule") 18 | fun getMaxId(): Int 19 | 20 | @Delete 21 | fun delete(redirectRule: RedirectRule) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/RegexRuleDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import androidx.room.* 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.RegexRule 5 | 6 | @Dao 7 | interface RegexRuleDao { 8 | @Insert(onConflict = OnConflictStrategy.REPLACE) 9 | fun insert(vararg regexRules: RegexRule) 10 | 11 | @Query("SELECT * FROM regexrule") 12 | fun getAll(): MutableList 13 | 14 | @Query("SELECT count(*) FROM regexrule") 15 | fun getCount(): Int 16 | 17 | @Query("SELECT max(id) FROM regexrule") 18 | fun getMaxId(): Int 19 | 20 | @Delete 21 | fun delete(regexRule: RegexRule) 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/dao/SettingsDao.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.dao 2 | 3 | import cn.ac.lz233.tarnhelm.App 4 | 5 | object SettingsDao { 6 | val workModeEditTextMenu 7 | get() = App.spSettings.getBoolean("workModeEditTextMenu", true) 8 | 9 | val workModeCopyMenu 10 | get() = App.spSettings.getBoolean("workModeCopyMenu", true) 11 | 12 | val workModeShare 13 | get() = App.spSettings.getBoolean("workModeShare", true) 14 | 15 | val workModeBackgroundMonitoring 16 | get() = App.spSettings.getBoolean("workModeBackgroundMonitoring", false) 17 | 18 | val alwaysSendProcessingNotification 19 | get() = App.spSettings.getBoolean("alwaysSendProcessingNotification", false) 20 | 21 | val exportRulesAsLink 22 | get() = App.spSettings.getBoolean("exportRulesAsLink", false) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/module/meta/Extension.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.module.meta 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class Extension( 8 | @PrimaryKey val packageName: String, 9 | val name: String, 10 | val description: String, 11 | val regexArray: String, 12 | val author: String, 13 | val url: String, 14 | val enabled: Boolean, 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/module/meta/ParameterRule.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.module.meta 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class ParameterRule( 8 | @PrimaryKey val id: Int, 9 | val description: String, 10 | val domain: String, 11 | val mode: Int, // 0:whiteList 1:blackList 12 | val parametersArray: String, 13 | val author: String, 14 | val sourceType: Int, // 0:manual 1:paste 2:?? 15 | val enabled: Boolean, 16 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/module/meta/RedirectRule.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.module.meta 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class RedirectRule( 8 | @PrimaryKey val id: Int, 9 | val description: String, 10 | val domain: String, 11 | val userAgent: String?, 12 | val author: String, 13 | val sourceType: Int, // 0:manual 1:paste 2:?? 14 | val enabled: Boolean, 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/logic/module/meta/RegexRule.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.logic.module.meta 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | 6 | @Entity 7 | data class RegexRule( 8 | @PrimaryKey val id: Int, 9 | val description: String, 10 | val regexArray: String, 11 | val replaceArray: String, 12 | val author: String, 13 | val sourceType: Int, // 0:manual 1:paste 2:?? 14 | val enabled: Boolean, 15 | ) -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/receiver/BootBroadcast.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.receiver 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Build 7 | import cn.ac.lz233.tarnhelm.App 8 | import cn.ac.lz233.tarnhelm.logic.dao.SettingsDao 9 | import cn.ac.lz233.tarnhelm.service.ClipboardService 10 | import kotlin.system.exitProcess 11 | 12 | class BootBroadcast : BroadcastReceiver() { 13 | override fun onReceive(context: Context, intent: Intent) { 14 | var shouldExit = true 15 | when (intent.action) { 16 | Intent.ACTION_BOOT_COMPLETED -> if (SettingsDao.workModeBackgroundMonitoring) { 17 | context.startForegroundService(Intent(App.context, ClipboardService::class.java).apply { 18 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 19 | }) 20 | shouldExit = false 21 | } 22 | } 23 | 24 | if (shouldExit) { 25 | // On Android 15, if a app is started after force stop, the ACTION_BOOT_COMPLETED broadcast will be delivered to the app again. 26 | // https://developer.android.com/about/versions/15/behavior-changes-all#enhanced-stop-states 27 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) 28 | App.activityManager.killBackgroundProcesses(context.packageName) 29 | else 30 | exitProcess(0) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/service/ModuleDataBridgeService.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.service 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 7 | import cn.ac.lz233.tarnhelm.xposed.ModuleDataBridge 8 | 9 | class ModuleDataBridgeService : Service() { 10 | 11 | private val binder = object : ModuleDataBridge.Stub() { 12 | 13 | override fun doTarnhelms(string: String): String { 14 | return string.doTarnhelms(true)!!.second 15 | } 16 | 17 | override fun ping(): Int = 1 18 | } 19 | 20 | override fun onBind(p0: Intent): IBinder { 21 | return binder 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/BaseActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.View 6 | import androidx.appcompat.app.AppCompatActivity 7 | import androidx.core.view.WindowCompat 8 | import com.google.android.material.appbar.MaterialToolbar 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.MainScope 11 | 12 | abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() { 13 | lateinit var rootView: View 14 | var toolbar: MaterialToolbar? = null 15 | 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | rootView = findViewById(android.R.id.content) 19 | WindowCompat.setDecorFitsSystemWindows(window, false) 20 | // ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> 21 | // val systemBarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 22 | // LogUtil.d("systemBarInsets: $systemBarInsets") 23 | // rootView.updatePadding(systemBarInsets.left, systemBarInsets.top, systemBarInsets.right, systemBarInsets.bottom) 24 | // WindowInsetsCompat.CONSUMED 25 | // } 26 | } 27 | 28 | override fun onStart() { 29 | super.onStart() 30 | } 31 | 32 | override fun attachBaseContext(newBase: Context) { 33 | super.attachBaseContext(newBase) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/SecondaryBaseActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.content.res.AppCompatResources 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.lifecycleScope 7 | import androidx.lifecycle.repeatOnLifecycle 8 | import androidx.window.embedding.SplitAttributes 9 | import androidx.window.embedding.SplitController 10 | import cn.ac.lz233.tarnhelm.R 11 | import kotlinx.coroutines.launch 12 | 13 | abstract class SecondaryBaseActivity : BaseActivity() { 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | 17 | lifecycleScope.launch { 18 | lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { 19 | SplitController.getInstance(this@SecondaryBaseActivity).splitInfoList(this@SecondaryBaseActivity) 20 | .collect { list -> 21 | toolbar?.navigationIcon = if (list.isEmpty() || (list[0].splitAttributes.splitType == SplitAttributes.SplitType.SPLIT_TYPE_EXPAND)) { 22 | toolbar?.setNavigationOnClickListener { 23 | onBackPressedDispatcher.onBackPressed() 24 | } 25 | AppCompatResources.getDrawable(this@SecondaryBaseActivity, R.drawable.ic_arrow_back) 26 | } else { 27 | null 28 | } 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/extensions/ExtensionsActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.extensions 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.os.Build 9 | import android.os.Bundle 10 | import android.util.Log 11 | import android.widget.Toast 12 | import androidx.activity.result.contract.ActivityResultContracts 13 | import cn.ac.lz233.tarnhelm.R 14 | import cn.ac.lz233.tarnhelm.databinding.ActivityExtensionsBinding 15 | import cn.ac.lz233.tarnhelm.extension.ExtensionManager 16 | import cn.ac.lz233.tarnhelm.ui.SecondaryBaseActivity 17 | import cn.ac.lz233.tarnhelm.util.LogUtil 18 | import cn.ac.lz233.tarnhelm.util.ktx.unzip 19 | import com.google.android.material.snackbar.Snackbar 20 | import com.permissionx.guolindev.PermissionX 21 | import kotlinx.coroutines.CoroutineExceptionHandler 22 | import kotlinx.coroutines.launch 23 | import java.io.BufferedInputStream 24 | import java.io.BufferedOutputStream 25 | import kotlin.system.exitProcess 26 | 27 | class ExtensionsActivity : SecondaryBaseActivity() { 28 | 29 | private val binding by lazy { ActivityExtensionsBinding.inflate(layoutInflater) } 30 | 31 | private val onExtInstallExceptionHandler = CoroutineExceptionHandler { _, exception -> 32 | // TODO: handle different exceptions during extension installation 33 | Log.e("ExtensionManager", "Failed to install extension", exception) 34 | } 35 | 36 | private val selectFileCallback = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 37 | if (result.resultCode == RESULT_OK) { 38 | val fileUri = result.data?.data ?: return@registerForActivityResult 39 | 40 | // check the file extension is .dex 41 | if (fileUri.path?.endsWith(".dex") == false) { 42 | // Snackbar.make(binding.root, R.string.extensionNotSupported, Snackbar.LENGTH_SHORT).show() // TODO 43 | return@registerForActivityResult 44 | } 45 | 46 | contentResolver.openInputStream(fileUri)?.let { 47 | launch(onExtInstallExceptionHandler) { 48 | ExtensionManager.installExtension(it) 49 | } 50 | } 51 | } 52 | } 53 | 54 | override fun onCreate(savedInstanceState: Bundle?) { 55 | super.onCreate(savedInstanceState) 56 | toolbar = binding.toolbar 57 | setContentView(binding.root) 58 | setSupportActionBar(toolbar) 59 | 60 | 61 | 62 | binding.openWebImageView.setOnClickListener { 63 | startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://tarnhelm.project.ac.cn/rules.html"))) 64 | } 65 | 66 | binding.importFab.setOnClickListener { 67 | startImport() 68 | } 69 | 70 | ExtensionManager.startExtensionConfigurationPanel("cn.ac.lz233.tarnhelm.ext.example", this) 71 | } 72 | 73 | private fun startImport() { 74 | // When using SAF, READ_EXTERNAL_STORAGE is not needed any more from Android Q 75 | val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) listOf() else listOf(Manifest.permission.READ_EXTERNAL_STORAGE) 76 | PermissionX.init(this) 77 | .permissions(permissions) 78 | .request { allGranted, grantedList, deniedList -> 79 | if (allGranted) { 80 | val selectFileIntent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { 81 | addCategory(Intent.CATEGORY_OPENABLE) 82 | type = "*/*" 83 | } 84 | selectFileCallback.launch(selectFileIntent) 85 | } else { 86 | Snackbar.make(binding.root, R.string.backupRequestPermissionFailedToast, Toast.LENGTH_SHORT).show() 87 | } 88 | } 89 | } 90 | 91 | companion object { 92 | fun actionStart(context: Context) = context.startActivity(Intent(context, ExtensionsActivity::class.java)) 93 | } 94 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/main/PlaceHolderActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.main 2 | 3 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 4 | 5 | class PlaceHolderActivity : BaseActivity() {} -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessCopyActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.ClipData 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import cn.ac.lz233.tarnhelm.App 7 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 8 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 9 | 10 | class ProcessCopyActivity : BaseActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) 15 | text?.doTarnhelms { success, result -> 16 | App.clipboardManager.setPrimaryClip(ClipData.newPlainText("Tarnhelm", result)) 17 | } 18 | finish() 19 | } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessEditTextActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import cn.ac.lz233.tarnhelm.R 6 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 7 | import cn.ac.lz233.tarnhelm.util.LogUtil 8 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 9 | import cn.ac.lz233.tarnhelm.util.ktx.getString 10 | 11 | class ProcessEditTextActivity : BaseActivity() { 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | val text = intent.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT) 16 | val readOnly = intent.getBooleanExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) 17 | if (readOnly) { 18 | LogUtil.toast(R.string.read_only_toast.getString()) 19 | } else { 20 | text?.let { 21 | setResult(RESULT_OK, Intent().apply { putExtra(Intent.EXTRA_PROCESS_TEXT, it.doTarnhelms(true).second) }) 22 | } 23 | } 24 | finish() 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessOverlayActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.ClipData 4 | import android.os.Bundle 5 | import cn.ac.lz233.tarnhelm.App 6 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 7 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 8 | 9 | class ProcessOverlayActivity : BaseActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | App.clipboardManager.primaryClip?.getItemAt(0)?.text?.doTarnhelms { success, result -> 14 | if (success) App.clipboardManager.setPrimaryClip(ClipData.newPlainText("Tarnhelm", result)) 15 | } 16 | finish() 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessRulesActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.os.Bundle 4 | import cn.ac.lz233.tarnhelm.R 5 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 6 | import cn.ac.lz233.tarnhelm.util.LogUtil 7 | import cn.ac.lz233.tarnhelm.util.ktx.decodeBase64 8 | import cn.ac.lz233.tarnhelm.util.ktx.decodeURL 9 | import cn.ac.lz233.tarnhelm.util.ktx.getString 10 | import cn.ac.lz233.tarnhelm.util.ktx.insertToParameterRules 11 | import cn.ac.lz233.tarnhelm.util.ktx.insertToRedirectRules 12 | import cn.ac.lz233.tarnhelm.util.ktx.insertToRegexRules 13 | import org.json.JSONObject 14 | 15 | class ProcessRulesActivity : BaseActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | 19 | val data = intent.data 20 | val parameterRuleString = data?.getQueryParameter("parameter") 21 | val regexRuleString = data?.getQueryParameter("regex") 22 | val redirectRuleString = data?.getQueryParameter("redirect") 23 | LogUtil._d("parameterRuleString: $parameterRuleString") 24 | LogUtil._d("regexRuleString: $regexRuleString") 25 | LogUtil._d("redirectRuleString: $redirectRuleString") 26 | try { 27 | when { 28 | parameterRuleString != null -> { 29 | val item = JSONObject(parameterRuleString.decodeURL().decodeBase64()).insertToParameterRules() 30 | LogUtil.toast(getString(R.string.rule_added_toast, item.description)) 31 | } 32 | 33 | regexRuleString != null -> { 34 | val item = JSONObject(regexRuleString.decodeURL().decodeBase64()).insertToRegexRules() 35 | LogUtil.toast(getString(R.string.rule_added_toast, item.description)) 36 | } 37 | 38 | redirectRuleString != null -> { 39 | val item = JSONObject(redirectRuleString.decodeURL().decodeBase64()).insertToRedirectRules() 40 | LogUtil.toast(getString(R.string.rule_added_toast, item.description)) 41 | } 42 | } 43 | } catch (e: Throwable) { 44 | LogUtil.toast(R.string.rule_added_failed_toast.getString()) 45 | LogUtil.e(e) 46 | } 47 | finish() 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessServiceActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import cn.ac.lz233.tarnhelm.App 6 | import cn.ac.lz233.tarnhelm.logic.dao.SettingsDao 7 | import cn.ac.lz233.tarnhelm.service.ClipboardService 8 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 9 | 10 | class ProcessServiceActivity : BaseActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | 14 | if (SettingsDao.workModeBackgroundMonitoring) startForegroundService(Intent(App.context, ClipboardService::class.java)) 15 | finish() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessShareActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.ClipData 4 | import android.content.ComponentName 5 | import android.content.Intent 6 | import android.os.Build 7 | import android.os.Bundle 8 | import cn.ac.lz233.tarnhelm.App 9 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 10 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 11 | import kotlinx.coroutines.delay 12 | import kotlinx.coroutines.launch 13 | 14 | class ProcessShareActivity : BaseActivity() { 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | overridePendingTransition(0, 0) 17 | super.onCreate(savedInstanceState) 18 | 19 | // must have a delay because Android uses a very fool method to detect if app is in focus 20 | if (Build.VERSION.SDK_INT >= 33) { 21 | launch { 22 | delay(50) 23 | if (App.clipboardManager.primaryClip?.getItemAt(0)?.text == intent.getCharSequenceExtra(Intent.EXTRA_TEXT)) 24 | copyToClipboard() 25 | else 26 | startChooser() 27 | } 28 | } else { 29 | startChooser() 30 | } 31 | 32 | //finish() 33 | } 34 | 35 | private fun startChooser() { 36 | finish() 37 | startActivity(Intent.createChooser(Intent().apply { 38 | action = Intent.ACTION_SEND 39 | putExtra(Intent.EXTRA_SUBJECT, (intent.getStringExtra(Intent.EXTRA_SUBJECT)?.doTarnhelms(true)?.second)) 40 | putExtra(Intent.EXTRA_TEXT, (intent.getStringExtra(Intent.EXTRA_TEXT)?.doTarnhelms(true)?.second)) 41 | //putExtra(Intent.EXTRA_TITLE, (intent.getStringExtra(Intent.EXTRA_TITLE)?.doTarnhelms() ?: "")) 42 | type = intent.type 43 | }, null).apply { 44 | putExtra(Intent.EXTRA_EXCLUDE_COMPONENTS, arrayOf(ComponentName(App.context, ProcessShareActivity::class.java))) 45 | }) 46 | } 47 | 48 | private fun copyToClipboard() { 49 | App.clipboardManager.primaryClip?.getItemAt(0)?.text?.doTarnhelms { success, result -> 50 | if (success) App.clipboardManager.setPrimaryClip(ClipData.newPlainText("Tarnhelm", result)) 51 | } 52 | finish() 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/process/ProcessShortcutActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.process 2 | 3 | import android.content.ClipData 4 | import android.os.Build 5 | import android.os.Bundle 6 | import cn.ac.lz233.tarnhelm.App 7 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 8 | import cn.ac.lz233.tarnhelm.util.ktx.doTarnhelms 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | 12 | class ProcessShortcutActivity : BaseActivity() { 13 | override fun onCreate(savedInstanceState: Bundle?) { 14 | super.onCreate(savedInstanceState) 15 | 16 | if (Build.VERSION.SDK_INT >= 33) { 17 | launch { 18 | delay(50) 19 | copyToClipboard() 20 | finish() 21 | } 22 | } else { 23 | copyToClipboard() 24 | finish() 25 | } 26 | } 27 | 28 | private fun copyToClipboard() { 29 | App.clipboardManager.primaryClip?.getItemAt(0)?.text?.doTarnhelms { success, result -> 30 | if (success) App.clipboardManager.setPrimaryClip(ClipData.newPlainText("Tarnhelm", result)) 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/rules/DragSwipeCallback.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.rules 2 | 3 | import android.graphics.Canvas 4 | import androidx.recyclerview.widget.ItemTouchHelper 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class DragSwipeCallback(private val adapter: IDragSwipe) : ItemTouchHelper.Callback() { 8 | override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { 9 | val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN 10 | val swipeFlags = ItemTouchHelper.START or ItemTouchHelper.END 11 | return makeMovementFlags(dragFlags, swipeFlags) 12 | } 13 | 14 | override fun isLongPressDragEnabled() = true 15 | 16 | override fun isItemViewSwipeEnabled() = false 17 | 18 | override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { 19 | adapter.onItemSwapped(viewHolder.adapterPosition, target.adapterPosition) 20 | return true 21 | } 22 | 23 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { 24 | when (direction) { 25 | ItemTouchHelper.END -> adapter.onItemCopy(viewHolder.adapterPosition) 26 | ItemTouchHelper.START -> adapter.onItemDeleted(viewHolder.adapterPosition) 27 | } 28 | } 29 | 30 | override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { 31 | when (actionState) { 32 | ItemTouchHelper.ACTION_STATE_DRAG -> { 33 | viewHolder.itemView.alpha = if (isCurrentlyActive) 0.6f else 1f 34 | super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) 35 | } 36 | /*ItemTouchHelper.ACTION_STATE_SWIPE -> { 37 | val contentCardView: MaterialCardView = viewHolder.itemView.findViewById(R.id.ruleContentCardView) 38 | if (dX<0){// delete 39 | contentCardView.translationX = dX 40 | } 41 | }*/ 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/rules/IDragSwipe.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.rules 2 | 3 | interface IDragSwipe { 4 | fun onItemSwapped(fromPosition: Int, toPosition: Int) 5 | 6 | fun onItemDeleted(position: Int) 7 | 8 | fun onItemCopy(position: Int) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/rules/parameter/ParameterRulesFragment.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.rules.parameter 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.ItemTouchHelper 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import cn.ac.lz233.tarnhelm.App 11 | import cn.ac.lz233.tarnhelm.databinding.FragmentParameterRulesBinding 12 | import cn.ac.lz233.tarnhelm.ui.rules.DragSwipeCallback 13 | 14 | class ParameterRulesFragment : Fragment() { 15 | 16 | private val binding by lazy { FragmentParameterRulesBinding.inflate(layoutInflater) } 17 | val rulesList by lazy { App.parameterRuleDao.getAll() } 18 | val adapter by lazy { ParameterRulesAdapter(rulesList) } 19 | private val touchHelper by lazy { ItemTouchHelper(DragSwipeCallback(adapter)) } 20 | private var previousRulesListCount = -1 21 | 22 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 23 | return binding.root 24 | } 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | binding.rulesRecyclerView.layoutManager = LinearLayoutManager(activity) 29 | binding.rulesRecyclerView.adapter = adapter 30 | touchHelper.attachToRecyclerView(binding.rulesRecyclerView) 31 | } 32 | 33 | override fun onResume() { 34 | super.onResume() 35 | refreshRulesList() 36 | } 37 | 38 | private fun refreshRulesList() { 39 | if (previousRulesListCount == -1) { 40 | previousRulesListCount = rulesList.size 41 | } else { 42 | previousRulesListCount = rulesList.size 43 | rulesList.clear() 44 | rulesList.addAll(App.parameterRuleDao.getAll()) 45 | adapter.notifyItemInserted(previousRulesListCount) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/rules/redirect/RedirectRulesFragment.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.rules.redirect 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.ItemTouchHelper 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import cn.ac.lz233.tarnhelm.App 11 | import cn.ac.lz233.tarnhelm.databinding.FragmentRedirectRulesBinding 12 | import cn.ac.lz233.tarnhelm.ui.rules.DragSwipeCallback 13 | 14 | class RedirectRulesFragment : Fragment() { 15 | 16 | private val binding by lazy { FragmentRedirectRulesBinding.inflate(layoutInflater) } 17 | val rulesList by lazy { App.redirectRuleDao.getAll() } 18 | val adapter by lazy { RedirectRulesAdapter(rulesList) } 19 | private val touchHelper by lazy { ItemTouchHelper(DragSwipeCallback(adapter)) } 20 | private var previousRulesListCount = -1 21 | 22 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 23 | return binding.root 24 | } 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | binding.rulesRecyclerView.layoutManager = LinearLayoutManager(activity) 29 | binding.rulesRecyclerView.adapter = adapter 30 | touchHelper.attachToRecyclerView(binding.rulesRecyclerView) 31 | } 32 | 33 | override fun onResume() { 34 | super.onResume() 35 | refreshRulesList() 36 | } 37 | 38 | private fun refreshRulesList() { 39 | if (previousRulesListCount == -1) { 40 | previousRulesListCount = rulesList.size 41 | } else { 42 | previousRulesListCount = rulesList.size 43 | rulesList.clear() 44 | rulesList.addAll(App.redirectRuleDao.getAll()) 45 | adapter.notifyItemInserted(previousRulesListCount) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/rules/regex/RegexRulesFragment.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.rules.regex 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.recyclerview.widget.ItemTouchHelper 9 | import androidx.recyclerview.widget.LinearLayoutManager 10 | import cn.ac.lz233.tarnhelm.App 11 | import cn.ac.lz233.tarnhelm.databinding.FragmentRegexRulesBinding 12 | import cn.ac.lz233.tarnhelm.ui.rules.DragSwipeCallback 13 | 14 | class RegexRulesFragment : Fragment() { 15 | 16 | private val binding by lazy { FragmentRegexRulesBinding.inflate(layoutInflater) } 17 | val rulesList by lazy { App.regexRuleDao.getAll() } 18 | val adapter by lazy { RegexRulesAdapter(rulesList) } 19 | private val touchHelper by lazy { ItemTouchHelper(DragSwipeCallback(adapter)) } 20 | private var previousRulesListCount = -1 21 | 22 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { 23 | return binding.root 24 | } 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | binding.rulesRecyclerView.layoutManager = LinearLayoutManager(activity) 29 | binding.rulesRecyclerView.adapter = adapter 30 | touchHelper.attachToRecyclerView(binding.rulesRecyclerView) 31 | } 32 | 33 | override fun onResume() { 34 | super.onResume() 35 | refreshRulesList() 36 | } 37 | 38 | private fun refreshRulesList() { 39 | if (previousRulesListCount == -1) { 40 | previousRulesListCount = rulesList.size 41 | } else { 42 | previousRulesListCount = rulesList.size 43 | rulesList.clear() 44 | rulesList.addAll(App.regexRuleDao.getAll()) 45 | adapter.notifyItemInserted(previousRulesListCount) 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/settings/SettingsActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.settings 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import cn.ac.lz233.tarnhelm.R 7 | import cn.ac.lz233.tarnhelm.databinding.ActivitySettingsBinding 8 | import cn.ac.lz233.tarnhelm.ui.SecondaryBaseActivity 9 | 10 | class SettingsActivity : SecondaryBaseActivity() { 11 | 12 | private val binding by lazy { ActivitySettingsBinding.inflate(layoutInflater) } 13 | 14 | override fun onCreate(savedInstanceState: Bundle?) { 15 | super.onCreate(savedInstanceState) 16 | theme.applyStyle(rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference, true) 17 | toolbar = binding.toolbar 18 | setContentView(binding.root) 19 | setSupportActionBar(toolbar) 20 | 21 | supportFragmentManager.beginTransaction() 22 | .replace(R.id.preferenceFragment, SettingsFragment(binding.root)) 23 | .commit() 24 | } 25 | 26 | companion object { 27 | fun actionStart(context: Context) = context.startActivity(Intent(context, SettingsActivity::class.java)) 28 | } 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/ui/settings/backup/SaveViaSAFActivity.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.ui.settings.backup 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | import android.os.Bundle 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import cn.ac.lz233.tarnhelm.ui.BaseActivity 8 | import kotlinx.coroutines.launch 9 | import java.io.BufferedInputStream 10 | import java.io.BufferedOutputStream 11 | 12 | class SaveViaSAFActivity : BaseActivity() { 13 | private val selectFileCallback = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 14 | if (result.resultCode == RESULT_OK) { 15 | val originalFileUri = intent.getParcelableExtra(Intent.EXTRA_STREAM) 16 | val outputFileUri = result.data?.data 17 | if (originalFileUri == null || outputFileUri == null) finish() 18 | launch { 19 | val originalStream = BufferedInputStream(contentResolver.openInputStream(originalFileUri!!)) 20 | val outputStream = BufferedOutputStream(contentResolver.openOutputStream(outputFileUri!!)) 21 | originalStream.copyTo(outputStream) 22 | originalStream.close() 23 | outputStream.close() 24 | finish() 25 | } 26 | } else { 27 | finish() 28 | } 29 | } 30 | 31 | override fun onCreate(savedInstanceState: Bundle?) { 32 | super.onCreate(savedInstanceState) 33 | if (intent.action == Intent.ACTION_SEND) { 34 | val selectFileIntent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { 35 | addCategory(Intent.CATEGORY_OPENABLE) 36 | type = "application/zip" 37 | putExtra(Intent.EXTRA_TITLE, intent.getStringExtra(Intent.EXTRA_TITLE)) 38 | } 39 | selectFileCallback.launch(selectFileIntent) 40 | } else { 41 | finish() 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/LogUtil.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util 2 | 3 | import android.os.Handler 4 | import android.os.Looper 5 | import android.widget.Toast 6 | import cn.ac.lz233.tarnhelm.App 7 | import cn.ac.lz233.tarnhelm.App.Companion.TAG 8 | import de.robv.android.xposed.XposedBridge 9 | import android.util.Log as ALog 10 | 11 | object LogUtil { 12 | 13 | private const val maxLength = 4000 14 | private val handler by lazy { Handler(Looper.getMainLooper()) } 15 | 16 | fun toast(msg: String) { 17 | handler.post { 18 | Toast.makeText(App.context, msg, Toast.LENGTH_SHORT).show() 19 | } 20 | } 21 | 22 | @JvmStatic 23 | private fun doLog(f: (String, String) -> Int, tag: String = TAG, obj: Any?, toToast: Boolean = false) { 24 | val str = if (obj is Throwable) ALog.getStackTraceString(obj) else obj.toString() 25 | if (str.length > maxLength) { 26 | val chunkCount: Int = str.length / maxLength 27 | for (i in 0..chunkCount) { 28 | val max: Int = 4000 * (i + 1) 29 | if (max >= str.length) { 30 | doLog(f, tag = tag, obj = str.substring(maxLength * i), toToast = toToast) 31 | } else { 32 | doLog(f, tag = tag, obj = str.substring(maxLength * i, max), toToast = toToast) 33 | } 34 | } 35 | } else { 36 | f(tag, str) 37 | if (toToast) toast(str) 38 | } 39 | } 40 | 41 | @JvmStatic 42 | fun _d(obj: Any?, tag: String = TAG) { 43 | doLog(ALog::d, tag = tag, obj = obj) 44 | } 45 | 46 | @JvmStatic 47 | @JvmOverloads 48 | fun d(obj: Any?, tag: String = TAG) { 49 | doLog(ALog::d, tag = tag, obj = obj) 50 | } 51 | 52 | @JvmStatic 53 | @JvmOverloads 54 | fun i(obj: Any?, tag: String = TAG) { 55 | doLog(ALog::i, tag = tag, obj = obj) 56 | } 57 | 58 | @JvmStatic 59 | @JvmOverloads 60 | fun e(obj: Any?, tag: String = TAG) { 61 | doLog(ALog::e, tag = tag, obj = obj) 62 | } 63 | 64 | @JvmStatic 65 | @JvmOverloads 66 | fun v(obj: Any?, tag: String = TAG) { 67 | doLog(ALog::v, tag = tag, obj = obj) 68 | } 69 | 70 | @JvmStatic 71 | @JvmOverloads 72 | fun w(obj: Any?, tag: String = TAG) { 73 | doLog(ALog::w, tag = tag, obj = obj) 74 | } 75 | 76 | @JvmStatic 77 | fun xp(obj: Any?, level: String = "normal") { 78 | doLog(::xposed, tag = level, obj = obj) 79 | } 80 | 81 | @JvmStatic 82 | fun xpw(obj: Any?, level: String = "warn") { 83 | doLog(::xposed, tag = level, obj = obj) 84 | } 85 | 86 | @JvmStatic 87 | fun xpd(obj: Any?, level: String = "debug") { 88 | doLog(::xposed, tag = level, obj = obj) 89 | } 90 | 91 | @JvmStatic 92 | fun xpe(obj: Any?, level: String = "error") { 93 | doLog(::xposed, tag = level, obj = obj) 94 | } 95 | 96 | private fun xposed(level: String, msg: String): Int { 97 | when(level) { 98 | "normal" -> XposedBridge.log("[${TAG}] $msg") 99 | "warn" -> XposedBridge.log("[$TAG] warn: $msg") 100 | "error" -> XposedBridge.log("[$TAG] error: $msg") 101 | "debug" -> XposedBridge.log("[$TAG] debug: $msg") 102 | else -> XposedBridge.log("[$TAG] $msg") 103 | } 104 | return 0 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/Extension.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import android.content.Context 4 | import cn.ac.lz233.tarnhelm.extension.ExtensionManager 5 | import cn.ac.lz233.tarnhelm.extension.ExtensionRecord 6 | import cn.ac.lz233.tarnhelm.extension.api.ITarnhelmExt.ExtInfo 7 | 8 | fun ExtInfo.getExtPath(context: Context): String { 9 | return "${context.filesDir.absolutePath}/extensions/${ExtensionManager.toMD5(this.id())}/" 10 | } 11 | 12 | fun ExtensionRecord.getExtPath(context: Context): String { 13 | return "${context.filesDir.absolutePath}/extensions/${ExtensionManager.toMD5(this.id)}/" 14 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/Float.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import android.util.TypedValue 4 | import cn.ac.lz233.tarnhelm.App 5 | 6 | fun Float.dp2px() = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, this, App.context.resources.displayMetrics) -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/IO.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import java.io.BufferedInputStream 4 | import java.io.BufferedOutputStream 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.io.FileOutputStream 8 | import java.io.InputStream 9 | import java.util.zip.ZipEntry 10 | import java.util.zip.ZipInputStream 11 | import java.util.zip.ZipOutputStream 12 | 13 | fun File.ensure(): File = if (this.exists()) { 14 | this 15 | } else if (this.isDirectory) { 16 | this.mkdirs() 17 | this 18 | } else { 19 | this.parentFile?.mkdirs() 20 | this.createNewFile() 21 | this 22 | } 23 | 24 | fun File.listFilesRecursively(): List { 25 | val result = mutableListOf() 26 | this.listFiles()?.forEach { 27 | if (it.isDirectory) { 28 | result += it.listFilesRecursively() 29 | } else { 30 | result += it 31 | } 32 | } 33 | return result 34 | } 35 | 36 | fun List.listFilesRecursively(): List { 37 | val result = mutableListOf() 38 | this.forEach { 39 | result += it.listFilesRecursively() 40 | } 41 | return result 42 | } 43 | 44 | fun List.zip(baseDir: File, output: File) { 45 | val zipOutputStream = ZipOutputStream(BufferedOutputStream(FileOutputStream(output.ensure()))) 46 | this.forEach { 47 | val inputStream = BufferedInputStream(FileInputStream(it)) 48 | // baseDir: /data/user/0/cn.ac.lz233.tarnhelm 49 | // it: /data/user/0/cn.ac.lz233.tarnhelm/shared_prefs/test.xml 50 | // it.toRelativeString(baseDir): shared_prefs/test.xml 51 | val zipEntry = ZipEntry(it.toRelativeString(baseDir)) 52 | zipOutputStream.putNextEntry(zipEntry) 53 | inputStream.copyTo(zipOutputStream) 54 | inputStream.close() 55 | zipOutputStream.closeEntry() 56 | } 57 | zipOutputStream.close() 58 | } 59 | 60 | fun InputStream.unzip(outputDir: File) { 61 | val zipInputStream = ZipInputStream(BufferedInputStream(this)) 62 | while (true) { 63 | val zipEntry = zipInputStream.nextEntry ?: break 64 | val outputStream = BufferedOutputStream(FileOutputStream(File(outputDir, zipEntry.name).ensure())) 65 | zipInputStream.copyTo(outputStream) 66 | zipInputStream.closeEntry() 67 | outputStream.close() 68 | } 69 | } 70 | 71 | fun File.unzip(outputDir: File) { 72 | FileInputStream(this).unzip(outputDir) 73 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/Int.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import cn.ac.lz233.tarnhelm.App 4 | import cn.ac.lz233.tarnhelm.R 5 | 6 | fun Int.getString() = App.context.getString(this) 7 | 8 | fun Int.getModeButtonId() = when (this) { 9 | 0 -> R.id.whiteListModeButton 10 | 1 -> R.id.blackListModeButton 11 | else -> R.id.whiteListModeButton 12 | } 13 | 14 | fun Int.getModeId() = when (this) { 15 | R.id.whiteListModeButton -> 0 16 | R.id.blackListModeButton -> 1 17 | else -> R.id.whiteListModeButton 18 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/Intent.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import android.content.Intent 4 | 5 | fun Intent.useFlymeChooser(use: Boolean = true) { 6 | this.putExtra("KEY_DISABLE_FLYME_CHOOSER", !use) 7 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/JSONArray.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import org.json.JSONArray 4 | 5 | fun JSONArray.toMultiString() = StringBuilder().apply { 6 | for (i in 0 until this@toMultiString.length()) { 7 | append(this@toMultiString[i]) 8 | append('\n') 9 | } 10 | deleteCharAt(lastIndex) 11 | }.toString() 12 | 13 | fun JSONArray.contain(element: T): Boolean { 14 | var result = false 15 | for (index in 0 until this.length()) { 16 | if (this[index] == element) { 17 | result = true 18 | break 19 | } 20 | } 21 | return result 22 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/JSONObject.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import cn.ac.lz233.tarnhelm.App 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.ParameterRule 5 | import cn.ac.lz233.tarnhelm.logic.module.meta.RedirectRule 6 | import cn.ac.lz233.tarnhelm.logic.module.meta.RegexRule 7 | import org.json.JSONObject 8 | 9 | fun JSONObject.insertToParameterRules(type: Int = 1, enabled: Boolean = true): ParameterRule { 10 | val item = ParameterRule( 11 | App.parameterRuleDao.getMaxId() + 1, 12 | this.getString("a"), 13 | this.getString("e"), 14 | this.getInt("f"), 15 | this.getJSONArray("g").toString(), 16 | this.getString("d"), 17 | type, 18 | enabled 19 | ) 20 | App.parameterRuleDao.insert(item) 21 | return item 22 | } 23 | 24 | fun JSONObject.insertToRegexRules(type: Int = 1, enabled: Boolean = true): RegexRule { 25 | val item = RegexRule( 26 | App.regexRuleDao.getMaxId() + 1, 27 | this.getString("a"), 28 | this.getJSONArray("b").toString(), 29 | this.getJSONArray("c").toString(), 30 | this.getString("d"), 31 | type, 32 | enabled 33 | ) 34 | App.regexRuleDao.insert(item) 35 | return item 36 | } 37 | 38 | fun JSONObject.insertToRedirectRules(type: Int = 1, enabled: Boolean = true): RedirectRule { 39 | val item = RedirectRule( 40 | App.redirectRuleDao.getMaxId() + 1, 41 | this.getString("a"), 42 | this.getString("e"), 43 | this.getString("h"), 44 | this.getString("d"), 45 | type, 46 | enabled 47 | ) 48 | App.redirectRuleDao.insert(item) 49 | return item 50 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/List.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | fun List.toString(insert: String) = StringBuilder().apply { 4 | this@toString.forEach { 5 | append(it) 6 | append(insert) 7 | } 8 | delete(lastIndex - insert.length + 1, lastIndex + 1) 9 | }.toString() 10 | 11 | fun List.toString(insert: String, insertLast: String) = StringBuilder().apply { 12 | for ((index, string) in this@toString.withIndex()) { 13 | append(string) 14 | if (index == this@toString.lastIndex - 1) 15 | append(insertLast) 16 | else if (index != this@toString.lastIndex) 17 | append(insert) 18 | } 19 | }.toString() 20 | 21 | fun List.toString(insertList: List) = StringBuilder().apply { 22 | var insertListIndex = 0 23 | this@toString.forEach { 24 | append(it) 25 | append(insertList.getOrNull(insertListIndex++)) 26 | } 27 | }.toString() 28 | 29 | fun List>.toFlowString() = StringBuilder().apply { 30 | this@toFlowString.forEach { innerList -> 31 | append(" → ") 32 | innerList.forEach { 33 | append(it) 34 | append(" → ") 35 | } 36 | deleteRange(length - 3, length) 37 | append(" | ") 38 | append('\n') 39 | } 40 | deleteCharAt(lastIndex) 41 | }.toString() -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/OkHttp.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import cn.ac.lz233.tarnhelm.logic.Network 4 | import okhttp3.HttpUrl 5 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull 6 | import okhttp3.Request 7 | import okhttp3.internal.commonToString 8 | import org.json.JSONObject 9 | import java.io.IOException 10 | 11 | fun HttpUrl.followRedirect(userAgent: String?): HttpUrl { 12 | val response = Network.okHttpClientNoRedirect 13 | .newCall( 14 | Request.Builder() 15 | .url(this) 16 | .let { 17 | if (userAgent.isNullOrEmpty()) it else it.addHeader("User-Agent", userAgent) 18 | } 19 | .build() 20 | ) 21 | .execute() 22 | return if (response.isSuccessful) { 23 | // bilibili returns 404 but still using 200 header 24 | val body = response.body.string() 25 | runCatching { 26 | if (JSONObject(body).get("code").toString().contains("404")) throw IOException(body) 27 | }.onFailure { 28 | if (it is IOException) throw it 29 | } 30 | this 31 | } else if (response.isRedirect) { 32 | (response.header("Location")?.toHttpUrlOrNull() ?: response.header("location")?.toHttpUrlOrNull()) 33 | ?.followRedirect(userAgent) ?: this 34 | } else { 35 | throw IOException(response.commonToString()) 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/util/ktx/Rule.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.util.ktx 2 | 3 | import cn.ac.lz233.tarnhelm.logic.module.meta.ParameterRule 4 | import cn.ac.lz233.tarnhelm.logic.module.meta.RedirectRule 5 | import cn.ac.lz233.tarnhelm.logic.module.meta.RegexRule 6 | import org.json.JSONArray 7 | import org.json.JSONObject 8 | 9 | fun ParameterRule.toJSONObject() = JSONObject().apply { 10 | put("a", this@toJSONObject.description) 11 | put("e", this@toJSONObject.domain) 12 | put("f", this@toJSONObject.mode) 13 | put("g", JSONArray(this@toJSONObject.parametersArray)) 14 | put("d", this@toJSONObject.author) 15 | } 16 | 17 | fun RegexRule.toJSONObject() = JSONObject().apply { 18 | put("a", this@toJSONObject.description) 19 | put("b", JSONArray(this@toJSONObject.regexArray)) 20 | put("c", JSONArray(this@toJSONObject.replaceArray)) 21 | put("d", this@toJSONObject.author) 22 | } 23 | 24 | fun RedirectRule.toJSONObject() = JSONObject().apply { 25 | put("a", this@toJSONObject.description) 26 | put("e", this@toJSONObject.domain) 27 | put("h", this@toJSONObject.userAgent) 28 | put("d", this@toJSONObject.author) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/view/EditText.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.view 2 | 3 | import android.content.Context 4 | import android.util.AttributeSet 5 | import android.view.MotionEvent 6 | import com.google.android.material.textfield.TextInputEditText 7 | 8 | class EditText @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : TextInputEditText(context, attrs) { 9 | private var canTouch = true 10 | 11 | override fun onTouchEvent(event: MotionEvent?): Boolean { 12 | return if (canTouch) { 13 | super.onTouchEvent(event) 14 | } else { 15 | true 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/Config.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed 2 | 3 | import cn.ac.lz233.tarnhelm.BuildConfig 4 | import de.robv.android.xposed.XSharedPreferences 5 | 6 | object Config { 7 | const val packageName = BuildConfig.APPLICATION_ID 8 | const val bridgeAction = "${BuildConfig.APPLICATION_ID}.bridge" 9 | lateinit var classLoader: ClassLoader 10 | val sp: XSharedPreferences 11 | get() = XSharedPreferences(BuildConfig.APPLICATION_ID, "${BuildConfig.APPLICATION_ID}_xposed") 12 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/XposedEntry.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed 2 | 3 | import cn.ac.lz233.tarnhelm.xposed.module.Android 4 | import cn.ac.lz233.tarnhelm.xposed.module.Self 5 | import cn.ac.lz233.tarnhelm.xposed.module.SystemUI 6 | import de.robv.android.xposed.IXposedHookLoadPackage 7 | import de.robv.android.xposed.callbacks.XC_LoadPackage 8 | 9 | class XposedEntry : IXposedHookLoadPackage { 10 | 11 | override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) { 12 | Config.classLoader = lpparam.classLoader 13 | when (lpparam.packageName) { 14 | Config.packageName -> Self.init() 15 | "android" -> Android.init() 16 | "com.android.systemui" -> SystemUI.init() 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/module/Self.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed.module 2 | 3 | import cn.ac.lz233.tarnhelm.xposed.util.setReturnConstant 4 | 5 | object Self { 6 | 7 | fun init() { 8 | "cn.ac.lz233.tarnhelm.App\$Companion".setReturnConstant("isXposedActive", result = true) 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/module/SystemUI.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed.module 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.PendingIntent 5 | import android.app.RemoteAction 6 | import android.content.ClipData 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.graphics.drawable.Icon 10 | import android.view.View 11 | import android.widget.LinearLayout 12 | import androidx.core.view.get 13 | import androidx.core.view.size 14 | import cn.ac.lz233.tarnhelm.BuildConfig 15 | import cn.ac.lz233.tarnhelm.R 16 | import cn.ac.lz233.tarnhelm.ui.main.MainActivity 17 | import cn.ac.lz233.tarnhelm.ui.process.ProcessOverlayActivity 18 | import cn.ac.lz233.tarnhelm.util.LogUtil 19 | import cn.ac.lz233.tarnhelm.xposed.Config 20 | import cn.ac.lz233.tarnhelm.xposed.util.* 21 | 22 | object SystemUI { 23 | private var text1: CharSequence = "" 24 | private var text2: CharSequence = "" 25 | 26 | @SuppressLint("ResourceType") 27 | fun init() { 28 | runCatching { 29 | if ("com.android.systemui.clipboardoverlay.ClipboardOverlayView".findClassOrNull() == null) { 30 | // ????<=x<=TQ2A 31 | "com.android.systemui.clipboardoverlay.ClipboardOverlayController".hookAfterMethod( 32 | "setClipData", 33 | ClipData::class.java, 34 | String::class.java 35 | ) { 36 | if (!Config.sp.getBoolean("overrideClipboardOverlay", false)) return@hookAfterMethod 37 | LogUtil.xpd("setClipData ????<=x<=TQ2A") 38 | 39 | val clipboardOverlayController = it.thisObject 40 | val context = clipboardOverlayController.getObjectField("mContext") as Context 41 | //val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager 42 | val actionContainer = clipboardOverlayController.getObjectField("mActionContainer") as LinearLayout 43 | // Android may call setClipData twice 44 | if ((actionContainer[actionContainer.size - 1] as View).contentDescription != "Tarnhelm") { 45 | val chip = it.thisObject.callMethod( 46 | "constructActionChip", 47 | RemoteAction( 48 | Icon.createWithResource(BuildConfig.APPLICATION_ID, R.drawable.ic_icon), 49 | "", 50 | "Tarnhelm", 51 | PendingIntent.getActivity(context, 1, Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE) 52 | ) 53 | ) as View 54 | chip.contentDescription = "Tarnhelm" 55 | chip.setOnClickListener { 56 | clipboardOverlayController.callMethod("animateOut") 57 | context.startActivity(Intent().setClassName(BuildConfig.APPLICATION_ID, ProcessOverlayActivity::class.java.name).apply { 58 | flags = Intent.FLAG_ACTIVITY_NEW_TASK 59 | }) 60 | } 61 | actionContainer.addView(chip) 62 | } 63 | } 64 | } else { 65 | // >=TQ2A 66 | "com.android.systemui.clipboardoverlay.ClipboardOverlayView".hookAfterMethod( 67 | "resetActionChips" 68 | ) { 69 | if (!Config.sp.getBoolean("overrideClipboardOverlay", false)) return@hookAfterMethod 70 | LogUtil.xpd("setClipData >=TQ2A") 71 | 72 | val clipboardOverlayView = it.thisObject as View 73 | clipboardOverlayView.callMethod( 74 | "setActionChip", 75 | RemoteAction( 76 | Icon.createWithResource(BuildConfig.APPLICATION_ID, R.drawable.ic_icon), 77 | "", 78 | "Tarnhelm", 79 | PendingIntent.getActivity( 80 | clipboardOverlayView.context, 81 | 1, 82 | Intent().setClassName(BuildConfig.APPLICATION_ID, ProcessOverlayActivity::class.java.name), 83 | PendingIntent.FLAG_IMMUTABLE 84 | ) 85 | ), 86 | object : Runnable { 87 | override fun run() { 88 | //clipboardOverlayController.callMethod("animateOut") 89 | } 90 | } 91 | ) 92 | } 93 | } 94 | }.onFailure { LogUtil.xpe(it) } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/util/AMSHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed.util 2 | 3 | import android.content.ComponentName 4 | import android.content.Context 5 | import android.content.pm.ApplicationInfo 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import cn.ac.lz233.tarnhelm.xposed.Config 9 | 10 | object AMSHelper { 11 | 12 | private fun getApplicationInfo(ams: Any): ApplicationInfo { 13 | val context = ams.getObjectFieldAs("mContext") 14 | return context.packageManager.getApplicationInfo(Config.packageName, PackageManager.GET_META_DATA) 15 | } 16 | 17 | private fun getHostingRecord(ams: Any): Any { 18 | val context = ams.getObjectFieldAs("mContext") 19 | return "com.android.server.am.HostingRecord" 20 | .findClass(context.classLoader) 21 | .getConstructor(String::class.java, ComponentName::class.java) 22 | .newInstance("service", ComponentName(Config.packageName, "cn.ac.lz233.tarnhelm.service.ModuleDataBridgeService")) 23 | } 24 | 25 | // 8.1 26 | private fun startProcessLockedO(ams: Any) { 27 | ams.callMethod( 28 | "startProcessLocked", 29 | Config.packageName, 30 | getApplicationInfo(ams), 31 | true, 32 | 0, 33 | "service", 34 | ComponentName(Config.packageName, "cn.ac.lz233.tarnhelm.service.ModuleDataBridgeService"), 35 | false, 36 | false, 37 | false, 38 | "others" 39 | ) 40 | } 41 | 42 | // 9 43 | private fun startProcessLockedP(ams: Any) { 44 | startProcessLockedO(ams) 45 | } 46 | 47 | // 10 48 | private fun startProcessLockedQ(ams: Any) { 49 | ams.callMethod( 50 | "startProcessLocked", 51 | Config.packageName, 52 | getApplicationInfo(ams), 53 | true, 54 | 0, 55 | getHostingRecord(ams), 56 | false, 57 | false, 58 | false, 59 | "others" 60 | ) 61 | } 62 | 63 | // 11 64 | private fun startProcessLockedR(ams: Any) { 65 | ams.callMethod( 66 | "startProcessLocked", 67 | Config.packageName, 68 | getApplicationInfo(ams), 69 | true, 70 | 0, 71 | getHostingRecord(ams), 72 | 1, 73 | false, 74 | false, 75 | false, 76 | "others" 77 | ) 78 | } 79 | 80 | // 12 81 | private fun startProcessLockedS(ams: Any) { 82 | ams.callMethod( 83 | "startProcessLocked", 84 | Config.packageName, 85 | getApplicationInfo(ams), 86 | true, 87 | 0, 88 | getHostingRecord(ams), 89 | 1, 90 | false, 91 | false, 92 | "others" 93 | ) 94 | } 95 | 96 | fun startModuleAppProcess(ams: Any): Boolean { 97 | val func = when (Build.VERSION.SDK_INT) { 98 | Build.VERSION_CODES.O_MR1 -> ::startProcessLockedO 99 | Build.VERSION_CODES.P -> ::startProcessLockedP 100 | Build.VERSION_CODES.Q -> ::startProcessLockedQ 101 | Build.VERSION_CODES.R -> ::startProcessLockedR 102 | Build.VERSION_CODES.S -> ::startProcessLockedS 103 | else -> ::startProcessLockedS 104 | } 105 | return startModuleAppProcessInner(ams, func) 106 | } 107 | 108 | private fun startModuleAppProcessInner(ams: Any, block: (Any) -> Unit): Boolean { 109 | return try { 110 | block(ams) 111 | true 112 | } catch (thr: Throwable) { 113 | false 114 | } 115 | } 116 | 117 | } -------------------------------------------------------------------------------- /app/src/main/java/cn/ac/lz233/tarnhelm/xposed/util/ModuleBridgeHelper.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.xposed.util 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.ServiceConnection 8 | import android.os.Build 9 | import android.os.IBinder 10 | import cn.ac.lz233.tarnhelm.util.LogUtil 11 | import cn.ac.lz233.tarnhelm.xposed.Config 12 | import cn.ac.lz233.tarnhelm.xposed.ModuleDataBridge 13 | import cn.ac.lz233.tarnhelm.xposed.module.Android 14 | 15 | @SuppressLint("StaticFieldLeak") 16 | object ModuleBridgeHelper { 17 | 18 | private var bridge: ModuleDataBridge? = null 19 | var isBridgeAvailable = false 20 | var mContext: Context? = null 21 | 22 | private val serviceConnection = object : ServiceConnection { 23 | override fun onServiceConnected(name: ComponentName, binder: IBinder) { 24 | bridge = ModuleDataBridge.Stub.asInterface(binder) 25 | isBridgeAvailable = true 26 | } 27 | 28 | override fun onServiceDisconnected(name: ComponentName) { 29 | bridge = null 30 | isBridgeAvailable = false 31 | unbindBridgeService(mContext) 32 | Android.startModuleAppProcess() 33 | bindBridgeService() 34 | } 35 | } 36 | 37 | fun isBridgeActive(): Boolean { 38 | try { 39 | if (bridge == null) { return false } 40 | bridge!!.ping() 41 | return true 42 | } catch (thr: Throwable) { 43 | thr.printStackTrace() 44 | return false 45 | } 46 | } 47 | 48 | @SuppressLint("MissingPermission") 49 | fun bindBridgeService(context: Context? = mContext) { 50 | LogUtil.xp("bind bridge service") 51 | runCatching { 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 53 | context?.bindServiceAsUser( 54 | Intent().apply { 55 | `package` = Config.packageName 56 | action = Config.bridgeAction 57 | }, 58 | serviceConnection, 59 | Context.BIND_AUTO_CREATE, 60 | android.os.Process.myUserHandle() 61 | ) 62 | } else { 63 | context?.bindService( 64 | Intent().apply { 65 | `package` = Config.packageName 66 | action = Config.bridgeAction 67 | }, 68 | serviceConnection, 69 | Context.BIND_AUTO_CREATE 70 | ) 71 | } 72 | }.onFailure { LogUtil.xpe(it) } 73 | } 74 | 75 | fun unbindBridgeService(context: Context? = mContext) { 76 | LogUtil.xp("unbind bridge service") 77 | runCatching { 78 | context?.unbindService(serviceConnection) 79 | } 80 | } 81 | 82 | fun doTarnhelms(string: String): String { 83 | if (!(isBridgeAvailable && isBridgeActive())) { 84 | Android.startModuleAppProcess() 85 | bindBridgeService() 86 | } 87 | bridge?.let { 88 | return it.doTarnhelms(string) 89 | } 90 | LogUtil.xpe("Bridge is not available") 91 | return string 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_copy.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_error.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_extension.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_icon.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 11 | 14 | 17 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_open.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_paste.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_rule_folder.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_share.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 31 | 32 | 41 | 42 | 43 | 44 | 45 | 54 | 55 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 31 | 32 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 55 | 56 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 22 | 23 | 31 | 32 | 33 | 34 | 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_about.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 22 | 23 | 29 | 30 | 36 | 37 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_redirect_rule_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 21 | 22 | 33 | 34 | 42 | 43 | 44 | 50 | 51 | 55 | 56 | 57 | 58 | 64 | 65 | 71 | 72 | 73 | 74 | 80 | 81 | 87 | 88 | 89 | 90 | 95 | 96 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_regex_rule_add.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 15 | 16 | 21 | 22 | 32 | 33 | 41 | 42 | 43 | 49 | 50 | 54 | 55 | 56 | 57 | 63 | 64 | 68 | 69 | 70 | 71 | 77 | 78 | 82 | 83 | 84 | 85 | 90 | 91 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_backup.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 14 | 15 | 18 | 19 | 26 | 27 | 34 | 35 | 41 | 42 | 49 | 50 | 55 | 56 | 64 | 65 | 72 | 73 | 74 | 75 | 81 | 82 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_parameter_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_redirect_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_regex_rules.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_extension.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 27 | 28 | 36 | 37 | 45 | 46 | 52 | 53 | 59 | 60 | 66 | 67 | 73 | 74 | 80 | 81 | 87 | 88 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_regex_rule.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 21 | 22 | 29 | 30 | 38 | 39 | 47 | 48 | 54 | 55 | 61 | 62 | 68 | 69 | 75 | 76 | 82 | 83 | 89 | 90 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /app/src/main/res/layout/m3_icon_frame_open.xml: -------------------------------------------------------------------------------- 1 | 13 | 27 | 28 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/m3_preference.xml: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | 27 | 28 | 29 | 35 | 36 | 44 | 45 | 57 | 58 | 59 | 60 | 61 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/res/layout/m3_preference_category.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | 29 | 30 | 31 | 37 | 38 | 46 | 47 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /app/src/main/res/values-night-v32/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #8DCDFF 4 | #00344F 5 | #004B70 6 | #CAE6FF 7 | #5FD4FD 8 | #003544 9 | #004D62 10 | #B9EAFF 11 | #4CD9DE 12 | #003738 13 | #004F52 14 | #6FF6FB 15 | #FFB4AB 16 | #93000A 17 | #690005 18 | #FFDAD6 19 | #386a20 20 | #FFFFFF 21 | #1A1C1E 22 | #E2E2E5 23 | #1A1C1E 24 | #E2E2E5 25 | #41474D 26 | #C1C7CE 27 | #8B9198 28 | #1A1C1E 29 | #E2E2E5 30 | #006493 31 | #000000 32 | #8DCDFF 33 | #8DCDFF 34 | #3f98d1 35 | -------------------------------------------------------------------------------- /app/src/main/res/values-v29/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values-v32/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/values/array.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | android 5 | com.android.systemui 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #006493 4 | #FFFFFF 5 | #CAE6FF 6 | #001E30 7 | #006781 8 | #FFFFFF 9 | #B9EAFF 10 | #001F29 11 | #00696C 12 | #FFFFFF 13 | #6FF6FB 14 | #002021 15 | #BA1A1A 16 | #FFDAD6 17 | #FFFFFF 18 | #410002 19 | #b6f397 20 | #042100 21 | #FCFCFF 22 | #1A1C1E 23 | #FCFCFF 24 | #1A1C1E 25 | #DDE3EA 26 | #41474D 27 | #72787E 28 | #F0F0F3 29 | #2E3133 30 | #8DCDFF 31 | #000000 32 | #006493 33 | #006493 34 | #3f98d1 35 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 158dp 4 | 162dp 5 | 16dp 6 | 100dp 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6F7CBA 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 24 | 25 | 52 | 53 | 60 | 61 | 65 | 66 | 71 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/xml/locale_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/xml/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | 27 | 28 | 32 | 33 | 34 | 37 | 38 | 42 | 43 | 46 | 47 | 48 | 49 | 50 | 54 | 55 | 59 | 60 | 61 | 62 | 63 | 67 | 68 | 69 | 70 | 71 | 74 | 75 | 80 | 81 | 86 | 87 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /app/src/main/res/xml/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/test/java/cn/ac/lz233/tarnhelm/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /art/animation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/animation.pptx -------------------------------------------------------------------------------- /art/animation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/animation.webp -------------------------------------------------------------------------------- /art/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/banner.png -------------------------------------------------------------------------------- /art/banner.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/banner.psd -------------------------------------------------------------------------------- /art/fdroid-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/fdroid-badge.png -------------------------------------------------------------------------------- /art/google-play-badge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/google-play-badge.png -------------------------------------------------------------------------------- /art/icon-color-coolapk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/icon-color-coolapk.png -------------------------------------------------------------------------------- /art/icon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/art/icon-color.png -------------------------------------------------------------------------------- /art/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 11 | 12 | 13 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /base/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /base/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'cn.ac.lz233.tarnhelm.base' 8 | compileSdk 35 9 | 10 | defaultConfig { 11 | minSdk 27 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_17 25 | targetCompatibility JavaVersion.VERSION_17 26 | } 27 | kotlinOptions { 28 | jvmTarget = '17' 29 | } 30 | } 31 | 32 | dependencies { 33 | 34 | 35 | testImplementation 'junit:junit:4.13.2' 36 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 37 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 38 | } -------------------------------------------------------------------------------- /base/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/base/consumer-rules.pro -------------------------------------------------------------------------------- /base/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /base/src/androidTest/java/cn/ac/lz233/tarnhelm/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("cn.ac.lz233.tarnhelm.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /base/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /base/src/test/java/cn/ac/lz233/tarnhelm/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /bin/test_cloud_backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | : "${1?"Usage: $0 package name"}" 3 | 4 | # Initialize and create a backup 5 | adb shell bmgr enable true 6 | adb shell bmgr transport com.android.localtransport/.LocalTransport | grep -q "Selected transport" || (echo "Error: error selecting local transport"; exit 1) 7 | adb shell settings put secure backup_local_transport_parameters 'is_encrypted=true' 8 | adb shell bmgr backupnow "$1" | grep -F "Package $1 with result: Success" || (echo "Backup failed"; exit 1) 9 | 10 | # Uninstall and reinstall the app to clear the data and trigger a restore 11 | apk_path_list=$(adb shell pm path "$1") 12 | OIFS=$IFS 13 | IFS=$'\n' 14 | apk_number=0 15 | for apk_line in $apk_path_list 16 | do 17 | (( ++apk_number )) 18 | apk_path=${apk_line:8:1000} 19 | adb pull "$apk_path" "myapk${apk_number}.apk" 20 | done 21 | IFS=$OIFS 22 | adb shell pm uninstall --user 0 "$1" 23 | apks=$(seq -f 'myapk%.f.apk' 1 $apk_number) 24 | adb install-multiple -t --user 0 $apks 25 | 26 | # Clean up 27 | adb shell bmgr transport com.google.android.gms/.backup.BackupTransportService 28 | rm $apks 29 | 30 | echo "Done" -------------------------------------------------------------------------------- /bin/test_d2d_backup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | : "${1?"Usage: $0 package name"}" 3 | 4 | # Initialize and create a backup 5 | adb shell bmgr enable true 6 | adb shell settings put secure backup_enable_d2d_test_mode 1 7 | adb shell bmgr transport com.google.android.gms/.backup.migrate.service.D2dTransport 8 | adb shell bmgr init com.google.android.gms/.backup.migrate.service.D2dTransport 9 | adb shell bmgr list transports | grep -q -F " * com.google.android.gms/.backup.migrate.service.D2dTransport" || (echo "Failed to select and initialize backup transport"; exit 1) 10 | adb shell bmgr backupnow "$1" | grep -F "Package $1 with result: Success" || (echo "Backup failed"; exit 1) 11 | 12 | # Uninstall and reinstall the app to clear the data and trigger a restore 13 | apk_path_list=$(adb shell pm path "$1") 14 | OIFS=$IFS 15 | IFS=$'\n' 16 | apk_number=0 17 | for apk_line in $apk_path_list 18 | do 19 | (( ++apk_number )) 20 | apk_path=${apk_line:8:1000} 21 | adb pull "$apk_path" "myapk${apk_number}.apk" 22 | done 23 | IFS=$OIFS 24 | adb shell pm uninstall --user 0 "$1" 25 | adb shell bmgr transport com.google.android.gms/.backup.BackupTransportService 26 | apks=$(seq -f 'myapk%.f.apk' 1 $apk_number) 27 | adb install-multiple -t --user 0 $apks 28 | 29 | # Clean up 30 | adb shell bmgr init com.google.android.gms/.backup.migrate.service.D2dTransport 31 | adb shell settings put secure backup_enable_d2d_test_mode 0 32 | adb shell bmgr transport com.google.android.gms/.backup.BackupTransportService 33 | rm $apks 34 | 35 | echo "Done" -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | plugins { 3 | id 'com.android.application' version '8.8.1' apply false 4 | id 'com.android.library' version '8.8.1' apply false 5 | id 'org.jetbrains.kotlin.android' version '2.0.21' apply false 6 | id 'dev.rikka.tools.materialthemebuilder' version '1.3.1' 7 | id 'com.google.devtools.ksp' version '2.0.21-1.0.27' apply false 8 | } 9 | 10 | tasks.register('clean', Delete) { 11 | delete rootProject.layout.buildDirectory 12 | } 13 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Tarnhelm can help you clean the tracking parameters in the links shared from the apps and keep the process natural. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | The magic to clean sharing links up -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/full_description.txt: -------------------------------------------------------------------------------- 1 | Tarnhelm 会帮助你清理应用分享链接中的追踪参数,并尽可能保持这个过程的自然。 -------------------------------------------------------------------------------- /fastlane/metadata/android/zh-CN/short_description.txt: -------------------------------------------------------------------------------- 1 | 清理分享链接的魔法 -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 30 23:13:06 CST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /hidden-api/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /hidden-api/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | } 5 | 6 | android { 7 | namespace 'cn.ac.lz233.tarnhelm.hidden_api' 8 | compileSdk 35 9 | 10 | defaultConfig { 11 | minSdk 27 12 | 13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 14 | consumerProguardFiles "consumer-rules.pro" 15 | } 16 | 17 | buildTypes { 18 | release { 19 | minifyEnabled false 20 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 21 | } 22 | } 23 | compileOptions { 24 | sourceCompatibility JavaVersion.VERSION_17 25 | targetCompatibility JavaVersion.VERSION_17 26 | } 27 | kotlinOptions { 28 | jvmTarget = '17' 29 | } 30 | } 31 | 32 | dependencies { 33 | annotationProcessor 'dev.rikka.tools.refine:annotation-processor:4.4.0' 34 | compileOnly 'dev.rikka.tools.refine:annotation:4.4.0' 35 | 36 | testImplementation 'junit:junit:4.13.2' 37 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 39 | } -------------------------------------------------------------------------------- /hidden-api/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/hidden-api/consumer-rules.pro -------------------------------------------------------------------------------- /hidden-api/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /hidden-api/src/androidTest/java/cn/ac/lz233/tarnhelm/hidden_api/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.hidden_api 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("cn.ac.lz233.tarnhelm.hidden_api.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /hidden-api/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /hidden-api/src/main/java/android/app/AppOpsManagerHidden.java: -------------------------------------------------------------------------------- 1 | package android.app; 2 | 3 | import android.content.Context; 4 | 5 | import dev.rikka.tools.refine.RefineAs; 6 | 7 | @RefineAs(AppOpsManager.class) 8 | public class AppOpsManagerHidden { 9 | 10 | public void startWatchingNoted(int[] ops, OnOpNotedListener listener) { 11 | throw new RuntimeException("Stub!"); 12 | } 13 | 14 | public void stopWatchingNoted(OnOpNotedListener listener) { 15 | throw new RuntimeException("Stub!"); 16 | } 17 | 18 | public void setMode(String op, int uid, String packageName, int mode) { 19 | throw new RuntimeException("Stub!"); 20 | } 21 | 22 | public interface OnOpNotedListener { 23 | /** 24 | * Called when an app-op is noted. 25 | * 26 | *

Implement this method and not 27 | * {@link #onOpNoted(String, int, String, String, int, int, int)} if callbacks are 28 | * required only on op notes for the default device {@link Context#DEVICE_ID_DEFAULT}. 29 | * 30 | * @param op The operation that was noted. 31 | * @param uid The UID performing the operation. 32 | * @param packageName The package performing the operation. 33 | * @param attributionTag The attribution tag performing the operation. 34 | * @param flags The flags of this op 35 | * @param result The result of the note. 36 | */ 37 | void onOpNoted(String op, int uid, String packageName, String attributionTag, int flags, int result); 38 | 39 | /** 40 | * Similar to {@link #onOpNoted(String, int, String, String, int, int, int)}, 41 | * but also includes the virtual device id of the op is now active or inactive. 42 | * 43 | *

Implement this method if callbacks are required for op notes on all devices. 44 | * If not implemented explicitly, the default implementation will notify for the 45 | * default device {@link Context#DEVICE_ID_DEFAULT} only. 46 | * 47 | *

If implemented, {@link #onOpNoted(String, int, String, String, int, int)} 48 | * will not be called automatically. 49 | * 50 | * @param op The operation that was noted. 51 | * @param uid The UID performing the operation. 52 | * @param packageName The package performing the operation. 53 | * @param attributionTag The attribution tag performing the operation. 54 | * @param virtualDeviceId the device that noted the operation 55 | * @param flags The flags of this op 56 | * @param result The result of the note. 57 | */ 58 | default void onOpNoted(String op, int uid, String packageName, String attributionTag, int virtualDeviceId, int flags, int result) { 59 | if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) { 60 | onOpNoted(op, uid, packageName, attributionTag, flags, result); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /hidden-api/src/test/java/cn/ac/lz233/tarnhelm/hidden_api/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.hidden_api 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven { 14 | url "https://api.xposed.info/" 15 | } 16 | } 17 | } 18 | rootProject.name = "Tarnhelm" 19 | include ':app' 20 | include ':hidden-api' 21 | include ':shizuku-service' 22 | include ':base' 23 | -------------------------------------------------------------------------------- /shizuku-service/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /shizuku-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'dev.rikka.tools.refine' version '4.4.0' 5 | } 6 | 7 | android { 8 | namespace 'cn.ac.lz233.tarnhelm.shizuku_service' 9 | compileSdk 35 10 | 11 | defaultConfig { 12 | minSdk 27 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | configureEach { 20 | buildConfigField 'String', 'APPLICATION_ID', '"cn.ac.lz233.tarnhelm"' 21 | } 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_17 29 | targetCompatibility JavaVersion.VERSION_17 30 | } 31 | kotlinOptions { 32 | jvmTarget = '17' 33 | } 34 | buildFeatures { 35 | buildConfig true 36 | aidl true 37 | } 38 | } 39 | 40 | dependencies { 41 | compileOnly project(":hidden-api") 42 | 43 | implementation "dev.rikka.tools.refine:runtime:4.4.0" 44 | 45 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' 46 | 47 | implementation 'androidx.core:core-ktx:1.15.0' 48 | implementation 'androidx.appcompat:appcompat:1.7.0' 49 | implementation 'com.google.android.material:material:1.12.0' 50 | testImplementation 'junit:junit:4.13.2' 51 | androidTestImplementation 'androidx.test.ext:junit:1.2.1' 52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' 53 | } -------------------------------------------------------------------------------- /shizuku-service/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lz233/Tarnhelm/4b314c2c7e4feaba1adc8736580a318f14d67652/shizuku-service/consumer-rules.pro -------------------------------------------------------------------------------- /shizuku-service/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /shizuku-service/src/androidTest/java/cn/ac/lz233/tarnhelm/shizuku_service/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.shizuku_service 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("cn.ac.lz233.tarnhelm.shizuku_service.test", appContext.packageName) 21 | } 22 | } -------------------------------------------------------------------------------- /shizuku-service/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /shizuku-service/src/main/aidl/cn/ac/lz233/tarnhelm/service/IClipboardShizukuService.aidl: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.service; 2 | 3 | import cn.ac.lz233.tarnhelm.service.ShizukuCallback; 4 | 5 | interface IClipboardShizukuService { 6 | void destroy() = 16777114; // Destroy method defined by Shizuku server 7 | void exit() = 1; // Exit method defined by user 8 | void start() = 2; 9 | void addCallback(ShizukuCallback callback) = 3; 10 | } -------------------------------------------------------------------------------- /shizuku-service/src/main/aidl/cn/ac/lz233/tarnhelm/service/ShizukuCallback.aidl: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.service; 2 | 3 | interface ShizukuCallback { 4 | void onOpNoted(String op, int uid, String packageName, String attributionTag, int flags, int result); 5 | } -------------------------------------------------------------------------------- /shizuku-service/src/main/java/cn/ac/lz233/tarnhelm/service/ClipboardShizukuService.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.service 2 | 3 | import android.app.AppOpsManager 4 | import android.app.AppOpsManagerHidden 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import androidx.annotation.Keep 9 | import androidx.annotation.RequiresApi 10 | import cn.ac.lz233.tarnhelm.shizuku_service.BuildConfig 11 | import dev.rikka.tools.refine.Refine 12 | import org.lsposed.hiddenapibypass.HiddenApiBypass 13 | 14 | @Keep 15 | class ClipboardShizukuService(private val context: Context) : IClipboardShizukuService.Stub() { 16 | private lateinit var appOpsManager: AppOpsManager 17 | private lateinit var packageManager: PackageManager 18 | private lateinit var shizukuCallback: ShizukuCallback 19 | private lateinit var opNotedListener: AppOpsManagerHidden.OnOpNotedListener 20 | 21 | override fun exit() { 22 | destroy() 23 | } 24 | 25 | override fun destroy() { 26 | // LogUtil._d("ClipboardShizukuService destroy") 27 | Refine.unsafeCast(appOpsManager).stopWatchingNoted(opNotedListener) 28 | } 29 | 30 | @RequiresApi(Build.VERSION_CODES.Q) 31 | override fun start() { 32 | HiddenApiBypass.addHiddenApiExemptions("Landroid/app"); 33 | // LogUtil._d("ClipboardShizukuService init") 34 | appOpsManager = context.getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager 35 | packageManager = context.packageManager 36 | // DO NOT convert it to lambda due to R8 will break it down 37 | opNotedListener = object : AppOpsManagerHidden.OnOpNotedListener { 38 | override fun onOpNoted(op: String?, uid: Int, packageName: String?, attributionTag: String?, flags: Int, result: Int) { 39 | shizukuCallback.onOpNoted(op, uid, packageName, attributionTag, flags, result) 40 | } 41 | } 42 | // Allow self to draw floating window 43 | Refine.unsafeCast(appOpsManager) 44 | .setMode( 45 | AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, 46 | packageManager.getPackageUid(BuildConfig.APPLICATION_ID, 0), 47 | BuildConfig.APPLICATION_ID, 48 | AppOpsManager.MODE_ALLOWED 49 | ) 50 | // Register AppOps Note listener 51 | Refine.unsafeCast(appOpsManager) 52 | .startWatchingNoted(intArrayOf(30), opNotedListener) 53 | } 54 | 55 | override fun addCallback(shizukuCallback: ShizukuCallback) { 56 | this.shizukuCallback = shizukuCallback 57 | } 58 | } -------------------------------------------------------------------------------- /shizuku-service/src/test/java/cn/ac/lz233/tarnhelm/shizuku_service/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package cn.ac.lz233.tarnhelm.shizuku_service 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } --------------------------------------------------------------------------------