move non-composable code to viewmodels

This commit is contained in:
X1nto 2022-04-21 01:08:26 +04:00
parent e7e170c264
commit e9f7e63569
10 changed files with 369 additions and 279 deletions

View file

@ -15,8 +15,6 @@ import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.platform.LocalContext
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.preferences.Prefs
@ -44,6 +42,13 @@ class MainActivity : ComponentActivity() {
sharedPreferences = getPreferences(Context.MODE_PRIVATE) 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 { setContent {
ManagerTheme( ManagerTheme(
isBlack = Prefs.useBlack.get(), isBlack = Prefs.useBlack.get(),
@ -53,17 +58,6 @@ class MainActivity : ComponentActivity() {
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK 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() ManagerScaffold()
} }
} }

View file

@ -9,27 +9,21 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.* import androidx.compose.foundation.lazy.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.models.github.GithubUser 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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun ContributorsCard() = ElevatedCard( fun ContributorsCard(
contributors: List<GithubUser>,
) = ElevatedCard(
modifier = Modifier.wrapContentHeight().fillMaxWidth() modifier = Modifier.wrapContentHeight().fillMaxWidth()
) { ) {
val contributors = remember { mutableStateListOf<GithubUser>() }
Column( Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
@ -54,12 +48,6 @@ fun ContributorsCard() = ElevatedCard(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() } content = { CircularProgressIndicator() }
) )
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
contributors.addAll(httpClient.get(Github.contributorsUrl).body<List<GithubUser>>().sortedByDescending { it.contributions })
}
}
} }
} }
} }

View file

