From 335f2ca7a3bf660816a25e7884047bcabd2c4d67 Mon Sep 17 00:00:00 2001 From: rushii <33725716+DiamondMiner88@users.noreply.github.com> Date: Sun, 30 Oct 2022 08:19:24 -0700 Subject: [PATCH] feat: aliu kotlin install (#33) --- README.md | 2 +- .../com/aliucord/manager/di/ManagerModule.kt | 3 +- .../aliucord/manager/di/RepositoryModule.kt | 2 + .../com/aliucord/manager/di/ServiceModule.kt | 2 + .../aliucord/manager/di/ViewModelModule.kt | 2 +- .../manager/domain/manager/DownloadManager.kt | 85 ++++-- .../repository/AliucordMavenRepository.kt | 29 +++ .../domain/repository/GithubRepository.kt | 2 +- .../manager/network/service/GithubService.kt | 2 + .../manager/network/service/HttpService.kt | 8 +- .../manager/network/service/MavenService.kt | 26 ++ .../manager/network/utils/ApiResponse.kt | 27 ++ .../manager/ui/dialog/InstallerDialog.kt | 162 +++++++----- .../aliucord/manager/ui/screen/HomeScreen.kt | 40 +-- .../manager/ui/viewmodel/HomeViewModel.kt | 2 +- .../manager/ui/viewmodel/InstallViewModel.kt | 242 ++++++++++++++++-- app/src/main/res/values-ru/strings.xml | 76 ++++++ app/src/main/res/values/strings.xml | 13 +- 18 files changed, 574 insertions(+), 151 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/domain/repository/AliucordMavenRepository.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/network/service/MavenService.kt create mode 100644 app/src/main/res/values-ru/strings.xml diff --git a/README.md b/README.md index 3c275b6..48e033e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt index 1a76776..efe7655 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt @@ -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) diff --git a/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt index 43d7b88..8f06217 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt index 9d78e6b..b6454ef 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt index e383dd3..0a28d2e 100644 --- a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt +++ b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt @@ -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) } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt index b71fe30..5dd404f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt @@ -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()!! + private val downloadManager = application.getSystemService()!! - 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/" } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/repository/AliucordMavenRepository.kt b/app/src/main/kotlin/com/aliucord/manager/domain/repository/AliucordMavenRepository.kt new file mode 100644 index 0000000..2b4d75e --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/repository/AliucordMavenRepository.kt @@ -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 { + return maven.getArtifactMetadata(BASE_URL, ALIUHOOK).transform { + "(.+?)".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") + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt index 6c90256..5eaf83c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt @@ -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() diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt index 603fe8c..6f23965 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt @@ -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" diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/HttpService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/HttpService.kt index d654b21..4cab7c1 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/service/HttpService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/HttpService.kt @@ -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(body)) } else { body = try { diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/MavenService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/MavenService.kt new file mode 100644 index 0000000..78b03ee --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/MavenService.kt @@ -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 = 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" + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt b/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt index 3ef33c9..b426047 100644 --- a/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/utils/ApiResponse.kt @@ -46,3 +46,30 @@ inline fun ApiResponse.transform(block: (T) -> R): ApiResponse { ApiResponse.Success(block(data)) } } + +inline fun ApiResponse.getOrThrow(): T { + return fold( + success = { it }, + fail = { throw it } + ) +} + +@Suppress("UNCHECKED_CAST") +inline fun ApiResponse.chain(block: (T) -> ApiResponse): ApiResponse { + return if (this !is ApiResponse.Success) { + // Error and Failure do not use the generic value + this as ApiResponse + } else { + block(data) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun ApiResponse.chain(secondary: ApiResponse): ApiResponse { + return if (secondary is ApiResponse.Success) { + secondary + } else { + // Error and Failure do not use the generic value + this as ApiResponse + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/dialog/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/dialog/InstallerDialog.kt index 3e4a71b..1560424 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/dialog/InstallerDialog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/dialog/InstallerDialog.kt @@ -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(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 - ) + } + } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt index b9137fd..397dba9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt @@ -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 ) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt index d779db9..01b5e29 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt @@ -74,7 +74,7 @@ class HomeViewModel( } private suspend fun _fetchSupportedVersion() { - val version = githubRepository.getVersion() + val version = githubRepository.getDiscordKtVersion() version.fold( success = { diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt index ee1d712..036799a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt @@ -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 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), diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..61f6db9 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,76 @@ + + + Менеджер Алиюкорда + A modification for the Discord Android App + Алиюкорд + GitHub + Support Server + Commits + Team + Website + Installer + Повторить + Применить + Подтвердить + Скрыть + Установить + Переустановить + Обновить + Стереть + Запустить + Удалить + Закрыть + Открыть настройки + Поиск + Loading… + Grant Permissions + 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. + Successfully installed Aliucord + Aborted Aliucord installation + Установка завершилась с ошибкой: %o + Failed to launch Aliucord + Тема + Advanced + Appearance + Developer options + Clear cache + Dex location + Use Injector.dex from storage + Replace app icon + Uses the Aliucord variant as opposed to Discord\'s own icon + Debuggable + Enable debuggable flag + Manager appearance + App Name + Package Name + Версия загрузки + Dynamic color + Системная + Тёмная + Светлая + Плагины + Открыть список изменений для %1$s + Are you sure you want to uninstall this plugin? + Поиск плагинов + No plugins installed! + Changelog Media + §plugin§%1$s§ v%2$s + Назад + Главная + About + Настройки + Contributors + %o contributions + Contributor + §name§%1$s§ §middle§создал§ §commit§%2$s§ + Download APK + Select APK + Download Method + Ошибка загрузки + Текущая версия: + Установленная версия: + Stable + Beta + Alpha + Failed to retrieve version + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9d50a65..cdafc5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -13,7 +13,7 @@ Retry Apply Confirm - Dismiss + Cancel Install Reinstall Update @@ -74,8 +74,10 @@ §name§%1$s§ §middle§authored§ §commit§%2$s§ Download APK - Select APK - Download Method + Discord Type + 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! + New (Recommended) + Old Failed to load @@ -113,6 +115,11 @@ Downloading resource apk Downloading hermes library Downloading AliucordNative library + Fetching target Discord version + Downloading Discord APK + Downloading Kotlin library + Downloading injector + Downloading Aliuhook library Success Ongoing