Skip to content

Commit

Permalink
Move app initialization to onResume() to prevent blank screen (#1946)
Browse files Browse the repository at this point in the history
* Move app initialization to onResume() to prevent app freeze when the app is launched from sleep state

* Convert Rx to coroutine

* Create ViewModel for StartupFragment
  • Loading branch information
shobhitagarwal1612 authored Oct 1, 2023
1 parent 8700520 commit 0d0db89
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,13 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.ground.rx.RxCompletable
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import io.reactivex.Completable
import io.reactivex.CompletableEmitter
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.rx2.await

private val INSTALL_API_REQUEST_CODE = GoogleApiAvailability::class.java.hashCode() and 0xffff

@ActivityScoped
@Singleton
class GoogleApiManager
@Inject
constructor(
Expand All @@ -37,34 +36,36 @@ constructor(
) {

/**
* Returns a stream that either completes immediately if Google Play Services are already
* installed, otherwise shows install dialog. Terminates with error if install not possible or
* cancelled.
* Displays a dialog to install Google Play Services, if missing. Throws an error if install not
* possible or cancelled.
*/
fun installGooglePlayServices(): Completable =
requestInstallOrComplete().ambWith(getNextInstallApiResult())
suspend fun installGooglePlayServices() {
val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
if (status == ConnectionResult.SUCCESS) return

private fun requestInstallOrComplete(): Completable =
Completable.create { emitter: CompletableEmitter ->
val status = googleApiAvailability.isGooglePlayServicesAvailable(context)
if (status == ConnectionResult.SUCCESS) {
emitter.onComplete()
} else if (googleApiAvailability.isUserResolvableError(status)) {
activityStreams.withActivity {
googleApiAvailability.showErrorDialogFragment(it, status, INSTALL_API_REQUEST_CODE) {
emitter.onError(Exception("Google play services not available"))
}
}
} else {
emitter.onError(Exception("Google play services not available"))
}
}
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

private fun getNextInstallApiResult(): Completable =
activityStreams.getNextActivityResult(INSTALL_API_REQUEST_CODE).flatMapCompletable {
RxCompletable.completeOrError(
{ it.isOk() },
Exception::class.java // TODO: Throw appropriate Exception.
)
activityStreams.withActivity {
googleApiAvailability.showErrorDialogFragment(it, status, requestCode) { throw throwable }
}
}

private suspend fun getNextResult(requestCode: Int) =
activityStreams
.getNextActivityResult(requestCode)
.flatMapCompletable {
RxCompletable.completeOrError(
{ it.isOk() },
Exception::class.java // TODO: Throw appropriate Exception.
)
}
.await()

class GooglePlayServicesMissingException : Error("Google play services not available")
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import com.google.android.ground.ui.offlineareas.OfflineAreasViewModel
import com.google.android.ground.ui.offlineareas.selector.OfflineAreaSelectorViewModel
import com.google.android.ground.ui.offlineareas.viewer.OfflineAreaViewerViewModel
import com.google.android.ground.ui.signin.SignInViewModel
import com.google.android.ground.ui.startup.StartupViewModel
import com.google.android.ground.ui.submissiondetails.SubmissionDetailsViewModel
import com.google.android.ground.ui.surveyselector.SurveySelectorViewModel
import com.google.android.ground.ui.syncstatus.SyncStatusViewModel
Expand Down Expand Up @@ -189,5 +190,10 @@ abstract class ViewModelModule {
@ViewModelKey(CaptureLocationMapViewModel::class)
abstract fun bindCaptureLocationMapViewModel(viewModel: CaptureLocationMapViewModel): ViewModel

@Binds
@IntoMap
@ViewModelKey(StartupViewModel::class)
abstract fun bindStartupViewModel(viewModel: StartupViewModel): ViewModel

@Binds abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,49 +15,54 @@
*/
package com.google.android.ground.ui.startup

import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import com.google.android.ground.R
import com.google.android.ground.repository.UserRepository
import com.google.android.ground.rx.RxAutoDispose
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
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber

@AndroidEntryPoint(AbstractFragment::class)
class StartupFragment : Hilt_StartupFragment() {

@Inject lateinit var googleApiManager: GoogleApiManager
@Inject lateinit var popups: EphemeralPopups
@Inject lateinit var userRepository: UserRepository

private lateinit var viewModel: StartupViewModel

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = getViewModel(StartupViewModel::class.java)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.startup_frag, container, false)

override fun onAttach(context: Context) {
super.onAttach(context)
googleApiManager
.installGooglePlayServices()
.`as`(RxAutoDispose.autoDisposable<Any>(requireActivity()))
.subscribe({ onGooglePlayServicesReady() }) { t: Throwable -> onGooglePlayServicesFailed(t) }
}

private fun onGooglePlayServicesReady() {
userRepository.init()
override fun onResume() {
super.onResume()
viewLifecycleOwner.lifecycleScope.launch {
try {
viewModel.initializeLogin()
} catch (t: Throwable) {
onInitFailed(t)
}
}
}

private fun onGooglePlayServicesFailed(t: Throwable) {
Timber.e(t, "Google Play Services install failed")
popups.showError(R.string.google_api_install_failed)
private fun onInitFailed(t: Throwable) {
Timber.e(t, "Failed to launch app")
if (t is GoogleApiManager.GooglePlayServicesMissingException) {
popups.showError(R.string.google_api_install_failed)
}
requireActivity().finish()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.ground.ui.startup

import com.google.android.ground.repository.UserRepository
import com.google.android.ground.system.GoogleApiManager
import com.google.android.ground.ui.common.AbstractViewModel
import javax.inject.Inject

class StartupViewModel
@Inject
internal constructor(
private val googleApiManager: GoogleApiManager,
private val userRepository: UserRepository
) : AbstractViewModel() {

/** Checks & installs Google Play Services and initializes the login flow. */
suspend fun initializeLogin() {
googleApiManager.installGooglePlayServices()
userRepository.init()
}
}

0 comments on commit 0d0db89

Please sign in to comment.