Skip to content

SplitInstall: Add sessions and SplitCompat delivery - #3903

Open
DaVinci9196 wants to merge 5 commits into
microg:masterfrom
DaVinci9196:fix_split_install_error
Open

DaVinci9196 wants to merge 5 commits into
microg:masterfrom
DaVinci9196:fix_split_install_error

Conversation

@DaVinci9196

Copy link
Copy Markdown
Contributor
  • Add persistent sessions, queries, cancellation and deferred operations.
  • Deliver eligible feature modules through SplitCompat; retain native installation for language and mixed requests.
  • Improve recovery, cached URI grants and notification cleanup.

Known limitation: SplitCompat routing relies on Play Core version and install permissions, which do not reliably indicate client support.

@grenadin

grenadin commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

@DaVinci9196
I built this PR and tested it on a Huawei device, first at 46ec692 and again at f4768736.

Setup

  • Device: Huawei MatePad Mini (MLR-W09), HarmonyOS 4.3 (EMUI 15 / Android 12, API 31)
  • Build: huawei debug build of GmsCore and Companion from this PR. I also merged Auth: Fix Aurora Store login stuck on consent prompt #3868 locally so Aurora Store can log in; it does not touch vending-app.
  • "On-demand component installation" enabled
  • Apps installed with Aurora Store 4.8.4-hw through the microG installer, so Companion is the installer of record
  • Tested apps: Facebook, Netflix, TikTok, Zoom

Background: why Huawei is different
On EMUI, every PackageInstaller session from Companion is checked by SilentUpdatePolicy, and it always answers:

D SilentUpdatePolicy: installer: com.android.vending/10267, target: com.facebook.katana/10268, allowed: false

So a native split install can never finish silently there. It ends in status 8 (requires user confirmation).

For comparison, with the current release (0.3.17) one Facebook install from Aurora showed the Huawei install dialog 4 times in about an hour: once for the base APK and once each for maplibre, papaya and pytorch.

Results with 46ec692

  • All modules went through native sessions and ended in status 8. No dialog was shown, and no module was installed.
  • Facebook cancelled and retried, downloading the modules again each time.
  • TikTok and Netflix kept calling startInstall.
  • The sessions stayed pending, even after a reboot.

Results with f4768736
Modules that the app requests while it is open now come through SplitCompat (status 3), with no dialog:

App Modules Result
Facebook papaya, pytorch Loaded, no dialog
Netflix twilio Delivered, no dialog
TikTok 12 modules (e.g. df_search_biz, df_camera_biz, df_im_bootfinish) Delivered, no dialog, no retry loop
Zoom none requested —

This is a big improvement. Remaining issues on Huawei:

  1. Deferred requests still use native sessions. installDeferred() calls startInstall(packageName, splits) without the Play Core version, so useSplitCompat is always false. Facebook's deferred requests (papaya, heliumcore/heliumiab) ended in status 8, and Facebook cancelled them. While such a session was active, Facebook's other requests failed with -1 Too many sessions, and once papaya failed to load (Failed to load papaya voltron module).
  2. Language splits use native sessions. Netflix requested config.th, got status 8, showed the confirmation itself, and installed it after one tap. That works, but it still needs the user.
  3. A status 8 session blocks the app if the app never cancels it. TikTok doesn't cancel these sessions. In my first f4768736 run, two TikTok sessions left over from 46ec692 were still stored. They survived a TikTok reinstall, probably because they had no client_install_time. Every TikTok request then failed with -1 or -8, and TikTok retried about every 2 seconds (105 calls in 7 minutes). After I removed those two records, everything worked. The leftover records came from the older commit, but the deferred path in point 1 can create the same state.

Suggestions

  • Store the Play Core version for deferred requests and pass it on, so they can use SplitCompat too.
  • When a session ends in STATUS_PENDING_USER_ACTION and nobody is waiting for it, abandon it instead of keeping it as the active session.

About #3543
Thank you for this work. I learned a lot of good practices from your code, and your implementation is better than mine, so I'm happy to drop the splitDeferred part from #3543.

The rest of #3543 is small: it sets USER_ACTION_NOT_REQUIRED for every session, not only for split installs, and requests UPDATE_PACKAGES_WITHOUT_USER_ACTION, which this PR already does. On stock Android 12 this removes the confirmation for updates through the installer proxy, for example Aurora's "Update all" (tested on an AOSP emulator; it doesn't help on Huawei because of SilentUpdatePolicy).

If you're willing, I'd be glad if you could include that in this PR as well. You probably know a better way to implement it, and I'd like to learn from it. Then I'll close #3543.

Logs (filtered, no account data)

Current release 0.3.17 (Mate X6, Facebook, 4 dialogs):
x6-facebook-microg-installer-official-2026-10-21.log

