move non-composable code to viewmodels
This commit is contained in:
parent
e7e170c264
commit
e9f7e63569
10 changed files with 369 additions and 279 deletions
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<GithubUser>,
|
||||
) = ElevatedCard(
|
||||
modifier = Modifier.wrapContentHeight().fillMaxWidth()
|
||||
) {
|
||||
val contributors = remember { mutableStateListOf<GithubUser>() }
|
||||
|
||||
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<List<GithubUser>>().sortedByDescending { it.contributions })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue