implement proper installation

This commit is contained in:
X1nto 2021-11-03 00:35:12 +04:00
parent 9d3529e8a5
commit f7d0afc3b6
25 changed files with 644 additions and 386 deletions

View File

@ -79,12 +79,12 @@ val languages: String get() {
dependencies {
implementation(kotlin("reflect"))
implementation("androidx.core:core-ktx:1.6.0")
implementation("androidx.core:core-ktx:1.7.0")
implementation("androidx.appcompat:appcompat:1.3.1")
implementation("com.google.android.material:material:1.4.0")
implementation("androidx.browser:browser:1.3.0")
val composeVersion = "1.1.0-alpha06"
val composeVersion = "1.1.0-beta01"
implementation("androidx.compose.compiler:compiler:$composeVersion")
implementation("androidx.compose.foundation:foundation:$composeVersion")
implementation("androidx.compose.material:material-icons-core:$composeVersion")
@ -96,9 +96,11 @@ dependencies {
implementation("androidx.compose.ui:ui-util:$composeVersion")
implementation("androidx.compose.ui:ui:$composeVersion")
implementation("com.github.zsoltk:compose-router:0.28.0")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.activity:activity-compose:1.3.1")
implementation("androidx.activity:activity-compose:1.4.0")
val lifecycleVersion = "2.4.0-beta01"
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")

View File

@ -43,6 +43,8 @@
<service android:name="com.xinto.apkhelper.services.PackageManagerService" />
<service android:name=".core.installer.service.AppInstallService" />
</application>
</manifest>

View File

@ -18,7 +18,7 @@ abstract class AppDownloader {
private lateinit var call: DownloadCall
abstract suspend fun download(
appVersions: List<String>,
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
)
@ -41,9 +41,7 @@ abstract class AppDownloader {
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
writeFile(body, downloadFile.fileName) { progress ->
onProgress(progress)
}
writeFile(body, downloadFile.fileName, onProgress)
}
continue
}

View File

@ -1,6 +1,7 @@
package com.vanced.manager.core.downloader.impl
import android.content.Context
import android.util.Log
import com.vanced.manager.core.downloader.api.MicrogAPI
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
@ -12,7 +13,7 @@ class MicrogDownloader(
) : AppDownloader() {
override suspend fun download(
appVersions: List<String>,
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
downloadFiles(

View File

@ -17,18 +17,18 @@ class MusicDownloader(
private val version by musicVersionPref
private val variant by managerVariantPref
private lateinit var correctVersion: String
private lateinit var absoluteVersion: String
override suspend fun download(
appVersions: List<String>,
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
correctVersion = getLatestOrProvidedAppVersion(version, appVersions)
absoluteVersion = getLatestOrProvidedAppVersion(version, appVersions)
downloadFiles(
downloadFiles = arrayOf(DownloadFile(
call = musicAPI.getFiles(
version = correctVersion,
version = absoluteVersion,
variant = variant,
),
fileName = "music.apk"
@ -53,7 +53,7 @@ class MusicDownloader(
override fun getSavedFilePath(): String {
val directory =
File(context.getExternalFilesDir("vancedmusic")!!.path + "$correctVersion/$variant")
File(context.getExternalFilesDir("vancedmusic")!!.path + "$absoluteVersion/$variant")
if (!directory.exists())
directory.mkdirs()

View File

@ -22,13 +22,13 @@ class VancedDownloader(
private val variant by managerVariantPref
private val languages by vancedLanguagesPref
private lateinit var correctVersion: String
private lateinit var absoluteVersion: String
override suspend fun download(
appVersions: List<String>,
appVersions: List<String>?,
onStatus: (DownloadStatus) -> Unit
) {
correctVersion = getLatestOrProvidedAppVersion(version, appVersions)
absoluteVersion = getLatestOrProvidedAppVersion(version, appVersions)
val files = arrayOf(
getFile(
@ -68,7 +68,7 @@ class VancedDownloader(
override fun getSavedFilePath(): String {
val directory =
File(context.getExternalFilesDir("vanced")!!.path + "$correctVersion/$variant")
File(context.getExternalFilesDir("vanced")!!.path + "/$absoluteVersion/$variant")
if (!directory.exists())
directory.mkdirs()
@ -81,7 +81,7 @@ class VancedDownloader(
apkName: String,
) = DownloadFile(
call = vancedAPI.getFiles(
version = correctVersion,
version = absoluteVersion,
variant = variant,
type = type,
apkName = apkName

View File

@ -1,36 +1,7 @@
package com.vanced.manager.core.installer.base
import android.content.Context
import androidx.annotation.CallSuper
import com.vanced.manager.core.util.log
import com.xinto.apkhelper.statusCallback
import com.xinto.apkhelper.statusCallbackBuilder
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class AppInstaller {
open class AppInstaller : KoinComponent {
val context: Context by inject()
@CallSuper
open fun install(
onDone: () -> Unit
) {
setStatusCallback(onDone = onDone)
}
private fun setStatusCallback(
onDone: () -> Unit
) {
statusCallback = statusCallbackBuilder(
onInstall = { _, _ ->
onDone()
},
onInstallFailed = { error, _, _ ->
onDone()
log("install", error)
}
)
}
abstract fun install(appVersions: List<String>?)
}

View File

@ -1,19 +1,17 @@
package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.installer.base.AppInstaller
import com.xinto.apkhelper.installApk
import com.vanced.manager.core.installer.util.installApp
import java.io.File
class MicrogInstaller : AppInstaller() {
class MicrogInstaller(
private val context: Context
) : AppInstaller() {
override fun install(
onDone: () -> Unit
) {
super.install(onDone)
installApk(
apkPath = context.getExternalFilesDir("microg/microg.apk")!!.path,
context = context
)
override fun install(appVersions: List<String>?) {
val musicApk = File(context.getExternalFilesDir("microg/microg.apk")!!.path)
installApp(musicApk, context)
}
}

View File

@ -1,24 +1,23 @@
package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.installApp
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicVersionPref
import com.xinto.apkhelper.installApk
import java.io.File
class MusicInstaller : AppInstaller() {
override fun install(
onDone: () -> Unit
) {
super.install(onDone)
class MusicInstaller(
private val context: Context
) : AppInstaller() {
override fun install(appVersions: List<String>?) {
val version by musicVersionPref
val variant by managerVariantPref
installApk(
apkPath = context.getExternalFilesDir("music/$version/$variant/music.apk")!!.path,
context = context
)
val musicApk = File(context.getExternalFilesDir("music/$version/$variant/music.apk")!!.path)
installApp(musicApk, context)
}
}

View File

@ -1,24 +1,29 @@
package com.vanced.manager.core.installer.impl
import android.content.Context
import com.vanced.manager.core.installer.base.AppInstaller
import com.vanced.manager.core.installer.util.installSplitApp
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.vancedVersionPref
import com.xinto.apkhelper.installSplitApks
import com.vanced.manager.core.util.getLatestOrProvidedAppVersion
class VancedInstaller : AppInstaller() {
override fun install(
onDone: () -> Unit
) {
super.install(onDone)
class VancedInstaller(
private val context: Context
) : AppInstaller() {
override fun install(appVersions: List<String>?) {
val version by vancedVersionPref
val variant by managerVariantPref
installSplitApks(
apksPath = context.getExternalFilesDir("vanced/$version/$variant")!!.path,
context = context
)
val absoluteVersion = getLatestOrProvidedAppVersion(version, appVersions)
val apks = context
.getExternalFilesDir("vanced/$absoluteVersion/$variant")!!
.listFiles { file ->
file.extension == "apk"
}
installSplitApp(apks!!, context)
}
}

View File

@ -0,0 +1,44 @@
package com.vanced.manager.core.installer.service
import android.app.Service
import android.content.Intent
import android.content.pm.PackageInstaller
import android.os.IBinder
class AppInstallService : Service() {
override fun onStartCommand(
intent: Intent,
flags: Int,
startId: Int
): Int {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
startActivity(
intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT).apply {
this?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
else -> {
sendBroadcast(Intent().apply {
action = APP_INSTALL_STATUS
putExtra(EXTRA_INSTALL_STATUS, status)
putExtra(EXTRA_INSTALL_EXTRA, intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE))
})
}
}
stopSelf()
return START_NOT_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
companion object {
const val APP_INSTALL_STATUS = "APP_INSTALL_STATUS"
const val EXTRA_INSTALL_STATUS = "EXTRA_INSTALL_STATUS"
const val EXTRA_INSTALL_EXTRA = "EXTRA_INSTALL_EXTRA"
}
}

View File

@ -0,0 +1,73 @@
package com.vanced.manager.core.installer.util
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.os.Build
import com.vanced.manager.core.installer.service.AppInstallService
import java.io.File
import java.io.FileInputStream
private const val byteArraySize = 1024 * 1024 // Because 1,048,576 is not readable
fun installApp(apk: File, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
writeApkToSession(apk, session)
session.commit(getIntentSender(context))
session.close()
}
fun installSplitApp(apks: Array<File>, context: Context) {
val packageInstaller = context.packageManager.packageInstaller
val session =
packageInstaller.openSession(packageInstaller.createSession(sessionParams))
for (apk in apks) {
writeApkToSession(apk, session)
}
session.commit(getIntentSender(context))
session.close()
}
private val intentFlags
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
PendingIntent.FLAG_MUTABLE
else
0
private val sessionParams
get() = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setInstallReason(PackageManager.INSTALL_REASON_USER)
}
}
private fun getIntentSender(context: Context) =
PendingIntent.getService(
context,
0,
Intent(context, AppInstallService::class.java),
intentFlags
).intentSender
private fun writeApkToSession(
apk: File,
session: PackageInstaller.Session
) {
val inputStream = FileInputStream(apk)
val outputStream = session.openWrite(apk.name, 0, apk.length())
val buffer = ByteArray(byteArraySize)
var length: Int
while (inputStream.read(buffer).also { length = it } > 0) {
outputStream.write(buffer, 0, length)
}
session.fsync(outputStream)
inputStream.close()
outputStream.flush()
outputStream.close()
}

View File

@ -2,8 +2,11 @@ package com.vanced.manager.core.util
fun getLatestOrProvidedAppVersion(
version: String,
appVersions: List<String>
appVersions: List<String>?
): String {
if (appVersions == null)
return version
if (appVersions.contains(version))
return version

View File

@ -1,5 +1,6 @@
package com.vanced.manager.di
import android.content.Context
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
@ -7,13 +8,19 @@ import org.koin.dsl.module
val installerModule = module {
fun provideVancedInstaller() = VancedInstaller()
fun provideVancedInstaller(
context: Context
) = VancedInstaller(context)
fun provideMusicInstaller() = MusicInstaller()
fun provideMusicInstaller(
context: Context
) = MusicInstaller(context)
fun provideMicrogInstaller() = MicrogInstaller()
fun provideMicrogInstaller(
context: Context
) = MicrogInstaller(context)
single { provideVancedInstaller() }
single { provideMusicInstaller() }
single { provideMicrogInstaller() }
single { provideVancedInstaller(get()) }
single { provideMusicInstaller(get()) }
single { provideMicrogInstaller(get()) }
}

View File

@ -1,9 +1,11 @@
package com.vanced.manager.di
import com.vanced.manager.ui.viewmodel.InstallViewModel
import com.vanced.manager.ui.viewmodel.MainViewModel
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
val viewModelModule = module {
viewModel { MainViewModel(get()) }
viewModel { InstallViewModel(get(), get(), get(), get(), get(), get()) }
}

View File

@ -29,6 +29,6 @@ sealed class InstallationOption(
@Parcelize
data class InstallationOptionItem(
val displayText: String,
val key: String,
val displayText: (key: String) -> String,
) : Parcelable

View File

@ -23,7 +23,7 @@ class AppDtoMapper(
private val latestVersionRadioButton =
InstallationOptionItem(
displayText = context.getString(R.string.app_version_dialog_option_latest),
displayText = { context.getString(R.string.app_version_dialog_option_latest) },
key = "latest"
)
@ -83,8 +83,10 @@ class AppDtoMapper(
},
items = appThemes?.map { theme ->
InstallationOptionItem(
displayText = theme.replaceFirstChar {
it.titlecase(Locale.getDefault())
displayText = {
theme.replaceFirstChar {
it.titlecase(Locale.getDefault())
}
},
key = theme
)
@ -98,7 +100,7 @@ class AppDtoMapper(
},
items = appVersions?.map { version ->
InstallationOptionItem(
displayText = version,
displayText = { "v$version" },
key = version
)
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList(),
@ -112,12 +114,15 @@ class AppDtoMapper(
removeOption = {
vancedLanguagesPref.save(vancedLanguagesPref.value.value - it)
},
items = appLanguages?.map { version ->
items = appLanguages?.map { language ->
InstallationOptionItem(
displayText = version,
key = version
displayText = {
val locale = Locale(it)
locale.getDisplayName(locale)
},
key = language
)
}?.plus(latestVersionRadioButton)?.reversed() ?: emptyList(),
} ?: emptyList(),
),
)
MUSIC_NAME -> listOf(
@ -129,7 +134,7 @@ class AppDtoMapper(
},
items = appVersions?.map { version ->
InstallationOptionItem(
displayText = version,
displayText = { version },
key = version
)
} ?: emptyList(),

View File

@ -1,9 +1,12 @@
package com.vanced.manager.ui
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.material.*
@ -14,12 +17,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import com.google.accompanist.navigation.animation.AnimatedNavHost
import com.google.accompanist.navigation.animation.composable
import com.google.accompanist.navigation.animation.rememberAnimatedNavController
import com.github.zsoltk.compose.backpress.BackPressHandler
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
import com.github.zsoltk.compose.router.Router
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.vanced.manager.core.installer.service.AppInstallService
import com.vanced.manager.ui.component.color.managerAnimatedColor
import com.vanced.manager.ui.component.color.managerSurfaceColor
import com.vanced.manager.ui.component.color.managerTextColor
@ -30,160 +32,173 @@ import com.vanced.manager.ui.screens.*
import com.vanced.manager.ui.theme.ManagerTheme
import com.vanced.manager.ui.theme.isDark
import com.vanced.manager.ui.util.Screen
import com.vanced.manager.ui.viewmodel.InstallViewModel
import org.koin.android.ext.android.inject
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ManagerTheme {
MainActivityLayout()
}
private val installViewModel: InstallViewModel by inject()
private val backPressHandler = BackPressHandler()
private val installBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != AppInstallService.APP_INSTALL_STATUS) return
installViewModel.postInstallStatus(
pmStatus = intent.getIntExtra(AppInstallService.EXTRA_INSTALL_STATUS, -999),
extra = intent.getStringExtra(AppInstallService.EXTRA_INSTALL_EXTRA)!!,
)
}
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun MainActivityLayout() {
val isMenuExpanded = remember { mutableStateOf(false) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ManagerTheme {
var isMenuExpanded by remember { mutableStateOf(false) }
var currentScreen by remember { mutableStateOf<Screen>(Screen.Home) }
val surfaceColor = managerSurfaceColor()
val surfaceColor = managerSurfaceColor()
val isDark = isDark()
val isDark = isDark()
val navController = rememberAnimatedNavController()
val systemUiController = rememberSystemUiController()
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setSystemBarsColor(
color = surfaceColor,
darkIcons = !isDark
)
}
Scaffold(
topBar = {
MainToolbar(
navController = navController,
isMenuExpanded = isMenuExpanded
)
},
backgroundColor = surfaceColor
) {
AnimatedNavHost(
navController = navController,
startDestination = Screen.Home.route,
enterTransition = { _, _ ->
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.Start
)
},
exitTransition = { _, _ ->
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.End
)
},
popEnterTransition = { _, _ ->
slideIntoContainer(
towards = AnimatedContentScope.SlideDirection.End
)
},
popExitTransition = { _, _ ->
slideOutOfContainer(
towards = AnimatedContentScope.SlideDirection.Start
SideEffect {
systemUiController.setSystemBarsColor(
color = surfaceColor,
darkIcons = !isDark
)
}
) {
composable(Screen.Home.route) {
HomeLayout(navController)
}
composable(Screen.Settings.route) {
SettingsLayout()
}
composable(Screen.About.route) {
AboutLayout()
}
composable(Screen.InstallPreferences.route) {
val arguments = navController.previousBackStackEntry?.arguments
InstallPreferencesScreen(
installationOptions = arguments?.getParcelableArrayList("app")!!
)
}
composable(Screen.Install.route,) {
val arguments = navController.previousBackStackEntry?.arguments
CompositionLocalProvider(
LocalBackPressHandler provides backPressHandler
) {
Router<Screen>("VancedManager", Screen.Home) { backStack ->
val screen = backStack.last()
currentScreen = screen
InstallScreen(
appName = arguments?.getString("appName")!!,
appVersions = arguments.getParcelableArrayList("appVersions")!!
)
Scaffold(
topBar = {
TopAppBar(
title = {
ToolbarTitleText(
text = managerString(
stringId = currentScreen.displayName
)
)
},
backgroundColor = managerAnimatedColor(color = MaterialTheme.colors.surface),
actions = {
if (currentScreen is Screen.Home) {
IconButton(
onClick = { isMenuExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = managerTextColor()
)
}
DropdownMenu(
expanded = isMenuExpanded,
onDismissRequest = {
isMenuExpanded = false
},
modifier = Modifier.background(MaterialTheme.colors.surface),
) {
ManagerDropdownMenuItem(
title = stringResource(id = Screen.Settings.displayName)
) {
isMenuExpanded = false
backStack.push(Screen.Settings)
}
}
}
},
navigationIcon = if (currentScreen !is Screen.Home) {
{
IconButton(onClick = {
backStack.pop()
}) {
Icon(
imageVector = Icons.Default.ArrowBackIos,
contentDescription = null,
tint = managerTextColor()
)
}
}
} else null,
elevation = 0.dp
)
},
backgroundColor = surfaceColor
) {
when (screen) {
is Screen.Home -> {
HomeLayout(
onAppInstallPress = { appName, appVersions, installationOptions ->
if (installationOptions != null) {
backStack.push(Screen.InstallPreferences(appName, appVersions, installationOptions))
} else {
backStack.push(Screen.Install(appName, appVersions))
}
}
)
}
is Screen.Settings -> {
SettingsLayout()
}
is Screen.About -> {
AboutLayout()
}
is Screen.Logs -> {
}
is Screen.InstallPreferences -> {
InstallPreferencesScreen(
installationOptions = screen.appInstallationOptions,
onDoneClick = {
backStack.push(Screen.Install(screen.appName, screen.appVersions))
}
)
}
is Screen.Install -> {
InstallScreen(screen.appName, screen.appVersions)
}
}
}
}
}
}
}
}
@Composable
fun MainToolbar(
navController: NavHostController,
isMenuExpanded: MutableState<Boolean>
) {
val currentScreenRoute =
navController.currentBackStackEntryAsState().value?.destination?.route
override fun onBackPressed() {
if (!backPressHandler.handle())
super.onBackPressed()
}
TopAppBar(
title = {
ToolbarTitleText(
text = managerString(
stringId = Screen.values().find { it.route == currentScreenRoute }?.displayName
)
)
},
backgroundColor = managerAnimatedColor(color = MaterialTheme.colors.surface),
actions = {
if (currentScreenRoute == Screen.Home.route) {
IconButton(
onClick = { isMenuExpanded.value = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = null,
tint = managerTextColor()
)
}
override fun onStart() {
super.onStart()
DropdownMenu(
expanded = isMenuExpanded.value,
onDismissRequest = {
isMenuExpanded.value = false
},
modifier = Modifier.background(MaterialTheme.colors.surface),
) {
for (screen in Screen.values()) {
ManagerDropdownMenuItem(
title = stringResource(id = screen.displayName)
) {
isMenuExpanded.value = false
navController.navigate(screen.route)
}
}
}
}
},
navigationIcon = if (currentScreenRoute != Screen.Home.route) {
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(
imageVector = Icons.Default.ArrowBackIos,
contentDescription = null,
tint = managerTextColor()
)
}
}
} else null,
elevation = 0.dp
registerReceiver(
installBroadcastReceiver,
IntentFilter().apply {
addAction(AppInstallService.APP_INSTALL_STATUS)
}
)
}
override fun onStop() {
super.onStop()
unregisterReceiver(installBroadcastReceiver)
}
}

View File

@ -8,15 +8,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
@Composable
fun ManagerLazyColumn(
modifier: Modifier = Modifier,
itemSpacing: Dp = 0.dp,
contentPadding: PaddingValues = PaddingValues(12.dp),
content: LazyListScope.() -> Unit
) {
LazyColumn(
contentPadding = PaddingValues(12.dp),
modifier = modifier,
contentPadding = contentPadding,
verticalArrangement = Arrangement.spacedBy(itemSpacing),
content = content
)

View File

@ -16,6 +16,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vanced.manager.R
import com.vanced.manager.core.util.socialMedia
import com.vanced.manager.core.util.sponsors
import com.vanced.manager.domain.model.InstallationOption
import com.vanced.manager.ui.component.card.ManagerLinkCard
import com.vanced.manager.ui.component.dialog.ManagerDialog
import com.vanced.manager.ui.component.layout.ManagerScrollableColumn
@ -35,7 +36,11 @@ import org.koin.androidx.compose.getViewModel
@ExperimentalAnimationApi
@Composable
fun HomeLayout(
navController: NavController
onAppInstallPress: (
appName: String,
appVersions: List<String>?,
installationOptions: List<InstallationOption>?
) -> Unit
) {
val viewModel: MainViewModel = getViewModel()
val appState by viewModel.appState.collectAsState()
@ -78,12 +83,7 @@ fun HomeLayout(
appInstalledVersion = app.installedVersion,
appRemoteVersion = app.remoteVersion,
onDownloadClick = {
if (app.installationOptions != null) {
navController.navigate(Screen.InstallPreferences.route)
} else {
navController.navigate(Screen.Install.route)
}
onAppInstallPress(app.name, app.versions, app.installationOptions)
},
onUninstallClick = { /*TODO*/ },
onLaunchClick = { /*TODO*/ },

View File

@ -5,67 +5,93 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.Scaffold
import androidx.compose.runtime.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Done
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.util.fastForEachIndexed
import com.vanced.manager.domain.model.InstallationOption
import com.vanced.manager.ui.component.card.ManagerCard
import com.vanced.manager.ui.component.layout.ManagerLazyColumn
import com.vanced.manager.ui.component.card.ManagerClickableThemedCard
import com.vanced.manager.ui.component.layout.ManagerScrollableColumn
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.resources.managerString
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
import com.vanced.manager.ui.widget.list.CheckboxItem
import com.vanced.manager.ui.widget.list.RadiobuttonItem
@Composable
fun InstallPreferencesScreen(
installationOptions: List<InstallationOption>
installationOptions: List<InstallationOption>,
onDoneClick: () -> Unit
) {
var selectedOptionIndex by rememberSaveable { mutableStateOf(0) }
Scaffold(
floatingActionButton = {
FloatingActionButton(
onClick = onDoneClick
) {
Icon(
imageVector = Icons.Rounded.Done,
contentDescription = "Done"
)
}
}
) { paddingValues ->
ManagerScrollableColumn(
modifier = Modifier.padding(paddingValues)
modifier = Modifier.padding(paddingValues),
itemSpacing = DefaultContentPaddingVertical
) {
installationOptions.fastForEachIndexed { index, installationOption ->
ManagerCard(onClick = {
selectedOptionIndex = index
}) {
Column {
ManagerClickableThemedCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = DefaultContentPaddingHorizontal),
onClick = {
selectedOptionIndex = index
}
) {
Column(
modifier = Modifier.padding(12.dp)
) {
ManagerText(
modifier = Modifier.fillMaxWidth(),
text = managerString(installationOption.itemTitleId),
textStyle = TextStyle(
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold
)
),
textAlign = TextAlign.Center
)
AnimatedVisibility(
visible = index == selectedOptionIndex
) {
ManagerLazyColumn(
ManagerScrollableColumn(
modifier = Modifier.sizeIn(
minHeight = 400.dp,
maxHeight = 400.dp
)
) {
when (installationOption) {
is InstallationOption.MultiSelect -> {
items(installationOption.items) { item ->
installationOption.items.fastForEach { item ->
val preference = installationOption.getOption()
CheckboxItem(
modifier = Modifier.fillMaxWidth(),
text = item.displayText,
text = item.displayText(item.key),
isChecked = preference.contains(item.key),
onCheck = {
if (it) {
@ -78,11 +104,11 @@ fun InstallPreferencesScreen(
}
}
is InstallationOption.SingleSelect -> {
items(installationOption.items) { item ->
installationOption.items.fastForEach { item ->
val preference = installationOption.getOption()
RadiobuttonItem(
modifier = Modifier.fillMaxWidth(),
text = item.displayText,
text = item.displayText(item.key),
isSelected = preference == item.key,
onSelect = {
installationOption.setOption(item.key)

View File

@ -2,19 +2,16 @@ package com.vanced.manager.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.items
import androidx.compose.material.FloatingActionButton
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowDropDown
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.rotate
import androidx.compose.ui.graphics.Color
@ -25,161 +22,123 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vanced.manager.core.downloader.impl.MicrogDownloader
import com.vanced.manager.core.downloader.impl.MusicDownloader
import com.vanced.manager.core.downloader.impl.VancedDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
import com.vanced.manager.network.util.MICROG_NAME
import com.vanced.manager.network.util.MUSIC_NAME
import com.vanced.manager.network.util.VANCED_NAME
import com.vanced.manager.ui.component.layout.ManagerLazyColumn
import com.vanced.manager.ui.component.modifier.managerClickable
import com.vanced.manager.ui.component.progressindicator.ManagerProgressIndicator
import com.vanced.manager.ui.component.text.ManagerText
import com.vanced.manager.ui.viewmodel.MainViewModel
import org.koin.androidx.compose.get
import com.vanced.manager.ui.util.DefaultContentPaddingHorizontal
import com.vanced.manager.ui.util.DefaultContentPaddingVertical
import com.vanced.manager.ui.viewmodel.InstallViewModel
import org.koin.androidx.compose.getViewModel
sealed class Log {
data class Info(val infoText: String) : Log()
data class Success(val successText: String) : Log()
data class Error(
val displayText: String,
val stacktrace: String,
) : Log()
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun InstallScreen(
appName: String,
appVersions: List<String>
appVersions: List<String>?
) {
val logs = rememberSaveable { mutableStateListOf<Log>() }
val viewModel: InstallViewModel = getViewModel()
var progress by rememberSaveable { mutableStateOf(0f) }
var installing by rememberSaveable { mutableStateOf(false) }
val viewModel: MainViewModel = getViewModel()
val downloader = when (appName) {
VANCED_NAME -> get<VancedDownloader>()
MUSIC_NAME -> get<MusicDownloader>()
MICROG_NAME -> get<MicrogDownloader>()
else -> throw IllegalArgumentException("$appName is not a valid app")
}
val installer = when (appName) {
VANCED_NAME -> get<VancedInstaller>()
MUSIC_NAME -> get<MusicInstaller>()
MICROG_NAME -> get<MicrogInstaller>()
else -> throw IllegalArgumentException("$appName is not a valid app")
}
//FIXME this is absolutely bad, must move to WorkManager
LaunchedEffect(true) {
downloader.download(appVersions) { status ->
when (status) {
is DownloadStatus.File -> logs.add(Log.Info("Downloading ${status.fileName}"))
is DownloadStatus.Error -> logs.add(Log.Error(
displayText = status.displayError,
stacktrace = status.stacktrace
))
is DownloadStatus.Progress -> progress = status.progress
is DownloadStatus.StartInstall -> {
installing = true
installer.install {
viewModel.fetch()
}
}
}
}
}
viewModel.startAppProcess(appName, appVersions)
Scaffold(
topBar = {
if (installing) {
ManagerProgressIndicator()
} else {
ManagerProgressIndicator(progress)
}
},
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
}
}
) { paddingValues ->
ManagerLazyColumn(
modifier = Modifier.padding(paddingValues)
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues),
contentPadding = PaddingValues(0.dp),
) {
items(logs) { log ->
stickyHeader {
when (val status = viewModel.status) {
is InstallViewModel.Status.Progress -> {
ManagerProgressIndicator(status.progress)
}
is InstallViewModel.Status.Installing -> {
ManagerProgressIndicator()
}
else -> {}
}
}
item {
Spacer(Modifier.height(DefaultContentPaddingVertical))
}
items(viewModel.logs) { log ->
when (log) {
is Log.Success -> {
is InstallViewModel.Log.Success -> {
ManagerText(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = DefaultContentPaddingHorizontal),
text = log.successText,
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
color = Color.Blue
),
)
}
is Log.Info -> {
is InstallViewModel.Log.Info -> {
ManagerText(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = DefaultContentPaddingHorizontal),
text = log.infoText,
textStyle = TextStyle(
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
color = Color.Green
color = Color.Black
),
)
}
is Log.Error -> {
is InstallViewModel.Log.Error -> {
var visible by remember { mutableStateOf(false) }
val iconRotation by animateFloatAsState(if (visible) 0f else 90f)
Row(
modifier = Modifier.managerClickable {
visible = !visible
},
horizontalArrangement = Arrangement.spacedBy(8.dp)
val iconRotation by animateFloatAsState(if (visible) -90f else 0f)
Column(
modifier = Modifier
.fillMaxWidth()
.managerClickable {
visible = !visible
}
.padding(horizontal = DefaultContentPaddingHorizontal),
) {
Icon(
modifier = Modifier.rotate(iconRotation),
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = "expand",
)
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ManagerText(
text = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colors.error)) {
append(log.displayText)
append(": ")
}
withStyle(SpanStyle(color = MaterialTheme.colors.error.copy(alpha = 0.7f))) {
append(log.stacktrace)
}
},
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
),
)
AnimatedVisibility(visible) {
ManagerText(
text = log.stacktrace,
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = Color.Green
),
)
}
Icon(
modifier = Modifier.rotate(iconRotation),
imageVector = Icons.Rounded.ArrowDropDown,
contentDescription = "expand",
tint = MaterialTheme.colors.error
)
}
AnimatedVisibility(visible) {
ManagerText(
text = log.stacktrace,
textStyle = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 14.sp,
color = MaterialTheme.colors.error.copy(alpha = 0.7f)
),
)
}
}
}

View File

@ -3,37 +3,48 @@ package com.vanced.manager.ui.util
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import com.vanced.manager.R
import com.vanced.manager.domain.model.InstallationOption
import com.vanced.manager.ui.screens.AboutLayout
import com.vanced.manager.ui.screens.HomeLayout
import com.vanced.manager.ui.screens.LogLayout
import com.vanced.manager.ui.screens.SettingsLayout
enum class Screen(
sealed class Screen(
val route: String,
@StringRes val displayName: Int,
) {
Home(
object Home : Screen(
route = "home",
displayName = R.string.app_name
),
Settings(
)
object Settings: Screen(
route = "settings",
displayName = R.string.toolbar_settings,
),
About(
)
object About: Screen(
route = "about",
displayName = R.string.toolbar_about,
),
Logs(
)
object Logs : Screen(
route = "logs",
displayName = R.string.toolbar_logs,
),
InstallPreferences(
)
data class InstallPreferences(
val appName: String,
val appVersions: List<String>?,
val appInstallationOptions: List<InstallationOption>
) : Screen(
route = "installpreferences",
displayName = R.string.toolbar_installation_preferences
),
Install(
)
data class Install(
val appName: String,
val appVersions: List<String>?
) : Screen(
route = "install",
displayName = R.string.toolbar_install
),
)
}

View File

@ -0,0 +1,131 @@
package com.vanced.manager.ui.viewmodel
import android.content.pm.PackageInstaller
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vanced.manager.core.downloader.impl.MicrogDownloader
import com.vanced.manager.core.downloader.impl.MusicDownloader
import com.vanced.manager.core.downloader.impl.VancedDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.core.installer.impl.MicrogInstaller
import com.vanced.manager.core.installer.impl.MusicInstaller
import com.vanced.manager.core.installer.impl.VancedInstaller
import com.vanced.manager.network.util.MICROG_NAME
import com.vanced.manager.network.util.MUSIC_NAME
import com.vanced.manager.network.util.VANCED_NAME
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class InstallViewModel(
private val vancedDownloader: VancedDownloader,
private val musicDownloader: MusicDownloader,
private val microgDownloader: MicrogDownloader,
private val vancedInstaller: VancedInstaller,
private val musicInstaller: MusicInstaller,
private val microgInstaller: MicrogInstaller,
) : ViewModel() {
sealed class Log {
data class Info(val infoText: String) : Log()
data class Success(val successText: String) : Log()
data class Error(
val displayText: String,
val stacktrace: String,
) : Log()
}
sealed class Status {
object Idle : Status()
object Installing : Status()
object Installed : Status()
object Failure : Status()
data class Progress(val progress: Float) : Status()
}
val logs = mutableStateListOf<Log>()
var status by mutableStateOf<Status>(Status.Idle)
private set
//TODO Move to WorkManager
fun startAppProcess(
appName: String,
appVersions: List<String>?
) {
viewModelScope.launch(Dispatchers.IO) {
downloadApp(appName, appVersions)
}
}
fun postInstallStatus(pmStatus: Int, extra: String) {
if (pmStatus == PackageInstaller.STATUS_SUCCESS) {
status = Status.Installed
logs.add(Log.Success("Successfully installed"))
} else {
status = Status.Failure
logs.add(Log.Error("Failed to install app", extra))
}
}
private suspend fun downloadApp(
appName: String,
appVersions: List<String>?,
) {
val downloader = getDownloader(appName)
downloader.download(appVersions) { downloadStatus ->
when (downloadStatus) {
is DownloadStatus.File -> logs.add(Log.Info("Downloading ${downloadStatus.fileName}"))
is DownloadStatus.Error -> logs.add(Log.Error(
displayText = downloadStatus.displayError,
stacktrace = downloadStatus.stacktrace
))
is DownloadStatus.Progress -> status = Status.Progress(downloadStatus.progress / 100)
is DownloadStatus.StartInstall -> {
logs.add(Log.Success("Successfully downloaded $appName"))
installApp(appName, appVersions)
}
}
}
}
private fun installApp(
appName: String,
appVersions: List<String>?,
) {
val installer = getInstaller(appName)
status = Status.Installing
installer.install(appVersions)
}
private fun getDownloader(
appName: String
) = when (appName) {
VANCED_NAME -> vancedDownloader
MUSIC_NAME -> musicDownloader
MICROG_NAME -> microgDownloader
else -> throw IllegalArgumentException("$appName is not a valid app")
}
private fun getInstaller(
appName: String
) = when (appName) {
VANCED_NAME -> vancedInstaller
MUSIC_NAME -> musicInstaller
MICROG_NAME -> microgInstaller
else -> throw IllegalArgumentException("$appName is not a valid app")
}
private fun clear() {
logs.clear()
status = Status.Idle
}
}

View File

@ -3,6 +3,9 @@ package com.vanced.manager.ui.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vanced.manager.core.downloader.base.AppDownloader
import com.vanced.manager.core.downloader.util.DownloadStatus
import com.vanced.manager.domain.model.App
import com.vanced.manager.core.preferences.holder.managerVariantPref
import com.vanced.manager.core.preferences.holder.musicEnabled