46ec692:
facebook-dfm-filtered.log
tiktok-and-afterboot-filtered.log
zoom-netflix-filtered.log

f4768736 (the TikTok log has extra debug logging I added locally to see the module names and error codes):
facebook-v2-f4768736-2026-10-21.log
netflix-v2-f4768736-2026-10-21.log
tiktok-v2-debug-2026-10-21.log

grenadin added a commit to grenadin/GmsCore that referenced this pull request Oct 21, 2026
Split installs are handled by microg#3903. Also correct the comment on
USER_ACTION_NOT_REQUIRED: it does not skip the EMUI install dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@grenadin

Copy link
Copy Markdown
Contributor

@DaVinci9196
I tested bfba381e on the same Huawei MatePad Mini (HarmonyOS 4.3 / EMUI 15, Android 12), with #3868 merged locally (only so that I can sign in to Aurora; it doesn't touch Companion or split install). The apps were installed by Aurora Store 4.8.3 with the microG installer, with split install enabled in Companion.

App Result
Facebook No dialog. executorch, supmediastreamcontroller, papaya and pytorch were delivered with SplitCompat. The deferred requests for heliumcore and pytorch got status 8 and were canceled without a dialog. pytorch was delivered with SplitCompat later, when the app requested it.
TikTok 47.4.3 No dialog. 12 modules delivered with SplitCompat, no errors.
Netflix twilio was delivered with SplitCompat, no dialog. After login, Netflix requested the Thai language split (languages: [th]). It was installed with a session, so EMUI's SilentUpdatePolicy rejected it and the Huawei dialog was shown once (status 8, then 5 after I confirmed).

The base install through Aurora still shows the Huawei dialog, as expected.

With 0.3.17, the dialog appears for every module (for example, 14 times for TikTok in 7 minutes). With this commit, feature modules install without any dialog. The only one left is for language splits, which I understand go through a session on purpose. Is there a reason they can't use SplitCompat too?

One small thing in the Facebook log (19:47:54–19:47:58): Facebook made a foreground request for pytorch while deferred sessions were still active. It got -1 while the deferred heliumcore session was installing, then -8 while a deferred pytorch session was active. Facebook logged "Failed to load papaya" after each error. Both deferred sessions were then canceled (status 8 → 7), and pytorch was delivered with SplitCompat at 19:56. I didn't notice anything in the app.

About #3543: I'd like to close it, since this PR covers almost all of it. The only part left is setting USER_ACTION_NOT_REQUIRED for non-split installs too. Here it's only set when splitInstall is true, so updates through the installer proxy (for example Aurora's "Update all") still ask for confirmation on stock Android 12+, even though this PR already requests UPDATE_PACKAGES_WITHOUT_USER_ACTION. Would you consider dropping the splitInstall && condition here? If you'd rather keep this PR focused on split installs, that's fine too, and I'll send it as a small separate PR after this one is merged.

Thanks again, this works very well on Huawei.

Logs (filtered, no account data)
Facebook: 3903-bfba381e-huawei-facebook.log

TikTok: 3903-bfba381e-huawei-tiktok.log

Netflix: 3903-bfba381e-huawei-netflix.log

@DaVinci9196

Copy link
Copy Markdown
Contributor Author

@grenadin
Thank you very much for your testing and for verifying these scenarios.

We are currently evaluating whether SplitCompat mode can support language switching across different applications. However, our tests have revealed a couple of issues: Chrome fails to launch when a language pack is installed via SplitCompat, while in apps like Reddit, the language reverts to its original setting after the app is closed.

If possible, could you try removing the conditional checks and test whether language switching works as expected in these applications? We would greatly appreciate your help in determining whether these issues can be resolved and, if so, identifying a potential fix.

@grenadin

Copy link
Copy Markdown
Contributor

@DaVinci9196
Thank you sir,
Before this I didn't know that Play Core copies the splits into the app itself through split_file_intents. Reading how your PR delivers them that way, and how it blocks deferred sessions instead of showing a prompt, taught me a lot about this part of Companion.
I tested it on Huawei and compared it with the current release and with the real Play Store. For each test I picked a language that wasn't installed yet.

  • MatePad Mini (Huawei, Android 12): this PR at bfba381e as is, with && languages.isEmpty() removed (Japanese), and with both languages.isEmpty() and deferredRequest == null removed (Polish)
  • Mate X6 (Huawei, Android 12): microG 0.3.17 release (Japanese)
  • Honor 90 (Android 15): real Play Store (Japanese)
