diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index e96ca34..ffa143e 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -11,8 +11,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - submodules: recursive - uses: actions/setup-java@v4 with: @@ -30,9 +28,6 @@ jobs: org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 EOF - - name: Build libxposed to mavenLocal - run: ./gradlew buildLibxposed - - name: Decode keystore env: KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed66ad6..7cd7804 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,6 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - submodules: recursive - name: Set up JDK 21 uses: actions/setup-java@v4 @@ -35,9 +34,6 @@ jobs: org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 EOF - - name: Build libxposed to mavenLocal - run: ./gradlew buildLibxposed - - name: Decode keystore env: KEYSTORE_BASE64: ${{ secrets.RELEASE_KEYSTORE }} diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 52e251d..0000000 --- a/.gitmodules +++ /dev/null @@ -1,8 +0,0 @@ -[submodule "libxposed/api"] - path = libxposed/api - url = https://github.com/libxposed/api.git - ignore = dirty -[submodule "libxposed/service"] - path = libxposed/service - url = https://github.com/libxposed/service - ignore = dirty diff --git a/README.md b/README.md index cd4ba8f..b2600d2 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ CleanShare is an Xposed module that removes Direct Share's suggested contact/con from Android's Share Sheet. ![Android CI](https://github.com/hxreborn/cleanshare/actions/workflows/android.yml/badge.svg) -![Kotlin](https://img.shields.io/badge/Kotlin-2.1.21-7F52FF?style=flat&logo=kotlin&logoColor=white) +![Kotlin](https://img.shields.io/badge/Kotlin-2.3.10-7F52FF?style=flat&logo=kotlin&logoColor=white) ![Android](https://img.shields.io/badge/API-30%2B-3DDC84?logo=android&logoColor=white) +![Xposed Repo](https://img.shields.io/github/downloads/Xposed-Modules-Repo/eu.hxreborn.cleanshare/total?label=Xposed%20Repo&logo=android&logoColor=white)
Direct Share targets row removed @@ -37,7 +38,7 @@ it also blocks backend shortcut queries to prevent share target profiling. ## Requirements - Android 11 (API 30) or higher -- [LSPosed](https://github.com/JingMatrix/LSPosed) (JingMatrix fork recommended) +- [LSPosed](https://github.com/JingMatrix/LSPosed) 2.0.0+ (JingMatrix fork recommended) - Pixel or AOSP-based ROM (Other OEMs are untested) ## Installation @@ -128,9 +129,8 @@ app's [storage sandbox](https://developer.android.com/training/data-storage#scop ## Build ```bash -git clone --recurse-submodules https://github.com/hxreborn/cleanshare.git +git clone https://github.com/hxreborn/cleanshare.git cd cleanshare -./gradlew buildLibxposed ./gradlew assembleRelease ``` diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5915d03..327eaa7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,10 +1,8 @@ plugins { alias(libs.plugins.agp.app) - alias(libs.plugins.kotlin) alias(libs.plugins.compose.compiler) alias(libs.plugins.serialization) alias(libs.plugins.aboutlibraries) - alias(libs.plugins.ktlint) } android { @@ -15,8 +13,8 @@ android { applicationId = "eu.hxreborn.cleanshare" minSdk = 30 targetSdk = 36 - versionCode = 210 - versionName = "2.1.0" + versionCode = 300 + versionName = "3.0.0" } signingConfigs { @@ -71,7 +69,7 @@ android { packaging { resources { - pickFirsts += "META-INF/xposed/*" + merges += "META-INF/xposed/*" } } @@ -90,10 +88,30 @@ kotlin { jvmToolchain(21) } -ktlint { - version.set("1.8.0") - android.set(true) - ignoreFailures.set(false) +val copyAboutLibraries by tasks.registering(Copy::class) { + dependsOn("exportLibraryDefinitions") + from("build/generated/aboutLibraries/aboutlibraries.json") + into("build/generated/aboutLibrariesRes/raw") +} +android.sourceSets["main"].res.directories.add("build/generated/aboutLibrariesRes") +tasks.named("preBuild").configure { dependsOn(copyAboutLibraries) } + +val ktlintCli = "com.pinterest.ktlint:ktlint-cli:1.8.0" + +val ktlintCheck by tasks.registering(JavaExec::class) { + group = "verification" + description = "Check Kotlin code style" + classpath = configurations.detachedConfiguration(dependencies.create(ktlintCli)) + mainClass.set("com.pinterest.ktlint.Main") + args("src/**/*.kt") +} + +val ktlintFormat by tasks.registering(JavaExec::class) { + group = "verification" + description = "Fix Kotlin code style" + classpath = configurations.detachedConfiguration(dependencies.create(ktlintCli)) + mainClass.set("com.pinterest.ktlint.Main") + args("-F", "src/**/*.kt") } dependencies { diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index ea5919e..8a3a979 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,27 +1,18 @@ # LSPosed module entry point -keep class eu.hxreborn.cleanshare.CleanShareModule { *; } -# Keep all @XposedHooker annotated classes --keep @io.github.libxposed.api.annotations.XposedHooker class * { *; } - -# Keep annotation classes --keep class io.github.libxposed.api.annotations.** { *; } +# Prevent R8 from merging hook classes into app process code (compileOnly API) +-keep,allowobfuscation class eu.hxreborn.cleanshare.hook.** { *; } # Preserve annotations --keepattributes *Annotation* -keepattributes RuntimeVisibleAnnotations -# Keep hook methods --keepclassmembers class * { - @io.github.libxposed.api.annotations.BeforeInvocation ; - @io.github.libxposed.api.annotations.AfterInvocation ; -} - # Keep XposedModule subclasses -adaptresourcefilecontents META-INF/xposed/java_init.list -keep,allowobfuscation,allowoptimization public class * extends io.github.libxposed.api.XposedModule { - public (...); + public (); public void onPackageLoaded(...); + public void onPackageReady(...); } # ContentProvider registered in manifest must survive shrinking diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/CleanShareModule.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/CleanShareModule.kt index ee283cb..c2ca844 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/CleanShareModule.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/CleanShareModule.kt @@ -1,34 +1,36 @@ package eu.hxreborn.cleanshare +import android.app.Activity import android.app.ActivityManager import android.content.IntentFilter +import android.content.pm.ResolveInfo import android.content.pm.ShortcutManager import android.os.Build import android.os.Bundle +import android.util.Log import eu.hxreborn.cleanshare.hook.deletion.CheckboxHook import eu.hxreborn.cleanshare.hook.deletion.DeletionHook -import eu.hxreborn.cleanshare.hook.directshare.LowRamHooker -import eu.hxreborn.cleanshare.hook.directshare.ShareTargetsHooker -import eu.hxreborn.cleanshare.hook.quickshare.QuickShareFilterHooker +import eu.hxreborn.cleanshare.util.PREFS_FILE_NAME +import eu.hxreborn.cleanshare.util.PREF_KEY_HIDE_DIRECT_SHARE +import eu.hxreborn.cleanshare.util.PREF_KEY_HIDE_QUICK_SHARE +import eu.hxreborn.cleanshare.util.QUICK_SHARE_ACTIVITY +import eu.hxreborn.cleanshare.util.debugLog import eu.hxreborn.cleanshare.util.findClass import eu.hxreborn.cleanshare.util.findMethodByName -import io.github.libxposed.api.XposedInterface import io.github.libxposed.api.XposedModule import io.github.libxposed.api.XposedModuleInterface.ModuleLoadedParam -import io.github.libxposed.api.XposedModuleInterface.PackageLoadedParam +import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam private const val ANDROID_FRAMEWORK_PKG = "android" private const val INTENT_RESOLVER_PKG = "com.android.intentresolver" private const val AIAI_PKG = "com.google.android.as" -// ChooserActivity location differs by API level private val CHOOSER_CLASS_NAMES = listOf( - "com.android.intentresolver.ChooserActivity", // API 33+ - "com.android.internal.app.ChooserActivity", // API 30-32 + "com.android.intentresolver.ChooserActivity", + "com.android.internal.app.ChooserActivity", ) -// Share sheet lives in framework on 11–12, IntentResolver from 13+ private val SHARE_SHEET_PKG: String = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { INTENT_RESOLVER_PKG @@ -36,21 +38,19 @@ private val SHARE_SHEET_PKG: String = ANDROID_FRAMEWORK_PKG } -class CleanShareModule( - base: XposedInterface, - param: ModuleLoadedParam, -) : XposedModule(base, param) { +class CleanShareModule : XposedModule() { companion object { + const val TAG = "CleanShare" var instance: CleanShareModule? = null private set } - init { + override fun onModuleLoaded(param: ModuleLoadedParam) { instance = this - log("CleanShare v${BuildConfig.VERSION_NAME} loaded in ${param.processName}") + log(Log.INFO, TAG, "v${BuildConfig.VERSION_NAME} loaded in ${param.processName}") } - override fun onPackageLoaded(param: PackageLoadedParam) { + override fun onPackageReady(param: PackageReadyParam) { when (param.packageName) { SHARE_SHEET_PKG -> { hookLowRam() @@ -64,91 +64,103 @@ class CleanShareModule( } } - // Spoof low-RAM so the share sheet skips creating ShortcutLoader private fun hookLowRam() { runCatching { val method = ActivityManager::class.java.getDeclaredMethod("isLowRamDeviceStatic") method.isAccessible = true - hook(method, LowRamHooker::class.java) - log("Hooked ActivityManager.isLowRamDeviceStatic") - }.onFailure { - log("LowRam hook failed: ${it.message}") - } + hook(method).intercept { chain -> + val prefs = getRemotePreferences(PREFS_FILE_NAME) + val enabled = prefs?.getBoolean(PREF_KEY_HIDE_DIRECT_SHARE, true) ?: true + debugLog { "[DirectShare] isLowRamDeviceStatic called, enabled=$enabled" } + if (enabled) return@intercept true + chain.proceed() + } + log(Log.INFO, TAG, "Hooked ActivityManager.isLowRamDeviceStatic") + }.onFailure { log(Log.WARN, TAG, "LowRam hook failed: ${it.message}") } } - // Insert "Delete after sharing" checkbox for screenshot shares private fun hookScreenshotDelete(classLoader: ClassLoader) { - val chooserClass = findClass(classLoader, CHOOSER_CLASS_NAMES) - if (chooserClass == null) { - log("ChooserActivity not found, skipping deletion hooks") - return - } + val chooserClass = + findClass(classLoader, CHOOSER_CLASS_NAMES) ?: run { + log(Log.WARN, TAG, "ChooserActivity not found, skipping deletion hooks") + return + } - // Hook onCreate for checkbox insertion runCatching { val method = chooserClass.getDeclaredMethod("onCreate", Bundle::class.java) method.isAccessible = true - hook(method, CheckboxHook::class.java) - log("Hooked ${chooserClass.simpleName}.onCreate") - }.onFailure { - log("Checkbox hook failed: ${it.message}") - } + hook(method).intercept { chain -> + chain.proceed() + CheckboxHook.handleOnCreate(chain.thisObject as? Activity ?: return@intercept null) + null + } + log(Log.INFO, TAG, "Hooked ${chooserClass.simpleName}.onCreate") + }.onFailure { log(Log.WARN, TAG, "Checkbox hook failed: ${it.message}") } - // Hook startSelected for deletion trigger - val startSelected = findMethodByName(chooserClass, "startSelected") - if (startSelected == null) { - log("startSelected not found on ${chooserClass.name}") - return - } + val startSelected = + findMethodByName(chooserClass, "startSelected") ?: run { + log(Log.WARN, TAG, "startSelected not found on ${chooserClass.name}") + return + } runCatching { startSelected.isAccessible = true - hook(startSelected, DeletionHook::class.java) - log("Hooked ${chooserClass.simpleName}.startSelected") - }.onFailure { - log("Deletion hook failed: ${it.message}") - } + hook(startSelected).intercept { chain -> + chain.proceed() + DeletionHook.handleStartSelected() + null + } + log(Log.INFO, TAG, "Hooked ${chooserClass.simpleName}.startSelected") + }.onFailure { log(Log.WARN, TAG, "Deletion hook failed: ${it.message}") } } - // Block AiAi shortcut queries to prevent share target profiling private fun hookShareTargets() { runCatching { val method = - ShortcutManager::class.java.getDeclaredMethod( - "getShareTargets", - IntentFilter::class.java, - ) + ShortcutManager::class.java + .getDeclaredMethod("getShareTargets", IntentFilter::class.java) method.isAccessible = true - hook(method, ShareTargetsHooker::class.java) - log("Hooked ShortcutManager.getShareTargets") - }.onFailure { - log("ShareTargets hook failed: ${it.message}") - } + hook(method).intercept { chain -> + val prefs = getRemotePreferences(PREFS_FILE_NAME) + val enabled = prefs?.getBoolean(PREF_KEY_HIDE_DIRECT_SHARE, true) ?: true + debugLog { "[DirectShare] getShareTargets called, enabled=$enabled" } + if (enabled) return@intercept emptyList() + chain.proceed() + } + log(Log.INFO, TAG, "Hooked ShortcutManager.getShareTargets") + }.onFailure { log(Log.WARN, TAG, "ShareTargets hook failed: ${it.message}") } } - // Filter Quick Share from share targets by hooking queryIntentActivitiesAsUser - // Works on both framework (A11-12) and IntentResolver (A13+) + @Suppress("UNCHECKED_CAST") private fun hookQuickShareFilter(classLoader: ClassLoader) { val pmClass = runCatching { classLoader.loadClass("android.app.ApplicationPackageManager") }.getOrNull() ?: run { - log("Quick Share hook: ApplicationPackageManager not found") + log(Log.WARN, TAG, "Quick Share hook: ApplicationPackageManager not found") return } val methods = pmClass.declaredMethods.filter { it.name == "queryIntentActivitiesAsUser" } if (methods.isEmpty()) { - log("Quick Share hook: no queryIntentActivitiesAsUser methods found") + log(Log.WARN, TAG, "Quick Share hook: no queryIntentActivitiesAsUser methods found") return } methods.forEach { method -> runCatching { method.isAccessible = true - hook(method, QuickShareFilterHooker::class.java) + hook(method).intercept { chain -> + val result = chain.proceed() + val prefs = getRemotePreferences(PREFS_FILE_NAME) + val enabled = prefs?.getBoolean(PREF_KEY_HIDE_QUICK_SHARE, false) ?: false + if (!enabled) return@intercept result + val list = result as? MutableList ?: return@intercept result + list.removeAll { it.activityInfo?.name == QUICK_SHARE_ACTIVITY } + result + } } } - log("Hooked queryIntentActivitiesAsUser (${methods.size} overloads)") + log(Log.INFO, TAG, "Hooked queryIntentActivitiesAsUser (${methods.size} overloads)") } } diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/CheckboxHook.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/CheckboxHook.kt index 302a37e..dcc0bd4 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/CheckboxHook.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/CheckboxHook.kt @@ -16,141 +16,118 @@ import eu.hxreborn.cleanshare.util.PREF_KEY_DELETE_AFTER_SHARE import eu.hxreborn.cleanshare.util.PREF_KEY_DELETION_ENABLED import eu.hxreborn.cleanshare.util.PREF_KEY_DELETION_MODE import eu.hxreborn.cleanshare.util.debugLog -import io.github.libxposed.api.XposedInterface.AfterHookCallback -import io.github.libxposed.api.XposedInterface.Hooker -import io.github.libxposed.api.annotations.AfterInvocation -import io.github.libxposed.api.annotations.XposedHooker - -@XposedHooker -class CheckboxHook : Hooker { - companion object { - @JvmStatic - @AfterInvocation - @SuppressLint("NewApi") - fun after(callback: AfterHookCallback) { - debugLog { "onCreate hook fired" } - ShareState.clear() - val activity = callback.thisObject as? Activity ?: return - val rawIntent = activity.intent ?: return - debugLog { "rawIntent action=${rawIntent.action}" } - - val shareIntent = extractShareIntent(rawIntent) ?: return - val uri = extractImageUri(shareIntent) ?: return - - val remotePrefs = - runCatching { - CleanShareModule.instance?.getRemotePreferences(PREFS_FILE_NAME) - }.getOrNull() ?: return - - val enabled = remotePrefs.getBoolean(PREF_KEY_DELETION_ENABLED, false) - if (!enabled) return - - val modeKey = - remotePrefs.getString( - PREF_KEY_DELETION_MODE, - DeletionMode.ASK_EACH_TIME.key, - ) ?: DeletionMode.ASK_EACH_TIME.key - val mode = DeletionMode.fromKey(modeKey) - - // Sync path: original file (MediaStore / SystemUI) - val screenshotInfo = getScreenshotInfo(activity, uri) - if (screenshotInfo != null) { - val targetUri = screenshotInfo.resolvedUri ?: uri - applyDeletionMode(activity, mode, targetUri, screenshotInfo) - return - } - // Async path: editor share -> resolve original via IPC - if (!isOriginalFileUri(uri)) { - Thread { - val resolved = resolveOriginalScreenshot(activity) ?: return@Thread - val targetUri = resolved.resolvedUri ?: uri - activity.runOnUiThread { - applyDeletionMode(activity, mode, targetUri, resolved) - } - }.start() - } +internal object CheckboxHook { + @SuppressLint("NewApi") + fun handleOnCreate(activity: Activity) { + debugLog { "onCreate hook fired" } + ShareState.clear() + val rawIntent = activity.intent ?: return + debugLog { "rawIntent action=${rawIntent.action}" } + + val shareIntent = extractShareIntent(rawIntent) ?: return + val uri = extractImageUri(shareIntent) ?: return + + val remotePrefs = + runCatching { + CleanShareModule.instance?.getRemotePreferences(PREFS_FILE_NAME) + }.getOrNull() ?: return + + val enabled = remotePrefs.getBoolean(PREF_KEY_DELETION_ENABLED, false) + if (!enabled) return + + val modeKey = + remotePrefs.getString( + PREF_KEY_DELETION_MODE, + DeletionMode.ASK_EACH_TIME.key, + ) ?: DeletionMode.ASK_EACH_TIME.key + val mode = DeletionMode.fromKey(modeKey) + + val screenshotInfo = getScreenshotInfo(activity, uri) + if (screenshotInfo != null) { + val targetUri = screenshotInfo.resolvedUri ?: uri + applyDeletionMode(activity, mode, targetUri, screenshotInfo) + return } - private fun applyDeletionMode( - activity: Activity, - mode: DeletionMode, - uri: Uri, - info: ScreenshotInfo, - ) { - when (mode) { - DeletionMode.ALWAYS -> { - ShareState.set( - uri, - info.filename, - info.filePath, - shouldDelete = true, - activity, - ) + if (!isOriginalFileUri(uri)) { + Thread { + val resolved = resolveOriginalScreenshot(activity) ?: return@Thread + val targetUri = resolved.resolvedUri ?: uri + activity.runOnUiThread { + applyDeletionMode(activity, mode, targetUri, resolved) } + }.start() + } + } - DeletionMode.ASK_EACH_TIME -> { - activity.window.decorView.post { - insertCheckbox(activity, uri, info) - } - } + private fun applyDeletionMode( + activity: Activity, + mode: DeletionMode, + uri: Uri, + info: ScreenshotInfo, + ) { + when (mode) { + DeletionMode.ALWAYS -> { + ShareState.set(uri, info.filename, info.filePath, shouldDelete = true, activity) + } + + DeletionMode.ASK_EACH_TIME -> { + activity.window.decorView.post { insertCheckbox(activity, uri, info) } } } + } - private fun insertCheckbox( - activity: Activity, - uri: Uri, - screenshotInfo: ScreenshotInfo, - ) { - runCatching { - val localPrefs = - activity.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE) - val checked = localPrefs.getBoolean(PREF_KEY_DELETE_AFTER_SHARE, false) - val isResolved = screenshotInfo.resolvedUri != null - val label = if (isResolved) "Delete original screenshot" else "Delete after sharing" - - val checkBox = - CheckBox(activity).apply { - text = label - tag = CHECKBOX_VIEW_TAG - textSize = CHECKBOX_TEXT_SIZE_SP - isChecked = checked - setOnCheckedChangeListener { _, isChecked -> - ShareState.updateShouldDelete(isChecked) - localPrefs.edit { - putBoolean(PREF_KEY_DELETE_AFTER_SHARE, isChecked) - } - } + private fun insertCheckbox( + activity: Activity, + uri: Uri, + screenshotInfo: ScreenshotInfo, + ) { + runCatching { + val localPrefs = activity.getSharedPreferences(PREFS_FILE_NAME, Context.MODE_PRIVATE) + val checked = localPrefs.getBoolean(PREF_KEY_DELETE_AFTER_SHARE, false) + val isResolved = screenshotInfo.resolvedUri != null + val label = if (isResolved) "Delete original screenshot" else "Delete after sharing" + + val checkBox = + CheckBox(activity).apply { + text = label + tag = CHECKBOX_VIEW_TAG + textSize = CHECKBOX_TEXT_SIZE_SP + isChecked = checked + setOnCheckedChangeListener { _, isChecked -> + ShareState.updateShouldDelete(isChecked) + localPrefs.edit { putBoolean(PREF_KEY_DELETE_AFTER_SHARE, isChecked) } } + } - ShareState.set( - uri, - screenshotInfo.filename, - screenshotInfo.filePath, - checked, - activity, + ShareState.set( + uri, + screenshotInfo.filename, + screenshotInfo.filePath, + checked, + activity, + ) + + val inserted = CheckboxInserter.insert(activity, checkBox) + debugLog { "checkbox inserted=$inserted" } + + if (!inserted) { + activity.window.decorView.postDelayed( + { retryInsert(activity, checkBox) }, + CHECKBOX_INSERT_RETRY_DELAY_MS, ) + } + }.onFailure { debugLog(it) { "insertCheckbox failed" } } + } - val inserted = CheckboxInserter.insert(activity, checkBox) - debugLog { "checkbox inserted=$inserted" } - - if (!inserted) { - activity.window.decorView.postDelayed( - { retryInsert(activity, checkBox) }, - CHECKBOX_INSERT_RETRY_DELAY_MS, - ) - } - }.onFailure { debugLog(it) { "insertCheckbox failed" } } - } - - private fun retryInsert( - activity: Activity, - checkBox: CheckBox, - ) { - runCatching { - val inserted = CheckboxInserter.insert(activity, checkBox) - debugLog { "checkbox retry inserted=$inserted" } - }.onFailure { debugLog(it) { "retryInsert failed" } } - } + private fun retryInsert( + activity: Activity, + checkBox: CheckBox, + ) { + runCatching { + val inserted = CheckboxInserter.insert(activity, checkBox) + debugLog { "checkbox retry inserted=$inserted" } + }.onFailure { debugLog(it) { "retryInsert failed" } } } } diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/DeletionHook.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/DeletionHook.kt index 131eec9..3b32700 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/DeletionHook.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/deletion/DeletionHook.kt @@ -8,46 +8,37 @@ import eu.hxreborn.cleanshare.util.DELETION_PROVIDER_AUTHORITY import eu.hxreborn.cleanshare.util.PREFS_FILE_NAME import eu.hxreborn.cleanshare.util.PREF_KEY_DELETION_DELAY_MS import eu.hxreborn.cleanshare.util.debugLog -import io.github.libxposed.api.XposedInterface.AfterHookCallback -import io.github.libxposed.api.XposedInterface.Hooker -import io.github.libxposed.api.annotations.AfterInvocation -import io.github.libxposed.api.annotations.XposedHooker -@XposedHooker -class DeletionHook : Hooker { - companion object { - @JvmStatic - @AfterInvocation - fun after(callback: AfterHookCallback) { - debugLog { "startSelected hook fired" } +internal object DeletionHook { + fun handleStartSelected() { + debugLog { "startSelected hook fired" } - val pending = ShareState.consumeIfPendingDeletion() ?: return + val pending = ShareState.consumeIfPendingDeletion() ?: return - debugLog { "Enqueuing deletion filename=${pending.filename}" } - runCatching { - val delayMs = - CleanShareModule.instance - ?.getRemotePreferences(PREFS_FILE_NAME) - ?.getInt(PREF_KEY_DELETION_DELAY_MS, DEFAULT_DELETION_DELAY_MS) - ?.toLong() - ?: DEFAULT_DELETION_DELAY_MS.toLong() + debugLog { "Enqueuing deletion filename=${pending.filename}" } + runCatching { + val delayMs = + CleanShareModule.instance + ?.getRemotePreferences(PREFS_FILE_NAME) + ?.getInt(PREF_KEY_DELETION_DELAY_MS, DEFAULT_DELETION_DELAY_MS) + ?.toLong() + ?: DEFAULT_DELETION_DELAY_MS.toLong() - val extras = - Bundle().apply { - putString("uri", pending.uri.toString()) - putString("filename", pending.filename) - putString("file_path", pending.filePath) - putLong("delay_ms", delayMs) - } + val extras = + Bundle().apply { + putString("uri", pending.uri.toString()) + putString("filename", pending.filename) + putString("file_path", pending.filePath) + putLong("delay_ms", delayMs) + } - pending.activity.contentResolver.call( - Uri.parse("content://$DELETION_PROVIDER_AUTHORITY"), - "enqueue", - null, - extras, - ) - debugLog { "ContentProvider enqueue called" } - }.onFailure { debugLog(it) { "Failed to enqueue deletion" } } - } + pending.activity.contentResolver.call( + Uri.parse("content://$DELETION_PROVIDER_AUTHORITY"), + "enqueue", + null, + extras, + ) + debugLog { "ContentProvider enqueue called" } + }.onFailure { debugLog(it) { "Failed to enqueue deletion" } } } } diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/directshare/DirectShareHooks.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/directshare/DirectShareHooks.kt deleted file mode 100644 index c0da76e..0000000 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/directshare/DirectShareHooks.kt +++ /dev/null @@ -1,43 +0,0 @@ -package eu.hxreborn.cleanshare.hook.directshare - -import eu.hxreborn.cleanshare.CleanShareModule -import eu.hxreborn.cleanshare.util.PREFS_FILE_NAME -import eu.hxreborn.cleanshare.util.PREF_KEY_HIDE_DIRECT_SHARE -import io.github.libxposed.api.XposedInterface.BeforeHookCallback -import io.github.libxposed.api.XposedInterface.Hooker -import io.github.libxposed.api.annotations.BeforeInvocation -import io.github.libxposed.api.annotations.XposedHooker - -@XposedHooker -class LowRamHooker : Hooker { - companion object { - @JvmStatic - @BeforeInvocation - fun before(callback: BeforeHookCallback) { - val module = CleanShareModule.instance ?: return - val prefs = module.getRemotePreferences(PREFS_FILE_NAME) - val enabled = prefs?.getBoolean(PREF_KEY_HIDE_DIRECT_SHARE, true) ?: true - module.log("[DirectShare] isLowRamDeviceStatic called, enabled=$enabled") - if (enabled) { - callback.returnAndSkip(true) - } - } - } -} - -@XposedHooker -class ShareTargetsHooker : Hooker { - companion object { - @JvmStatic - @BeforeInvocation - fun before(callback: BeforeHookCallback) { - val module = CleanShareModule.instance ?: return - val prefs = module.getRemotePreferences(PREFS_FILE_NAME) - val enabled = prefs?.getBoolean(PREF_KEY_HIDE_DIRECT_SHARE, true) ?: true - module.log("[DirectShare] getShareTargets called, enabled=$enabled") - if (enabled) { - callback.returnAndSkip(emptyList()) - } - } - } -} diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/quickshare/QuickShareFilterHooker.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/quickshare/QuickShareFilterHooker.kt deleted file mode 100644 index 0cacb53..0000000 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/hook/quickshare/QuickShareFilterHooker.kt +++ /dev/null @@ -1,43 +0,0 @@ -package eu.hxreborn.cleanshare.hook.quickshare - -import android.content.pm.ResolveInfo -import eu.hxreborn.cleanshare.CleanShareModule -import eu.hxreborn.cleanshare.util.PREFS_FILE_NAME -import eu.hxreborn.cleanshare.util.PREF_KEY_HIDE_QUICK_SHARE -import eu.hxreborn.cleanshare.util.QUICK_SHARE_ACTIVITY -import eu.hxreborn.cleanshare.util.debugLog -import io.github.libxposed.api.XposedInterface.AfterHookCallback -import io.github.libxposed.api.XposedInterface.Hooker -import io.github.libxposed.api.annotations.AfterInvocation -import io.github.libxposed.api.annotations.XposedHooker - -@XposedHooker -class QuickShareFilterHooker : Hooker { - companion object { - @JvmStatic - @AfterInvocation - fun after(callback: AfterHookCallback) { - val module = CleanShareModule.instance ?: return - - debugLog { "[QuickShare] queryIntentActivitiesAsUser called" } - - val prefs = module.getRemotePreferences(PREFS_FILE_NAME) - val enabled = prefs?.getBoolean(PREF_KEY_HIDE_QUICK_SHARE, false) ?: false - if (!enabled) return - - val result = callback.result - - @Suppress("UNCHECKED_CAST") - val list = result as? MutableList - if (list == null) { - debugLog { "[QuickShare] result is not MutableList" } - return - } - - debugLog { "[QuickShare] list size before filter: ${list.size}" } - - val removed = list.removeAll { it.activityInfo?.name == QUICK_SHARE_ACTIVITY } - debugLog { "[QuickShare] removed=$removed, list size after: ${list.size}" } - } - } -} diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/screen/SettingsScreen.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/screen/SettingsScreen.kt index edfab67..fdab76a 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/screen/SettingsScreen.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/screen/SettingsScreen.kt @@ -1,4 +1,4 @@ -@file:Suppress("ktlint:standard:function-naming", "AssignedValueIsNeverRead") +@file:Suppress("ktlint:standard:function-naming") package eu.hxreborn.cleanshare.ui.screen diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/util/RegexEditDialog.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/util/RegexEditDialog.kt index e3c254c..c562614 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/util/RegexEditDialog.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/util/RegexEditDialog.kt @@ -58,7 +58,7 @@ fun RegexEditDialog( pattern = value isValid = value.isNotBlank() && runCatching { Regex(value) }.isSuccess }, - label = { Text("Regex") }, + label = { Text(stringResource(R.string.regex_label)) }, isError = !isValid, supportingText = if (!isValid) { diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/viewmodel/SettingsViewModel.kt index 4c184a7..6dd1734 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/ui/viewmodel/SettingsViewModel.kt @@ -61,7 +61,7 @@ class SettingsViewModelImpl( prefsRepository.observeBoolean(Prefs.SHOW_DELETION_TOAST), prefsRepository.observeString(Prefs.SCREENSHOT_PATTERN), rootAvailable, - ) { values -> + ) { values: Array -> SettingsUiState.Ready( hideDirectShare = values[0] as Boolean, hideQuickShare = values[1] as Boolean, diff --git a/app/src/main/kotlin/eu/hxreborn/cleanshare/util/Log.kt b/app/src/main/kotlin/eu/hxreborn/cleanshare/util/Log.kt index 740229d..73c6c7f 100644 --- a/app/src/main/kotlin/eu/hxreborn/cleanshare/util/Log.kt +++ b/app/src/main/kotlin/eu/hxreborn/cleanshare/util/Log.kt @@ -1,14 +1,20 @@ package eu.hxreborn.cleanshare.util +import android.util.Log import eu.hxreborn.cleanshare.BuildConfig import eu.hxreborn.cleanshare.CleanShareModule +import eu.hxreborn.cleanshare.CleanShareModule.Companion.TAG internal fun log( msg: String, t: Throwable? = null, ) { val module = CleanShareModule.instance ?: return - t?.let { module.log(msg, it) } ?: module.log(msg) + if (t != null) { + module.log(Log.ERROR, TAG, msg, t) + } else { + module.log(Log.INFO, TAG, msg) + } } internal inline fun debugLog( diff --git a/app/src/main/res/raw/keep.xml b/app/src/main/res/raw/keep.xml new file mode 100644 index 0000000..8f8b7f6 --- /dev/null +++ b/app/src/main/res/raw/keep.xml @@ -0,0 +1,4 @@ + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e45e27c..066ba0b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -49,6 +49,7 @@ Learn more Invalid pattern Default + Regex Back diff --git a/app/src/main/resources/META-INF/xposed/module.prop b/app/src/main/resources/META-INF/xposed/module.prop index 8dc7ff3..c3975fe 100644 --- a/app/src/main/resources/META-INF/xposed/module.prop +++ b/app/src/main/resources/META-INF/xposed/module.prop @@ -1,3 +1,3 @@ -minApiVersion=100 -targetApiVersion=100 +minApiVersion=101 +targetApiVersion=101 staticScope=true diff --git a/build.gradle.kts b/build.gradle.kts index f82001c..e547f4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,34 +19,3 @@ tasks.register("cleanBuild") { description = "Cleans the project and then assembles all builds in the app module." dependsOn("clean", "assembleDebugRelease") } - -tasks.register("buildLibxposedApi") { - group = "libxposed" - description = "Builds libxposed/api and publishes to mavenLocal" - workingDir = layout.projectDirectory.dir("libxposed/api").asFile - commandLine( - "./gradlew", - ":api:publishApiPublicationToMavenLocal", - "-x", - ":checks:compileKotlin", - "--no-daemon", - ) -} - -tasks.register("buildLibxposedService") { - group = "libxposed" - description = "Builds libxposed/service and publishes to mavenLocal" - workingDir = layout.projectDirectory.dir("libxposed/service").asFile - commandLine( - "./gradlew", - ":interface:publishInterfacePublicationToMavenLocal", - ":service:publishServicePublicationToMavenLocal", - "--no-daemon", - ) -} - -tasks.register("buildLibxposed") { - group = "libxposed" - description = "Builds both libxposed/api and libxposed/service" - dependsOn("buildLibxposedApi", "buildLibxposedService") -} diff --git a/gradle.properties b/gradle.properties index e2b92ad..d7834aa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -15,7 +15,8 @@ android.useAndroidX=true kotlin.code.style=official # Optimize R class and resource IDs android.nonTransitiveRClass=true -android.nonFinalResIds=false +# Disable AGP 9 resource name obfuscation (breaks aboutlibraries runtime lookup) +android.enableResourceOptimizations=false # Enable incremental compilation kotlin.incremental=true kotlin.incremental.java=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 88ec6a8..3ec79cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,21 @@ [versions] -agp = "8.11.0" -kotlin = "2.1.21" -ktlint = "14.0.1" -xposed = "100" +agp = "9.1.0" +kotlin = "2.3.10" +xposed = "101.0.0" lifecycle = "2.10.0" core-ktx = "1.17.0" compose-bom = "2026.01.00" activity-compose = "1.12.3" material3 = "1.5.0-alpha13" compose-preference = "1.1.1" -aboutlibraries = "11.6.3" +aboutlibraries = "12.1.2" splashscreen = "1.0.1" libsu = "6.0.0" navigation3 = "1.1.0-alpha03" [libraries] libxposed-api = { group = "io.github.libxposed", name = "api", version.ref = "xposed" } -libxposed-service = { group = "io.github.libxposed", name = "service", version = "100-1.0.0" } +libxposed-service = { group = "io.github.libxposed", name = "service", version.ref = "xposed" } lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" } @@ -26,8 +25,8 @@ compose-material3 = { group = "androidx.compose.material3", name = "material3", compose-material-icons = { group = "androidx.compose.material", name = "material-icons-extended" } activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } compose-preference = { group = "me.zhanghai.compose.preference", name = "library", version.ref = "compose-preference" } -aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", version = "12.1.2" } -aboutlibraries-compose = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version = "12.1.2" } +aboutlibraries-core = { group = "com.mikepenz", name = "aboutlibraries-core", version.ref = "aboutlibraries" } +aboutlibraries-compose = { group = "com.mikepenz", name = "aboutlibraries-compose-m3", version.ref = "aboutlibraries" } splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" } libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } @@ -36,8 +35,6 @@ navigation3-ui = { group = "androidx.navigation3", name = "navigation3-ui", vers [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } -kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" } serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ff23a68..37f78a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libxposed/api b/libxposed/api deleted file mode 160000 index 55efdf9..0000000 --- a/libxposed/api +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 55efdf9d159195261d7326e9e125965a90025a12 diff --git a/libxposed/service b/libxposed/service deleted file mode 160000 index 496b76f..0000000 --- a/libxposed/service +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 496b76fa3e5af87958ebef97bd160319e05da79b diff --git a/settings.gradle.kts b/settings.gradle.kts index 8506785..4097b4b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -10,11 +10,6 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - mavenLocal { - content { - includeGroup("io.github.libxposed") - } - } maven("https://jitpack.io") { content { includeGroup("com.github.topjohnwu.libsu")