mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-06-17 12:19:52 -04:00
Auto-try legacy installer when install verification fails
This is useful for users with Google Advanced Protection who can use this to circumvent install restrictions and still use F-Droid. See #3201
This commit is contained in:
@@ -145,11 +145,21 @@
|
||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
tools:node="merge" />
|
||||
|
||||
<provider
|
||||
android:name="org.fdroid.install.ApkFileProvider"
|
||||
android:authorities="${applicationId}.install.ApkFileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.fileprovider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<!-- disable WorkManager initialization, needed for Hilt -->
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
|
||||
@@ -7,6 +7,11 @@ import android.app.PendingIntent.FLAG_MUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.Intent.ACTION_INSTALL_PACKAGE
|
||||
import android.content.Intent.EXTRA_NOT_UNKNOWN_SOURCE
|
||||
import android.content.Intent.EXTRA_RETURN_RESULT
|
||||
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
import android.content.IntentFilter
|
||||
import android.content.IntentSender
|
||||
import android.content.pm.PackageInfo
|
||||
@@ -23,6 +28,7 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
|
||||
import androidx.core.content.ContextCompat.registerReceiver
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.File
|
||||
@@ -290,14 +296,16 @@ constructor(
|
||||
)
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
if (status == PackageInstaller.STATUS_FAILURE_ABORTED) {
|
||||
cont.resume(InstallState.UserAborted)
|
||||
} else if (
|
||||
status == PackageInstaller.STATUS_FAILURE &&
|
||||
msg != null &&
|
||||
msg.contains("PreapprovalDetails")
|
||||
) {
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
if (msg != null && msg.contains("INSTALL_FAILED_VERIFICATION_FAILURE")) {
|
||||
// This is useful for users with Google Advanced Protection
|
||||
// who can use this to circumvent install restrictions (see #3201)
|
||||
installLegacy(apkFile)
|
||||
}
|
||||
cont.resume(InstallState.UserAborted)
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE -> {
|
||||
if (msg != null && msg.contains("PreapprovalDetails")) {
|
||||
val newState =
|
||||
InstallState.PreApproved(
|
||||
name = state.name,
|
||||
@@ -312,6 +320,7 @@ constructor(
|
||||
cont.resume(InstallState.Error(msg, state))
|
||||
}
|
||||
}
|
||||
else -> cont.resume(InstallState.Error(msg, state))
|
||||
}
|
||||
}
|
||||
registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED)
|
||||
@@ -385,14 +394,15 @@ constructor(
|
||||
// and just return this error as if it was the user who aborted. See #3254
|
||||
// So we count fast aborts as not supported, so normal installation can proceed.
|
||||
log.warn { "Fast pre-approval abort for ${state.name}, trying without..." }
|
||||
val state = InstallState.PreApproved(
|
||||
name = state.name,
|
||||
versionName = state.versionName,
|
||||
currentVersionName = state.currentVersionName,
|
||||
lastUpdated = state.lastUpdated,
|
||||
iconModel = state.iconModel,
|
||||
result = PreApprovalResult.NotSupported,
|
||||
)
|
||||
val state =
|
||||
InstallState.PreApproved(
|
||||
name = state.name,
|
||||
versionName = state.versionName,
|
||||
currentVersionName = state.currentVersionName,
|
||||
lastUpdated = state.lastUpdated,
|
||||
iconModel = state.iconModel,
|
||||
result = PreApprovalResult.NotSupported,
|
||||
)
|
||||
cont.resume(state)
|
||||
} else {
|
||||
log.info { "User aborted confirmation for ${state.name}" }
|
||||
@@ -519,6 +529,28 @@ constructor(
|
||||
block(safeCont)
|
||||
}
|
||||
|
||||
private fun installLegacy(apkFile: File) {
|
||||
val uri =
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
apkFile,
|
||||
)
|
||||
@Suppress("DEPRECATION", "RequestInstallPackagesPolicy")
|
||||
val intent =
|
||||
Intent(ACTION_INSTALL_PACKAGE).apply {
|
||||
setAction(ACTION_INSTALL_PACKAGE)
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_GRANT_READ_URI_PERMISSION)
|
||||
putExtra(EXTRA_RETURN_RESULT, true)
|
||||
putExtra(EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||
}
|
||||
log.warn { "Trying legacy install for ${apkFile.name} with $intent..." }
|
||||
// doesn't need to use startActivitySafe(intent) because exceptions get caught
|
||||
// and shown to the user which is better than hiding it
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
private interface SafeContinuation<T> {
|
||||
fun resume(value: T)
|
||||
|
||||
|
||||
6
app/src/main/res/xml/file_paths.xml
Normal file
6
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<cache-path
|
||||
name="apks"
|
||||
path="." />
|
||||
</paths>
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.fdroid.install
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
@@ -9,9 +10,11 @@ import android.content.pm.InstallSourceInfo
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageInstaller.Session
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.telephony.TelephonyManager
|
||||
import androidx.core.content.ContextCompat.registerReceiver
|
||||
import androidx.core.content.FileProvider
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
@@ -24,6 +27,7 @@ import io.mockk.unmockkAll
|
||||
import io.mockk.verify
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import kotlin.test.assertContains
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertIs
|
||||
import kotlin.test.assertNotNull
|
||||
@@ -437,6 +441,46 @@ internal class SessionInstallManagerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `install calls installLegacy when STATUS_FAILURE_ABORTED with INSTALL_FAILED_VERIFICATION_FAILURE`() =
|
||||
runBlocking {
|
||||
mockkStatic(FileProvider::class)
|
||||
val uri: Uri = mockk()
|
||||
every { FileProvider.getUriForFile(any(), any(), any()) } returns uri
|
||||
every { context.startActivity(any()) } just runs
|
||||
|
||||
val result =
|
||||
installForStatus(
|
||||
status = PackageInstaller.STATUS_FAILURE_ABORTED,
|
||||
msg =
|
||||
"INSTALL_FAILED_VERIFICATION_FAILURE: " +
|
||||
"Install not allowed for file:///data/app/vmdl403786248.tmp",
|
||||
)
|
||||
|
||||
assertIs<InstallState.UserAborted>(result)
|
||||
// ensure that the legacy installer gets invoked
|
||||
verify(exactly = 1) { context.startActivity(any()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `install returns Error when installLegacy throws ActivityNotFoundException`() = runBlocking {
|
||||
mockkStatic(FileProvider::class)
|
||||
val uri: Uri = mockk()
|
||||
every { FileProvider.getUriForFile(any(), any(), any()) } returns uri
|
||||
every { context.startActivity(any()) } throws ActivityNotFoundException("no handler")
|
||||
|
||||
val result =
|
||||
installForStatus(
|
||||
status = PackageInstaller.STATUS_FAILURE_ABORTED,
|
||||
msg =
|
||||
"INSTALL_FAILED_VERIFICATION_FAILURE: " +
|
||||
"Install not allowed for file:///data/app/vmdl403786248.tmp",
|
||||
)
|
||||
|
||||
assertIs<InstallState.Error>(result)
|
||||
assertContains(result.msg ?: "", "ActivityNotFoundException")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `install cancellation abandons session`() = runBlocking {
|
||||
val apkFile: File =
|
||||
|
||||
Reference in New Issue
Block a user