Chrome Reddit
Language split via SplitCompat (conditions removed) config.ja / config.pl delivered (status 3), and Chrome logged SplitCompat: Splits copied / verified / installed. After the restart, every launch of the browser crashed with a native SIGTRAP in libchrome.so, during BrowserStartupControllerImpl. Same with both conditions removed: Chrome's deferredLanguageInstall() was merged into the same session. Japanese: Reddit restarted its process (ProcessPhoenix) right after the delivery and came back in English, 3 times. Polish (both conditions removed): English after the first restart, but after selecting Polish again it restarted in Polish and stayed in Polish after closing it.
Language split installed natively (bfba381e as is) Status 8. Chrome doesn't open the confirmation, so its download screen stays there. Companion's "tap to install" notification works (Huawei dialog, status 5, Chrome in Japanese after the restart), but it's on the low-importance packageInstall channel, so at first I thought Chrome was stuck. Status 8, Reddit opened the confirmation itself, status 5, and it restarted in Japanese.
0.3.17 release Companion opened the confirmation itself, and Chrome switched to Japanese. Installed after the confirmation, but Reddit didn't switch until I selected the language again.
Play Store Works, split_config.ja.apk installed natively. Works, split_config.ja.apk installed natively.

To your question whether these issues can be fixed: for Chrome, I don't think it can be fixed in Companion. Chromium says so in LanguageSplitInstaller.java: "|SplitCompatEngine| should be modified to support language split installs, https://crbug.com/40172694". Chrome's native UI strings are in assets/locales#lang_xx/*.pak inside the language split, and native code opens them through ApkAssets. I guess they aren't found through the SplitCompat copy at browser startup, but I couldn't confirm it, because the release build doesn't log the failed check.

For Reddit, the result wasn't consistent. It restarts itself within about 40 ms after the split is delivered, so it may be a timing issue between that restart and SplitCompat copying the split, but I couldn't confirm it. I haven't decompiled it, and I can't explain why Japanese never worked.

Real Play installs language splits natively, so I think keeping them native, as languages.isEmpty() does now, is the safer choice.

About status 8 on Huawei

Chrome doesn't handle REQUIRES_USER_CONFIRMATION (its listener only handles INSTALLED, FAILED, CANCELED and DOWNLOADED), so the notification is the only way for it to continue.

First I tried to let Companion open the confirmation itself when the app doesn't within 3 seconds, like 0.3.17 does. Android blocked it as a background activity start, also when I opened it while still handling the installer result. As far as I can tell, 0.3.17 only gets away with it because the client is still bound to SplitInstallService during the whole install.

What worked instead was a separate high-importance channel for the install prompt, so it pops up as a heads-up notification. While that notification can be shown, I didn't report status 8 to the app, so every app gets the same prompt. If Companion's notifications or that channel are turned off, status 8 is reported as now, so apps like Reddit can still show the confirmation. Results on the MatePad:

  • Chrome (nl, bs) and Reddit (de, es, nl, el): heads-up notification, tap, Huawei dialog, status 5, language switched.
  • Reddit (it) with Companion's notifications turned off: no notification, status 8 reported, Reddit opened the confirmation itself, status 5.

This differs from Play, which reports status 8 to the app. I also didn't change what getSessionStates() returns while the prompt is waiting. It's only a suggestion, so feel free to ignore it or do it differently.

Diff against bfba381
diff --git a/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt
index 33d3b9c2..0166446b 100644
--- a/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt
+++ b/vending-app/src/main/java/org/microg/vending/ui/InstallProgressNotification.kt
@@ -25,6 +25,7 @@ import org.microg.vending.enterprise.InstallError
 import org.microg.vending.enterprise.InstallProgress
 
 private const val INSTALL_NOTIFICATION_CHANNEL_ID = "packageInstall"
