feat: aliu kotlin install (#33)

This commit is contained in:
rushii 2022-10-30 08:19:24 -07:00 • committed by GitHub
parent 10da841a3b
commit 335f2ca7a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 574 additions and 151 deletions

View file

@ -1,4 +1,4 @@
# Aliucord Manager
# Aliucord Manager [![Crowdin](https://badges.crowdin.net/aliucord-manager/localized.svg)](https://crowdin.com/project/aliucord-manager)
Aliucord Manager - Aliucord Installer rewrite in [Kotlin](https://kotlinlang.org) and [Jetpack Compose](https://developer.android.com/jetpack/compose).
INFO: This app is not functional yet, if you want to install Aliucord - [read this](https://github.com/Aliucord/Aliucord#-installation)

View file

@ -1,5 +1,6 @@
package com.aliucord.manager.di
import android.app.Application
import android.content.Context
import com.aliucord.manager.domain.manager.DownloadManager
import com.aliucord.manager.domain.manager.PreferencesManager
@ -11,7 +12,7 @@ val managerModule = module {
return PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
}
fun provideDownloadManager(context: Context) = DownloadManager(context)
fun provideDownloadManager(application: Application) = DownloadManager(application)
singleOf(::providePreferences)
singleOf(::provideDownloadManager)

View file

@ -1,9 +1,11 @@
package com.aliucord.manager.di
import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.domain.repository.GithubRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::GithubRepository)
singleOf(::AliucordMavenRepository)
}

View file

@ -1,9 +1,11 @@
package com.aliucord.manager.di
import com.aliucord.manager.network.service.GithubService
import com.aliucord.manager.network.service.MavenService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
singleOf(::GithubService)
singleOf(::MavenService)
}

View file

@ -9,6 +9,6 @@ val viewModelModule = module {
viewModelOf(::HomeViewModel)
viewModelOf(::PluginsViewModel)
viewModelOf(::AboutViewModel)
viewModel { params -> InstallViewModel(get(), get(), get(), get(), params.get()) }
viewModelOf(::InstallViewModel)
viewModelOf(::SettingsViewModel)
}

View file

@ -1,27 +1,41 @@
package com.aliucord.manager.domain.manager
import android.app.Application
import android.app.DownloadManager
import android.content.*
import androidx.annotation.StringRes
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.network.service.GithubService
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.coroutines.*
class DownloadManager(
private val context: Context
private val application: Application
) {
private val downloadManager = context.getSystemService<DownloadManager>()!!
private val downloadManager = application.getSystemService<DownloadManager>()!!
suspend fun downloadDiscordApk(version: String): File {
return download("$BACKEND_HOST/download/discord?v=$version", "base-$version.apk")
}
// Discord APK downloading
suspend fun downloadDiscordApk(version: String, out: File): File =
download("$BACKEND_HOST/download/discord?v=$version", out)
suspend fun downloadSplit(version: String, split: String): File {
return download("$BACKEND_HOST/download/discord?v=$version&split=$split", "$split-$version.apk")
}
suspend fun downloadSplit(version: String, split: String, out: File): File =
download("$BACKEND_HOST/download/discord?v=$version&split=$split", out)
suspend fun download(url: String, fileName: String): File {
// Aliucord Kotlin downloads
suspend fun downloadKtInjector(out: File): File =
download(GithubService.KT_INJECTOR_URL, out)
suspend fun downloadAliuhook(version: String, out: File): File =
download(AliucordMavenRepository.getAliuhookUrl(version), out)
suspend fun downloadKotlinDex(out: File): File =
download(GithubService.KOTLIN_DEX_URL, out)
suspend fun download(url: String, out: File): File {
return suspendCoroutine { continuation ->
val receiver = object : BroadcastReceiver() {
var downloadId = 0L
@ -29,23 +43,60 @@ class DownloadManager(
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (downloadId == id) {
context.unregisterReceiver(this)
continuation.resume(context.externalCacheDir!!.resolve(fileName))
if (downloadId != id) return
val (status, reason) = DownloadManager.Query().run {
setFilterById(downloadId)
val cursor = downloadManager.query(this)
.apply { moveToFirst() }
val status = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_STATUS)) }
val reason = cursor.run { getInt(getColumnIndex(DownloadManager.COLUMN_REASON)) }
status to reason
}
when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
context.unregisterReceiver(this)
continuation.resume(out)
}
DownloadManager.STATUS_FAILED -> {
context.unregisterReceiver(this)
continuation.resumeWithException(Error("Failed to download $url because of: ${reasonToString(reason)}"))
}
else -> {}
}
}
}
context.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
application.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
setDestinationUri(context.externalCacheDir!!.resolve(fileName).toUri())
setTitle("Aliucord Manager: Downloading ${out.name}")
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
setDestinationUri(out.toUri())
addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}")
downloadManager.enqueue(this)
}
}
}
private fun reasonToString(int: Int): String {
return when (int) {
DownloadManager.ERROR_UNKNOWN -> "Unknown Error"
DownloadManager.ERROR_FILE_ERROR -> "File Error"
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP Code"
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP Data Error"
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Device not found"
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
else -> "Unknown error"
}
}
companion object {
private const val BACKEND_HOST = "https://aliucord.com/"
}

