Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package com.flipcash.app.updates.internal

import android.app.Activity
import android.os.RemoteException
import com.flipcash.app.updates.OptionalUpdate
import com.getcode.utils.trace
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.InstallException
import com.google.android.play.core.install.InstallStateUpdatedListener
import com.google.android.play.core.install.model.InstallErrorCode
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.InstallStatus
import com.google.android.play.core.install.model.UpdateAvailability
import com.google.android.gms.tasks.Task
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
Expand Down Expand Up @@ -56,9 +60,14 @@ internal class FlexibleUpdates(
return
}

val info = runCatching { manager.appUpdateInfo.await() }
.onFailure { trace(tag = TAG, message = "Failed to check for an optional update", error = it) }
.getOrNull()
val info = try {
manager.appUpdateInfo.await()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
traceCheckFailure(e)
null
}
this.info = info
if (info == null) {
_update.value = null
Expand Down Expand Up @@ -154,7 +163,33 @@ internal class FlexibleUpdates(
}
}

/**
* Play refusing the check because of the device (its state, a missing or outdated Store, a
* sideloaded install) or the Store process dying mid-call is not something the app can fix,
* so it stays a breadcrumb. Anything else, like a malformed request, still goes to Bugsnag.
*/
private fun traceCheckFailure(error: Exception) {
val fromDevice = error is RemoteException ||
(error is InstallException && error.errorCode in deviceInstallErrors)
if (fromDevice) {
trace(
tag = TAG,
message = "Optional update check unavailable on this device",
metadata = { "error" to (error.message ?: error::class.simpleName) },
)
} else {
trace(tag = TAG, message = "Failed to check for an optional update", error = error)
}
}

private companion object {
const val TAG = "App Updates"

val deviceInstallErrors = setOf(
InstallErrorCode.ERROR_API_NOT_AVAILABLE,
InstallErrorCode.ERROR_INSTALL_NOT_ALLOWED,
InstallErrorCode.ERROR_PLAY_STORE_NOT_FOUND,
InstallErrorCode.ERROR_APP_NOT_OWNED,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ import com.flipcash.app.updates.OptionalUpdate
import com.flipcash.app.userflags.UserFlagsCoordinator
import com.flipcash.libs.coroutines.TestDispatcherProvider
import com.google.android.play.core.appupdate.testing.FakeAppUpdateManager
import android.os.RemoteException
import com.getcode.utils.ErrorReporter
import com.getcode.utils.ErrorUtils
import com.getcode.utils.TraceManager
import com.google.android.gms.tasks.Tasks
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.install.InstallException
import com.google.android.play.core.install.model.InstallErrorCode
import io.mockk.every
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
Expand Down Expand Up @@ -41,6 +50,59 @@ class FlexibleUpdatesTest {

private fun idleMain() = shadowOf(Looper.getMainLooper()).idle()

/** Runs [block] with a reporter attached and returns what reached Bugsnag. */
private suspend fun reportedDuring(block: suspend () -> Unit): List<Throwable> {
val reported = mutableListOf<Throwable>()
val reporter = object : ErrorReporter {
override fun report(error: Throwable, cause: Throwable, isNotifiable: Boolean) { reported += error }
}
TraceManager.initialize()
ErrorUtils.addReporter(reporter)
try {
block()
} finally {
ErrorUtils.removeReporter(reporter)
}
return reported
}

private fun failCheckWith(error: Exception) {
every { fake.appUpdateInfo } returns Tasks.forException<AppUpdateInfo>(error)
}

@Test
fun `a check Play refuses for device state is not reported`() = runTest {
failCheckWith(InstallException(InstallErrorCode.ERROR_INSTALL_NOT_ALLOWED))
val updates = flexibleUpdates()

val reported = reportedDuring { updates.check() }

assertNull(updates.update.value)
assertTrue(reported.isEmpty())
}

@Test
fun `a check cut off by the Play Store dying is not reported`() = runTest {
failCheckWith(RemoteException("AppUpdateService : Binder has died."))
val updates = flexibleUpdates()

val reported = reportedDuring { updates.check() }

assertNull(updates.update.value)
assertTrue(reported.isEmpty())
}

@Test
fun `a malformed check request is still reported`() = runTest {
failCheckWith(InstallException(InstallErrorCode.ERROR_INVALID_REQUEST))
val updates = flexibleUpdates()

val reported = reportedDuring { updates.check() }

assertNull(updates.update.value)
assertEquals(1, reported.size)
}

@Test
fun `reads priority and treats unknown staleness as zero`() = runTest {
fake.setUpdateAvailable(currentVersion + 1)
Expand Down
Loading