From e83374253c89a4d65b0bef78cd3cc477bfced2fb Mon Sep 17 00:00:00 2001
From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com>
Date: Mon, 23 Jun 2025 23:36:11 -0700
Subject: [PATCH] feat(Settings): allow directly exporting patched apk
---
INFO.md | 17 ---------
.../aliucord/manager/manager/PathManager.kt | 5 +++
.../network/services/AliucordGithubService.kt | 1 -
.../steps/download/CopyDependenciesStep.kt | 3 +-
.../ui/screens/settings/SettingsModel.kt | 36 +++++++++++++++++++
.../ui/screens/settings/SettingsScreen.kt | 36 ++++++++-----------
app/src/main/res/values/strings.xml | 5 +--
app/src/main/res/xml/provider_paths.xml | 3 ++
8 files changed, 62 insertions(+), 44 deletions(-)
diff --git a/INFO.md b/INFO.md
index f72ecbd..d08f190 100644
--- a/INFO.md
+++ b/INFO.md
@@ -1,20 +1,3 @@
-## Obtaining Patched APKs
-
-Unfortunately, starting with Android 10, file managers cannot access external data of other apps without root.
-You will have to manually download the patched APK via `adb`.
-
-ADB command (assuming the first user profile):
-
-```shell
-$ adb pull /storage/emulated/0/Android/data/com.aliucord.manager/cache/patched/patched.apk
-```
-
-This file should only be used after the patching process has successfully completed.
-If the process failed at any step before "Installing", then the APK will be unusable.
-
-The patched APK **should not be reused on another device**. Refer to the section [below](#backing-up-aliucord)
-for more information.
-
## Backing up Aliucord
Note: This section is about the app installation itself, not the installed plugins, themes, and settings.
diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt
index f8e49c0..74640f6 100644
--- a/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/manager/PathManager.kt
@@ -101,6 +101,11 @@ class PathManager(
fun patchingWorkingDir() = patchingDir
.resolve("patched")
+ /**
+ * The APK that is worked on during the patching process.
+ */
+ fun patchedApk() = patchingWorkingDir().resolve("patched.apk")
+
private companion object {
/**
* List all the files that follow the ```[SemVer].custom.*``` naming scheme.
diff --git a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt
index 10e6782..526b785 100644
--- a/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/network/services/AliucordGithubService.kt
@@ -38,6 +38,5 @@ class AliucordGithubService(
const val MANAGER_REPO = "Manager"
const val DATA_JSON_URL = "https://raw.githubusercontent.com/$ORG/$MAIN_REPO/builds/data.json"
- const val PATCHED_APKS_INFO_URL = "https://github.com/$ORG/$MANAGER_REPO/blob/main/INFO.md#obtaining-patched-apks"
}
}
diff --git a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt
index 50523f2..7ef16c4 100644
--- a/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/patcher/steps/download/CopyDependenciesStep.kt
@@ -18,8 +18,7 @@ class CopyDependenciesStep : Step(), KoinComponent {
/**
* The target APK file which can be modified during patching
*/
- val patchedApk: File = paths.patchingWorkingDir()
- .resolve("patched.apk")
+ val patchedApk: File = paths.patchedApk()
override val group = StepGroup.Download
override val localizedName = R.string.patch_step_copy_deps
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsModel.kt
index d01a563..c0916f5 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsModel.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsModel.kt
@@ -1,7 +1,10 @@
package com.aliucord.manager.ui.screens.settings
import android.app.Application
+import android.content.Intent
+import android.util.Log
import androidx.compose.runtime.*
+import androidx.core.content.FileProvider
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig
@@ -18,6 +21,9 @@ class SettingsModel(
) : ScreenModel {
val installInfo = InstallInfo
+ var patchedApkExists by mutableStateOf(paths.patchedApk().exists())
+ private set
+
var showThemeDialog by mutableStateOf(false)
private set
@@ -39,6 +45,7 @@ class SettingsModel(
fun clearCache() = screenModelScope.launchBlock {
paths.clearCache()
+ patchedApkExists = false
application.showToast(R.string.action_cleared_cache)
}
@@ -47,6 +54,35 @@ class SettingsModel(
application.showToast(R.string.action_copied)
}
+ fun shareApk() = screenModelScope.launchBlock {
+ val file = paths.patchingWorkingDir().resolve("patched.apk")
+ val fileUri = FileProvider.getUriForFile(
+ /* context = */ application,
+ /* authority = */ "${BuildConfig.APPLICATION_ID}.provider",
+ /* file = */ file,
+ /* displayName = */ "Aliucord.apk",
+ )
+
+ val intent = Intent(Intent.ACTION_SEND)
+ .setType("application/vnd.android.package-archive")
+ .putExtra(Intent.EXTRA_STREAM, fileUri)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ .let {
+ Intent.createChooser(
+ /* target = */ it,
+ /* title = */ application.getString(R.string.log_action_export_apk),
+ )
+ }
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+
+ try {
+ application.startActivity(intent)
+ } catch (t: Throwable) {
+ Log.w(BuildConfig.TAG, "Failed to share APK", t)
+ application.showToast(R.string.status_failed)
+ }
+ }
+
companion object {
@Suppress("KotlinConstantConditions")
private val InstallInfo: String = """
diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt
index 3c1f327..37babc5 100644
--- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt
+++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/settings/SettingsScreen.kt
@@ -15,14 +15,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.R
-import com.aliucord.manager.network.services.AliucordGithubService
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.components.MainActionButton
import com.aliucord.manager.ui.components.settings.*
@@ -94,10 +92,7 @@ class SettingsScreen : Screen, Parcelable {
secondaryLabel = stringResource(R.string.setting_developer_options_desc),
pref = preferences.devMode,
icon = { Icon(painterResource(R.drawable.ic_code), null) },
- onPrefChange = {
- preferences.devMode = it
- clearedCache = false // Cache dir changes with this setting
- },
+ onPrefChange = { preferences.devMode = it },
)
SettingsSwitch(
@@ -105,13 +100,22 @@ class SettingsScreen : Screen, Parcelable {
secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc),
icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) },
pref = preferences.keepPatchedApks,
- onPrefChange = {
- model.setKeepPatchedApks(it)
- clearedCache = false // Cache dir changes with this setting
- },
+ onPrefChange = { model.setKeepPatchedApks(it) },
modifier = Modifier.padding(bottom = 18.dp),
)
+ if (preferences.keepPatchedApks) {
+ MainActionButton(
+ text = stringResource(R.string.log_action_export_apk),
+ icon = painterResource(R.drawable.ic_save),
+ enabled = model.patchedApkExists,
+ onClick = model::shareApk,
+ modifier = Modifier
+ .padding(horizontal = 18.dp, vertical = 9.dp)
+ .fillMaxWidth()
+ )
+ }
+
MainActionButton(
text = stringResource(R.string.settings_clear_cache),
icon = painterResource(R.drawable.ic_delete_forever),
@@ -128,18 +132,6 @@ class SettingsScreen : Screen, Parcelable {
.fillMaxWidth()
)
- if (preferences.keepPatchedApks) {
- val handler = LocalUriHandler.current
- MainActionButton(
- text = stringResource(R.string.settings_see_patched_apks),
- icon = painterResource(R.drawable.ic_launch),
- onClick = { handler.openUri(AliucordGithubService.PATCHED_APKS_INFO_URL) },
- modifier = Modifier
- .padding(horizontal = 18.dp, vertical = 9.dp)
- .fillMaxWidth()
- )
- }
-
SettingsHeader(stringResource(R.string.settings_header_info))
Row(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 31e670e..788bdcb 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -53,9 +53,9 @@
Developer options
Don\'t enable this unless you know what it does! (yes, really)
Keep patched APKs
- Keep all patched files and APKs after installing for debugging. This setting may drastically slow down (20x) patching on certain devices!
+ Keep all patched files and APKs after installation for debugging purposes. Note that exporting the APK is only useful after a successful patching session.
Clear cache
- See patched APKs
+ Export APK
System
Dark
@@ -205,6 +205,7 @@
Error Stacktrace
Installation Log
Export log
+ Export APK
Share log
Logs
diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml
index f3830f2..9354caf 100644
--- a/app/src/main/res/xml/provider_paths.xml
+++ b/app/src/main/res/xml/provider_paths.xml
@@ -3,4 +3,7 @@
+