View file

@ -0,0 +1,29 @@
package com.aliucord.manager.domain.repository
import com.aliucord.manager.network.service.MavenService
import com.aliucord.manager.network.utils.ApiError
import com.aliucord.manager.network.utils.ApiResponse
import com.aliucord.manager.network.utils.transform
import io.ktor.http.*
class AliucordMavenRepository(
private val maven: MavenService
) {
suspend fun getAliuhookVersion(): ApiResponse<String> {
return maven.getArtifactMetadata(BASE_URL, ALIUHOOK).transform {
"<release>(.+?)</release>".toRegex()
.find(it)
?.groupValues?.get(1)
?: return ApiResponse.Error(ApiError(HttpStatusCode.OK, "No version in the aliuhook artifact metadata"))
}
}
companion object {
private const val BASE_URL = "https://maven.aliucord.com/snapshots"
private const val ALIUHOOK = "com.aliucord:Aliuhook"
fun getAliuhookUrl(version: String): String {
return MavenService.getAARUrl(BASE_URL, "$ALIUHOOK:$version")
}
}
}

View file

@ -12,7 +12,7 @@ class GithubRepository(
suspend fun getContributors() = service.getContributors()
.transform { it.sortedByDescending(GithubUser::contributions) }
suspend fun getVersion() = service.getVersion()
suspend fun getDiscordKtVersion() = service.getVersion()
suspend fun getHermesReleases() = service.getHermesReleases()

View file

@ -53,6 +53,8 @@ class GithubService(
private const val CONTRIBUTORS_URL = "https://api.github.com/repos/$ORG/$REPO/contributors"
private const val DATA_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/data.json"
const val KT_INJECTOR_URL = "https://raw.githubusercontent.com/$ORG/$REPO/builds/Injector.dex"
const val KOTLIN_DEX_URL = "https://raw.githubusercontent.com/$ORG/$REPO/main/installer/android/app/src/main/assets/kotlin/classes.dex"
private const val HERMES_RELEASES_URL = "https://api.github.com/repos/$ORG/$HERMES_REPO/releases"
private const val ALIUCORD_NATIVE_RELEASES_URL = "https://api.github.com/repos/$ORG/$NATIVE_REPO/releases"

View file

@ -2,9 +2,7 @@ package com.aliucord.manager.network.service
import android.util.Log
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.network.utils.ApiError
import com.aliucord.manager.network.utils.ApiFailure
import com.aliucord.manager.network.utils.ApiResponse
import com.aliucord.manager.network.utils.*
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
@ -25,6 +23,10 @@ class HttpService(
if (response.status.isSuccess()) {
body = response.bodyAsText()
if (T::class == String::class) {
return ApiResponse.Success(body as T)
}
ApiResponse.Success(json.decodeFromString<T>(body))
} else {
body = try {

View file

@ -0,0 +1,26 @@
package com.aliucord.manager.network.service
import com.aliucord.manager.network.utils.ApiResponse
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MavenService(
private val http: HttpService,
) {
suspend fun getArtifactMetadata(baseUrl: String, artifactCoords: String): ApiResponse<String> = withContext(Dispatchers.IO) {
http.request {
val coords = artifactCoords.replace("[:.]".toRegex(), "/")
url("$baseUrl/${coords}/maven-metadata.xml")
}
}
companion object {
fun getAARUrl(baseUrl: String, coords: String): String {
val parts = coords.split("[:.]".toRegex())
val artifact = parts[parts.lastIndex - 1]
val version = parts.last()
return "$baseUrl/${parts.joinToString("/")}/$artifact-$version.aar"
}
}
}

View file

@ -46,3 +46,30 @@ inline fun <T, R> ApiResponse<T>.transform(block: (T) -> R): ApiResponse<R> {
ApiResponse.Success(block(data))
}
}
inline fun <T> ApiResponse<T>.getOrThrow(): T {
return fold(
success = { it },
fail = { throw it }
)
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> ApiResponse<T>.chain(block: (T) -> ApiResponse<R>): ApiResponse<R> {
return if (this !is ApiResponse.Success) {
// Error and Failure do not use the generic value
this as ApiResponse<R>
} else {
block(data)
}
}
@Suppress("UNCHECKED_CAST")
inline fun <T, R> ApiResponse<T>.chain(secondary: ApiResponse<R>): ApiResponse<R> {
return if (secondary is ApiResponse.Success) {
secondary
} else {
// Error and Failure do not use the generic value
this as ApiResponse<R>
}
}

View file

@ -5,7 +5,6 @@
package com.aliucord.manager.ui.dialog
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
@ -14,83 +13,120 @@ import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.aliucord.manager.R
import com.aliucord.manager.ui.screen.InstallData
enum class DownloadMethod { DOWNLOAD, SELECT }
enum class DownloadMethod { DOWNLOAD }
enum class DiscordType { KOTLIN, REACT_NATIVE }
@Composable
fun InstallerDialog(
onDismissRequest: () -> Unit,
onDismiss: () -> Unit,
onConfirm: (InstallData) -> Unit
) {
var selectedMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
val downloadMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
var discordType by rememberSaveable { mutableStateOf<DiscordType?>(null) }
AlertDialog(
icon = {
Icon(
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.selector_download_method)
fun triggerConfirm() {
onDismiss()
onConfirm(
InstallData(
downloadMethod,
discordType ?: return
)
},
title = {
Text(stringResource(R.string.selector_download_method))
},
text = {
Column {
mapOf(
DownloadMethod.DOWNLOAD to stringResource(R.string.selector_download_apk),
// DownloadMethod.SELECT to stringResource(R.string.select_apk)
).forEach { (method, name) ->
Column {
Row(
modifier = Modifier.clickable { selectedMethod = method },
verticalAlignment = Alignment.CenterVertically
)
}
Dialog(
onDismissRequest = onDismiss,
) {
Surface(
color = AlertDialogDefaults.containerColor,
tonalElevation = AlertDialogDefaults.TonalElevation,
shape = AlertDialogDefaults.shape,
) {
Column(
modifier = Modifier
.sizeIn(minWidth = 380.dp)
.padding(24.dp)
) {
// Icon
Box(
Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp)
) {
Icon(
imageVector = Icons.Default.Download,
tint = AlertDialogDefaults.iconContentColor,
contentDescription = null
)
}
// Title
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 30.dp)
) {
Text(
stringResource(R.string.selector_discord_type),
color = AlertDialogDefaults.titleContentColor,
style = MaterialTheme.typography.headlineSmall
)
}
// Body
Box(
modifier = Modifier
.padding(bottom = 30.dp)
) {
Text(
stringResource(R.string.selector_discord_type_body),
textAlign = TextAlign.Center,
color = AlertDialogDefaults.textContentColor,
style = MaterialTheme.typography.bodyMedium
)
}
// Buttons
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.clip(MaterialTheme.shapes.large)
) {
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
Button(
shape = MaterialTheme.shapes.extraSmall,
onClick = {
discordType = DiscordType.REACT_NATIVE
triggerConfirm()
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(
text = name,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = selectedMethod == method,
onClick = { selectedMethod = method }
)
Text(stringResource(R.string.selector_discord_type_kotlin))
}
Button(
shape = MaterialTheme.shapes.extraSmall,
onClick = {
discordType = DiscordType.KOTLIN
triggerConfirm()
},
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(stringResource(R.string.selector_discord_type_old))
}
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(
InstallData(
selectedMethod,
DiscordType.REACT_NATIVE
)
)
onDismissRequest()
}
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
Button(
onClick = onDismissRequest,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(R.string.action_dismiss))
}
},
onDismissRequest = onDismissRequest
)
}
}
}

View file

@ -6,15 +6,13 @@
package com.aliucord.manager.ui.screen
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.aliucord.manager.ui.component.home.CommitList
import com.aliucord.manager.ui.component.home.InfoCard
import com.aliucord.manager.ui.dialog.*
import com.aliucord.manager.ui.dialog.InstallerDialog
import com.aliucord.manager.ui.viewmodel.HomeViewModel
import org.koin.androidx.compose.getViewModel
@ -23,27 +21,14 @@ fun HomeScreen(
viewModel: HomeViewModel = getViewModel(),
onClickInstall: (InstallData) -> Unit
) {
var showOptionsDialog by remember { mutableStateOf(false) }
if (showOptionsDialog) {
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
onClickInstall(
InstallData(
DownloadMethod.SELECT,
DiscordType.REACT_NATIVE
)
)
}
var showInstallerDialog by remember { mutableStateOf(false) }
if (showInstallerDialog) {
InstallerDialog(
onDismissRequest = { showOptionsDialog = false },
onDismiss = { showInstallerDialog = false },
onConfirm = { data ->
when (data.downloadMethod) {
DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream"))
DownloadMethod.DOWNLOAD -> onClickInstall(data)
}
showInstallerDialog = false
onClickInstall(data)
}
)
}
@ -59,18 +44,7 @@ fun HomeScreen(
supportedVersion = viewModel.supportedVersion,
supportedVersionType = viewModel.supportedVersionType,
currentVersion = viewModel.installedVersion,
onDownloadClick = {
if (viewModel.preferences.devMode) {
showOptionsDialog = true
} else {
onClickInstall(
InstallData(
DownloadMethod.DOWNLOAD,
DiscordType.REACT_NATIVE
)
)
}
},
onDownloadClick = { showInstallerDialog = true },
onLaunchClick = viewModel::launchAliucord,
onUninstallClick = viewModel::uninstallAliucord
)

View file

@ -74,7 +74,7 @@ class HomeViewModel(
}
private suspend fun _fetchSupportedVersion() {
val version = githubRepository.getVersion()
val version = githubRepository.getDiscordKtVersion()
version.fold(
success = {

View file

@ -11,9 +11,11 @@ import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.domain.manager.DownloadManager
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.installer.util.*
import com.aliucord.manager.network.utils.fold
import com.aliucord.manager.network.utils.getOrThrow
import com.aliucord.manager.ui.component.installer.InstallStatus
import com.aliucord.manager.ui.component.installer.InstallStepData
import com.aliucord.manager.ui.dialog.DiscordType
@ -35,6 +37,7 @@ class InstallViewModel(
private val downloadManager: DownloadManager,
private val preferences: PreferencesManager,
private val githubRepository: GithubRepository,
private val aliucordMaven: AliucordMavenRepository,
private val installData: InstallData
) : ViewModel() {
private val externalCacheDir = application.externalCacheDir!!
@ -90,8 +93,9 @@ class InstallViewModel(
try {
when (installData.discordType) {
DiscordType.REACT_NATIVE -> installReactNative()
DiscordType.KOTLIN -> {}
DiscordType.KOTLIN -> installKotlin()
}
delay(5000)
_returnToHome.emit(true)
} catch (t: Throwable) {
Log.e(
@ -134,7 +138,7 @@ class InstallViewModel(
if (file.exists()) {
cached = true
} else {
downloadManager.downloadDiscordApk(supportedVersion)
downloadManager.downloadDiscordApk(supportedVersion, file)
}
file.copyTo(
@ -154,7 +158,8 @@ class InstallViewModel(
cached = true
} else downloadManager.downloadSplit(
version = supportedVersion,
split = "config.$libArch"
split = "config.$libArch",
out = file
)
file.copyTo(
@ -173,7 +178,8 @@ class InstallViewModel(
cached = true
} else downloadManager.downloadSplit(
version = supportedVersion,
split = "config.en"
split = "config.en",
out = file
)
file.copyTo(
@ -193,7 +199,8 @@ class InstallViewModel(
cached = true
} else downloadManager.downloadSplit(
version = supportedVersion,
split = "config.xxhdpi"
split = "config.xxhdpi",
out = file
)
file.copyTo(
@ -219,23 +226,21 @@ class InstallViewModel(
val hermes = externalCacheDir.resolve("hermes-release-${latestHermesRelease.tagName}.aar").also { file ->
if (file.exists()) return@also
downloadManager.download(
url = latestHermesRelease.assets.find { it.name == "hermes-release.aar" }!!.browserDownloadUrl,
fileName = "hermes-release-${latestHermesRelease.tagName}.aar"
)
latestHermesRelease.assets
.find { it.name == "hermes-release.aar" }!!.browserDownloadUrl
.also { downloadManager.download(it, file) }
}
// Download the hermes-cppruntime-release.aar file to replace in the apk
val cppruntime = externalCacheDir.resolve("hermes-cppruntime-release-${latestHermesRelease.tagName}.aar").also { file ->
val cppRuntime = externalCacheDir.resolve("hermes-cppruntime-release-${latestHermesRelease.tagName}.aar").also { file ->
if (file.exists()) return@also
downloadManager.download(
url = latestHermesRelease.assets.find { it.name == "hermes-cppruntime-release.aar" }!!.browserDownloadUrl,
fileName = "hermes-cppruntime-release-${latestHermesRelease.tagName}.aar"
)
latestHermesRelease.assets
.find { it.name == "hermes-cppruntime-release.aar" }!!.browserDownloadUrl
.also { downloadManager.download(it, file) }
}
Pair(hermes, cppruntime)
Pair(hermes, cppRuntime)
}
// Download Aliucord Native lib
@ -249,20 +254,12 @@ class InstallViewModel(
)
// Download the Aliucord classes.dex file to add to the apk
externalCacheDir.resolve("classes-${latestAliucordNativeRelease.tagName}.dex").also { file ->
externalCacheDir.resolve("aliucord-${latestAliucordNativeRelease.tagName}.dex").also { file ->
if (file.exists()) return@also
downloadManager.download(
url = latestAliucordNativeRelease.assets.find { it.name == "classes.dex" }!!.browserDownloadUrl,
fileName = "classes-${latestAliucordNativeRelease.tagName}.dex"
).apply {
copyTo(
externalCacheDir
.resolve("patched")
.resolve(this.name),
true
)
}
latestAliucordNativeRelease.assets
.find { it.name == "classes.dex" }!!.browserDownloadUrl
.also { downloadManager.download(it, file) }
}
}
@ -374,6 +371,187 @@ class InstallViewModel(
}
}
private suspend fun installKotlin() {
steps += listOfNotNull(
InstallStep.FETCH_KT_VERSION,
InstallStep.DL_KT_APK,
InstallStep.DL_KOTLIN,
InstallStep.DL_INJECTOR,
InstallStep.DL_ALIUHOOK,
if (preferences.replaceIcon) InstallStep.PATCH_APP_ICON else null,
InstallStep.PATCH_MANIFEST,
InstallStep.PATCH_DEX,
InstallStep.PATCH_LIBS,
InstallStep.SIGN_APK,
InstallStep.INSTALL_APK,
).map {
it to InstallStepData(it.nameResId, InstallStatus.QUEUED)
}
val dataJson = step(InstallStep.FETCH_KT_VERSION) {
githubRepository.getDiscordKtVersion().getOrThrow()
}
val arch = Build.SUPPORTED_ABIS.first()
val cacheDir = externalCacheDir
val discordCacheDir = externalCacheDir.resolve(dataJson.versionCode)
val patchedDir = discordCacheDir.resolve("patched").also { it.deleteRecursively() }
// Download base.apk
val baseApkFile = step(InstallStep.DL_KT_APK) {
discordCacheDir.resolve("base.apk").let { file ->
if (file.exists()) {
cached = true
} else {
downloadManager.downloadDiscordApk(dataJson.versionCode, file)
}
file.copyTo(
patchedDir.resolve(file.name),
true
)
}
}
val kotlinFile = step(InstallStep.DL_KOTLIN) {
cacheDir.resolve("kotlin.dex").also { file ->
if (file.exists()) {
cached = true
} else {
downloadManager.downloadKotlinDex(file)
}
}
}
// Download the injector dex
val injectorFile = step(InstallStep.DL_INJECTOR) {
cacheDir.resolve("injector-${dataJson.aliucordHash}.dex").also { file ->
if (file.exists()) {
cached = true
} else {
downloadManager.downloadKtInjector(file)
}
}
}
// Download Aliuhook aar
val aliuhookAarFile = step(InstallStep.DL_ALIUHOOK) {
// Fetch aliuhook version
val aliuhookVersion = aliucordMaven.getAliuhookVersion().getOrThrow()
// Download aliuhook aar
cacheDir.resolve("aliuhook-${aliuhookVersion}.aar").also { file ->
if (file.exists()) {
cached = true
} else {
downloadManager.downloadAliuhook(aliuhookVersion, file)
}
}
}
// Replace app icons
if (preferences.replaceIcon) {
step(InstallStep.PATCH_APP_ICON) {
ZipWriter(baseApkFile, true).use { baseApk ->
val foregroundIcon = application.assets.open("icons/ic_logo_foreground.png")
.use { it.readBytes() }
val squareIcon = application.assets.open("icons/ic_logo_square.png")
.use { it.readBytes() }
val replacements = mapOf(
arrayOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") to foregroundIcon,
arrayOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") to squareIcon
)
for ((files, replacement) in replacements) {
for (file in files) {
val path = "res/$file"
baseApk.deleteEntry(path)
baseApk.writeEntry(path, replacement)
}
}
}
}
}
// Patch manifests
step(InstallStep.PATCH_MANIFEST) {
val manifest = ZipReader(baseApkFile)
.use { zip -> zip.openEntry("AndroidManifest.xml")?.read() }
?: throw IllegalStateException("No manifest in base apk")
ZipWriter(baseApkFile, true).use { zip ->
val patchedManifestBytes = ManifestPatcher.patchManifest(
manifestBytes = manifest,
packageName = preferences.packageName,
appName = preferences.appName,
debuggable = preferences.debuggable,
)
zip.deleteEntry("AndroidManifest.xml")
zip.writeEntry("AndroidManifest.xml", patchedManifestBytes)
}
}
// Re-order dex files
val dexCount = step(InstallStep.PATCH_DEX) {
val (dexCount, firstDexBytes) = ZipReader(baseApkFile).use { zip ->
Pair(
// Find the amount of .dex files in apk
zip.entryNames.count { it.endsWith(".dex") },
// Get the first dex
zip.openEntry("classes.dex")?.read()
?: throw IllegalStateException("No classes.dex in base apk")
)
}
ZipWriter(baseApkFile, true).use { zip ->
// Move copied dex to end of dex list
zip.deleteEntry("classes.dex")
zip.writeEntry("classes${dexCount + 1}.dex", firstDexBytes)
// Add Kotlin & Aliucord's dex
zip.writeEntry("classes.dex", injectorFile.readBytes())
zip.writeEntry("classes${dexCount + 2}.dex", kotlinFile.readBytes())
}
dexCount
}
// Replace libs
step(InstallStep.PATCH_LIBS) {
ZipWriter(baseApkFile, true).use { baseApk ->
ZipReader(aliuhookAarFile).use { aliuhookAar ->
for (libFile in arrayOf("libaliuhook.so", "libc++_shared.so", "liblsplant.so")) {
val bytes = aliuhookAar.openEntry("jni/$arch/$libFile")?.read()
?: throw IllegalStateException("Failed to read $libFile from aliuhook aar")
baseApk.writeEntry("lib/$arch/$libFile", bytes)
}
// Add Aliuhook's dex file
val aliuhookDex = aliuhookAar.openEntry("classes.dex")?.read()
?: throw IllegalStateException("No classes.dex in aliuhook aar")
baseApk.writeEntry("classes${dexCount + 3}.dex", aliuhookDex)
}
}
}
step(InstallStep.SIGN_APK) {
Signer.signApk(baseApkFile)
}
step(InstallStep.INSTALL_APK) {
application.packageManager.packageInstaller
.installApks(application, baseApkFile)
}
patchedDir.deleteRecursively()
}
@OptIn(ExperimentalTime::class)
private inline fun <T> step(step: InstallStep, block: InstallStepData.() -> T): T {
steps[step] = steps[step]!!.copy(status = InstallStatus.ONGOING)
@ -415,12 +593,14 @@ class InstallViewModel(
INSTALLING(R.string.install_group_install)
}
// Order matters, define it in the same order as it is patched
enum class InstallStep(
val group: InstallStepGroup,
@StringRes
val nameResId: Int
) {
// React Native
DL_BASE_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_base),
DL_LIBS_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_lib),
DL_LANG_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_apk_locale),
@ -429,6 +609,14 @@ class InstallViewModel(
DL_HERMES(InstallStepGroup.LIB_DL, R.string.install_step_dl_lib_hermes),
DL_ALIUNATIVE(InstallStepGroup.LIB_DL, R.string.install_step_dl_lib_aliunative),
// Kotlin
FETCH_KT_VERSION(InstallStepGroup.APK_DL, R.string.install_step_fetch_kt_version),
DL_KT_APK(InstallStepGroup.APK_DL, R.string.install_step_dl_kt_apk),
DL_KOTLIN(InstallStepGroup.LIB_DL, R.string.install_step_dl_kotlin),
DL_INJECTOR(InstallStepGroup.LIB_DL, R.string.install_step_dl_injector),
DL_ALIUHOOK(InstallStepGroup.LIB_DL, R.string.install_step_dl_aliuhook),
// Common
PATCH_APP_ICON(InstallStepGroup.PATCHING, R.string.install_step_patch_icons),
PATCH_MANIFEST(InstallStepGroup.PATCHING, R.string.install_step_patch_manifests),
PATCH_DEX(InstallStepGroup.PATCHING, R.string.install_step_patch_dex),

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Менеджер Алиюкорда</string>
<string name="app_description">A modification for the Discord Android App</string>
<string name="aliucord">Алиюкорд</string>
<string name="github">GitHub</string>
<string name="support_server">Support Server</string>
<string name="commits">Commits</string>
<string name="team">Team</string>
<string name="website">Website</string>
<string name="installer">Installer</string>
<string name="action_retry">Повторить</string>
<string name="action_apply">Применить</string>
<string name="action_confirm">Подтвердить</string>
<string name="action_dismiss">Скрыть</string>
<string name="action_install">Установить</string>
<string name="action_reinstall">Переустановить</string>
<string name="action_update">Обновить</string>
<string name="action_clear">Стереть</string>
<string name="action_launch">Запустить</string>
<string name="action_uninstall">Удалить</string>
<string name="action_close">Закрыть</string>
<string name="action_open_settings">Открыть настройки</string>
<string name="action_search">Поиск</string>
<string name="action_loading_trailing">Loading…</string>
<string name="permissions_grant_title">Grant Permissions</string>
<string name="permissions_grant_body">In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function.</string>
<string name="install_success">Successfully installed Aliucord</string>
<string name="install_aborted">Aborted Aliucord installation</string>
<string name="install_failed">Установка завершилась с ошибкой: %o</string>
<string name="launch_aliucord_fail">Failed to launch Aliucord</string>
<string name="settings_theme">Тема</string>
<string name="settings_advanced">Advanced</string>
<string name="settings_appearance">Appearance</string>
<string name="settings_developer_options">Developer options</string>
<string name="setting_clear_cache">Clear cache</string>
<string name="setting_dex_location">Dex location</string>
<string name="setting_use_dex_from_storage">Use Injector.dex from storage</string>
<string name="setting_replace_icon">Replace app icon</string>
<string name="setting_replace_icon_desc">Uses the Aliucord variant as opposed to Discord\'s own icon</string>
<string name="setting_debuggable">Debuggable</string>
<string name="setting_debuggable_desc">Enable debuggable flag</string>
<string name="setting_theme_desc">Manager appearance</string>
<string name="setting_app_name">App Name</string>
<string name="setting_package_name">Package Name</string>
<string name="setting_target_version">Версия загрузки</string>
<string name="setting_dynamic_color">Dynamic color</string>
<string name="theme_system">Системная</string>
<string name="theme_dark">Тёмная</string>
<string name="theme_light">Светлая</string>
<string name="plugins_title">Плагины</string>
<string name="plugins_view_changelog">Открыть список изменений для %1$s</string>
<string name="plugins_delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="plugins_search">Поиск плагинов</string>
<string name="plugins_none_installed">No plugins installed!</string>
<string name="plugins_changelog_media">Changelog Media</string>
<string name="plugins_plugin_title">§plugin§%1$s§ v%2$s</string>
<string name="navigation_back">Назад</string>
<string name="navigation_home">Главная</string>
<string name="navigation_about">About</string>
<string name="navigation_settings">Настройки</string>
<string name="contributors">Contributors</string>
<string name="contributors_contributions">%o contributions</string>
<string name="contributors_contributor">Contributor</string>
<string name="contributors_commit_title">§name§%1$s§ §middle§создал§ §commit§%2$s§</string>
<string name="selector_download_apk">Download APK</string>
<string name="selector_select_apk">Select APK</string>
<string name="selector_download_method">Download Method</string>
<string name="network_load_fail">Ошибка загрузки</string>
<string name="version_supported">Текущая версия:</string>
<string name="version_installed">Установленная версия:</string>
<string name="version_stable">Stable</string>
<string name="version_beta">Beta</string>
<string name="version_alpha">Alpha</string>
<string name="version_load_fail">Failed to retrieve version</string>
</resources>

View file

@ -13,7 +13,7 @@
<string name="action_retry">Retry</string>
<string name="action_apply">Apply</string>
<string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string>
<string name="action_cancel">Cancel</string>
<string name="action_install">Install</string>
<string name="action_reinstall">Reinstall</string>
<string name="action_update">Update</string>
@ -74,8 +74,10 @@
<string name="contributors_commit_title">§name§%1$s§ §middle§authored§ §commit§%2$s§</string>
<string name="selector_download_apk">Download APK</string>
<string name="selector_select_apk">Select APK</string>
<string name="selector_download_method">Download Method</string>
<string name="selector_discord_type">Discord Type</string>
<string name="selector_discord_type_body">Aliucord supports both the old (Kotlin) and new (React Native) versions of Discord. Which type would you like to install? Note that these versions have different plugins!</string>
<string name="selector_discord_type_kotlin">New (Recommended)</string>
<string name="selector_discord_type_old">Old</string>
<string name="network_load_fail">Failed to load</string>
@ -113,6 +115,11 @@
<string name="install_step_dl_apk_resource">Downloading resource apk</string>
<string name="install_step_dl_lib_hermes">Downloading hermes library</string>
<string name="install_step_dl_lib_aliunative">Downloading AliucordNative library</string>
<string name="install_step_fetch_kt_version">Fetching target Discord version</string>
<string name="install_step_dl_kt_apk">Downloading Discord APK</string>
<string name="install_step_dl_kotlin">Downloading Kotlin library</string>
<string name="install_step_dl_injector">Downloading injector</string>
<string name="install_step_dl_aliuhook">Downloading Aliuhook library</string>
<string name="status_success">Success</string>
<string name="status_ongoing">Ongoing</string>