diff --git a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt index 468f652f68..53f7c6df7c 100644 --- a/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt +++ b/vending-app/src/main/kotlin/com/android/vending/installer/Install.kt @@ -13,6 +13,7 @@ import android.content.pm.PackageInfo import android.content.pm.PackageInstaller import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageManager +import android.os.Build import android.util.Log import androidx.annotation.RequiresApi import androidx.core.app.PendingIntentCompat @@ -118,6 +119,13 @@ private suspend fun Context.installPackagesInternal( val key = computeUniqueKey(packageName, componentNames) params.setAppLabel(key) params.setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) + // Suppress EMUI (Huawei) InstallDistActivity confirmation dialog for every session. + // Without this flag, EMUI intercepts each PackageInstaller session and shows a + // mandatory user-confirmation dialog — including for Dynamic Feature Module installs. + // USER_ACTION_NOT_REQUIRED requires API 31 (Android 12 / S). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + params.setRequireUserAction(SessionParams.USER_ACTION_NOT_REQUIRED) + } try { @SuppressLint("PrivateApi") val method = SessionParams::class.java.getDeclaredMethod( "setDontKillApp", Boolean::class.javaPrimitiveType diff --git a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt index 572cb080e6..4891b27676 100644 --- a/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt +++ b/vending-app/src/main/kotlin/com/google/android/finsky/splitinstallservice/SplitInstallService.kt @@ -91,8 +91,27 @@ class SplitInstallServiceImpl(private val installManager: SplitInstallManager, p override fun splitDeferred(targetPackage: String, splits: List, bundle0: Bundle, callback: ISplitInstallServiceCallback) { val packageName = PackageUtils.getAndCheckCallingPackage(context, targetPackage)!! - Log.w(TAG, "splitDeferred(${splits.joinToString()}) called for $packageName, but is not implemented") - runCatching { callback.onDeferredInstall(Bundle()) } + Log.d(TAG, "splitDeferred(${splits.joinToString()}) called for $packageName") + // Deferred installs are background pre-fetch requests (e.g. Facebook DFMs like pytorch/papaya). + // If we only ACK without actually installing, the app falls back to calling startInstall() + // for the same modules later — which triggers another PackageInstaller session and, on EMUI, + // another InstallDistActivity confirmation dialog per module. + // Fix: route deferred requests through the same install flow as startInstall(). + if (VendingPreferences.isSplitInstallEnabled(context)) { + lifecycleScope.launch { + runCatching { + installManager.splitInstallFlow(packageName, splits) + Log.d(TAG, "splitDeferred install complete for $packageName") + }.onFailure { e -> + Log.w(TAG, "splitDeferred install failed for $packageName: ${e.message}") + } + // Always ACK so the app is not left waiting. + runCatching { callback.onDeferredInstall(Bundle()) } + } + } else { + Log.w(TAG, "splitDeferred rejected for $packageName, service is disabled") + runCatching { callback.onDeferredInstall(Bundle()) } + } } override fun getSessionState2(targetPackage: String, sessionId: Int, callback: ISplitInstallServiceCallback) {