Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle play protect #72

Merged
merged 8 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ dependencies {
implementation(libs.bouncycastle)
implementation(libs.binaryResources)
implementation(libs.coil)
implementation(libs.microg)
implementation(variantOf(libs.zip) { artifactType("aar") })
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class KotlinInstallRunner(options: InstallOptions) : StepRunner() {
// Install
AlignmentStep(),
SigningStep(),
InstallStep(),
InstallStep(options),
CleanupStep(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ abstract class StepRunner : KoinComponent {
/**
* Get a step that has already been successfully executed.
* This is used to retrieve previously executed dependency steps from a later step.
* @param completed Only match steps that have finished executing.
*/
inline fun <reified T : Step> getStep(): T {
inline fun <reified T : Step> getStep(completed: Boolean = true): T {
val step = steps.asSequence()
.filterIsInstance<T>()
.filter { it.state.isFinished }
.filter { !completed || it.state.isFinished }
.firstOrNull()

if (step == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import com.aliucord.manager.installer.steps.patch.CopyDependenciesStep
import com.aliucord.manager.installers.InstallerResult
import com.aliucord.manager.manager.InstallerManager
import com.aliucord.manager.manager.PreferencesManager
import com.aliucord.manager.ui.components.dialogs.PlayProtectDialog
import com.aliucord.manager.ui.screens.installopts.InstallOptions
import com.aliucord.manager.ui.util.InstallNotifications
import com.aliucord.manager.util.isPackageInstalled
import com.aliucord.manager.util.isPlayProtectEnabled
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

Expand All @@ -20,14 +25,25 @@ private const val READY_NOTIF_ID = 200001
/**
* Install the final APK with the system's PackageManager.
*/
class InstallStep : Step(), KoinComponent {
class InstallStep(private val options: InstallOptions) : Step(), KoinComponent {
private val context: Context by inject()
private val installers: InstallerManager by inject()
private val prefs: PreferencesManager by inject()

/**
* Locks and unlocks the step's execution to start and wait until [PlayProtectDialog] dismissed.
*/
private val _gppWarningLock = MutableSharedFlow<Boolean>()
val gppWarningLock: Flow<Boolean>
get() = _gppWarningLock

override val group = StepGroup.Install
override val localizedName = R.string.install_step_installing

suspend fun dismissGPPWarning() {
_gppWarningLock.emit(false)
}

override suspend fun execute(container: StepRunner) {
val apk = container.getStep<CopyDependenciesStep>().patchedApk

Expand All @@ -44,6 +60,15 @@ class InstallStep : Step(), KoinComponent {
// Wait until app resumed
ProcessLifecycleOwner.get().lifecycle.withResumed {}

// Show [PlayProtectDialog] and wait until it gets dismissed
if (!context.isPackageInstalled(options.packageName) && context.isPlayProtectEnabled() == true) {
_gppWarningLock.emit(true)
_gppWarningLock
.filter { show -> !show }
.take(1)
.collect()
}

val result = installers.getActiveInstaller().waitInstall(
apks = listOf(apk),
silent = !prefs.devMode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.aliucord.manager.ui.components.dialogs

import android.content.Context
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.customColors

@Composable
fun PlayProtectDialog(
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current

AlertDialog(
onDismissRequest = onDismiss,
dismissButton = {
FilledTonalButton(onClick = context::launchPlayProtect) {
Text(stringResource(R.string.play_protect_warning_open_gpp))
}
},
confirmButton = {
FilledTonalButton(
onClick = onDismiss,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
),
) {
Text(stringResource(R.string.play_protect_warning_ok))
}
},
icon = {
Icon(
painter = painterResource(R.drawable.ic_protect_warning),
tint = MaterialTheme.customColors.warning,
contentDescription = null,
modifier = Modifier.size(36.dp),
)
},
title = { Text(stringResource(R.string.play_protect_warning_title)) },
text = {
Text(
text = stringResource(R.string.play_protect_warning_desc),
textAlign = TextAlign.Center,
)
},
properties = DialogProperties(
dismissOnBackPress = false,
usePlatformDefaultWidth = false,
),
modifier = modifier
.padding(25.dp),
)
}

private fun Context.launchPlayProtect() {
Intent("com.google.android.gms.settings.VERIFY_APPS_SETTINGS")
.setPackage("com.google.android.gms")
.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.also(::startActivity)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalCoroutinesApi::class)

package com.aliucord.manager.ui.screens.install

import android.annotation.SuppressLint
Expand All @@ -9,8 +11,7 @@ import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.installer.steps.KotlinInstallRunner
import com.aliucord.manager.installer.steps.StepGroup
import com.aliucord.manager.installer.steps.*
import com.aliucord.manager.installer.steps.base.Step
import com.aliucord.manager.installer.steps.base.StepState
import com.aliucord.manager.installer.steps.install.InstallStep
Expand All @@ -21,6 +22,7 @@ import com.aliucord.manager.util.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import java.text.SimpleDateFormat
import java.util.Date

Expand All @@ -29,13 +31,18 @@ class InstallModel(
private val paths: PathManager,
private val options: InstallOptions,
) : StateScreenModel<InstallScreenState>(InstallScreenState.Pending) {
private lateinit var startTime: Date
private var startTime: Date? = null
private var installJob: Job? = null
private var stepRunner: StepRunner? = null

private var autocloseCancelled: Boolean = false

var installSteps by mutableStateOf<ImmutableMap<StepGroup, ImmutableList<Step>>?>(null)
private set

var showGppWarning by mutableStateOf(false)
private set

init {
restart()
}
Expand All @@ -49,6 +56,7 @@ class InstallModel(
}

fun saveFailureLog() {
val startTime = startTime ?: return
val failureLog = (state.value as? InstallScreenState.Failed)?.failureLog
?: return

Expand All @@ -71,6 +79,20 @@ class InstallModel(
autocloseCancelled = true
}

/**
* Hide the 'Google Play Protect is enabled on your device' warning dialog
*/
fun dismissGPPWarning() {
showGppWarning = false

// Continue executing step
screenModelScope.launch {
stepRunner
?.getStep<InstallStep>(completed = false)
?.dismissGPPWarning()
}
}

fun restart() {
installJob?.cancel("Manual cancellation")
installSteps = null
Expand All @@ -80,6 +102,14 @@ class InstallModel(

val newInstallJob = screenModelScope.launch {
val runner = KotlinInstallRunner(options)
.also { stepRunner = it }

// Bind InstallStep's GPP Warning state to this
runner.getStep<InstallStep>(completed = false)
.gppWarningLock
.take(1) // Take only the initially trigger value
.onEach { showGppWarning = true }
.launchIn(this@launch)

installSteps = runner.steps.groupBy { it.group }
.mapValues { it.value.toUnsafeImmutable() }
Expand Down Expand Up @@ -137,17 +167,24 @@ class InstallModel(
installJob = newInstallJob
}

private fun getFailureInfo(stacktrace: Throwable): String {
private suspend fun getFailureInfo(stacktrace: Throwable): String {
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"
val soc = if (Build.VERSION.SDK_INT >= 31) (Build.SOC_MANUFACTURER + ' ' + Build.SOC_MODEL) else "Unavailable"
val playProtect = when (application.isPlayProtectEnabled()) {
null -> "Unavailable"
true -> "Enabled"
false -> "Disabled"
}

val header = """
Aliucord Manager v${BuildConfig.VERSION_NAME}
Built from commit ${BuildConfig.GIT_COMMIT} on ${BuildConfig.GIT_BRANCH} $gitChanges

Running Android ${Build.VERSION.RELEASE}, API level ${Build.VERSION.SDK_INT}
Android API: ${Build.VERSION.SDK_INT}
ROM: Android ${Build.VERSION.RELEASE} (Patch ${Build.VERSION.SECURITY_PATCH})
Supported ABIs: ${Build.SUPPORTED_ABIS.joinToString()}
Device: ${Build.MANUFACTURER} - ${Build.MODEL} (${Build.DEVICE})
Device: ${Build.MANUFACTURER} ${Build.MODEL} (${Build.DEVICE})
Play Protect: $playProtect
SOC: $soc
""".trimIndent()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package com.aliucord.manager.ui.screens.install

import android.os.Parcelable
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
Expand All @@ -28,18 +29,23 @@ import com.aliucord.manager.installer.steps.StepGroup
import com.aliucord.manager.ui.components.Wakelock
import com.aliucord.manager.ui.components.back
import com.aliucord.manager.ui.components.dialogs.InstallerAbortDialog
import com.aliucord.manager.ui.components.dialogs.PlayProtectDialog
import com.aliucord.manager.ui.screens.install.components.*
import com.aliucord.manager.ui.screens.installopts.InstallOptions
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.koin.core.parameter.parametersOf

class InstallScreen(private val data: InstallOptions) : Screen {
@Parcelize
class InstallScreen(private val data: InstallOptions) : Screen, Parcelable {
@IgnoredOnParcel
override val key = "Install"

@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = getScreenModel<InstallModel> { parametersOf(data) }
val state = model.state.collectAsState()
val state = model.state.collectAsState() // TODO: use `by`

LaunchedEffect(state.value) {
if (state.value is InstallScreenState.CloseScreen)
Expand Down Expand Up @@ -75,6 +81,10 @@ class InstallScreen(private val data: InstallOptions) : Screen {
BackHandler(onBack = onTryExit)
}

if (model.showGppWarning) {
PlayProtectDialog(onDismiss = model::dismissGPPWarning)
}

Scaffold(
topBar = { InstallAppBar(onTryExit) },
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package com.aliucord.manager.ui.screens.installopts

import android.os.Parcelable
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.aliucord.manager.util.ColorParceler
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.TypeParceler

@Immutable
@Parcelize
data class InstallOptions(
/**
* The app name that's user-facing in launchers.
Expand All @@ -30,25 +35,39 @@ data class InstallOptions(
* This is independent of [InstallOptions.iconReplacement]
*/
val monochromeIcon: Boolean,
) {
) : Parcelable {
@Immutable
sealed interface IconReplacement {
@Parcelize
sealed interface IconReplacement : Parcelable {
/**
* Keeps the original icons that are present in the APK.
*/
@Immutable
@Parcelize
data object Original : IconReplacement

/**
* Replaces the foreground image of the icon entirely and sets the background to transparent.
* This does not affect the monochrome icon.
* Changes the background of the icon to a specific color without
* altering the foreground or monochrome variants.
*/
data class CustomImage(val imageBytes: ByteArray) : IconReplacement
@Immutable
@Parcelize
data class CustomColor(
@TypeParceler<Color, ColorParceler>
val color: Color,
) : IconReplacement

/**
* Changes the background of the icon to a specific color without
* altering the foreground or monochrome variants.
* Replaces the foreground image of the icon entirely and sets the background to transparent.
* This does not affect the monochrome icon.
*/
data class CustomColor(val color: Color) : IconReplacement
@Immutable
@Parcelize
data class CustomImage(val imageBytes: ByteArray) : IconReplacement {
override fun hashCode() = imageBytes.contentHashCode()
override fun equals(other: Any?) = this === other
|| (javaClass == other?.javaClass && imageBytes.contentEquals((other as CustomImage).imageBytes))
}

companion object {
/**
Expand Down
Loading
Loading