diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt index cc854d5b..800fe175 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/StepRunner.kt @@ -20,7 +20,7 @@ abstract class StepRunner : KoinComponent { inline fun getStep(): T { val step = steps.asSequence() .filterIsInstance() - .filter { it.state == StepState.Success } + .filter { it.state.isFinished } .firstOrNull() if (step == null) { @@ -37,8 +37,8 @@ abstract class StepRunner : KoinComponent { // Add delay for human psychology and // better group visibility in UI (the active group can change way too fast) - if (!preferences.devMode && step.durationMs < 1000) { - delay(1000L - step.durationMs) + if (!preferences.devMode && step.durationMs < 500) { + delay(500L - step.durationMs) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt index 2fe08d57..c35e6253 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/DownloadStep.kt @@ -79,7 +79,7 @@ abstract class DownloadStep : Step(), KoinComponent { } is DownloadManager.Result.Cancelled -> - state = StepState.Cancelled + state = StepState.Error } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt index 72b60b2a..250d87e2 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/Step.kt @@ -63,7 +63,10 @@ abstract class Step { val (error, executionTimeMs) = measureTimedValue { try { execute(container) - state = StepState.Success + + if (state != StepState.Skipped) + state = StepState.Success + null } catch (t: Throwable) { state = StepState.Error diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt index 1024f3f8..eac39e79 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/base/StepState.kt @@ -5,6 +5,8 @@ enum class StepState { Running, Success, Error, - Skipped, - Cancelled, // TODO: something like the discord dnd sign except its not red, but gray maybe + Skipped; + + val isFinished: Boolean + get() = this == Success || this == Error || this == Skipped } diff --git a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt index 8b58c948..46bce400 100644 --- a/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt +++ b/app/src/main/kotlin/com/aliucord/manager/manager/DownloadManager.kt @@ -4,6 +4,8 @@ import android.app.Application import android.app.DownloadManager import android.database.Cursor import android.net.Uri +import android.os.Build +import android.util.Log import androidx.annotation.StringRes import androidx.core.content.getSystemService import com.aliucord.manager.BuildConfig @@ -40,10 +42,16 @@ class DownloadManager(application: Application) { .setTitle("Aliucord Manager") .setDescription("Downloading ${out.name}...") .setDestinationUri(Uri.fromFile(out)) - .setAllowedOverMetered(true) - .setAllowedOverRoaming(true) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) .addRequestHeader("User-Agent", "Aliucord Manager/${BuildConfig.VERSION_NAME}") + .apply { + // Disable gzip on emulator due to https compression bug + println(Build.PRODUCT) + // if (Build.PRODUCT == "google_sdk") { + Log.i(BuildConfig.TAG, "Disabling DownloadManager compression") + addRequestHeader("Accept-Encoding", null) + // } + } .let(downloadManager::enqueue) // Repeatedly request download state until it is finished diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt deleted file mode 100644 index 6236a798..00000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallGroup.kt +++ /dev/null @@ -1,176 +0,0 @@ -package com.aliucord.manager.ui.components.installer - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.* -import com.aliucord.manager.R -import com.aliucord.manager.installer.steps.base.Step -import kotlinx.collections.immutable.ImmutableList -import kotlin.math.floor - -@Composable -fun InstallGroup( - name: String, - isCurrent: Boolean, - subSteps: ImmutableList, - onClick: () -> Unit, -) { - val status = when { - subSteps.all { it.status == InstallStatus.QUEUED } -> - InstallStatus.QUEUED - - subSteps.all { it.status == InstallStatus.SUCCESSFUL } -> - InstallStatus.SUCCESSFUL - - subSteps.any { it.status == InstallStatus.ONGOING } -> - InstallStatus.ONGOING - - else -> InstallStatus.UNSUCCESSFUL - } - - Column( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(16.dp)) - .run { - if (isCurrent) { - background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) - } else this - } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .clickable(true, onClick = onClick) - .fillMaxWidth() - .padding(16.dp) - ) { - StepIcon(status, 24.dp) - - Text(text = name) - - Spacer(modifier = Modifier.weight(1f)) - - if (status != InstallStatus.ONGOING && status != InstallStatus.QUEUED) Text( - "%.2fs".format(subSteps.map { it.duration }.sum()), - style = MaterialTheme.typography.labelMedium - ) - - if (isCurrent) { - Icon( - painter = painterResource(R.drawable.ic_arrow_up_small), - contentDescription = stringResource(R.string.action_collapse) - ) - } else { - Icon( - painter = painterResource(R.drawable.ic_arrow_down_small), - contentDescription = stringResource(R.string.action_expand) - ) - } - } - - AnimatedVisibility(visible = isCurrent) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.background.copy(0.6f)) - .fillMaxWidth() - .padding(16.dp) - .padding(start = 4.dp) - ) { - subSteps.forEach { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - ) { - StepIcon(it.status, size = 18.dp) - - Text( - text = stringResource(it.nameResId), - style = MaterialTheme.typography.labelLarge, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, true), - ) - - if (it.status != InstallStatus.ONGOING && it.status != InstallStatus.QUEUED) { - if (it.cached) { - val style = MaterialTheme.typography.labelSmall.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - fontStyle = FontStyle.Italic, - fontSize = 11.sp - ) - Text( - text = stringResource(R.string.installer_cached), - style = style, - maxLines = 1, - ) - } - - Text( - text = "%.2fs".format(it.duration), - style = MaterialTheme.typography.labelSmall, - maxLines = 1, - ) - } - } - } - } - } - } -} - -@Composable -private fun StepIcon(status: InstallStatus, size: Dp) { - val strokeWidth = Dp(floor(size.value / 10) + 1) - val context = LocalContext.current - - when (status) { - InstallStatus.ONGOING -> CircularProgressIndicator( - strokeWidth = strokeWidth, - modifier = Modifier - .size(size) - .semantics { - contentDescription = context.getString(R.string.status_ongoing) - } - ) - - InstallStatus.SUCCESSFUL -> Icon( - painter = painterResource(R.drawable.ic_check_circle), - contentDescription = stringResource(R.string.status_success), - tint = Color(0xFF59B463), - modifier = Modifier.size(size) - ) - - InstallStatus.UNSUCCESSFUL -> Icon( - painter = painterResource(R.drawable.ic_canceled), - contentDescription = stringResource(R.string.status_failed), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(size) - ) - - InstallStatus.QUEUED -> Icon( - painter = painterResource(R.drawable.ic_circle), - contentDescription = stringResource(R.string.status_queued), - tint = MaterialTheme.colorScheme.onSurface.copy(0.4f), - modifier = Modifier.size(size) - ) - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt index ab9162b4..cedb5602 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallModel.kt @@ -115,7 +115,7 @@ class InstallModel( val gitChanges = if (BuildConfig.GIT_LOCAL_CHANGES || BuildConfig.GIT_LOCAL_COMMITS) "(Changes present)" else "" val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unknown" - return """ + val header = """ Aliucord Manager v${BuildConfig.VERSION_NAME} Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges @@ -123,8 +123,8 @@ class InstallModel( Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()} Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE}) SOC: $soc - - ${Log.getStackTraceString(stacktrace)} """.trimIndent() + + return header + "\n\n" + Log.getStackTraceString(stacktrace) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt index 64a4db4c..ca6ee4fb 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/InstallScreen.kt @@ -27,7 +27,7 @@ import com.aliucord.manager.R import com.aliucord.manager.installer.steps.StepGroup import com.aliucord.manager.ui.components.back import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog -import com.aliucord.manager.ui.components.installer.InstallGroup +import com.aliucord.manager.ui.screens.install.components.StepGroupCard class InstallScreen : Screen { override val key = "Install" @@ -97,11 +97,11 @@ class InstallScreen : Screen { model.installSteps?.let { groupedSteps -> for ((group, steps) in groupedSteps.entries) key(group) { - InstallGroup( + StepGroupCard( name = stringResource(group.localizedName), - isCurrent = group == expandedGroup, - onClick = remember { { expandedGroup = group } }, subSteps = steps, + isExpanded = expandedGroup == group, + onExpand = { expandedGroup = group }, ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt new file mode 100644 index 00000000..1d1471e8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepGroupCard.kt @@ -0,0 +1,104 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.Step +import com.aliucord.manager.installer.steps.base.StepState +import com.aliucord.manager.ui.util.thenIf +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun StepGroupCard( + name: String, + subSteps: ImmutableList, + isExpanded: Boolean, + onExpand: () -> Unit, +) { + val groupState by remember { + derivedStateOf { + when { + // If all steps are pending then show pending + subSteps.all { it.state == StepState.Pending } -> StepState.Pending + // If any step has finished with an error then default to error + subSteps.any { it.state == StepState.Error } -> StepState.Error + // If all steps have finished as Skipped/Success then show success + subSteps.all { it.state.isFinished } -> StepState.Success + + else -> StepState.Running + } + } + } + + LaunchedEffect(groupState) { + if (groupState != StepState.Pending) + onExpand() + } + + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .thenIf(isExpanded) { + background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .clickable(true, onClick = onExpand) + .fillMaxWidth() + .padding(16.dp) + ) { + StepStatusIcon(groupState, 24.dp) + + Text(text = name) + + Spacer(modifier = Modifier.weight(1f)) + + if (groupState.isFinished) Text( + "%.2fs".format(subSteps.sumOf { it.durationMs } / 1000f), + style = MaterialTheme.typography.labelMedium + ) + + if (isExpanded) { + Icon( + painter = painterResource(R.drawable.ic_arrow_up_small), + contentDescription = stringResource(R.string.action_collapse) + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_arrow_down_small), + contentDescription = stringResource(R.string.action_expand) + ) + } + } + + AnimatedVisibility(visible = isExpanded) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .background(MaterialTheme.colorScheme.background.copy(0.6f)) + .fillMaxWidth() + .padding(16.dp) + .padding(start = 4.dp) + ) { + for (step in subSteps) key(step) { + StepItem(step) + } + } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt new file mode 100644 index 00000000..95507357 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepItem.kt @@ -0,0 +1,44 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.aliucord.manager.installer.steps.base.Step + +@Composable +fun StepItem( + step: Step, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + StepStatusIcon(step.state, size = 18.dp) + + Text( + text = stringResource(step.localizedName), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, true), + ) + + // TODO: live step duration counter + if (step.state.isFinished) { + Text( + text = "%.2fs".format(step.durationMs / 1000f), + style = MaterialTheme.typography.labelSmall, + maxLines = 1, + ) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt new file mode 100644 index 00000000..983c52a8 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/install/components/StepStatusIcon.kt @@ -0,0 +1,59 @@ +package com.aliucord.manager.ui.screens.install.components + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import com.aliucord.manager.R +import com.aliucord.manager.installer.steps.base.StepState +import kotlin.math.floor + +@Composable +fun StepStatusIcon(status: StepState, size: Dp) { + val strokeWidth = Dp(floor(size.value / 10) + 1) + val context = LocalContext.current + + when (status) { + StepState.Pending -> Icon( + painter = painterResource(R.drawable.ic_circle), + contentDescription = stringResource(R.string.status_queued), + tint = MaterialTheme.colorScheme.onSurface.copy(0.4f), + modifier = Modifier.size(size) + ) + + StepState.Running -> CircularProgressIndicator( + strokeWidth = strokeWidth, + modifier = Modifier + .size(size) + .semantics { contentDescription = context.getString(R.string.status_ongoing) } + ) + + StepState.Success -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_success), + tint = Color(0xFF59B463), + modifier = Modifier.size(size) + ) + + StepState.Error -> Icon( + painter = painterResource(R.drawable.ic_canceled), + contentDescription = stringResource(R.string.status_failed), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(size) + ) + + StepState.Skipped -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_skipped), + tint = Color(0xFFAEAEAE), + modifier = Modifier.size(size) + ) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt new file mode 100644 index 00000000..7e82b946 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/IfModifier.kt @@ -0,0 +1,22 @@ +@file:Suppress("unused") + +package com.aliucord.manager.ui.util + +import androidx.compose.ui.Modifier + +/** + * Apply additional modifiers if [value] is not null. + */ +inline fun Modifier.thenIf(value: T?, block: Modifier.(T) -> Modifier): Modifier = + value?.let { block(it) } ?: this + +/** + * Apply additional modifiers if [predicate] is true. + */ +inline fun Modifier.thenIf(predicate: Boolean, block: Modifier.() -> Modifier): Modifier { + return if (predicate) { + block() + } else { + this + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b8265a0..7fdb02c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -126,10 +126,11 @@ Installing APKs Cleaning up - Success + Queued Ongoing + Skipped + Success Failed - Queued Really exit? Are you sure you really want to abort an in-progress installation? Cached files will be cleared to avoid corruption.