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 + } +}