feat: randomize package name
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user