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