diff --git a/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt index 5c1c938..ade8017 100644 --- a/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt +++ b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt @@ -15,8 +15,6 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.platform.LocalContext import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.aliucord.manager.preferences.Prefs @@ -44,6 +42,13 @@ class MainActivity : ComponentActivity() { sharedPreferences = getPreferences(Context.MODE_PRIVATE) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData("package:com.aliucord.manager".toUri()) + + startActivity(intent) + } + setContent { ManagerTheme( isBlack = Prefs.useBlack.get(), @@ -53,17 +58,6 @@ class MainActivity : ComponentActivity() { theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK } ) { - val context = LocalContext.current - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { - SideEffect { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - .setData("package:com.aliucord.manager".toUri()) - - context.startActivity(intent) - } - } - ManagerScaffold() } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt index e4c1308..dc07817 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt @@ -9,27 +9,21 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.aliucord.manager.R import com.aliucord.manager.models.github.GithubUser -import com.aliucord.manager.utils.Github -import com.aliucord.manager.utils.httpClient -import io.ktor.client.call.* -import io.ktor.client.request.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -fun ContributorsCard() = ElevatedCard( +fun ContributorsCard( + contributors: List, +) = ElevatedCard( modifier = Modifier.wrapContentHeight().fillMaxWidth() ) { - val contributors = remember { mutableStateListOf() } - Column( modifier = Modifier.padding(16.dp).fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp) @@ -54,12 +48,6 @@ fun ContributorsCard() = ElevatedCard( contentAlignment = Alignment.Center, content = { CircularProgressIndicator() } ) - - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - contributors.addAll(httpClient.get(Github.contributorsUrl).body>().sortedByDescending { it.contributions }) - } - } } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt index 517d8bd..5cff35e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt @@ -13,21 +13,23 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.aliucord.manager.R import com.aliucord.manager.ui.components.about.ContributorsCard import com.aliucord.manager.ui.components.about.TeamCard +import com.aliucord.manager.ui.viewmodels.AboutViewModel import com.ramcosta.composedestinations.annotation.Destination @Destination @Composable fun AboutScreen() { val uriHandler = LocalUriHandler.current - + val viewModel: AboutViewModel = viewModel() Column( verticalArrangement = Arrangement.spacedBy(8.dp) ) { TeamCard() - ContributorsCard() + ContributorsCard(viewModel.contributors) Spacer(Modifier.weight(1f, true)) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt index 7a7249d..e1fea42 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt @@ -6,9 +6,6 @@ package com.aliucord.manager.ui.screens -import android.content.Intent -import android.net.Uri -import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* @@ -18,56 +15,29 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.core.net.toFile +import androidx.lifecycle.viewmodel.compose.viewModel import com.aliucord.manager.R -import com.aliucord.manager.models.github.Version import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.ui.components.PluginsList import com.aliucord.manager.ui.components.installer.DownloadMethod import com.aliucord.manager.ui.components.installer.InstallerDialog import com.aliucord.manager.ui.screens.destinations.CommitsScreenDestination import com.aliucord.manager.ui.screens.destinations.InstallerScreenDestination -import com.aliucord.manager.utils.* +import com.aliucord.manager.ui.viewmodels.HomeViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import io.ktor.client.request.* -import io.ktor.client.statement.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.serialization.decodeFromString @OptIn(ExperimentalMaterial3Api::class) @Destination(start = true) @Composable fun HomeScreen(navigator: DestinationsNavigator) { - val packageManager = LocalContext.current.packageManager - var supportedVersion by remember { mutableStateOf(null) } - val installedVersion = remember { - try { - packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName - } catch (th: Throwable) { - "-" - } - } - - LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - val version = json.decodeFromString(httpClient.get(Github.dataUrl).bodyAsText()) - - supportedVersion = "${version.versionName} - " + when (version.versionCode[3].toString()) { - "0" -> "Stable" - "1" -> "Beta" - "2" -> "Alpha" - else -> throw NoWhenBranchMatchedException() - } - } - } + val viewModel: HomeViewModel = viewModel() var showOptionsDialog by remember { mutableStateOf(false) } @@ -113,20 +83,20 @@ fun HomeScreen(navigator: DestinationsNavigator) { append("Supported version: ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(supportedVersion ?: "?") + append(viewModel.supportedVersion) } append("\nInstalled version: ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(installedVersion) + append(viewModel.installedVersion) } }) } - val (drawable, description) = when (installedVersion) { + val (drawable, description) = when (viewModel.installedVersion) { "-" -> R.drawable.ic_download_24dp to R.string.install - supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall + viewModel.supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall else -> R.drawable.ic_update_24dp to R.string.update } @@ -157,7 +127,7 @@ fun HomeScreen(navigator: DestinationsNavigator) { Text(stringResource(description)) } - if (installedVersion == "-") Button( + if (viewModel.installedVersion == "-") Button( modifier = Modifier.wrapContentSize(), onClick = { navigator.navigate(CommitsScreenDestination) }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) @@ -171,21 +141,16 @@ fun HomeScreen(navigator: DestinationsNavigator) { } } - if (installedVersion != "-") { + if (viewModel.installedVersion != "-") { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - val context = LocalContext.current - Button( modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), onClick = { - val packageURI = Uri.parse("package:${Prefs.packageName.get()}") - val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI) - - context.startActivity(uninstallIntent) + viewModel.uninstallAliucord() } ) { Icon( @@ -198,12 +163,7 @@ fun HomeScreen(navigator: DestinationsNavigator) { Button( modifier = Modifier.weight(1f), - onClick = { - packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let { - context.startActivity(it) - } ?: Toast.makeText(context, "Failed to launch Aliucord", Toast.LENGTH_LONG) - .show() - }, + onClick = viewModel::launchAliucord, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) ) { Icon( diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt index 1005ec2..a2b6247 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt @@ -5,217 +5,51 @@ package com.aliucord.manager.ui.screens -import android.content.Intent import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.* -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.LinearProgressIndicator 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 com.aliucord.libzip.Zip -import com.aliucord.manager.BuildConfig -import com.aliucord.manager.preferences.Prefs +import androidx.lifecycle.viewmodel.compose.viewModel import com.aliucord.manager.ui.screens.destinations.HomeScreenDestination -import com.aliucord.manager.utils.* +import com.aliucord.manager.ui.viewmodels.InstallViewModel import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.io.File -import java.io.FileNotFoundException @OptIn(ExperimentalFoundationApi::class) @Destination @Composable fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) { - var log by remember { mutableStateOf("") } - val context = LocalContext.current + val viewModel: InstallViewModel = viewModel() + + val navigateMain by viewModel.returnToHome.collectAsState(initial = false) + if (navigateMain) { + navigator.navigate(HomeScreenDestination) + } LaunchedEffect(Unit) { - launch(Dispatchers.IO) { - val supportedVersion = Github.version.versionCode + viewModel.startInstallation(apk) + } - val discordApk = apk ?: run { - log += "Checking for cached APK...\n" - - context.externalCacheDir!!.resolve("discord-${supportedVersion}.apk").also { file -> - val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0) - - if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also - - log += "Downloading Discord APK...\n" - - DownloadUtils.downloadDiscordApk(context, supportedVersion) - - log += "Done\n" - } - } - - val injector = context.externalCacheDir!!.resolve("Injector.dex").also { file -> - if (file.exists()) return@also - - log += "Downloading injector...\n" - - DownloadUtils.downloadInjector(context) - - log += "Done\n" - } - - val outputDir = aliucordDir.also { - if (!it.exists() && !it.mkdirs()) throw FileNotFoundException() - } - - val outputApk = outputDir.resolve("Aliucord.apk") - discordApk.copyTo(outputApk, true) - - log += "Repacking APK\n" - - var patched = false - var manifestBytes: ByteArray? - - with(Zip(outputApk.absolutePath, 6, 'r')) { - for (index in 0 until totalEntries) { - openEntryByIndex(index) - val name = entryName - closeEntry() - if (name == "classes5.dex") { - patched = true - break - } - } - - val cacheDir = context.cacheDir.absolutePath - - if (!patched) (1..3).forEach { i -> - openEntry("classes${if (i == 1) "" else i}.dex") - extractEntry("$cacheDir/classes${i + 1}.dex") - closeEntry() - } - - openEntry("AndroidManifest.xml") - manifestBytes = readEntry() - closeEntry() - - close() - } - - val assetManager = context.assets - - with(Zip(outputApk.absolutePath, 6, 'a')) { - if (patched) { - deleteEntry("classes.dex") - deleteEntry("classes5.dex") - deleteEntry("classes6.dex") - } else { - (1..3).forEach { i -> deleteEntry("classes${if (i == 1) "" else i}.dex") } - } - - deleteEntry("AndroidManifest.xml") - - listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64").forEach { arch -> - listOf("/libaliuhook.so", "/liblsplant.so", "/libc++_shared.so").forEach { file -> - deleteEntry("lib/$arch$file") - } - } - - if (!patched) for (i in 2..4) { - val name = "classes$i.dex" - val cacheFile = context.cacheDir.resolve(name) - - openEntry(name) - compressFile(cacheFile.absolutePath) - closeEntry() - cacheFile.delete() - } - - openEntry("classes.dex") - compressFile(injector.absolutePath) - closeEntry() - - writeEntry("classes5.dex", assetManager.open("aliuhook/classes.dex")) - - listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64").forEach { arch -> - listOf("/libaliuhook.so", "/liblsplant.so", "/libc++_shared.so").forEach { file -> - writeEntry("lib/$arch$file", assetManager.open("aliuhook/$arch$file")) - } - } - - writeEntry("classes6.dex", assetManager.open("kotlin/classes.dex")) - - manifestBytes?.let { bytes -> - log += "Patching manifest\n" - val newManifestBytes = patchManifest(bytes) - openEntry("AndroidManifest.xml") - writeEntry(newManifestBytes, newManifestBytes.size.toLong()) - closeEntry() - } - - close() - } - - if (Prefs.replaceBg.get()) { - log += "Replacing icon background\n" - - val icon1Bytes = assetManager.open("icon1.png").readBytes() - val icon2Bytes = assetManager.open("icon2.png").readBytes() - - // use androguard to figure out entries - // androguard arsc resources.arsc --id 0x7f0f0000 (icon1) - // androguard arsc resources.arsc --id 0x7f0f0002 and androguard arsc resources.arsc --id 0x7f0f0006 (icon2) - val icon1Entries = listOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") - val icon2Entries = - listOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") - - with(Zip(outputApk.absolutePath, 0, 'a')) { - icon1Entries.forEach { entry -> deleteEntry("res/$entry") } - icon2Entries.forEach { entry -> deleteEntry("res/$entry") } - - icon1Entries.forEach { entry -> writeEntry("res/$entry", icon1Bytes) } - icon2Entries.forEach { entry -> writeEntry("res/$entry", icon2Bytes) } - - close() - } - } - - log += "Signing APK...\n" - - Signer.signApk(outputApk) - - log += "Signed APK\n" - - val intent = Intent(Intent.ACTION_VIEW).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - - setDataAndType( - FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", outputApk), - "application/vnd.android.package-archive" - ) - } - - context.startActivity(intent) - - launch(Dispatchers.Main) { - navigator.navigate(HomeScreenDestination) - } + LazyColumn( + modifier = Modifier.fillMaxSize(), + ) { + stickyHeader { + LinearProgressIndicator( + modifier = Modifier.fillParentMaxWidth() + ) + } + items(viewModel.logs) { log -> + Text( + text = log, + modifier = Modifier.padding(8.dp) + ) } } - - Column( - modifier = Modifier.fillMaxWidth() - ) { - Text(log) - - Spacer(Modifier.weight(1f, true)) - - CircularProgressIndicator( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(8.dp) - .size(32.dp) - ) - } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt index aec0386..c16ef05 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt @@ -17,14 +17,15 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel import com.aliucord.manager.R import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.ui.components.ListItem import com.aliucord.manager.ui.components.settings.* +import com.aliucord.manager.ui.viewmodels.SettingsViewModel import com.ramcosta.composedestinations.annotation.Destination @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @@ -34,7 +35,8 @@ fun SettingsScreen() = Column( modifier = Modifier.verticalScroll(state = rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - val context = LocalContext.current + val viewModifier: SettingsViewModel = viewModel() + var showThemeDialog by remember { mutableStateOf(false) } if (showThemeDialog) { @@ -133,7 +135,7 @@ fun SettingsScreen() = Column( modifier = Modifier .fillMaxWidth() .padding(8.dp), - onClick = context.cacheDir::deleteRecursively + onClick = viewModifier::clearCacheDir ) { Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/AboutViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/AboutViewModel.kt new file mode 100644 index 0000000..e36a0e6 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/AboutViewModel.kt @@ -0,0 +1,31 @@ +package com.aliucord.manager.ui.viewmodels + +import androidx.compose.runtime.mutableStateListOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aliucord.manager.models.github.GithubUser +import com.aliucord.manager.utils.Github +import com.aliucord.manager.utils.httpClient +import io.ktor.client.call.* +import io.ktor.client.request.* +import kotlinx.coroutines.launch + +class AboutViewModel : ViewModel() { + + val contributors = mutableStateListOf() + + fun load() { + viewModelScope.launch { + val githubContributors = httpClient.get(Github.contributorsUrl).body>() + .sortedByDescending { + it.contributions + } + contributors.addAll(githubContributors) + } + } + + init { + load() + } + +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/HomeViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/HomeViewModel.kt new file mode 100644 index 0000000..10a1659 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/HomeViewModel.kt @@ -0,0 +1,67 @@ +package com.aliucord.manager.ui.viewmodels + +import android.app.Application +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.compose.runtime.* +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.aliucord.manager.models.github.Version +import com.aliucord.manager.preferences.Prefs +import com.aliucord.manager.utils.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.serialization.decodeFromString + +class HomeViewModel(application: Application) : AndroidViewModel(application) { + + private val packageManager = getApplication().packageManager + + var supportedVersion by mutableStateOf("") + private set + + var installedVersion by mutableStateOf("-") + private set + + fun load() { + viewModelScope.launch(Dispatchers.IO) { + val version = json.decodeFromString(httpClient.get(Github.dataUrl).bodyAsText()) + + supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) { + '0' -> "Stable" + '1' -> "Beta" + '2' -> "Alpha" + else -> throw NoWhenBranchMatchedException() + } + installedVersion = try { + packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName + } catch (th: Throwable) { + "-" + } + } + } + + fun launchAliucord() { + packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let { + getApplication().startActivity(it) + } ?: Toast.makeText(getApplication(), "Failed to launch Aliucord", Toast.LENGTH_LONG) + .show() + } + + fun uninstallAliucord() { + val packageURI = Uri.parse("package:${Prefs.packageName.get()}") + val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + + getApplication().startActivity(uninstallIntent) + } + + init { + load() + } + +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/InstallViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/InstallViewModel.kt new file mode 100644 index 0000000..fdd0cb8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/InstallViewModel.kt @@ -0,0 +1,200 @@ +package com.aliucord.manager.ui.viewmodels + +import android.app.Application +import android.content.Intent +import androidx.compose.runtime.mutableStateListOf +import androidx.core.content.FileProvider +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import com.aliucord.libzip.Zip +import com.aliucord.manager.BuildConfig +import com.aliucord.manager.preferences.Prefs +import com.aliucord.manager.utils.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileNotFoundException + +class InstallViewModel(application: Application) : AndroidViewModel(application) { + + private val _returnToHome = MutableSharedFlow() + val returnToHome = _returnToHome.asSharedFlow() + + private var installationRunning: Boolean = false + + val logs = mutableStateListOf() + + fun startInstallation(apk: File?) { + if (installationRunning) return + + viewModelScope.launch(Dispatchers.IO) { + installationRunning = true + val supportedVersion = Github.version.versionCode + + val discordApk = apk ?: run { + logs.add("Checking for cached APK...") + + getApplication().externalCacheDir!!.resolve("discord-${supportedVersion}.apk").also { file -> + val archiveInfo = getApplication().packageManager.getPackageArchiveInfo(file.path, 0) + + if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also + + logs.add("Downloading Discord APK...") + + DownloadUtils.downloadDiscordApk(getApplication(), supportedVersion) + + logs.add("Done") + } + } + + val injector = getApplication().externalCacheDir!!.resolve("Injector.dex").also { file -> + if (file.exists()) return@also + + logs.add("Downloading injector...") + + DownloadUtils.downloadInjector(getApplication()) + + logs.add("Done") + } + + val outputDir = aliucordDir.also { + if (!it.exists() && !it.mkdirs()) throw FileNotFoundException() + } + + val outputApk = outputDir.resolve("Aliucord.apk") + discordApk.copyTo(outputApk, true) + + logs.add("Repacking APK") + + var patched = false + var manifestBytes: ByteArray? + + with(Zip(outputApk.absolutePath, 6, 'r')) { + for (index in 0 until totalEntries) { + openEntryByIndex(index) + val name = entryName + closeEntry() + if (name == "classes5.dex") { + patched = true + break + } + } + + val cacheDir = getApplication().cacheDir.absolutePath + + if (!patched) (1..3).forEach { i -> + openEntry("classes${if (i == 1) "" else i}.dex") + extractEntry("$cacheDir/classes${i + 1}.dex") + closeEntry() + } + + openEntry("AndroidManifest.xml") + manifestBytes = readEntry() + closeEntry() + + close() + } + + val assetManager = getApplication().assets + + with(Zip(outputApk.absolutePath, 6, 'a')) { + if (patched) { + deleteEntry("classes.dex") + deleteEntry("classes5.dex") + deleteEntry("classes6.dex") + } else { + (1..3).forEach { i -> deleteEntry("classes${if (i == 1) "" else i}.dex") } + } + + deleteEntry("AndroidManifest.xml") + + listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64").forEach { arch -> + listOf("/libaliuhook.so", "/liblsplant.so", "/libc++_shared.so").forEach { file -> + deleteEntry("lib/$arch$file") + } + } + + if (!patched) for (i in 2..4) { + val name = "classes$i.dex" + val cacheFile = getApplication().cacheDir.resolve(name) + + openEntry(name) + compressFile(cacheFile.absolutePath) + closeEntry() + cacheFile.delete() + } + + openEntry("classes.dex") + compressFile(injector.absolutePath) + closeEntry() + + writeEntry("classes5.dex", assetManager.open("aliuhook/classes.dex")) + + listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64").forEach { arch -> + listOf("/libaliuhook.so", "/liblsplant.so", "/libc++_shared.so").forEach { file -> + writeEntry("lib/$arch$file", assetManager.open("aliuhook/$arch$file")) + } + } + + writeEntry("classes6.dex", assetManager.open("kotlin/classes.dex")) + + manifestBytes?.let { bytes -> + logs.add("Patching manifest") + val newManifestBytes = patchManifest(bytes) + openEntry("AndroidManifest.xml") + writeEntry(newManifestBytes, newManifestBytes.size.toLong()) + closeEntry() + } + + close() + } + + if (Prefs.replaceBg.get()) { + logs.add("Replacing icon background") + + val icon1Bytes = assetManager.open("icon1.png").readBytes() + val icon2Bytes = assetManager.open("icon2.png").readBytes() + + // use androguard to figure out entries + // androguard arsc resources.arsc --id 0x7f0f0000 (icon1) + // androguard arsc resources.arsc --id 0x7f0f0002 and androguard arsc resources.arsc --id 0x7f0f0006 (icon2) + val icon1Entries = listOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") + val icon2Entries = + listOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") + + with(Zip(outputApk.absolutePath, 0, 'a')) { + icon1Entries.forEach { entry -> deleteEntry("res/$entry") } + icon2Entries.forEach { entry -> deleteEntry("res/$entry") } + + icon1Entries.forEach { entry -> writeEntry("res/$entry", icon1Bytes) } + icon2Entries.forEach { entry -> writeEntry("res/$entry", icon2Bytes) } + + close() + } + } + + logs.add("Signing APK...") + + Signer.signApk(outputApk) + + logs.add("Signed APK") + + val intent = Intent(Intent.ACTION_VIEW).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + + setDataAndType( + FileProvider.getUriForFile(getApplication(), "${BuildConfig.APPLICATION_ID}.provider", outputApk), + "application/vnd.android.package-archive" + ) + } + + getApplication().startActivity(intent) + + _returnToHome.emit(true) + installationRunning = false + } + } + +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/SettingsViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/SettingsViewModel.kt new file mode 100644 index 0000000..886bea0 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodels/SettingsViewModel.kt @@ -0,0 +1,12 @@ +package com.aliucord.manager.ui.viewmodels + +import android.app.Application +import androidx.lifecycle.AndroidViewModel + +class SettingsViewModel(application: Application) : AndroidViewModel(application) { + + fun clearCacheDir() { + getApplication().cacheDir.deleteRecursively() + } + +}