Skip to content

Commit

Permalink
feat: handle play protect (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
rushiiMachine committed May 16, 2024
1 parent 8898856 commit afba80f
Show file tree
Hide file tree
Showing 11 changed files with 206 additions and 13 deletions.
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 @@ -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
Expand All @@ -44,7 +45,7 @@ class InstallScreen(private val data: InstallOptions) : Screen, Parcelable {
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 @@ -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
Expand Down
36 changes: 35 additions & 1 deletion app/src/main/kotlin/com/aliucord/manager/util/Context.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -52,6 +56,15 @@ fun Context.getPackageVersion(pkg: String): Pair<String, Int> {
.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) {
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/ic_protect_warning.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M480,640q17,0 28.5,-11.5T520,600q0,-17 -11.5,-28.5T480,560q-17,0 -28.5,11.5T440,600q0,17 11.5,28.5T480,640ZM480,480q17,0 28.5,-11.5T520,440v-120q0,-17 -11.5,-28.5T480,280q-17,0 -28.5,11.5T440,320v120q0,17 11.5,28.5T480,480ZM480,876q-7,0 -13,-1t-12,-3q-135,-45 -215,-166.5T160,444v-189q0,-25 14.5,-45t37.5,-29l240,-90q14,-5 28,-5t28,5l240,90q23,9 37.5,29t14.5,45v189q0,140 -80,261.5T505,872q-6,2 -12,3t-13,1ZM480,796q104,-33 172,-132t68,-220v-189l-240,-90 -240,90v189q0,121 68,220t172,132ZM480,480Z" />
</vector>
5 changes: 5 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,9 @@
<string name="notif_install_ready_desc">Click to install…</string>
<string name="notif_install_fail_title">Aliucord failed to install</string>
<string name="notif_install_fail_desc">Click to view more info…</string>

<string name="play_protect_warning_title">Play Protect</string>
<string name="play_protect_warning_desc">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\"</string>
<string name="play_protect_warning_ok">Continue</string>
<string name="play_protect_warning_open_gpp">Open Play Protect</string>
</resources>
Loading

0 comments on commit afba80f

Please sign in to comment.