feat(manager): SE build downloader
This commit is contained in:
@@ -3,6 +3,7 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
||||
plugins {
|
||||
alias(libs.plugins.androidApplication)
|
||||
alias(libs.plugins.kotlinAndroid)
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -74,6 +75,7 @@ dependencies {
|
||||
implementation(libs.guava)
|
||||
implementation(libs.apksig)
|
||||
implementation(libs.gson)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.androidx.material3)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
|
||||
|
||||
<application
|
||||
android:label="SE Manager"
|
||||
tools:targetApi="34"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:icon="@android:drawable/ic_input_add">
|
||||
<activity android:name=".ui.MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -16,5 +19,15 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="me.rhunk.snapenhance.manager.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -0,0 +1,15 @@
|
||||
package me.rhunk.snapenhance.manager.data
|
||||
|
||||
import android.content.Context
|
||||
|
||||
class SharedConfig(
|
||||
context: Context
|
||||
) {
|
||||
private val sharedPreferences = context.getSharedPreferences("snapenhance", Context.MODE_PRIVATE)
|
||||
|
||||
var snapchatPackageName get() = sharedPreferences.getString("snapchatPackageName", "com.snapchat.android")?.takeIf { it.isNotEmpty() }
|
||||
set(value) = sharedPreferences.edit().putString("snapchatPackageName", value).apply()
|
||||
|
||||
var snapEnhancePackageName get() = sharedPreferences.getString("snapEnhancePackageName", "me.rhunk.snapenhance")?.takeIf { it.isNotEmpty() }
|
||||
set(value) = sharedPreferences.edit().putString("snapEnhancePackageName", value).apply()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package me.rhunk.snapenhance.manager.data.download
|
||||
|
||||
enum class InstallStage {
|
||||
DOWNLOADING,
|
||||
UNINSTALLING,
|
||||
INSTALLING,
|
||||
DONE,
|
||||
ERROR;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.rhunk.snapenhance.manager.data.download
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
class SEArtifact(
|
||||
val fileName: String,
|
||||
val size: Long,
|
||||
val downloadUrl: String,
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,7 @@
|
||||
package me.rhunk.snapenhance.manager.data.download
|
||||
|
||||
data class SEVersion(
|
||||
val versionName: String,
|
||||
val releaseDate: String,
|
||||
val downloadAssets: Map<String, SEArtifact>,
|
||||
)
|
||||
@@ -4,106 +4,66 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.BuildConfig
|
||||
import me.rhunk.snapenhance.manager.lspatch.LSPatch
|
||||
import java.io.File
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import me.rhunk.snapenhance.manager.data.SharedConfig
|
||||
import me.rhunk.snapenhance.manager.ui.tab.HomeTab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.SettingsTab
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
companion object{
|
||||
private val primaryTabs = listOf(HomeTab::class, SettingsTab::class)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val tabs = primaryTabs.mapNotNull {
|
||||
runCatching { it.java.constructors.first().newInstance() as Tab }.getOrNull()
|
||||
}.toMutableList().apply {
|
||||
forEach { it.init(this@MainActivity) }
|
||||
fun addNestedTabsRecursively(tabs: List<Tab>) {
|
||||
tabs.forEach { tab ->
|
||||
add(tab)
|
||||
addNestedTabsRecursively(tab.nestedTabs)
|
||||
}
|
||||
}
|
||||
toList().forEach { addNestedTabsRecursively(it.nestedTabs) }
|
||||
}
|
||||
setContent {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
MaterialTheme(
|
||||
colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
if (isSystemInDarkTheme()) dynamicDarkColorScheme(LocalContext.current)
|
||||
else dynamicLightColorScheme(LocalContext.current)
|
||||
} else MaterialTheme.colorScheme
|
||||
} else darkColorScheme()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val logs = remember { mutableStateListOf<String>() }
|
||||
fun printLog(data: Any) {
|
||||
when (data) {
|
||||
is Throwable -> {
|
||||
logs += data.message.toString()
|
||||
logs += StringWriter().apply {
|
||||
data.printStackTrace(PrintWriter(this))
|
||||
}.toString()
|
||||
val navHostController = rememberNavController()
|
||||
val sharedConfig = remember { SharedConfig(this) }
|
||||
|
||||
|
||||
val navigation = remember {
|
||||
Navigation(
|
||||
navHostController = navHostController,
|
||||
tabs = tabs,
|
||||
defaultTab = HomeTab::class
|
||||
).also {
|
||||
tabs.forEach { tab ->
|
||||
tab.navigation = it
|
||||
tab.sharedConfig = sharedConfig
|
||||
}
|
||||
else -> logs += data.toString()
|
||||
}
|
||||
}
|
||||
|
||||
val scrollState = rememberLazyListState(0)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
modifier = Modifier.padding(10.dp)
|
||||
Scaffold(
|
||||
bottomBar = { navigation.BottomBar() },
|
||||
topBar = { navigation.TopBar() },
|
||||
floatingActionButton = { navigation.FloatingActionButtons() },
|
||||
floatingActionButtonPosition = FabPosition.End,
|
||||
) {
|
||||
Text(text = "SE Manager")
|
||||
|
||||
Button(onClick = {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
val lspatch = LSPatch(
|
||||
context,
|
||||
mapOf(
|
||||
BuildConfig.APPLICATION_ID to File(context.packageManager.getPackageInfo(
|
||||
BuildConfig.APPLICATION_ID, 0).applicationInfo.sourceDir)
|
||||
)
|
||||
) { printLog(it) }
|
||||
lspatch.patch(
|
||||
File(context.packageManager.getPackageInfo("com.snapchat.android", 0).applicationInfo.sourceDir),
|
||||
File(context.filesDir, "patched.apk")
|
||||
)
|
||||
}.onFailure { printLog(it) }
|
||||
}
|
||||
}) {
|
||||
Text(text = "Test patch apk")
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = scrollState,
|
||||
modifier = Modifier.fillMaxWidth().padding(5.dp).height(500.dp).border(1.dp, color = Color.Black),
|
||||
content = {
|
||||
items(logs) {
|
||||
Text(text = it, modifier = Modifier.padding(2.dp))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LaunchedEffect(logs.size) {
|
||||
scrollState.scrollToItem((logs.size - 1).coerceAtLeast(0))
|
||||
}
|
||||
navigation.NavigationHost(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
package me.rhunk.snapenhance.manager.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
|
||||
class Navigation(
|
||||
val navHostController: NavHostController,
|
||||
private val tabs: List<Tab>,
|
||||
private val defaultTab: KClass<out Tab>
|
||||
) {
|
||||
|
||||
@Composable
|
||||
fun TopBar() {
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FloatingActionButtons() {
|
||||
|
||||
}
|
||||
|
||||
fun navigateTo(tab: KClass<out Tab>) {
|
||||
navHostController.navigate(tabs.first { it::class == tab }.route)
|
||||
}
|
||||
|
||||
fun navigateTo(tab: KClass<out Tab>, noHistory: Boolean) {
|
||||
navHostController.navigate(tabs.first { it::class == tab }.route) {
|
||||
restoreState = false
|
||||
launchSingleTop = true
|
||||
popUpTo(navHostController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(tab: KClass<out Tab>, args: Bundle) {
|
||||
navHostController.currentBackStackEntry?.savedStateHandle?.apply {
|
||||
set("args", args)
|
||||
}
|
||||
navigateTo(tab)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavigationHost(
|
||||
innerPadding: PaddingValues
|
||||
) {
|
||||
NavHost(
|
||||
navHostController,
|
||||
startDestination = tabs.first { it::class == defaultTab }.route,
|
||||
Modifier.padding(innerPadding),
|
||||
enterTransition = { fadeIn(tween(200)) },
|
||||
exitTransition = { fadeOut(tween(200)) }
|
||||
) {
|
||||
tabs.forEach { tab ->
|
||||
tab.build(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun BottomBar() {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by navHostController.currentBackStackEntryAsState()
|
||||
|
||||
remember { tabs.filter { it.isPrimary } }.forEach { tab ->
|
||||
val tabSubRoutes = remember { tab.nestedTabs.map { it.route } }
|
||||
NavigationBarItem(
|
||||
selected = navBackStackEntry?.destination?.hierarchy?.any { it.route == tab.route || tabSubRoutes.contains(it.route) } == true,
|
||||
alwaysShowLabel = false,
|
||||
modifier = Modifier.fillMaxHeight(),
|
||||
icon = {
|
||||
Icon(imageVector = tab.icon!!, contentDescription = null)
|
||||
},
|
||||
label = {
|
||||
Text(
|
||||
textAlign = TextAlign.Center,
|
||||
softWrap = false,
|
||||
fontSize = 12.sp,
|
||||
modifier = Modifier.wrapContentWidth(unbounded = true),
|
||||
text = tab.route
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
navHostController.navigate(tab.route) {
|
||||
popUpTo(navHostController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package me.rhunk.snapenhance.manager.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import me.rhunk.snapenhance.manager.data.SharedConfig
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
open class Tab(
|
||||
val route: String,
|
||||
val isPrimary: Boolean = false,
|
||||
val icon: ImageVector? = null,
|
||||
) {
|
||||
lateinit var activity: ComponentActivity
|
||||
val nestedTabs = mutableListOf<Tab>()
|
||||
|
||||
fun getNestedTab(tab: KClass<out Tab>) = nestedTabs.firstOrNull { it::class == tab }
|
||||
|
||||
fun registerNestedTab(tab: KClass<out Tab>) = nestedTabs.add((tab.java.constructors.first().newInstance() as Tab).also {
|
||||
it.init(activity)
|
||||
})
|
||||
|
||||
lateinit var navigation: Navigation
|
||||
lateinit var sharedConfig: SharedConfig
|
||||
|
||||
fun getArguments() = navigation.navHostController.previousBackStackEntry?.savedStateHandle?.get<Bundle>("args")
|
||||
|
||||
open fun init(activity: ComponentActivity) {
|
||||
this.activity = activity
|
||||
}
|
||||
|
||||
open fun build(navGraphBuilder: NavGraphBuilder) {
|
||||
navGraphBuilder.composable(route) {
|
||||
Content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
open fun TopBar() {}
|
||||
|
||||
@Composable
|
||||
open fun FloatingActionButtons() {}
|
||||
|
||||
@Composable
|
||||
open fun Content() {}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab
|
||||
|
||||
import android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE
|
||||
import android.content.pm.PackageInfo
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.BuildConfig
|
||||
import me.rhunk.snapenhance.manager.lspatch.config.Constants
|
||||
import me.rhunk.snapenhance.manager.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.download.SEDownloadTab
|
||||
|
||||
class HomeTab : Tab("home", true, icon = Icons.Default.Home) {
|
||||
override fun init(activity: ComponentActivity) {
|
||||
super.init(activity)
|
||||
registerNestedTab(SEDownloadTab::class)
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var snapchatAppInfo by remember { mutableStateOf(null as PackageInfo?) }
|
||||
var snapEnhanceInfo by remember { mutableStateOf(null as PackageInfo?) }
|
||||
|
||||
Column {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(text = "Snapchat", fontSize = 24.sp, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold)
|
||||
snapchatAppInfo?.let {
|
||||
Text(text = "${it.versionName} (${it.longVersionCode})", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = androidx.compose.ui.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)
|
||||
}
|
||||
OutlinedButton(onClick = {
|
||||
|
||||
}) {
|
||||
Text(text = if (isLSPatched) "Repatch" else "Patch")
|
||||
}
|
||||
} ?: run {
|
||||
Text(text = "Not installed", fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
navigation.navigateTo(SEDownloadTab::class)
|
||||
}
|
||||
.padding(16.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(text = "SnapEnhance", fontSize = 24.sp, color = MaterialTheme.colorScheme.onSurface, fontWeight = FontWeight.Bold)
|
||||
snapEnhanceInfo?.let {
|
||||
Text(text = "${it.versionName} (${it.longVersionCode}) - ${if ((it.applicationInfo.flags and FLAG_DEBUGGABLE) != 0) "Debug" else "Release"}", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
snapEnhanceInfo?.let {
|
||||
Text(text = "Installed", 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SideEffect {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
runCatching {
|
||||
snapchatAppInfo = runCatching {
|
||||
context.packageManager.getPackageInfo(sharedConfig.snapchatPackageName ?: "com.snapchat.android", 0)
|
||||
}.getOrNull()
|
||||
snapEnhanceInfo = runCatching {
|
||||
context.packageManager.getPackageInfo(sharedConfig.snapEnhancePackageName ?: BuildConfig.APPLICATION_ID, 0)
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.text.TextRange
|
||||
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
|
||||
|
||||
class SettingsTab : Tab("settings", isPrimary = true, icon = Icons.Default.Settings) {
|
||||
@Composable
|
||||
private fun ConfigEditRow(getValue: () -> String?, setValue: (String) -> Unit, label: String) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDialog) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
Dialog(onDismissRequest = {
|
||||
showDialog = false
|
||||
}) {
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(text = label)
|
||||
var textFieldValue by remember { mutableStateOf((getValue() ?: "").let {
|
||||
TextFieldValue(it, TextRange(it.length))
|
||||
}) }
|
||||
|
||||
TextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = {
|
||||
textFieldValue = it
|
||||
},
|
||||
modifier = Modifier
|
||||
.focusRequester(focusRequester)
|
||||
.onGloballyPositioned {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
Button(onClick = {
|
||||
showDialog = false
|
||||
}) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
Button(onClick = {
|
||||
setValue(textFieldValue.text)
|
||||
showDialog = false
|
||||
}) {
|
||||
Text(text = "Save")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
showDialog = true
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(text = label, fontSize = 16.sp)
|
||||
Text(text = getValue() ?: "(Not specified)", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
Icon(imageVector = Icons.Default.Edit, contentDescription = null, modifier = Modifier.padding(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ConfigEditRow(
|
||||
getValue = { sharedConfig.snapchatPackageName },
|
||||
setValue = { sharedConfig.snapchatPackageName = it },
|
||||
label = "Snapchat package name"
|
||||
)
|
||||
ConfigEditRow(
|
||||
getValue = { sharedConfig.snapEnhancePackageName },
|
||||
setValue = { sharedConfig.snapEnhancePackageName = it },
|
||||
label = "SnapEnhance package name"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.data.download.InstallStage
|
||||
import me.rhunk.snapenhance.manager.data.download.SEArtifact
|
||||
import me.rhunk.snapenhance.manager.ui.Tab
|
||||
import me.rhunk.snapenhance.manager.ui.tab.HomeTab
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.io.File
|
||||
|
||||
|
||||
class InstallAppTab : 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
|
||||
|
||||
override fun init(activity: ComponentActivity) {
|
||||
super.init(activity)
|
||||
installPackageIntentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
installPackageCallback?.invoke(it.resultCode, it.data)
|
||||
}
|
||||
uninstallPackageIntentLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
uninstallPackageCallback?.invoke(it.resultCode, it.data)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun downloadArtifact(artifact: SEArtifact, progress: (Int) -> Unit): File? {
|
||||
val endpoint = Request.Builder().url(artifact.downloadUrl).build()
|
||||
val response = OkHttpClient().newCall(endpoint).execute()
|
||||
if (!response.isSuccessful) throw Throwable("Failed to download artifact: ${response.code}")
|
||||
|
||||
return response.body.byteStream().use { input ->
|
||||
val file = activity.externalCacheDirs.first().resolve(artifact.fileName)
|
||||
runCatching {
|
||||
file.outputStream().use { output ->
|
||||
val buffer = ByteArray(4 * 1024)
|
||||
var read: Int
|
||||
var totalRead = 0L
|
||||
val totalSize = response.body.contentLength()
|
||||
while (input.read(buffer).also { read = it } != -1) {
|
||||
output.write(buffer, 0, read)
|
||||
totalRead += read
|
||||
progress((totalRead * 100 / totalSize).toInt())
|
||||
}
|
||||
}
|
||||
file
|
||||
}.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@Suppress("DEPRECATION")
|
||||
override fun Content() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
var installStage by remember { mutableStateOf(InstallStage.DOWNLOADING) }
|
||||
var downloadProgress by remember { mutableIntStateOf(0) }
|
||||
var downloadedFile by remember { mutableStateOf<File?>(null) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
uninstallPackageCallback = null
|
||||
installPackageCallback = null
|
||||
}
|
||||
|
||||
val artifact = getArguments()?.getParcelable<SEArtifact>("artifact") ?: return
|
||||
val appPackage = getArguments()?.getString("appPackage") ?: return
|
||||
val shouldUninstall = getArguments()?.getBoolean("uninstall") ?: false
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
if (installStage != InstallStage.DONE && installStage != InstallStage.ERROR) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
when (installStage) {
|
||||
InstallStage.DOWNLOADING -> {
|
||||
Text(text = "Downloading ${artifact.fileName}... ($downloadProgress%)")
|
||||
}
|
||||
InstallStage.UNINSTALLING -> {
|
||||
Text(text = "Uninstalling app $appPackage...")
|
||||
}
|
||||
InstallStage.INSTALLING -> {
|
||||
Text(text = "Installing ${artifact.fileName}...")
|
||||
}
|
||||
InstallStage.DONE -> {
|
||||
LaunchedEffect(Unit) {
|
||||
navigation.navigateTo(HomeTab::class, noHistory = true)
|
||||
Toast.makeText(context, "${artifact.fileName} installed successfully!", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
InstallStage.ERROR -> Text(text = "Failed to install ${artifact.fileName}. Check logcat for more details.")
|
||||
}
|
||||
}
|
||||
|
||||
fun installPackage() {
|
||||
installStage = InstallStage.INSTALLING
|
||||
installPackageCallback = resultCallbacks@{ code, _ ->
|
||||
installStage = if (code != ComponentActivity.RESULT_OK) {
|
||||
InstallStage.ERROR
|
||||
} else {
|
||||
InstallStage.DONE
|
||||
}
|
||||
downloadedFile?.delete()
|
||||
}
|
||||
|
||||
installPackageIntentLauncher.launch(Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
|
||||
println(context)
|
||||
data = FileProvider.getUriForFile(context, "me.rhunk.snapenhance.manager.provider", downloadedFile!!)
|
||||
setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
downloadedFile = downloadArtifact(artifact) {
|
||||
downloadProgress = it
|
||||
} ?: run {
|
||||
installStage = InstallStage.ERROR
|
||||
return@launch
|
||||
}
|
||||
if (shouldUninstall) {
|
||||
installStage = InstallStage.UNINSTALLING
|
||||
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE).apply {
|
||||
data = "package:$appPackage".toUri()
|
||||
putExtra(Intent.EXTRA_RETURN_RESULT, true)
|
||||
}
|
||||
uninstallPackageCallback = resultCallback@{ resultCode, _ ->
|
||||
if (resultCode != ComponentActivity.RESULT_OK) {
|
||||
installStage = InstallStage.ERROR
|
||||
downloadedFile?.delete()
|
||||
return@resultCallback
|
||||
}
|
||||
installPackage()
|
||||
}
|
||||
uninstallPackageIntentLauncher.launch(intent)
|
||||
} else {
|
||||
installPackage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package me.rhunk.snapenhance.manager.ui.tab.download
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Android
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import me.rhunk.snapenhance.manager.BuildConfig
|
||||
import me.rhunk.snapenhance.manager.data.download.SEArtifact
|
||||
import me.rhunk.snapenhance.manager.data.download.SEVersion
|
||||
import me.rhunk.snapenhance.manager.ui.Tab
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
|
||||
class SEDownloadTab : Tab("se_downloads") {
|
||||
private fun fetchSEReleases(): List<SEVersion>? {
|
||||
return runCatching {
|
||||
val endpoint =
|
||||
Request.Builder().url("https://api.github.com/repos/rhunk/SnapEnhance/releases")
|
||||
.build()
|
||||
val response = OkHttpClient().newCall(endpoint).execute()
|
||||
if (!response.isSuccessful) return null
|
||||
|
||||
val releases = JsonParser.parseString(response.body.string()).asJsonArray.also {
|
||||
if (it.size() == 0) return null
|
||||
}
|
||||
val isoDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.getDefault())
|
||||
|
||||
releases.map { releaseObject ->
|
||||
val release = releaseObject.asJsonObject
|
||||
val versionName = release.getAsJsonPrimitive("tag_name").asString
|
||||
val releaseDate = release.getAsJsonPrimitive("published_at").asString.let { time ->
|
||||
isoDateFormat.parse(time)?.let { date ->
|
||||
SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(date)
|
||||
} ?: time
|
||||
}
|
||||
val downloadAssets = release.getAsJsonArray("assets").associate { asset ->
|
||||
val assetObject = asset.asJsonObject
|
||||
SEArtifact(
|
||||
fileName = assetObject.getAsJsonPrimitive("name").asString,
|
||||
size = assetObject.getAsJsonPrimitive("size").asLong,
|
||||
downloadUrl = assetObject.getAsJsonPrimitive("browser_download_url").asString
|
||||
).let { it.fileName to it }
|
||||
}
|
||||
SEVersion(versionName, releaseDate, downloadAssets)
|
||||
}
|
||||
}.onFailure {
|
||||
it.printStackTrace()
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
override fun init(activity: ComponentActivity) {
|
||||
super.init(activity)
|
||||
registerNestedTab(InstallAppTab::class)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DowngradeNoticeDialog(onDismiss: () -> Unit, onSuccess: () -> Unit) {
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
Text(text = "Downgrade Notice", fontSize = 24.sp)
|
||||
Text(text = "You are about to update the app. If you're installing an older version over a newer one, make sure you have CorePatch installed. Otherwise, you will need to uninstall and install.", fontSize = 12.sp)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceAround
|
||||
) {
|
||||
Button(onClick = onDismiss) {
|
||||
Text(text = "Cancel")
|
||||
}
|
||||
Button(onClick = onSuccess) {
|
||||
Text(text = "Continue")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val snapEnhanceReleases = remember {
|
||||
mutableStateOf(null as List<SEVersion>?)
|
||||
}
|
||||
|
||||
var selectedVersion by remember { mutableStateOf(null as SEVersion?) }
|
||||
var selectedArtifact by remember { mutableStateOf(null as SEArtifact?) }
|
||||
val appPackageName = remember { sharedConfig.snapEnhancePackageName ?: BuildConfig.APPLICATION_ID }
|
||||
val isAppInstalled = remember { runCatching { activity.packageManager.getPackageInfo(appPackageName, 0) != null }.getOrNull() != null }
|
||||
|
||||
var showDowngradeNotice by remember { mutableStateOf(false) }
|
||||
|
||||
fun triggerPackageInstallation(shouldUninstall: Boolean) {
|
||||
navigation.navigateTo(InstallAppTab::class, Bundle().apply {
|
||||
putParcelable("artifact", selectedArtifact)
|
||||
putString(
|
||||
"appPackage",
|
||||
sharedConfig.snapEnhancePackageName ?: BuildConfig.APPLICATION_ID
|
||||
)
|
||||
putBoolean("uninstall", shouldUninstall)
|
||||
})
|
||||
}
|
||||
|
||||
if (showDowngradeNotice) {
|
||||
Dialog(onDismissRequest = { showDowngradeNotice = false }) {
|
||||
DowngradeNoticeDialog(onDismiss = { showDowngradeNotice = false }, onSuccess = {
|
||||
triggerPackageInstallation(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(text = "SnapEnhance Builds")
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight(0.8f),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
item {
|
||||
if (snapEnhanceReleases.value == null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
items(snapEnhanceReleases.value ?: listOf()) { version ->
|
||||
OutlinedCard(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
selectedArtifact =
|
||||
if (selectedVersion != version) null else selectedArtifact
|
||||
selectedVersion = if (selectedVersion == version) null else version
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(text = version.versionName, fontSize = 24.sp)
|
||||
Text(text = "Release ${version.releaseDate}", fontSize = 12.sp)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(text = "${version.downloadAssets.size} assets", fontSize = 12.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedVersion?.takeIf { it == version }?.let { selVersion ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
selVersion.downloadAssets.values.forEach { artifact ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.border(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
.clickable {
|
||||
selectedArtifact =
|
||||
if (selectedArtifact == artifact) null else artifact
|
||||
}
|
||||
.background(
|
||||
if (selectedArtifact == artifact) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.surface,
|
||||
shape = MaterialTheme.shapes.medium
|
||||
)
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(imageVector = Icons.Default.Android, contentDescription = null, modifier = Modifier.padding(start = 2.dp, end = 2.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 13.dp)
|
||||
) {
|
||||
Text(text = artifact.fileName, fontSize = 15.sp)
|
||||
Text(
|
||||
text = "${artifact.size / 1024 / 1024} MB",
|
||||
fontSize = 10.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (isAppInstalled) {
|
||||
Button(
|
||||
onClick = {
|
||||
triggerPackageInstallation(true)
|
||||
},
|
||||
enabled = selectedVersion != null && selectedArtifact != null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = "Uninstall & Install")
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
if (isAppInstalled) {
|
||||
showDowngradeNotice = true
|
||||
} else {
|
||||
triggerPackageInstallation(false)
|
||||
}
|
||||
},
|
||||
enabled = selectedVersion != null && selectedArtifact != null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = if (isAppInstalled) "Update" else "Install")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
snapEnhanceReleases.value = fetchSEReleases()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
manager/src/main/res/xml/provider_paths.xml
Normal file
4
manager/src/main/res/xml/provider_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path name="external_files" path="."/>
|
||||
</paths>
|
||||
Reference in New Issue
Block a user