Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 018cc72d authored by Sayantan Roychowdhury's avatar Sayantan Roychowdhury
Browse files

Issue 5404: Show timeout alert dialog (for Home tab only).

parent 5076760a
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ class MainActivity : AppCompatActivity() {
                    User.ANONYMOUS -> {
                        if (viewModel.authDataJson.value.isNullOrEmpty() && !viewModel.authRequestRunning) {
                            Log.d(TAG, "Fetching new authentication data")
                            viewModel.setFirstTokenFetchTime()
                            viewModel.getAuthData()
                        }
                    }
@@ -99,6 +100,7 @@ class MainActivity : AppCompatActivity() {
                    User.GOOGLE -> {
                        if (viewModel.authData.value == null && !viewModel.authRequestRunning) {
                            Log.d(TAG, "Fetching new authentication data")
                            viewModel.setFirstTokenFetchTime()
                            signInViewModel.fetchAuthData()
                        }
                    }
@@ -134,7 +136,11 @@ class MainActivity : AppCompatActivity() {
            if (it != true) {
                Log.d(TAG, "Authentication data validation failed!")
                viewModel.destroyCredentials { user ->
                    if (viewModel.isTimeEligibleForTokenRefresh()) {
                        generateAuthDataBasedOnUserType(user)
                    } else {
                        Log.d(TAG, "Timeout validating auth data!")
                    }
                }
            } else {
                Log.d(TAG, "Authentication data is valid!")
+138 −1
Original line number Diff line number Diff line
@@ -18,14 +18,18 @@

package foundation.e.apps

import android.app.AlertDialog
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.graphics.Bitmap
import android.os.Build
import android.os.SystemClock
import android.util.Base64
import android.util.Log
import android.view.KeyEvent
import android.widget.ImageView
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -37,16 +41,19 @@ import com.aurora.gplayapi.data.models.AuthData
import com.aurora.gplayapi.exceptions.ApiException
import com.google.gson.Gson
import dagger.hilt.android.lifecycle.HiltViewModel
import foundation.e.apps.api.fused.FusedAPIImpl
import foundation.e.apps.api.fused.FusedAPIRepository
import foundation.e.apps.api.fused.data.FusedApp
import foundation.e.apps.manager.database.fusedDownload.FusedDownload
import foundation.e.apps.manager.fused.FusedManagerRepository
import foundation.e.apps.manager.pkg.PkgManagerModule
import foundation.e.apps.manager.workmanager.InstallWorkManager
import foundation.e.apps.settings.SettingsFragment
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.Type
import foundation.e.apps.utils.enums.User
import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis
import foundation.e.apps.utils.modules.DataStoreModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -76,6 +83,116 @@ class MainActivityViewModel @Inject constructor(
    val purchaseDeclined: MutableLiveData<String> = MutableLiveData()
    var authRequestRunning = false

    /*
     * Store the time when auth data is fetched for the first time.
     * If we try to fetch auth data after timeout, then don't allow it.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    var firstAuthDataFetchTime = 0L

    /*
     * Alert dialog to show to user if App Lounge times out.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    private lateinit var timeoutAlertDialog: AlertDialog

    /**
     * Display timeout alert dialog.
     *
     * @param activity Activity class. Basically the MainActivity.
     * @param positiveButtonBlock Code block when "Retry" is pressed.
     * @param openSettings Code block when "Open Settings" button is pressed.
     * This should open the [SettingsFragment] fragment.
     * @param applicationTypeFromPreferences Application type string, can be one of
     * [FusedAPIImpl.APP_TYPE_ANY], [FusedAPIImpl.APP_TYPE_OPEN], [FusedAPIImpl.APP_TYPE_PWA]
     */
    fun displayTimeoutAlertDialog(
        activity: Activity,
        positiveButtonBlock: () -> Unit,
        openSettings: () -> Unit,
        applicationTypeFromPreferences: String,
    ) {
        if (!this::timeoutAlertDialog.isInitialized) {
            timeoutAlertDialog = AlertDialog.Builder(activity).apply {
                setTitle(R.string.timeout_title)
                /*
                 * Prevent dismissing the dialog from pressing outside as it will only
                 * show a blank screen below the dialog.
                 */
                setCancelable(false)
                /*
                 * If user presses back button to close the dialog without selecting anything,
                 * close App Lounge.
                 */
                setOnKeyListener { dialog, keyCode, _ ->
                    if (keyCode == KeyEvent.KEYCODE_BACK) {
                        dialog.dismiss()
                        activity.finish()
                    }
                    true
                }
            }.create()
        }

        timeoutAlertDialog.apply {
            /*
             * Set retry button.
             */
            setButton(DialogInterface.BUTTON_POSITIVE, activity.getString(R.string.retry)) {_, _ ->
                positiveButtonBlock()
            }
            /*
             * Set message based on apps from GPlay of cleanapk.
             */
            setMessage(
                activity.getString(
                    when (applicationTypeFromPreferences) {
                        FusedAPIImpl.APP_TYPE_ANY -> R.string.timeout_desc_gplay
                        else -> R.string.timeout_desc_cleanapk
                    }
                )
            )
            /*
             * Show "Open Setting" only for GPlay apps.
             */
            if (applicationTypeFromPreferences == FusedAPIImpl.APP_TYPE_ANY) {
                setButton(
                    DialogInterface.BUTTON_NEUTRAL,
                    activity.getString(R.string.open_settings)
                ) { _, _ ->
                    openSettings()
                }
            }
        }

        timeoutAlertDialog.show()
    }

    /**
     * Returns true if [timeoutAlertDialog] is displaying.
     * Returs false if it is not initialised.
     */
    fun isTimeoutDialogDisplayed(): Boolean {
        return if (this::timeoutAlertDialog.isInitialized) {
            timeoutAlertDialog.isShowing
        } else false
    }

    /**
     * Dismisses the [timeoutAlertDialog] if it is being displayed.
     * Does nothing if it is not being displayed.
     * Caller need not check if the dialog is being displayed.
     */
    fun dismissTimeoutDialog() {
        if (isTimeoutDialogDisplayed()) {
            try {
                timeoutAlertDialog.dismiss()
            } catch (_: Exception) {}
        }
    }

    // Downloads
    val downloadList = fusedManagerRepository.getDownloadLiveList()
    var installInProgress = false
@@ -92,6 +209,26 @@ class MainActivityViewModel @Inject constructor(
        private const val TAG = "MainActivityViewModel"
    }

    fun setFirstTokenFetchTime() {
        firstAuthDataFetchTime = SystemClock.uptimeMillis()
    }

    fun isTimeEligibleForTokenRefresh(): Boolean {
        return (SystemClock.uptimeMillis() - firstAuthDataFetchTime) <= timeoutDurationInMillis
    }

    /*
     * This method resets the last recorded token fetch time.
     * Then it posts authValidity as false. This causes the observer in MainActivity to destroyCredentials
     * and fetch new token.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    fun retryFetchingTokenAfterTimeout() {
        setFirstTokenFetchTime()
        authValidity.postValue(false)
    }

    fun getAuthData() {
        if (!authRequestRunning) {
            authRequestRunning = true
+84 −22
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ package foundation.e.apps.api.fused

import android.content.Context
import android.text.format.Formatter
import android.util.Log
import com.aurora.gplayapi.Constants
import com.aurora.gplayapi.SearchSuggestEntry
import com.aurora.gplayapi.data.models.App
@@ -46,8 +47,11 @@ import foundation.e.apps.utils.enums.AppTag
import foundation.e.apps.utils.enums.Origin
import foundation.e.apps.utils.enums.Status
import foundation.e.apps.utils.enums.Type
import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis
import foundation.e.apps.utils.modules.PWAManagerModule
import foundation.e.apps.utils.modules.PreferenceManagerModule
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import javax.inject.Inject
import javax.inject.Singleton

@@ -63,21 +67,72 @@ class FusedAPIImpl @Inject constructor(

    companion object {
        private const val CATEGORY_TITLE_REPLACEABLE_CONJUNCTION = "&"
        private const val APP_TYPE_ANY = "any"
        private const val APP_TYPE_OPEN = "open"
        private const val APP_TYPE_PWA = "pwa"
        /*
         * Removing "private" access specifier to allow access in
         * MainActivityViewModel.timeoutAlertDialog
         *
         * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
         */
        const val APP_TYPE_ANY = "any"
        const val APP_TYPE_OPEN = "open"
        const val APP_TYPE_PWA = "pwa"
        private const val CATEGORY_OPEN_GAMES_ID = "game_open_games"
        private const val CATEGORY_OPEN_GAMES_TITLE = "Open games"
    }

    private var TAG = FusedAPIImpl::class.java.simpleName

    suspend fun getHomeScreenData(authData: AuthData): List<FusedHome> {
        val list = mutableListOf<FusedHome>()
    /**
     * Pass application source type along with list of apps.
     * Application source type may change in case of timeout of GPlay/cleanapk api.
     *
     * The second item of the Pair can be one of [APP_TYPE_ANY], [APP_TYPE_OPEN], [APP_TYPE_PWA].
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    suspend fun getHomeScreenData(authData: AuthData): Pair<List<FusedHome>, String> {
        val preferredApplicationType = preferenceManagerModule.preferredApplicationType()
        val initialData = getHomeScreenDataBasedOnApplicationType(authData, preferredApplicationType)
        if (isFusedHomesEmpty(initialData.first)) {
            Log.d(TAG, "Received empty home data.")
        }
        return initialData
    }

        if (preferredApplicationType != APP_TYPE_ANY) {
            val response = if (preferredApplicationType == APP_TYPE_OPEN) {
    /**
     * Check if list in all the FusedHome is empty.
     * If any list is not empty, send false.
     * Else (if all lists are empty) send true.
     */
    fun isFusedHomesEmpty(fusedHomes: List<FusedHome>): Boolean {
        fusedHomes.forEach {
            if (it.list.isNotEmpty()) return false
        }
        return true
    }

    /*
     * Offload fetching application to a different method to dynamically fallback to a different
     * app source if the user selected app source times out.
     *
     * Issue: https://gitlab.e.foundation/e/backlog/-/issues/5404
     */
    private suspend fun getHomeScreenDataBasedOnApplicationType(
        authData: AuthData,
        applicationType: String
    ): Pair<List<FusedHome>, String> {
        val list = mutableListOf<FusedHome>()
        try {
            /*
             * Each category of home apps (example "Top Free Apps") will have its own timeout.
             * Fetching 6 such categories will have a total timeout to 2 mins 30 seconds
             * (considering each category having 25 seconds timeout).
             *
             * To prevent waiting so long and fail early, use withTimeout{}.
             */
            withTimeout(timeoutDurationInMillis) {
                if (applicationType != APP_TYPE_ANY) {
                    val response = if (applicationType == APP_TYPE_OPEN) {
                        cleanAPKRepository.getHomeScreenData(
                            CleanAPKInterface.APP_TYPE_ANY,
                            CleanAPKInterface.APP_SOURCE_FOSS
@@ -89,12 +144,19 @@ class FusedAPIImpl @Inject constructor(
                        ).body()
                    }
                    response?.home?.let {
                list.addAll(generateCleanAPKHome(it, preferredApplicationType))
                        list.addAll(generateCleanAPKHome(it, applicationType))
                    }
                } else {
                    list.addAll(fetchGPlayHome(authData))
                }
        return list
            }
        } catch (e: TimeoutCancellationException) {
            e.printStackTrace()
            Log.d(TAG, "Timed out fetching home data for type: $applicationType")
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return Pair(list, applicationType)
    }

    suspend fun getCategoriesList(type: Category.Type, authData: AuthData): List<FusedCategory> {
+5 −1
Original line number Diff line number Diff line
@@ -34,10 +34,14 @@ import javax.inject.Singleton
class FusedAPIRepository @Inject constructor(
    private val fusedAPIImpl: FusedAPIImpl
) {
    suspend fun getHomeScreenData(authData: AuthData): List<FusedHome> {
    suspend fun getHomeScreenData(authData: AuthData): Pair<List<FusedHome>, String> {
        return fusedAPIImpl.getHomeScreenData(authData)
    }

    fun isFusedHomesEmpty(fusedHomes: List<FusedHome>): Boolean {
        return fusedAPIImpl.isFusedHomesEmpty(fusedHomes)
    }

    suspend fun validateAuthData(authData: AuthData): Boolean {
        return fusedAPIImpl.validateAuthData(authData)
    }
+6 −4
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ import android.util.Log
import com.aurora.gplayapi.data.models.PlayResponse
import com.aurora.gplayapi.network.IHttpClient
import foundation.e.apps.BuildConfig
import foundation.e.apps.utils.modules.CommonUtilsModule.timeoutDurationInMillis
import okhttp3.Cache
import okhttp3.Headers.Companion.toHeaders
import okhttp3.HttpUrl
@@ -51,10 +52,11 @@ class GPlayHttpClient @Inject constructor(
    }

    private val okHttpClient = OkHttpClient().newBuilder()
        .connectTimeout(25, TimeUnit.SECONDS)
        .readTimeout(25, TimeUnit.SECONDS)
        .writeTimeout(25, TimeUnit.SECONDS)
        .retryOnConnectionFailure(true)
        .connectTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS)
        .readTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS)
        .writeTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS)
        .callTimeout(timeoutDurationInMillis, TimeUnit.MILLISECONDS)
        .retryOnConnectionFailure(false)
        .followRedirects(true)
        .followSslRedirects(true)
        .cache(cache)
Loading