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.

-
+

+

@@ -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")