From 5bb57e8153ee872396fe26e92a3dbd9105185a89 Mon Sep 17 00:00:00 2001 From: Daniel Alome Date: Fri, 23 Oct 2026 23:49:59 +0100 Subject: [PATCH] ADFA-4940: Track only CoGo's own install session PackageInstaller.SessionCallback reports every session the app can see, not just the ones it created. ApkInstallationViewModel took them all, so another installer's session moved CoGo's install progress, and one that failed after CoGo's own install succeeded flashed "Installation failed" next to the "App installed" prompt. onCreated now starts tracking a session only when it is one of mySessions, and progress and finish events count only for the tracked session. --- .../viewmodel/ApkInstallationViewModel.kt | 16 +++- ...kInstallationViewModelSessionFilterTest.kt | 82 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModelSessionFilterTest.kt diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModel.kt index 7ea0857b57..68784cbbbc 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModel.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.viewmodel import android.content.Context +import android.content.pm.PackageInstaller import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.itsaky.androidide.utils.ApkInstaller @@ -46,10 +47,15 @@ class ApkInstallationViewModel : ViewModel() { ) : SessionState() } + private lateinit var packageInstaller: PackageInstaller + private val callback = object : SingleSessionCallback() { override fun onCreated(sessionId: Int) { logger.debug("onCreated: sessionId={}", sessionId) + if (packageInstaller.mySessions.none { it.sessionId == sessionId }) { + return + } setSessionState(SessionState.InProgress(sessionId = sessionId, progress = 0)) } @@ -59,6 +65,9 @@ class ApkInstallationViewModel : ViewModel() { progress: Float, ) { logger.debug("onProgressChanged: sessionId={}, progress={}", sessionId, progress) + if (!isTrackedSession(sessionId)) { + return + } setSessionState( SessionState.InProgress( @@ -73,11 +82,16 @@ class ApkInstallationViewModel : ViewModel() { success: Boolean, ) { logger.debug("onFinished: sessionId={}, success={}", sessionId, success) + if (!isTrackedSession(sessionId)) { + return + } setSessionState(SessionState.Finished(sessionId = sessionId, isSuccess = success)) } } + private fun isTrackedSession(sessionId: Int): Boolean = (sessionState.value as? SessionState.InProgress)?.sessionId == sessionId + private val _sessionState = MutableStateFlow(SessionState.Idle) /** @@ -112,7 +126,7 @@ class ApkInstallationViewModel : ViewModel() { apk: File, launchInDebugMode: Boolean, ) { - val packageInstaller = context.packageManager.packageInstaller + packageInstaller = context.applicationContext.packageManager.packageInstaller packageInstaller.unregisterSessionCallback(callback) packageInstaller.registerSessionCallback(callback) diff --git a/app/src/test/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModelSessionFilterTest.kt b/app/src/test/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModelSessionFilterTest.kt new file mode 100644 index 0000000000..79e9637722 --- /dev/null +++ b/app/src/test/java/com/itsaky/androidide/viewmodel/ApkInstallationViewModelSessionFilterTest.kt @@ -0,0 +1,82 @@ +package com.itsaky.androidide.viewmodel + +import android.content.Context +import android.content.pm.PackageInstaller +import android.content.pm.PackageManager +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.viewmodel.ApkInstallationViewModel.SessionState +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.slot +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import java.io.File + +@RunWith(RobolectricTestRunner::class) +class ApkInstallationViewModelSessionFilterTest { + private val viewModel = ApkInstallationViewModel() + + private val callback = slot() + + private val ownSession: PackageInstaller.SessionInfo = mockk { every { sessionId } returns OWN_SESSION } + + private val installer = + mockk { + every { unregisterSessionCallback(any()) } just Runs + every { registerSessionCallback(capture(callback)) } just Runs + every { mySessions } returns listOf(ownSession) + } + + private val context: Context = + mockk { + val packageManager = mockk { every { packageInstaller } returns installer } + every { applicationContext } returns this + every { this@mockk.packageManager } returns packageManager + } + + private fun startInstall(): PackageInstaller.SessionCallback { + viewModel.installApk(context, File("absent.apk"), launchInDebugMode = false) + return callback.captured + } + + @Test + fun `another app's failed session does not report this install as failed`() { + val callback = startInstall() + callback.onCreated(OWN_SESSION) + + callback.onFinished(FOREIGN_SESSION, false) + + assertThat(viewModel.sessionState.value).isEqualTo(SessionState.InProgress(OWN_SESSION, 0)) + } + + @Test + fun `another app's session does not take over the install progress`() { + val callback = startInstall() + callback.onCreated(OWN_SESSION) + + callback.onCreated(FOREIGN_SESSION) + callback.onProgressChanged(FOREIGN_SESSION, 0.5f) + + assertThat(viewModel.sessionState.value).isEqualTo(SessionState.InProgress(OWN_SESSION, 0)) + } + + @Test + fun `this install's own result is still reported`() { + val callback = startInstall() + callback.onCreated(OWN_SESSION) + callback.onProgressChanged(OWN_SESSION, 0.5f) + assertThat(viewModel.sessionState.value).isEqualTo(SessionState.InProgress(OWN_SESSION, 50)) + + callback.onFinished(OWN_SESSION, true) + + assertThat(viewModel.sessionState.value).isEqualTo(SessionState.Finished(OWN_SESSION, true)) + } + + private companion object { + const val OWN_SESSION = 5 + const val FOREIGN_SESSION = 9 + } +}