@ -13,21 +13,23 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.ui.components.about.ContributorsCard import com.aliucord.manager.ui.components.about.ContributorsCard
import com.aliucord.manager.ui.components.about.TeamCard import com.aliucord.manager.ui.components.about.TeamCard
import com.aliucord.manager.ui.viewmodels.AboutViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@Destination @Destination
@Composable @Composable
fun AboutScreen() { fun AboutScreen() {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val viewModel: AboutViewModel = viewModel()
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
TeamCard() TeamCard()
ContributorsCard() ContributorsCard(viewModel.contributors)
Spacer(Modifier.weight(1f, true)) Spacer(Modifier.weight(1f, true))

View file

@ -6,9 +6,6 @@
package com.aliucord.manager.ui.screens 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.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -18,56 +15,29 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.models.github.Version
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.components.PluginsList import com.aliucord.manager.ui.components.PluginsList
import com.aliucord.manager.ui.components.installer.DownloadMethod import com.aliucord.manager.ui.components.installer.DownloadMethod
import com.aliucord.manager.ui.components.installer.InstallerDialog import com.aliucord.manager.ui.components.installer.InstallerDialog
import com.aliucord.manager.ui.screens.destinations.CommitsScreenDestination import com.aliucord.manager.ui.screens.destinations.CommitsScreenDestination
import com.aliucord.manager.ui.screens.destinations.InstallerScreenDestination 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.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator 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) @OptIn(ExperimentalMaterial3Api::class)
@Destination(start = true) @Destination(start = true)
@Composable @Composable
fun HomeScreen(navigator: DestinationsNavigator) { fun HomeScreen(navigator: DestinationsNavigator) {
val packageManager = LocalContext.current.packageManager val viewModel: HomeViewModel = viewModel()
var supportedVersion by remember { mutableStateOf<String?>(null) }
val installedVersion = remember {
try {
packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName
} catch (th: Throwable) {
"-"
}
}
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
val version = json.decodeFromString<Version>(httpClient.get(Github.dataUrl).bodyAsText())
supportedVersion = "${version.versionName} - " + when (version.versionCode[3].toString()) {
"0" -> "Stable"
"1" -> "Beta"
"2" -> "Alpha"
else -> throw NoWhenBranchMatchedException()
}
}
}
var showOptionsDialog by remember { mutableStateOf(false) } var showOptionsDialog by remember { mutableStateOf(false) }
@ -113,20 +83,20 @@ fun HomeScreen(navigator: DestinationsNavigator) {
append("Supported version: ") append("Supported version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(supportedVersion ?: "?") append(viewModel.supportedVersion)
} }
append("\nInstalled version: ") append("\nInstalled version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { 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 "-" -> 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 else -> R.drawable.ic_update_24dp to R.string.update
} }
@ -157,7 +127,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
Text(stringResource(description)) Text(stringResource(description))
} }
if (installedVersion == "-") Button( if (viewModel.installedVersion == "-") Button(
modifier = Modifier.wrapContentSize(), modifier = Modifier.wrapContentSize(),
onClick = { navigator.navigate(CommitsScreenDestination) }, onClick = { navigator.navigate(CommitsScreenDestination) },
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
@ -171,21 +141,16 @@ fun HomeScreen(navigator: DestinationsNavigator) {
} }
} }
if (installedVersion != "-") { if (viewModel.installedVersion != "-") {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
val context = LocalContext.current
Button( Button(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer),
onClick = { onClick = {
val packageURI = Uri.parse("package:${Prefs.packageName.get()}") viewModel.uninstallAliucord()
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI)
context.startActivity(uninstallIntent)
} }
) { ) {
Icon( Icon(
@ -198,12 +163,7 @@ fun HomeScreen(navigator: DestinationsNavigator) {
Button( Button(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { onClick = viewModel::launchAliucord,
packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let {
context.startActivity(it)
} ?: Toast.makeText(context, "Failed to launch Aliucord", Toast.LENGTH_LONG)
.show()
},
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) { ) {
Icon( Icon(

View file

@ -5,217 +5,51 @@
package com.aliucord.manager.ui.screens package com.aliucord.manager.ui.screens
import android.content.Intent
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator 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.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.lifecycle.viewmodel.compose.viewModel
import com.aliucord.libzip.Zip
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.screens.destinations.HomeScreenDestination 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.annotation.Destination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.io.FileNotFoundException
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Destination @Destination
@Composable @Composable
fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) { fun InstallerScreen(navigator: DestinationsNavigator, apk: File?) {
var log by remember { mutableStateOf("") } val viewModel: InstallViewModel = viewModel()
val context = LocalContext.current
val navigateMain by viewModel.returnToHome.collectAsState(initial = false)
if (navigateMain) {
navigator.navigate(HomeScreenDestination)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
launch(Dispatchers.IO) { viewModel.startInstallation(apk)
val supportedVersion = Github.version.versionCode }
val discordApk = apk ?: run { LazyColumn(
log += "Checking for cached APK...\n" modifier = Modifier.fillMaxSize(),
) {
context.externalCacheDir!!.resolve("discord-${supportedVersion}.apk").also { file -> stickyHeader {
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0) LinearProgressIndicator(
modifier = Modifier.fillParentMaxWidth()
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also )
}
log += "Downloading Discord APK...\n" items(viewModel.logs) { log ->
Text(
DownloadUtils.downloadDiscordApk(context, supportedVersion) text = log,
modifier = Modifier.padding(8.dp)
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)
}
} }
} }
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(log)
Spacer(Modifier.weight(1f, true))
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(8.dp)
.size(32.dp)
)
}
} }

View file

@ -17,14 +17,15 @@ import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.components.ListItem import com.aliucord.manager.ui.components.ListItem
import com.aliucord.manager.ui.components.settings.* import com.aliucord.manager.ui.components.settings.*
import com.aliucord.manager.ui.viewmodels.SettingsViewModel
import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.Destination
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@ -34,7 +35,8 @@ fun SettingsScreen() = Column(
modifier = Modifier.verticalScroll(state = rememberScrollState()), modifier = Modifier.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
val context = LocalContext.current val viewModifier: SettingsViewModel = viewModel()
var showThemeDialog by remember { mutableStateOf(false) } var showThemeDialog by remember { mutableStateOf(false) }
if (showThemeDialog) { if (showThemeDialog) {
@ -133,7 +135,7 @@ fun SettingsScreen() = Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .padding(8.dp),
onClick = context.cacheDir::deleteRecursively onClick = viewModifier::clearCacheDir
) { ) {
Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center) Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center)
} }

View file

@ -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<GithubUser>()
fun load() {
viewModelScope.launch {
val githubContributors = httpClient.get(Github.contributorsUrl).body<List<GithubUser>>()
.sortedByDescending {
it.contributions
}
contributors.addAll(githubContributors)
}
}
init {
load()
}
}

View file

@ -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<Application>().packageManager
var supportedVersion by mutableStateOf("")
private set
var installedVersion by mutableStateOf("-")
private set
fun load() {
viewModelScope.launch(Dispatchers.IO) {
val version = json.decodeFromString<Version>(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<Application>().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<Application>().startActivity(uninstallIntent)
}
init {
load()
}
}

View file

@ -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<Boolean>()
val returnToHome = _returnToHome.asSharedFlow()
private var installationRunning: Boolean = false
val logs = mutableStateListOf<String>()
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<Application>().externalCacheDir!!.resolve("discord-${supportedVersion}.apk").also { file ->
val archiveInfo = getApplication<Application>().packageManager.getPackageArchiveInfo(file.path, 0)
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also
logs.add("Downloading Discord APK...")
DownloadUtils.downloadDiscordApk(getApplication<Application>(), supportedVersion)
logs.add("Done")
}
}
val injector = getApplication<Application>().externalCacheDir!!.resolve("Injector.dex").also { file ->
if (file.exists()) return@also
logs.add("Downloading injector...")
DownloadUtils.downloadInjector(getApplication<Application>())
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<Application>().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<Application>().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<Application>().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<Application>(), "${BuildConfig.APPLICATION_ID}.provider", outputApk),
"application/vnd.android.package-archive"
)
}
getApplication<Application>().startActivity(intent)
_returnToHome.emit(true)
installationRunning = false
}
}
}

View file

@ -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<Application>().cacheDir.deleteRecursively()
}
}