+private const val INSTALL_PROMPT_NOTIFICATION_CHANNEL_ID = "packageInstallPrompt"
 
 internal fun Context.notifyInstallPrompt(packageName: String, sessionId: Int, installIntent: PendingIntent, deleteIntent: PendingIntent) {
     val notificationManager = NotificationManagerCompat.from(this)
@@ -35,6 +36,7 @@ internal fun Context.notifyInstallPrompt(packageName: String, sessionId: Int, in
         Log.e(TAG, "Couldn't load label for $packageName (${e.message}). Is it not installed?")
         return
     }
+    createPromptNotificationChannel()
     getInstallPromptNotificationBuilder().apply {
         setDeleteIntent(deleteIntent)
         setContentTitle(getString(R.string.installer_notification_progress_splitinstall_click_install, label))
@@ -203,8 +205,9 @@ internal fun Context.notifyInstallProgress(
 }
 
 private fun Context.getInstallPromptNotificationBuilder() =
-        NotificationCompat.Builder(this, INSTALL_NOTIFICATION_CHANNEL_ID)
-                .setPriority(NotificationCompat.PRIORITY_LOW)
+        NotificationCompat.Builder(this, INSTALL_PROMPT_NOTIFICATION_CHANNEL_ID)
+                .setPriority(NotificationCompat.PRIORITY_HIGH)
+                .setCategory(NotificationCompat.CATEGORY_RECOMMENDATION)
                 .setSmallIcon(android.R.drawable.stat_sys_download)
                 .setLocalOnly(true)
 
@@ -232,6 +235,31 @@ private fun Context.createNotificationChannel() {
     }
 }
 
+internal fun Context.canShowInstallPrompt(): Boolean {
+    val notificationManager = NotificationManagerCompat.from(this)
+    if (!notificationManager.areNotificationsEnabled()) return false
+    if (android.os.Build.VERSION.SDK_INT < 26) return true
+    createPromptNotificationChannel()
+    return notificationManager.getNotificationChannel(INSTALL_PROMPT_NOTIFICATION_CHANNEL_ID)?.importance != NotificationManager.IMPORTANCE_NONE
+}
+
+// Install prompts need user action, so they use their own channel that can pop up.
+private fun Context.createPromptNotificationChannel() {
+    if (android.os.Build.VERSION.SDK_INT >= 26) {
+        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+        notificationManager.createNotificationChannel(
+            NotificationChannel(
+                INSTALL_PROMPT_NOTIFICATION_CHANNEL_ID,
+                getString(R.string.installer_notification_prompt_channel_name),
+                NotificationManager.IMPORTANCE_HIGH
+            ).apply {
+                description = getString(R.string.installer_notification_prompt_channel_description)
+                setShowBadge(false)
+            }
+        )
+    }
+}
+
 private fun Context.isSystem(pm: PackageManager): Boolean {
     try {
         val ai = pm.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
diff --git a/vending-app/src/main/kotlin/org/microg/vending/splitinstall/SplitInstallSessionManager.kt b/vending-app/src/main/kotlin/org/microg/vending/splitinstall/SplitInstallSessionManager.kt
index e09af2f3..07aee9a4 100644
--- a/vending-app/src/main/kotlin/org/microg/vending/splitinstall/SplitInstallSessionManager.kt
+++ b/vending-app/src/main/kotlin/org/microg/vending/splitinstall/SplitInstallSessionManager.kt
@@ -35,6 +35,7 @@ import kotlinx.coroutines.delay
 import org.json.JSONArray
 import org.json.JSONObject
 import org.microg.vending.enterprise.Downloading
+import org.microg.vending.ui.canShowInstallPrompt
 import org.microg.vending.ui.notifyInstallPrompt
 import org.microg.vending.ui.notifySplitInstallProgress
 import java.io.File
@@ -314,7 +315,11 @@ internal class SplitInstallSessionManager(
                     session.confirmationIntent = action.toUri(Intent.URI_INTENT_SCHEME)
                     val pendingIntent = confirmation(session) ?: return
                     try {
-                        update(session, STATUS_REQUIRES_USER_CONFIRMATION) { putParcelable("user_confirmation_intent", pendingIntent) }
+                        // The heads-up notification asks for confirmation, so every app gets the same prompt.
+                        // Without notifications, the app has to show the confirmation itself.
+                        update(session, STATUS_REQUIRES_USER_CONFIRMATION, notifyClient = !context.canShowInstallPrompt()) {
+                            putParcelable("user_confirmation_intent", pendingIntent)
+                        }
                     } catch (e: SplitInstallException) {
                         update(session, STATUS_FAILED) { putInt("error_code", e.errorCode) }
                         abandonInstallerSession(session)
diff --git a/vending-app/src/main/res/values/strings.xml b/vending-app/src/main/res/values/strings.xml
index ea76fa2c..4a035e3c 100644
--- a/vending-app/src/main/res/values/strings.xml
+++ b/vending-app/src/main/res/values/strings.xml
@@ -46,6 +46,8 @@
 
     <string name="installer_notification_channel_name">App and component installation</string>
     <string name="installer_notification_channel_description">Shows app and component installation progress, success and failure messages.</string>
+    <string name="installer_notification_prompt_channel_name">Installation confirmation</string>
+    <string name="installer_notification_prompt_channel_description">Asks you to confirm app and component installations that need your approval.</string>
     <string name="installer_notification_progress_downloading">Downloading \"%s\"</string>
     <string name="installer_notification_progress_commiting">Installing \"%s\"</string>
     <string name="installer_notification_progress_complete">Installed \"%s\"</string>

The same change as a patch on top of bfba381e, so it can be applied with git am: 3903-headsup-prompt.patch

Logs (filtered, no account data):

3903-lang-huawei-splitcompat.log

3903-lang-huawei-splitcompat-no-conditions.log

3903-lang-huawei-native.log

3903-lang-huawei-headsup-prototype.log

lang-huawei-0.3.17.log

lang-play-store.log

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants