Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Play services failures more gracefully #2920

Merged
merged 10 commits into from
Dec 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@
package com.google.android.ground.system

import android.content.Context
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.ConnectionResult.SUCCESS
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.delay
import timber.log.Timber

private val INSTALL_API_REQUEST_CODE = GoogleApiAvailability::class.java.hashCode() and 0xffff
private const val PLAY_SERVICES_RETRY_DELAY_MS = 2500L

@Singleton
class GoogleApiManager
Expand All @@ -38,28 +44,36 @@
* possible or cancelled.
*/
suspend fun installGooglePlayServices() {
val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
if (status == ConnectionResult.SUCCESS) return

val requestCode = INSTALL_API_REQUEST_CODE
startResolution(status, requestCode, GooglePlayServicesMissingException())
getNextResult(requestCode)
}

private fun startResolution(status: Int, requestCode: Int, throwable: Throwable) {
if (!googleApiAvailability.isUserResolvableError(status)) throw throwable

activityStreams.withActivity {
googleApiAvailability.showErrorDialogFragment(it, status, requestCode) { throw throwable }
val status = isGooglePlayServicesAvailable()
if (status == SUCCESS) return
if (googleApiAvailability.isUserResolvableError(status)) {
showErrorDialog(status, INSTALL_API_REQUEST_CODE)
} else {
throw GooglePlayServicesNotAvailableException(status)

Check warning on line 52 in ground/src/main/java/com/google/android/ground/system/GoogleApiManager.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/system/GoogleApiManager.kt#L52

Added line #L52 was not covered by tests
}
}

private suspend fun getNextResult(requestCode: Int) {
val result = activityStreams.getNextActivityResult(requestCode)
if (!result.isOk()) {
error("Activity result failed: requestCode = $requestCode, result = $result")
// onActivityResult() is sometimes called with a failure prematurely or not at all. Instead, we
// poll for Play services.
while (isGooglePlayServicesAvailable() != SUCCESS) {
Timber.d("Waiting for Play services")
delay(PLAY_SERVICES_RETRY_DELAY_MS)

Check warning on line 58 in ground/src/main/java/com/google/android/ground/system/GoogleApiManager.kt

View check run for this annotation

Codecov / codecov/patch

ground/src/main/java/com/google/android/ground/system/GoogleApiManager.kt#L57-L58

Added lines #L57 - L58 were not covered by tests
}
}

class GooglePlayServicesMissingException : Error("Google play services not available")
private fun isGooglePlayServicesAvailable(): Int =
googleApiAvailability.isGooglePlayServicesAvailable(context)

/**
* Attempts to resolve the error indicated by the given `status` code, using the provided
* `requestCode` to differentiate Activity callbacks from others. Suspends until the dialog is
* dismissed.
*/
private suspend fun showErrorDialog(status: Int, requestCode: Int) =
suspendCoroutine { continuation ->
activityStreams.withActivity { activity ->
val dialog = googleApiAvailability.getErrorDialog(activity, status, requestCode)
dialog?.setCanceledOnTouchOutside(false)
dialog?.setOnDismissListener { continuation.resume(Unit) }
dialog?.show()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import com.google.android.ground.R
import com.google.android.ground.system.GoogleApiManager
import com.google.android.ground.ui.common.AbstractFragment
import com.google.android.ground.ui.common.EphemeralPopups
import dagger.hilt.android.AndroidEntryPoint
Expand Down Expand Up @@ -66,7 +66,7 @@ class StartupFragment : AbstractFragment() {

private fun onInitFailed(t: Throwable) {
Timber.e(t, "Failed to launch app")
if (t is GoogleApiManager.GooglePlayServicesMissingException) {
if (t is GooglePlayServicesNotAvailableException) {
popups.ErrorPopup().show(R.string.google_api_install_failed)
}
requireActivity().finish()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal constructor(
private val userRepository: UserRepository,
) : AbstractViewModel() {

/** Checks & installs Google Play Services and initializes the login flow. */
/** Initializes the login flow, installing Google Play Services if necessary. */
suspend fun initializeLogin() {
googleApiManager.installGooglePlayServices()
userRepository.init()
Expand Down
Loading