feat(manager): root installer
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[versions]
|
||||
agp = "8.1.2"
|
||||
apksig = "8.0.2"
|
||||
libsu = "5.2.1"
|
||||
guava = "32.1.3-jre"
|
||||
jsoup = "1.16.2"
|
||||
kotlin = "1.9.0"
|
||||
@@ -40,6 +41,7 @@ apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
|
||||
bcprov-jdk18on = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bcprov-jdk18on" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" }
|
||||
coil-video = { module = "io.coil-kt:coil-video", version.ref = "coil-compose" }
|
||||
libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
|
||||
coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
|
||||
dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" }
|
||||
ffmpeg-kit = { group = "com.arthenica", name = "ffmpeg-kit-full-gpl", version.ref = "ffmpeg-kit" }
|
||||
|
||||
@@ -72,6 +72,7 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
implementation(libs.libsu)
|
||||
implementation(libs.guava)
|
||||
implementation(libs.apksig)
|
||||
implementation(libs.gson)
|
||||
|
||||
2
manager/proguard-rules.pro
vendored
2
manager/proguard-rules.pro
vendored
@@ -1,3 +1,3 @@
|
||||
-dontwarn com.google.errorprone.annotations.**
|
||||
-dontwarn com.google.auto.value.**
|
||||
-keep enum me.rhunk.snapenhance.manager.** { *; }
|
||||
-keep class me.rhunk.snapenhance.manager.ui.tab.** { *; }
|
||||
@@ -18,4 +18,7 @@ class SharedConfig(
|
||||
|
||||
var snapEnhancePackageName get() = sharedPreferences.getString("snapEnhancePackageName", "me.rhunk.snapenhance")?.takeIf { it.isNotEmpty() } ?: "me.rhunk.snapenhance"
|
||||
set(value) = sharedPreferences.edit().putString("snapEnhancePackageName", value).apply()
|
||||
|
||||
var useRootInstaller get() = sharedPreferences.getBoolean("useRootInstaller", false)
|
||||
set(value) = sharedPreferences.edit().putBoolean("useRootInstaller", value).apply()
|
||||
}
|
||||
@@ -9,10 +9,13 @@ import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import me.rhunk.snapenhance.manager.BuildConfig
|
||||
import me.rhunk.snapenhance.manager.data.SharedConfig
|
||||
import me.rhunk.snapenhance.manager.ui.tab.HomeTab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.SettingsTab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.download.InstallPackageTab
|
||||
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
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
companion object{
|
||||
@@ -22,6 +25,11 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Shell.enableVerboseLogging = BuildConfig.DEBUG;
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setFlags(Shell.FLAG_REDIRECT_STDERR)
|
||||
.setTimeout(10)
|
||||
);
|
||||
val tabs = primaryTabs.mapNotNull {
|
||||
runCatching { it.java.constructors.first().newInstance() as Tab }.getOrNull()
|
||||
}.toMutableList().apply {
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui
|
||||
package me.rhunk.snapenhance.manager.ui.tab
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
@@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import me.rhunk.snapenhance.manager.data.SharedConfig
|
||||
import me.rhunk.snapenhance.manager.ui.Navigation
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
open class Tab(
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl
|
||||
|
||||
import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.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.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -26,9 +28,9 @@ 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.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.download.SEDownloadTab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.download.SnapchatPatchTab
|
||||
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
|
||||
|
||||
class HomeTab : Tab("home", true, icon = Icons.Default.Home) {
|
||||
override fun init(activity: ComponentActivity) {
|
||||
@@ -106,19 +108,22 @@ class HomeTab : Tab("home", true, icon = Icons.Default.Home) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(5.dp, Alignment.End),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
snapchatAppInfo?.let { appInfo ->
|
||||
val isLSPatched = appInfo.applicationInfo.appComponentFactory == Constants.PROXY_APP_COMPONENT_FACTORY
|
||||
if (isLSPatched) {
|
||||
Text(text = "Patched", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
Icon(imageVector = Icons.Default.Check, contentDescription = null)
|
||||
Text(text = "Patched", fontSize = 16.sp)
|
||||
} else {
|
||||
Icon(imageVector = Icons.Default.Close, contentDescription = null)
|
||||
Text(text = "Not patched", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
} ?: run {
|
||||
Text(text = "Not installed", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
|
||||
Icon(imageVector = Icons.AutoMirrored.Default.OpenInNew, contentDescription = null, Modifier.padding(10.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
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
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
|
||||
class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Settings) {
|
||||
@Composable
|
||||
@@ -98,6 +98,34 @@ class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Setti
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun ConfigBooleanRow(getValue: () -> Boolean, setValue: (Boolean) -> Unit, label: String) {
|
||||
var value by remember { mutableStateOf(getValue()) }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
value = !value
|
||||
setValue(value)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(text = label, fontSize = 16.sp)
|
||||
}
|
||||
Checkbox(checked = value, onCheckedChange = {
|
||||
value = it
|
||||
setValue(it)
|
||||
}, modifier = Modifier.padding(5.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column {
|
||||
@@ -105,12 +133,17 @@ class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Setti
|
||||
ConfigEditRow(
|
||||
getValue = { sharedConfig.snapchatPackageName },
|
||||
setValue = { sharedConfig.snapchatPackageName = it },
|
||||
label = "Snapchat package name"
|
||||
label = "Override Snapchat package name"
|
||||
)
|
||||
ConfigEditRow(
|
||||
getValue = { sharedConfig.snapEnhancePackageName },
|
||||
setValue = { sharedConfig.snapEnhancePackageName = it },
|
||||
label = "SnapEnhance package name"
|
||||
label = "Override SnapEnhance package name"
|
||||
)
|
||||
ConfigBooleanRow(
|
||||
getValue = { sharedConfig.useRootInstaller },
|
||||
setValue = { sharedConfig.useRootInstaller = it },
|
||||
label = "Use root installer"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl.download
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
@@ -18,11 +18,12 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import com.topjohnwu.superuser.Shell
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.data.download.InstallStage
|
||||
import me.rhunk.snapenhance.manager.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.HomeTab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.impl.HomeTab
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
@@ -31,16 +32,18 @@ import java.io.File
|
||||
class InstallPackageTab : Tab("install_app") {
|
||||
private lateinit var installPackageIntentLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var uninstallPackageIntentLauncher: ActivityResultLauncher<Intent>
|
||||
private var uninstallPackageCallback: ((resultCode: Int, data: Intent?) -> Unit)? = null
|
||||
private var installPackageCallback: ((resultCode: Int, data: Intent?) -> Unit)? = null
|
||||
private var uninstallPackageCallback: ((resultCode: Int) -> Unit)? = null
|
||||
private var installPackageCallback: ((resultCode: Int) -> Unit)? = null
|
||||
|
||||
private val hasRoot get() = sharedConfig.useRootInstaller
|
||||
|
||||
override fun init(activity: ComponentActivity) {
|
||||
super.init(activity)
|
||||
installPackageIntentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
installPackageCallback?.invoke(it.resultCode, it.data)
|
||||
installPackageCallback?.invoke(it.resultCode)
|
||||
}
|
||||
uninstallPackageIntentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
uninstallPackageCallback?.invoke(it.resultCode, it.data)
|
||||
uninstallPackageCallback?.invoke(it.resultCode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,9 +138,36 @@ class InstallPackageTab : Tab("install_app") {
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallPackageRoot(): Boolean {
|
||||
val result = Shell.su("pm uninstall $appPackage").exec()
|
||||
if (result.isSuccess) {
|
||||
return true
|
||||
}
|
||||
toast("Root uninstall failed: ${result.out}")
|
||||
return false
|
||||
}
|
||||
|
||||
fun installPackageRoot(): Boolean {
|
||||
val result = Shell.su(
|
||||
"cp \"${downloadedFile!!.absolutePath}\" /data/local/tmp/",
|
||||
"pm install -r \"/data/local/tmp/${downloadedFile!!.name}\"",
|
||||
"rm /data/local/tmp/${downloadedFile!!.name}"
|
||||
).exec()
|
||||
if (result.isSuccess) {
|
||||
installStage = InstallStage.DONE
|
||||
return true
|
||||
}
|
||||
toast("Root install failed: ${result.out}")
|
||||
return false
|
||||
}
|
||||
|
||||
fun installPackage() {
|
||||
installStage = InstallStage.INSTALLING
|
||||
installPackageCallback = resultCallbacks@{ code, _ ->
|
||||
if (hasRoot && installPackageRoot()) {
|
||||
downloadedFile?.delete()
|
||||
return
|
||||
}
|
||||
installPackageCallback = resultCallbacks@{ code ->
|
||||
installStage = if (code != ComponentActivity.RESULT_OK) {
|
||||
InstallStage.ERROR
|
||||
} else {
|
||||
@@ -163,11 +193,15 @@ class InstallPackageTab : Tab("install_app") {
|
||||
}
|
||||
if (shouldUninstall) {
|
||||
installStage = InstallStage.UNINSTALLING
|
||||
if (hasRoot && uninstallPackageRoot()) {
|
||||
installPackage()
|
||||
return@launch
|
||||
}
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
|
||||
data = "package:$appPackage".toUri()
|
||||
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
}
|
||||
uninstallPackageCallback = resultCallback@{ resultCode, _ ->
|
||||
uninstallPackageCallback = resultCallback@{ resultCode ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) {
|
||||
installStage = InstallStage.ERROR
|
||||
downloadedFile?.delete()
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.foundation.layout.*
|
||||
@@ -20,60 +20,64 @@ 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.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.components.DowngradeNoticeDialog
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
import okio.use
|
||||
import java.io.File
|
||||
|
||||
class LSPatchTab : Tab("lspatch") {
|
||||
private lateinit var downloadItem: DownloadItem
|
||||
private var localItemFile: File? = null
|
||||
private var downloadItem: DownloadItem? = null
|
||||
private var snapEnhanceModule: File? = null
|
||||
private var patchedApk by mutableStateOf<File?>(null)
|
||||
private val apkMirror = APKMirror()
|
||||
|
||||
private fun patch(log: (Any?) -> Unit, onProgress: (Float) -> Unit) {
|
||||
log("Fetching download link for ${downloadItem.title}...")
|
||||
val downloadLink = apkMirror.fetchDownloadLink(downloadItem.downloadPage) ?: run {
|
||||
log("== Failed to fetch download link ==")
|
||||
return
|
||||
}
|
||||
var apkFile: File? = localItemFile
|
||||
|
||||
log("Downloading apk...")
|
||||
|
||||
val downloadResponse = apkMirror.okhttpClient.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(downloadLink)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
if (!downloadResponse.isSuccessful) {
|
||||
log("== Failed to download apk ==")
|
||||
log("Response code: ${downloadResponse.code}")
|
||||
return
|
||||
}
|
||||
|
||||
val apkFile = sharedConfig.apkCache.resolve("${downloadItem.hash}.apk")
|
||||
apkFile.outputStream().use { outputStream ->
|
||||
runCatching {
|
||||
downloadResponse.body.byteStream().use { inputStream ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var totalRead = 0L
|
||||
val totalSize = downloadResponse.body.contentLength()
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
totalRead += read
|
||||
onProgress(totalRead.toFloat() / totalSize.toFloat())
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
log("== Failed to download apk ==")
|
||||
log(it)
|
||||
downloadItem?.let {
|
||||
log("Fetching download link for ${it.title}...")
|
||||
val downloadLink = apkMirror.fetchDownloadLink(it.downloadPage) ?: run {
|
||||
log("== Failed to fetch download link ==")
|
||||
return
|
||||
}
|
||||
}
|
||||
log("Downloading apk...")
|
||||
|
||||
apkFile.renameTo(File(activity.externalCacheDir!!, "base.apk"))
|
||||
val downloadResponse = apkMirror.okhttpClient.newCall(
|
||||
okhttp3.Request.Builder()
|
||||
.url(downloadLink)
|
||||
.build()
|
||||
).execute()
|
||||
|
||||
if (!downloadResponse.isSuccessful) {
|
||||
log("== Failed to download apk ==")
|
||||
log("Response code: ${downloadResponse.code}")
|
||||
return
|
||||
}
|
||||
|
||||
apkFile = sharedConfig.apkCache.resolve("${it.hash}.apk")
|
||||
apkFile!!.outputStream().use { outputStream ->
|
||||
runCatching {
|
||||
downloadResponse.body.byteStream().use { inputStream ->
|
||||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
|
||||
var read: Int
|
||||
var totalRead = 0L
|
||||
val totalSize = downloadResponse.body.contentLength()
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
totalRead += read
|
||||
onProgress(totalRead.toFloat() / totalSize.toFloat())
|
||||
}
|
||||
}
|
||||
}.onFailure { throwable ->
|
||||
log("== Failed to download apk ==")
|
||||
log(throwable)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
apkFile!!.renameTo(File(activity.externalCacheDir!!, "base.apk"))
|
||||
}
|
||||
|
||||
log("== Downloaded apk ==")
|
||||
snapEnhanceModule?.let { module ->
|
||||
@@ -84,13 +88,13 @@ class LSPatchTab : Tab("lspatch") {
|
||||
})
|
||||
|
||||
log("== Patching apk ==")
|
||||
val outputFiles = lsPatch.patchSplits(listOf(apkFile))
|
||||
val outputFiles = lsPatch.patchSplits(listOf(apkFile!!))
|
||||
|
||||
patchedApk = outputFiles["base.apk"] ?: run {
|
||||
log("== Failed to patch apk ==")
|
||||
return
|
||||
}
|
||||
return@let
|
||||
return
|
||||
}
|
||||
patchedApk = apkFile
|
||||
}
|
||||
@@ -98,7 +102,8 @@ class LSPatchTab : Tab("lspatch") {
|
||||
@Composable
|
||||
@Suppress("DEPRECATION")
|
||||
override fun Content() {
|
||||
this.downloadItem = remember { getArguments()?.getParcelable<DownloadItem>("downloadItem") } ?: return
|
||||
this.localItemFile = remember { getArguments()?.getString("localItemFile")?.let { File(it) } }
|
||||
this.downloadItem = remember { getArguments()?.getParcelable("downloadItem") }
|
||||
this.snapEnhanceModule = remember {
|
||||
getArguments()?.getString("modulePath")?.let {
|
||||
File(it)
|
||||
@@ -111,7 +116,7 @@ class LSPatchTab : Tab("lspatch") {
|
||||
var status by remember { mutableStateOf("") }
|
||||
var progress by remember { mutableFloatStateOf(-1f) }
|
||||
|
||||
LaunchedEffect(this.downloadItem.hash) {
|
||||
LaunchedEffect(this.snapEnhanceModule) {
|
||||
patchedApk = null
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
@@ -1,4 +1,4 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
@@ -26,8 +26,8 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.data.download.SEArtifact
|
||||
import me.rhunk.snapenhance.manager.data.download.SEVersion
|
||||
import me.rhunk.snapenhance.manager.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.components.DowngradeNoticeDialog
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.text.SimpleDateFormat
|
||||
@@ -1,10 +1,12 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
package me.rhunk.snapenhance.manager.ui.tab.impl.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.DeleteForever
|
||||
@@ -21,8 +23,11 @@ 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.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.lspatch.config.Constants
|
||||
import me.rhunk.snapenhance.manager.ui.components.ConfirmationDialog
|
||||
import me.rhunk.snapenhance.manager.ui.tab.Tab
|
||||
import java.io.File
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
class SnapchatPatchTab : Tab("snapchat_download") {
|
||||
@@ -145,10 +150,71 @@ class SnapchatPatchTab : Tab("snapchat_download") {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
var showSelectSnapchatVersionDialog by remember { mutableStateOf(false) }
|
||||
var showRestoreMenuDialog by remember { mutableStateOf(false) }
|
||||
|
||||
var selectedSnapchatVersion by remember { mutableStateOf(null as DownloadItem?) }
|
||||
val installedSnapEnhanceVersion = remember { runCatching { activity.packageManager.getPackageInfo(
|
||||
sharedConfig.snapEnhancePackageName, 0) }.getOrNull() }
|
||||
|
||||
val installedSnapchatPackage = remember { runCatching { activity.packageManager.getPackageInfo(
|
||||
sharedConfig.snapchatPackageName, 0) }.getOrNull() }
|
||||
val isInstalledSnapchatPatched = remember { installedSnapchatPackage?.applicationInfo?.appComponentFactory == Constants.PROXY_APP_COMPONENT_FACTORY }
|
||||
val isSnapchatNotSplitConfig = remember {
|
||||
installedSnapchatPackage?.applicationInfo?.let { it.splitSourceDirs == null || it.splitSourceDirs?.isEmpty() == true } ?: false
|
||||
}
|
||||
|
||||
if (showRestoreMenuDialog) {
|
||||
fun triggerSnapchatInstallation(shouldUninstall: Boolean) {
|
||||
val apkFile = File(installedSnapchatPackage?.applicationInfo?.sourceDir ?: return).also {
|
||||
if (!it.exists()) return
|
||||
}
|
||||
toast("Extracting origin apk")
|
||||
val originApk = File.createTempFile("origin", ".apk", activity.externalCacheDirs.first()).also {
|
||||
it.deleteOnExit()
|
||||
}
|
||||
ZipFile(apkFile).let { zipFile ->
|
||||
zipFile.getEntry("assets/lspatch/origin.apk")?.apply {
|
||||
originApk.outputStream().use { output ->
|
||||
zipFile.getInputStream(this).copyTo(output)
|
||||
}
|
||||
} ?: run {
|
||||
toast("Failed to extract origin apk")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
showRestoreMenuDialog = false
|
||||
|
||||
navigation.navigateTo(InstallPackageTab::class, args = Bundle().apply {
|
||||
putString("downloadPath", originApk.absolutePath)
|
||||
putString("appPackage", sharedConfig.snapchatPackageName)
|
||||
putBoolean("uninstall", shouldUninstall)
|
||||
}, noHistory = true)
|
||||
}
|
||||
|
||||
AlertDialog(onDismissRequest = { showRestoreMenuDialog = false }) {
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp).fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
triggerSnapchatInstallation(true)
|
||||
}) {
|
||||
Text("Uninstall & Install")
|
||||
}
|
||||
Button(onClick = {
|
||||
triggerSnapchatInstallation(false)
|
||||
}) {
|
||||
Text("Update")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (showSelectSnapchatVersionDialog) {
|
||||
AlertDialog(onDismissRequest = { showSelectSnapchatVersionDialog = false }) {
|
||||
SelectSnapchatVersionDialog {
|
||||
@@ -159,11 +225,11 @@ class SnapchatPatchTab : Tab("snapchat_download") {
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
Text("Select a version to download and patch")
|
||||
Text("Patch Snapchat")
|
||||
|
||||
ElevatedCard(
|
||||
modifier = Modifier.padding(10.dp),
|
||||
@@ -203,6 +269,7 @@ class SnapchatPatchTab : Tab("snapchat_download") {
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 10.dp, bottom = 10.dp, start = 20.dp, end = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(5.dp)
|
||||
) {
|
||||
Button(
|
||||
@@ -218,16 +285,42 @@ class SnapchatPatchTab : Tab("snapchat_download") {
|
||||
Text("Download & Patch")
|
||||
}
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = selectedSnapchatVersion != null,
|
||||
onClick = {
|
||||
navigation.navigateTo(LSPatchTab::class, args = Bundle().apply {
|
||||
putParcelable("downloadItem", selectedSnapchatVersion)
|
||||
}, noHistory = true)
|
||||
if (isSnapchatNotSplitConfig) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
navigation.navigateTo(LSPatchTab::class, args = Bundle().apply {
|
||||
putString("localItemFile", installedSnapchatPackage?.applicationInfo?.sourceDir ?: return@apply)
|
||||
putString("modulePath", installedSnapEnhanceVersion?.applicationInfo?.sourceDir ?: return@apply)
|
||||
}, noHistory = true)
|
||||
}
|
||||
) {
|
||||
Text("Patch from existing installation")
|
||||
}
|
||||
}
|
||||
|
||||
if (isInstalledSnapchatPatched) {
|
||||
Text("Restore Snapchat", modifier = Modifier.padding(20.dp))
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = selectedSnapchatVersion != null,
|
||||
onClick = {
|
||||
navigation.navigateTo(LSPatchTab::class, args = Bundle().apply {
|
||||
putParcelable("downloadItem", selectedSnapchatVersion)
|
||||
}, noHistory = true)
|
||||
}
|
||||
) {
|
||||
Text("Install/Restore Original Snapchat")
|
||||
}
|
||||
|
||||
if (isSnapchatNotSplitConfig) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = { showRestoreMenuDialog = true }
|
||||
) {
|
||||
Text("Restore Snapchat from existing installation")
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text("Install/Restore Original Snapchat")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ dependencyResolutionManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user