feat: randomize package name

This commit is contained in:
rhunk
2023-11-11 18:15:27 +01:00
parent dc30d4ee25
commit 0f1cd7157a
20 changed files with 334 additions and 112 deletions

View File

@@ -1,6 +1,7 @@
package me.rhunk.snapenhance.manager.data
import android.content.Context
import me.rhunk.snapenhance.manager.BuildConfig
class SharedConfig(
context: Context
@@ -16,8 +17,10 @@ class SharedConfig(
var snapchatPackageName get() = sharedPreferences.getString("snapchatPackageName", "com.snapchat.android")?.takeIf { it.isNotEmpty() } ?: "com.snapchat.android"
set(value) = sharedPreferences.edit().putString("snapchatPackageName", value).apply()
var snapEnhancePackageName get() = sharedPreferences.getString("snapEnhancePackageName", "me.rhunk.snapenhance")?.takeIf { it.isNotEmpty() } ?: "me.rhunk.snapenhance"
var snapEnhancePackageName get() = sharedPreferences.getString("snapEnhancePackageName", BuildConfig.APPLICATION_ID)?.takeIf { it.isNotEmpty() } ?: BuildConfig.APPLICATION_ID
set(value) = sharedPreferences.edit().putString("snapEnhancePackageName", value).apply()
var enableRepackage get() = sharedPreferences.getBoolean("enableRepackage", false)
set(value) = sharedPreferences.edit().putBoolean("enableRepackage", value).apply()
var useRootInstaller get() = sharedPreferences.getBoolean("useRootInstaller", false)
set(value) = sharedPreferences.edit().putBoolean("useRootInstaller", value).apply()

View File

@@ -1,8 +1,6 @@
package me.rhunk.snapenhance.manager.lspatch
package me.rhunk.snapenhance.manager.patch
import android.content.Context
import com.android.tools.build.apkzlib.sign.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions
import com.android.tools.build.apkzlib.zip.AlignmentRules
import com.android.tools.build.apkzlib.zip.ZFile
import com.android.tools.build.apkzlib.zip.ZFileOptions
@@ -10,14 +8,13 @@ import com.google.gson.Gson
import com.wind.meditor.core.ManifestEditor
import com.wind.meditor.property.AttributeItem
import com.wind.meditor.property.ModificationProperty
import me.rhunk.snapenhance.manager.lspatch.config.Constants.PROXY_APP_COMPONENT_FACTORY
import me.rhunk.snapenhance.manager.lspatch.config.PatchConfig
import me.rhunk.snapenhance.manager.lspatch.util.ApkSignatureHelper
import me.rhunk.snapenhance.manager.patch.config.Constants.PROXY_APP_COMPONENT_FACTORY
import me.rhunk.snapenhance.manager.patch.config.PatchConfig
import me.rhunk.snapenhance.manager.patch.util.ApkSignatureHelper
import me.rhunk.snapenhance.manager.patch.util.ApkSignatureHelper.provideSigningExtension
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.security.KeyStore
import java.security.cert.X509Certificate
import java.util.zip.ZipFile
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
@@ -45,22 +42,6 @@ class LSPatch(
}.toByteArray()
}
private fun provideSigningExtension(): SigningExtension {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(context.assets.open("lspatch/keystore.jks"), "android".toCharArray())
val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry
val certificates = key.certificateChain.mapNotNull { it as? X509Certificate }.toTypedArray()
return SigningExtension(
SigningOptions.builder().apply {
setMinSdkVersion(28)
setV2SigningEnabled(true)
setCertificates(*certificates)
setKey(key.privateKey)
}.build()
)
}
private fun resignApk(inputApkFile: File, outputFile: File) {
printLog("Resigning ${inputApkFile.absolutePath} to ${outputFile.absolutePath}")
val dstZFile = ZFile.openReadWrite(outputFile, ZFileOptions())
@@ -72,7 +53,7 @@ class LSPatch(
// sign apk
runCatching {
provideSigningExtension().register(dstZFile)
provideSigningExtension(context.assets.open("lspatch/keystore.jks")).register(dstZFile)
}.onFailure {
throw Exception("Failed to sign apk", it)
}
@@ -131,7 +112,7 @@ class LSPatch(
// sign apk
runCatching {
provideSigningExtension().register(dstZFile)
provideSigningExtension(context.assets.open("lspatch/keystore.jks")).register(dstZFile)
}.onFailure {
throw Exception("Failed to sign apk", it)
}

View File

@@ -1,12 +1,6 @@
package me.rhunk.snapenhance.manager.lspatch
package me.rhunk.snapenhance.manager.patch
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.dexbacked.DexBackedDexFile
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.writer.io.FileDataStore
import org.jf.dexlib2.writer.pool.DexPool
import org.jf.dexlib2.writer.pool.StringPool
import java.io.BufferedInputStream
import me.rhunk.snapenhance.manager.patch.util.obfuscateDexFile
import java.io.File
import java.io.InputStream
@@ -27,52 +21,9 @@ class LSPatchObfuscation(
private val cacheFolder: File,
private val printLog: (String) -> Unit = { println(it) }
) {
private fun obfuscateDexFile(dexStrings: Map<String, String?>, inputStream: InputStream): File {
val dexFile = DexBackedDexFile.fromInputStream(Opcodes.forApi(29), BufferedInputStream(inputStream))
val dexPool = object: DexPool(dexFile.opcodes) {
override fun getSectionProvider(): SectionProvider {
val dexPool = this
return object: DexPoolSectionProvider() {
override fun getStringSection() = object: StringPool(dexPool) {
private val cacheMap = mutableMapOf<String, String>()
override fun intern(string: CharSequence) {
dexStrings[string.toString()]?.let {
cacheMap[string.toString()] = it
printLog("mapping $string to $it")
super.intern(it)
return
}
super.intern(string)
}
override fun getItemIndex(key: CharSequence): Int {
return cacheMap[key.toString()]?.let {
internedItems[it]
} ?: super.getItemIndex(key)
}
override fun getItemIndex(key: StringReference): Int {
return cacheMap[key.toString()]?.let {
internedItems[it]
} ?: super.getItemIndex(key)
}
}
}
}
}
dexFile.classes.forEach { dexBackedClassDef ->
dexPool.internClass(dexBackedClassDef)
}
val outputFile = File.createTempFile("obf", ".dex", cacheFolder)
dexPool.writeTo(FileDataStore(outputFile))
return outputFile
}
fun obfuscateMetaLoader(inputStream: InputStream, config: DexObfuscationConfig): File {
return obfuscateDexFile(mapOf(
return inputStream.obfuscateDexFile(cacheFolder, mapOf(
"assets/lspatch/config.json" to "assets/${config.configFilePath}",
"assets/lspatch/loader.dex" to "assets/${config.loaderFilePath}",
) + (config.libNativeFilePath.takeIf { it.isNotEmpty() }?.let {
@@ -85,11 +36,11 @@ class LSPatchObfuscation(
"x86" to config.libNativeFilePath["x86"],
"x86_64" to config.libNativeFilePath["x86_64"],
)
} ?: mapOf()), inputStream)
} ?: mapOf()))
}
fun obfuscateLoader(inputStream: InputStream, config: DexObfuscationConfig): File {
return obfuscateDexFile(mapOf(
return inputStream.obfuscateDexFile(cacheFolder, mapOf(
"assets/lspatch/config.json" to config.configFilePath?.let { "assets/$it" },
"assets/lspatch/loader.dex" to config.loaderFilePath?.let { "assets/$it" },
"assets/lspatch/metaloader.dex" to config.metaLoaderFilePath?.let { "assets/$it" },
@@ -102,6 +53,6 @@ class LSPatchObfuscation(
"lspatch/modules/" to config.assetModuleFolderPath?.let { "$it/" }, // LocalApplicationService.java => try (var is = context.getAssets().open("lspatch/modules/" + name)) {
"lspatch" to config.metadataManifestField, // SigBypass.java => "lspatch",
"org.lsposed.lspatch" to config.cachedOriginApkPath?.let { "$it/${config.packageName}/" }, // Constants.java => "org.lsposed.lspatch", (Used in LSPatchUpdater.kt)
), inputStream)
))
}
}

View File

@@ -0,0 +1,89 @@
package me.rhunk.snapenhance.manager.patch
import android.content.Context
import com.android.tools.build.apkzlib.zip.AlignmentRules
import com.android.tools.build.apkzlib.zip.ZFile
import com.android.tools.build.apkzlib.zip.ZFileOptions
import com.wind.meditor.core.ManifestEditor
import com.wind.meditor.property.AttributeItem
import com.wind.meditor.property.ModificationProperty
import me.rhunk.snapenhance.manager.BuildConfig
import me.rhunk.snapenhance.manager.patch.util.ApkSignatureHelper.provideSigningExtension
import me.rhunk.snapenhance.manager.patch.util.obfuscateDexFile
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
class Repackager(
private val context: Context,
private val cacheFolder: File,
private val packageName: String,
) {
private fun patchManifest(data: ByteArray): ByteArray {
val property = ModificationProperty()
property.addManifestAttribute(AttributeItem("package", packageName).apply {
type = 3
namespace = null
})
return ByteArrayOutputStream().apply {
ManifestEditor(ByteArrayInputStream(data), this, property).processManifest()
flush()
close()
}.toByteArray()
}
fun patch(apkFile: File): File {
val outputFile = File(cacheFolder, "patched-${apkFile.name}")
runCatching {
patch(apkFile, outputFile)
}.onFailure {
outputFile.delete()
throw it
}
return outputFile
}
fun patch(apkFile: File, outputFile: File) {
val dstZFile = ZFile.openReadWrite(outputFile, ZFileOptions().setAlignmentRule(
AlignmentRules.compose(AlignmentRules.constantForSuffix(".so", 4096))
))
provideSigningExtension(context.assets.open("lspatch/keystore.jks")).register(dstZFile)
val srcZFile = ZFile.openReadOnly(apkFile)
val dexFiles = mutableListOf<File>()
for (entry in srcZFile.entries()) {
val name = entry.centralDirectoryHeader.name
if (name.startsWith("AndroidManifest.xml")) {
dstZFile.add(name, ByteArrayInputStream(
patchManifest(entry.read())
), false)
continue
}
if (name.startsWith("classes") && name.endsWith(".dex")) {
println("obfuscating $name")
val inputStream = entry.open() ?: continue
val obfuscatedDexFile = inputStream.obfuscateDexFile(cacheFolder, { dexFile ->
dexFile.classes.firstOrNull { it.type == "Lme/rhunk/snapenhance/common/Constants;" } != null
}, mapOf(
BuildConfig.APPLICATION_ID to packageName
))?.also { dexFiles.add(it) }
if (obfuscatedDexFile == null) {
inputStream.close()
dstZFile.add(name, entry.open(), false)
continue
}
dstZFile.add(name, obfuscatedDexFile.inputStream(), false)
continue
}
dstZFile.add(name, entry.open(), false)
}
dstZFile.realign()
dstZFile.close()
srcZFile.close()
dexFiles.forEach { it.delete() }
}
}

View File

@@ -1,4 +1,4 @@
package me.rhunk.snapenhance.manager.lspatch.config
package me.rhunk.snapenhance.manager.patch.config
//https://github.com/LSPosed/LSPatch/blob/master/share/java/src/main/java/org/lsposed/lspatch/share/Constants.java
object Constants {

View File

@@ -1,4 +1,4 @@
package me.rhunk.snapenhance.manager.lspatch.config
package me.rhunk.snapenhance.manager.patch.config
data class PatchConfig(
val useManager: Boolean = false,

View File

@@ -1,11 +1,16 @@
package me.rhunk.snapenhance.manager.lspatch.util;
package me.rhunk.snapenhance.manager.patch.util;
import com.android.tools.build.apkzlib.sign.SigningExtension
import com.android.tools.build.apkzlib.sign.SigningOptions
import java.io.IOException
import java.io.InputStream
import java.io.RandomAccessFile
import java.io.UnsupportedEncodingException
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.security.KeyStore
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import java.util.Enumeration
import java.util.jar.JarEntry
import java.util.jar.JarFile
@@ -16,6 +21,22 @@ object ApkSignatureHelper {
private val APK_V2_MAGIC = charArrayOf('A', 'P', 'K', ' ', 'S', 'i', 'g', ' ',
'B', 'l', 'o', 'c', 'k', ' ', '4', '2')
fun provideSigningExtension(keyStoreInputStream: InputStream): SigningExtension {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(keyStoreInputStream, "android".toCharArray())
val key = keyStore.getEntry("androiddebugkey", KeyStore.PasswordProtection("android".toCharArray())) as KeyStore.PrivateKeyEntry
val certificates = key.certificateChain.mapNotNull { it as? X509Certificate }.toTypedArray()
return SigningExtension(
SigningOptions.builder().apply {
setMinSdkVersion(28)
setV2SigningEnabled(true)
setCertificates(*certificates)
setKey(key.privateKey)
}.build()
)
}
private fun toChars(mSignature: ByteArray): CharArray {
val N = mSignature.size
val N2 = N * 2

View File

@@ -0,0 +1,63 @@
package me.rhunk.snapenhance.manager.patch.util
import org.jf.dexlib2.Opcodes
import org.jf.dexlib2.dexbacked.DexBackedDexFile
import org.jf.dexlib2.iface.DexFile
import org.jf.dexlib2.iface.reference.StringReference
import org.jf.dexlib2.writer.io.FileDataStore
import org.jf.dexlib2.writer.pool.DexPool
import org.jf.dexlib2.writer.pool.StringPool
import java.io.BufferedInputStream
import java.io.File
import java.io.InputStream
private fun obfuscateStrings(dexFile: DexFile, dexStrings: Map<String, String?>): DexPool {
val dexPool = object: DexPool(dexFile.opcodes) {
override fun getSectionProvider(): SectionProvider {
val dexPool = this
return object: DexPoolSectionProvider() {
override fun getStringSection() = object: StringPool(dexPool) {
private val cacheMap = mutableMapOf<String, String>()
override fun intern(string: CharSequence) {
dexStrings[string.toString()]?.let {
cacheMap[string.toString()] = it
println("mapping $string to $it")
super.intern(it)
return
}
super.intern(string)
}
override fun getItemIndex(key: CharSequence): Int {
return cacheMap[key.toString()]?.let {
internedItems[it]
} ?: super.getItemIndex(key)
}
override fun getItemIndex(key: StringReference): Int {
return cacheMap[key.toString()]?.let {
internedItems[it]
} ?: super.getItemIndex(key)
}
}
}
}
}
dexFile.classes.forEach { dexBackedClassDef ->
dexPool.internClass(dexBackedClassDef)
}
return dexPool
}
fun InputStream.obfuscateDexFile(cacheFolder: File, dexStrings: Map<String, String?>)
= this.obfuscateDexFile(cacheFolder, { true }, dexStrings)!!
fun InputStream.obfuscateDexFile(cacheFolder: File, filter: (DexFile) -> Boolean, dexStrings: Map<String, String?>): File? {
val dexFile = DexBackedDexFile.fromInputStream(Opcodes.forApi(29), BufferedInputStream(this))
if (!filter(dexFile)) return null
val outputFile = File.createTempFile("dexobf", ".dex", cacheFolder)
obfuscateStrings(dexFile, dexStrings).writeTo(FileDataStore(outputFile))
return outputFile
}

View File

@@ -16,10 +16,11 @@ import me.rhunk.snapenhance.manager.ui.tab.Tab
import me.rhunk.snapenhance.manager.ui.tab.impl.HomeTab
import me.rhunk.snapenhance.manager.ui.tab.impl.SettingsTab
import me.rhunk.snapenhance.manager.ui.tab.impl.download.InstallPackageTab
import me.rhunk.snapenhance.manager.ui.tab.impl.download.RepackageTab
class MainActivity : ComponentActivity() {
companion object{
private val primaryTabs = listOf(HomeTab::class, SettingsTab::class, InstallPackageTab::class)
private val primaryTabs = listOf(HomeTab::class, SettingsTab::class, InstallPackageTab::class, RepackageTab::class)
}
override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -10,10 +10,10 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -27,7 +27,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.manager.lspatch.config.Constants
import me.rhunk.snapenhance.manager.patch.config.Constants
import me.rhunk.snapenhance.manager.ui.tab.Tab
import me.rhunk.snapenhance.manager.ui.tab.impl.download.SEDownloadTab
import me.rhunk.snapenhance.manager.ui.tab.impl.download.SnapchatPatchTab
@@ -66,6 +66,7 @@ class HomeTab : Tab("home", true, icon = Icons.Default.Home) {
Text(text = "SnapEnhance", fontSize = 24.sp, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold)
snapEnhanceInfo?.let {
Text(text = "${it.versionName} (${it.longVersionCode}) - ${if ((it.applicationInfo.flags and FLAG_DEBUGGABLE) != 0) "Debug" else "Release"}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(it.packageName, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
}
Row(

View File

@@ -22,10 +22,11 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import me.rhunk.snapenhance.manager.ui.tab.Tab
import kotlin.random.Random
class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Settings) {
@Composable
private fun ConfigEditRow(getValue: () -> String?, setValue: (String) -> Unit, label: String) {
private fun ConfigEditRow(getValue: () -> String?, setValue: (String) -> Unit, label: String, randomValueProvider: (() -> String)? = null) {
var showDialog by remember { mutableStateOf(false) }
if (showDialog) {
@@ -65,6 +66,13 @@ class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Setti
}) {
Text(text = "Cancel")
}
if (randomValueProvider != null) {
Button(onClick = {
textFieldValue = TextFieldValue(randomValueProvider(), TextRange(0))
}) {
Text(text = "Random")
}
}
Button(onClick = {
setValue(textFieldValue.text)
showDialog = false
@@ -130,15 +138,18 @@ class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Setti
override fun Content() {
Column {
Spacer(modifier = Modifier.height(16.dp))
ConfigEditRow(
getValue = { sharedConfig.snapchatPackageName },
setValue = { sharedConfig.snapchatPackageName = it },
label = "Override Snapchat package name"
)
ConfigEditRow(
getValue = { sharedConfig.snapEnhancePackageName },
setValue = { sharedConfig.snapEnhancePackageName = it },
label = "Override SnapEnhance package name"
label = "Override SnapEnhance package name",
randomValueProvider = {
(0..Random.nextInt(7, 16)).map { ('a'..'z').random() }.joinToString("").chunked(4).joinToString(".")
}
)
ConfigBooleanRow(
getValue = { sharedConfig.enableRepackage },
setValue = { sharedConfig.enableRepackage = it },
label = "Repackage SnapEnhance (experimental)"
)
ConfigBooleanRow(
getValue = { sharedConfig.useRootInstaller },

View File

@@ -20,7 +20,7 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.manager.data.APKMirror
import me.rhunk.snapenhance.manager.data.DownloadItem
import me.rhunk.snapenhance.manager.lspatch.LSPatch
import me.rhunk.snapenhance.manager.patch.LSPatch
import me.rhunk.snapenhance.manager.ui.components.DowngradeNoticeDialog
import me.rhunk.snapenhance.manager.ui.tab.Tab
import okio.use

View File

@@ -0,0 +1,86 @@
package me.rhunk.snapenhance.manager.ui.tab.impl.download
import android.os.Bundle
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.manager.patch.Repackager
import me.rhunk.snapenhance.manager.ui.tab.Tab
import java.io.File
enum class RepackageState {
IDLE,
WORKING,
SUCCESS,
FAILED
}
class RepackageTab : Tab("repackage") {
private var throwable: Throwable? = null
private suspend fun repackage(apk: File, oldPackage: String, state: MutableState<RepackageState>) {
state.value = RepackageState.WORKING
val repackager = Repackager(activity, activity.externalCacheDirs.first(), sharedConfig.snapEnhancePackageName)
runCatching {
repackager.patch(apk)
}.onFailure {
throwable = it
state.value = RepackageState.FAILED
return
}.onSuccess { originApk ->
state.value = RepackageState.SUCCESS
withContext(Dispatchers.Main) {
navigation.navigateTo(InstallPackageTab::class, Bundle().apply {
putString("downloadPath", originApk.absolutePath)
putString("appPackage", oldPackage)
putBoolean("uninstall", true)
}, noHistory = true)
}
return
}
}
@Composable
override fun Content() {
val apkPath = remember { getArguments()?.getString("apkPath") } ?: return
val oldPackage = remember { getArguments()?.getString("oldPackage") } ?: return
val state = remember { mutableStateOf(RepackageState.IDLE) }
LaunchedEffect(apkPath) {
launch(Dispatchers.IO) {
repackage(File(apkPath), oldPackage, state)
}
}
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
when (state.value) {
RepackageState.WORKING -> Text(text = "Repackaging ...")
RepackageState.FAILED -> {
Text(text = "Failed")
Text(text = (throwable?.localizedMessage + throwable?.stackTraceToString()))
}
else -> {}
}
}
}
}

View File

@@ -24,6 +24,7 @@ import androidx.compose.ui.window.Dialog
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import me.rhunk.snapenhance.manager.BuildConfig
import me.rhunk.snapenhance.manager.data.download.SEArtifact
import me.rhunk.snapenhance.manager.data.download.SEVersion
import me.rhunk.snapenhance.manager.ui.components.DowngradeNoticeDialog
@@ -82,7 +83,9 @@ class SEDownloadTab : Tab("se_download") {
var selectedVersion by remember { mutableStateOf(null as SEVersion?) }
var selectedArtifact by remember { mutableStateOf(null as SEArtifact?) }
val isAppInstalled = remember { runCatching { activity.packageManager.getPackageInfo(sharedConfig.snapEnhancePackageName, 0) != null }.getOrNull() != null }
val snapEnhanceApp = remember {
runCatching { activity.packageManager.getPackageInfo(BuildConfig.APPLICATION_ID, 0) }.getOrNull()
}
var showDowngradeNotice by remember { mutableStateOf(false) }
@@ -209,7 +212,21 @@ class SEDownloadTab : Tab("se_download") {
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isAppInstalled) {
if (snapEnhanceApp != null) {
if (sharedConfig.enableRepackage && sharedConfig.snapEnhancePackageName != snapEnhanceApp.packageName) {
Button(
onClick = {
navigation.navigateTo(RepackageTab::class, Bundle().apply {
putString("apkPath", snapEnhanceApp.applicationInfo.sourceDir)
putString("oldPackage", snapEnhanceApp.packageName)
}, noHistory = true)
},
enabled = true,
modifier = Modifier.fillMaxWidth()
) {
Text(text = "Repackage installed version (>=2.0.0)")
}
}
Button(
onClick = {
triggerPackageInstallation(true)
@@ -222,7 +239,7 @@ class SEDownloadTab : Tab("se_download") {
}
Button(
onClick = {
if (isAppInstalled) {
if (snapEnhanceApp != null) {
showDowngradeNotice = true
} else {
triggerPackageInstallation(false)
@@ -231,7 +248,7 @@ class SEDownloadTab : Tab("se_download") {
enabled = selectedVersion != null && selectedArtifact != null,
modifier = Modifier.fillMaxWidth()
) {
Text(text = if (isAppInstalled) "Update" else "Install")
Text(text = if (snapEnhanceApp != null) "Update" else "Install")
}
}
}

View File

@@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
import me.rhunk.snapenhance.manager.R
import me.rhunk.snapenhance.manager.data.APKMirror
import me.rhunk.snapenhance.manager.data.DownloadItem
import me.rhunk.snapenhance.manager.lspatch.config.Constants
import me.rhunk.snapenhance.manager.patch.config.Constants
import me.rhunk.snapenhance.manager.ui.components.ConfirmationDialog
import me.rhunk.snapenhance.manager.ui.tab.Tab
import java.io.File