diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 077694a7..2614b470 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index b2120c67..bfb814b2 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -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) diff --git a/manager/proguard-rules.pro b/manager/proguard-rules.pro index 806f959a..6e9225ad 100644 --- a/manager/proguard-rules.pro +++ b/manager/proguard-rules.pro @@ -1,3 +1,3 @@ -dontwarn com.google.errorprone.annotations.** -dontwarn com.google.auto.value.** --keep enum me.rhunk.snapenhance.manager.** { *; } \ No newline at end of file +-keep class me.rhunk.snapenhance.manager.ui.tab.** { *; } \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/SharedConfig.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/SharedConfig.kt index 3b482a15..53541aaf 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/SharedConfig.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/data/SharedConfig.kt @@ -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() } \ No newline at end of file diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/MainActivity.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/MainActivity.kt index 85afeaa8..2bc3b8bd 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/MainActivity.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/MainActivity.kt @@ -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 { diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt index 4d2ddb97..fbf9945d 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Navigation.kt @@ -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 diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/Tab.kt similarity index 94% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/Tab.kt index 10d551d8..3c7487fe 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/Tab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/Tab.kt @@ -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( diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/HomeTab.kt similarity index 88% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/HomeTab.kt index d6b62437..351b4c4f 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/HomeTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/HomeTab.kt @@ -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)) } } diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/SettingsTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/SettingsTab.kt similarity index 76% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/SettingsTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/SettingsTab.kt index f41d29de..8db18b7e 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/SettingsTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/SettingsTab.kt @@ -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" ) } } diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/InstallPackageTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/InstallPackageTab.kt similarity index 81% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/InstallPackageTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/InstallPackageTab.kt index 12d4bede..bd7578c8 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/InstallPackageTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/InstallPackageTab.kt @@ -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 private lateinit var uninstallPackageIntentLauncher: ActivityResultLauncher - 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() diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/LSPatchTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/LSPatchTab.kt similarity index 72% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/LSPatchTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/LSPatchTab.kt index eabc54e7..d5e35933 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/LSPatchTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/LSPatchTab.kt @@ -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(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") } ?: 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 { diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SEDownloadTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SEDownloadTab.kt similarity index 99% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SEDownloadTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SEDownloadTab.kt index 61cf51e9..3343a312 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SEDownloadTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SEDownloadTab.kt @@ -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 diff --git a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SnapchatPatchTab.kt similarity index 63% rename from manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt rename to manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SnapchatPatchTab.kt index 17d04095..2083fe79 100644 --- a/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/download/SnapchatPatchTab.kt +++ b/manager/src/main/kotlin/me/rhunk/snapenhance/manager/ui/tab/impl/download/SnapchatPatchTab.kt @@ -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") } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 9adbce0f..8f585870 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,6 +12,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { url = uri("https://jitpack.io") } } }