feat: handle play protect (#72)
This commit is contained in:
parent
8898856766
commit
afba80fe4b
11 changed files with 206 additions and 13 deletions
|
|
@ -137,6 +137,7 @@ dependencies {
|
|||
implementation(libs.bouncycastle)
|
||||
implementation(libs.binaryResources)
|
||||
implementation(libs.coil)
|
||||
implementation(libs.microg)
|
||||
implementation(variantOf(libs.zip) { artifactType("aar") })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ class KotlinInstallRunner(options: InstallOptions) : StepRunner() {
|
|||
// Install
|
||||
AlignmentStep(),
|
||||
SigningStep(),
|
||||
InstallStep(),
|
||||
InstallStep(options),
|
||||
CleanupStep(),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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<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
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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>(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()
|
||||
}
|
||||
|
|
@ -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<InstallStep>(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<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() }
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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<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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<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) {
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_protect_warning.xml
Normal file
9
app/src/main/res/drawable/ic_protect_warning.xml
Normal file
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue