From afba80fe4b1c8a034b762013961c62f92e7a3b1a Mon Sep 17 00:00:00 2001 From: rushii <33725716+rushiiMachine@users.noreply.github.com> Date: Thu, 16 May 2024 10:57:11 -0700 Subject: [PATCH] feat: handle play protect (#72) --- app/build.gradle.kts | 1 + .../installer/steps/KotlinInstallRunner.kt | 2 +- .../manager/installer/steps/StepRunner.kt | 5 +- .../installer/steps/install/InstallStep.kt | 27 ++++++- .../components/dialogs/PlayProtectDialog.kt | 74 +++++++++++++++++++ .../ui/screens/install/InstallModel.kt | 51 +++++++++++-- .../ui/screens/install/InstallScreen.kt | 7 +- .../com/aliucord/manager/util/Context.kt | 36 ++++++++- .../main/res/drawable/ic_protect_warning.xml | 9 +++ app/src/main/res/values/strings.xml | 5 ++ gradle/libs.versions.toml | 2 + 11 files changed, 206 insertions(+), 13 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/PlayProtectDialog.kt create mode 100644 app/src/main/res/drawable/ic_protect_warning.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c2a4d46f..6b2baa47 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -137,6 +137,7 @@ dependencies { implementation(libs.bouncycastle) implementation(libs.binaryResources) implementation(libs.coil) + implementation(libs.microg) implementation(variantOf(libs.zip) { artifactType("aar") }) } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt index fb8b445b..2a806a94 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/KotlinInstallRunner.kt @@ -33,7 +33,7 @@ class KotlinInstallRunner(options: InstallOptions) : StepRunner() { // Install AlignmentStep(), SigningStep(), - InstallStep(), + InstallStep(options), CleanupStep(), ) } 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 587b42e4..21a706a4 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 @@ -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 getStep(): T { + inline fun getStep(completed: Boolean = true): T { val step = steps.asSequence() .filterIsInstance() - .filter { it.state.isFinished } + .filter { !completed || it.state.isFinished } .firstOrNull() if (step == null) { diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt index d1b21bed..00aa9aed 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/steps/install/InstallStep.kt @@ -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 @@ -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() + val gppWarningLock: Flow + 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().patchedApk @@ -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, diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/PlayProtectDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/PlayProtectDialog.kt new file mode 100644 index 00000000..1431374f --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/dialogs/PlayProtectDialog.kt @@ -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) +} 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 1287b74c..2ff0ca51 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 @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.aliucord.manager.ui.screens.install import android.annotation.SuppressLint @@ -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 @@ -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 @@ -29,13 +31,18 @@ class InstallModel( private val paths: PathManager, private val options: InstallOptions, ) : StateScreenModel(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>?>(null) private set + var showGppWarning by mutableStateOf(false) + private set + init { restart() } @@ -49,6 +56,7 @@ class InstallModel( } fun saveFailureLog() { + val startTime = startTime ?: return val failureLog = (state.value as? InstallScreenState.Failed)?.failureLog ?: return @@ -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(completed = false) + ?.dismissGPPWarning() + } + } + fun restart() { installJob?.cancel("Manual cancellation") installSteps = null @@ -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(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() } @@ -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() 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 2975e73f..6b28cc97 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 @@ -29,6 +29,7 @@ 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 @@ -44,7 +45,7 @@ class InstallScreen(private val data: InstallOptions) : Screen, Parcelable { override fun Content() { val navigator = LocalNavigator.currentOrThrow val model = getScreenModel { parametersOf(data) } - val state = model.state.collectAsState() + val state = model.state.collectAsState() // TODO: use `by` LaunchedEffect(state.value) { if (state.value is InstallScreenState.CloseScreen) @@ -80,6 +81,10 @@ class InstallScreen(private val data: InstallOptions) : Screen, Parcelable { BackHandler(onBack = onTryExit) } + if (model.showGppWarning) { + PlayProtectDialog(onDismiss = model::dismissGPPWarning) + } + Scaffold( topBar = { InstallAppBar(onTryExit) }, modifier = Modifier diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Context.kt b/app/src/main/kotlin/com/aliucord/manager/util/Context.kt index 5af069e0..b2e30130 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/Context.kt +++ b/app/src/main/kotlin/com/aliucord/manager/util/Context.kt @@ -3,6 +3,7 @@ package com.aliucord.manager.util import android.annotation.SuppressLint import android.app.Activity import android.content.* +import android.content.pm.PackageManager import android.net.Uri import android.os.* import android.provider.Settings @@ -13,8 +14,11 @@ import androidx.annotation.AnyRes import androidx.annotation.StringRes import com.aliucord.manager.BuildConfig import com.aliucord.manager.R +import com.google.android.gms.safetynet.SafetyNet import java.io.File import java.io.InputStream +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine fun Context.copyToClipboard(text: String) { val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager @@ -52,6 +56,15 @@ fun Context.getPackageVersion(pkg: String): Pair { .let { it.versionName to it.versionCode } } +fun Context.isPackageInstalled(packageName: String): Boolean { + return try { + packageManager.getPackageInfo(packageName, 0) + true + } catch (_: PackageManager.NameNotFoundException) { + false + } +} + fun Context.findActivity(): Activity? { var context = this while (context is ContextWrapper) { @@ -90,7 +103,7 @@ fun Context.requestNoBatteryOptimizations() { startActivity(intent) } -/** +/* * Get the raw bytes for a resource. * @param id The resource identifier * @return The resource's raw bytes as stored inside the APK @@ -110,3 +123,24 @@ fun Context.getResBytes(@AnyRes id: Int): ByteArray { ?.use(InputStream::readBytes) ?: error("Failed to get resource file $resPath from APK") } + +/** + * Checks if the Play Protect/Verify Apps feature is enabled on this device. + * @return `null` if failed to obtain, otherwise whether it's enabled. + */ +suspend fun Context.isPlayProtectEnabled(): Boolean? { + return suspendCoroutine { continuation -> + SafetyNet.getClient(this) + .isVerifyAppsEnabled + .addOnCompleteListener { task -> + if (task.isSuccessful) { + val enabled = task.result.isVerifyAppsEnabled + Log.d(BuildConfig.TAG, "Play Protect enabled: $enabled") + continuation.resume(enabled) + } else { + Log.d(BuildConfig.TAG, "Failed to check Play Protect status", task.exception) + continuation.resume(null) + } + } + } +} diff --git a/app/src/main/res/drawable/ic_protect_warning.xml b/app/src/main/res/drawable/ic_protect_warning.xml new file mode 100644 index 00000000..97d98792 --- /dev/null +++ b/app/src/main/res/drawable/ic_protect_warning.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 18df6e1e..65ddb166 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,4 +154,9 @@ Click to install… Aliucord failed to install Click to view more info… + + Play Protect + Google Play Protect appears to be enabled on your device. It may attempt to interfere with a new installation due to the usage of a unique signing key. You can disable it in Play Protect\'s settings.\n\nIf it does show a warning dialog, press\n\"More Details\" -> \"Install anyway\" + Continue + Open Play Protect diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5d26fe3c..66ed93e6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ kotlin = "1.9.22" kotlinx-immutable = "0.3.7" kotlinx-serialization = "1.5.0" ktor = "2.3.7" +microg = "0.3.2.240913" voyager = "1.0.0" zip = "2.1.1" @@ -72,6 +73,7 @@ axml = { module = "com.aliucord:axml", version.ref = "axml" } binaryResources = { module = "com.aliucord:binary-resources", version.ref = "binary-resources" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } +microg = { module = "org.microg.gms:play-services-safetynet", version.ref = "microg" } zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact [bundles]