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

Commit 286d8928 authored by Catherine Liang's avatar Catherine Liang Committed by Android Build Coastguard Worker
Browse files

Fix empty picker when launcher isn't ready

Grid flow wasn't emitting when PreviewUtils is null, blocking the
isAvailable check, which blocked initializing customization picker. Make
isAvailable flow in grid repository depend directly on PreviewUtils flow
so that isAvailable is always emitted, and always refreshed if
necessary. Also take care of case where picker is launched first in SUW
when Launcher isn't available, then relaunched after finishing SUW, and
make sure shape and grid options appear correctly. This requires
changing shape to observe provider changes instead of getting options
only in init in the Singleton repository.

Flag: com.android.systemui.shared.new_customization_picker_ui
Bug: 423648739
Bug: 421526288
Test: manually verified in and out of SUW
(cherry picked from https://googleplex-android-review.googlesource.com/q/commit:84fa39df8500f859d1da12260f880680e3c3f73c)
Merged-In: I31596b2b88e0f2b26d90200f42b354cb0f87db82
Change-Id: I31596b2b88e0f2b26d90200f42b354cb0f87db82
parent bab373b4
Loading
Loading
Loading
Loading
+156 −123
Original line number Diff line number Diff line
@@ -21,7 +21,6 @@ import android.content.Context
import android.content.res.Resources
import android.database.ContentObserver
import android.graphics.drawable.Drawable
import android.net.Uri
import android.util.Log
import androidx.core.content.res.ResourcesCompat
import com.android.wallpaper.R
@@ -33,7 +32,6 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -42,6 +40,8 @@ import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext

@@ -57,6 +57,7 @@ constructor(

    private val authorityMetadataKey: String =
        context.getString(R.string.grid_control_metadata_name)
    // TODO (b/424856247): test the retry logic for getting PreviewUtils
    private var previewUtils: PreviewUtils? = null
    private val previewUtilsFlow = flow {
        // If PreviewUtils is created too early on start up, the provider (e.g. Launcher) may not be
@@ -72,16 +73,19 @@ constructor(
        emit(previewUtils)
    }

    override val isCustomizationAvailable: Flow<Boolean> = previewUtilsFlow.map { it != null }

    override val gridOptions: Flow<List<GridOptionModel>> =
        previewUtilsFlow
            .flatMapLatest {
                if (it == null) {
                    return@flatMapLatest flowOf(emptyList())
                }
                callbackFlow {
                    var disposableHandle: DisposableHandle? = null
                    if (it != null) {
                    val contentObserver =
                        object : ContentObserver(null) {
                            override fun onChange(selfChange: Boolean) {
                                    trySend(getGridOptions(it.getUri(GRID_OPTIONS)))
                                trySend(getGridOptions(it))
                            }
                        }
                    context.contentResolver.registerContentObserver(
@@ -90,29 +94,29 @@ constructor(
                        contentObserver,
                    )

                        trySend(getGridOptions(it.getUri(GRID_OPTIONS)))
                    trySend(getGridOptions(it))

                        disposableHandle = DisposableHandle {
                    awaitClose {
                        context.contentResolver.unregisterContentObserver(contentObserver)
                    }
                }
                    awaitClose { disposableHandle?.dispose() }
                }
            }
            .shareIn(scope = bgScope, started = SharingStarted.WhileSubscribed(), replay = 1)

    override suspend fun getGridOptions(): List<GridOptionModel> =
        withContext(bgDispatcher) {
            val uri = previewUtilsFlow.first()?.getUri(GRID_OPTIONS)
            if (uri != null) {
                getGridOptions(uri)
            val previewUtils = previewUtilsFlow.first()
            if (previewUtils != null) {
                getGridOptions(previewUtils)
            } else {
                emptyList()
            }
        }

    private fun getGridOptions(uri: Uri): List<GridOptionModel> {
        return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
    private fun getGridOptions(previewUtils: PreviewUtils): List<GridOptionModel> {
        return context.contentResolver
            .query(previewUtils.getUri(GRID_OPTIONS), null, null, null, null)
            ?.use { cursor ->
                buildList {
                        while (cursor.moveToNext()) {
                            try {
@@ -145,11 +149,16 @@ constructor(
                                                .toBoolean(),
                                        rows = rows,
                                        cols = cols,
                                    iconId = cursor.getInt(cursor.getColumnIndex(KEY_GRID_ICON_ID)),
                                        iconId =
                                            cursor.getInt(cursor.getColumnIndex(KEY_GRID_ICON_ID)),
                                    )
                                )
                            } catch (e: IllegalStateException) {
                            Log.e(TAG, "Fail to read from the cursor to build GridOptionModel", e)
                                Log.e(
                                    TAG,
                                    "Fail to read from the cursor to build GridOptionModel",
                                    e,
                                )
                            }
                        }
                    }
@@ -175,32 +184,59 @@ constructor(
            } ?: emptyList()
    }

    override val shapeOptions: Flow<List<ShapeOptionModel>> =
        previewUtilsFlow
            .flatMapLatest {
                if (it == null) {
                    return@flatMapLatest flowOf(emptyList())
                }
                callbackFlow {
                    val contentObserver =
                        object : ContentObserver(null) {
                            override fun onChange(selfChange: Boolean) {
                                trySend(getShapeOptions(it))
                            }
                        }
                    context.contentResolver.registerContentObserver(
                        it.getUri(SET_SHAPE),
                        /* notifyForDescendants= */ true,
                        contentObserver,
                    )

                    trySend(getShapeOptions(it))

                    awaitClose {
                        context.contentResolver.unregisterContentObserver(contentObserver)
                    }
                }
            }
            .shareIn(scope = bgScope, started = SharingStarted.WhileSubscribed(), replay = 1)

    override suspend fun getShapeOptions(): List<ShapeOptionModel> =
        withContext(bgDispatcher) {
            val previewUtils = previewUtilsFlow.first()
            if (previewUtils != null) {
                context.contentResolver
                getShapeOptions(previewUtils)
            } else {
                emptyList()
            }
        }

    private fun getShapeOptions(previewUtils: PreviewUtils): List<ShapeOptionModel> {
        return context.contentResolver
            .query(previewUtils.getUri(SHAPE_OPTIONS), null, null, null, null)
            ?.use { cursor ->
                buildList {
                        while (cursor.moveToNext()) {
                            add(
                                ShapeOptionModel(
                                            key =
                                                cursor.getString(
                                                    cursor.getColumnIndex(COL_SHAPE_KEY)
                                                ),
                                    key = cursor.getString(cursor.getColumnIndex(COL_SHAPE_KEY)),
                                    title =
                                                cursor.getString(
                                                    cursor.getColumnIndex(COL_SHAPE_TITLE)
                                                ),
                                            path =
                                                cursor.getString(cursor.getColumnIndex(COL_PATH)),
                                        cursor.getString(cursor.getColumnIndex(COL_SHAPE_TITLE)),
                                    path = cursor.getString(cursor.getColumnIndex(COL_PATH)),
                                    isCurrent =
                                        cursor
                                                    .getString(
                                                        cursor.getColumnIndex(COL_IS_DEFAULT)
                                                    )
                                            .getString(cursor.getColumnIndex(COL_IS_DEFAULT))
                                            .toBoolean(),
                                )
                            )
@@ -225,9 +261,6 @@ constructor(
                        }
                    }
            } ?: emptyList()
            } else {
                emptyList()
            }
    }

    override fun applyGridOption(gridKey: String) {
+14 −0
Original line number Diff line number Diff line
@@ -27,6 +27,13 @@ interface ShapeGridManager {
     */
    suspend fun getGridOptions(): List<GridOptionModel>

    /**
     * A flow representing whether shape and grid customization option provider is available.
     * Collecting from this flow also triggers a retry to get customization provider if it is not
     * available.
     */
    val isCustomizationAvailable: Flow<Boolean>

    /**
     * A flow of the current list of grid options, updated when the grid options change.
     *
@@ -34,6 +41,13 @@ interface ShapeGridManager {
     */
    val gridOptions: Flow<List<GridOptionModel>>

    /**
     * A flow of the current list of shape options, updated when the shape options change.
     *
     * @return It will return an empty list if there are no available shape options.
     */
    val shapeOptions: Flow<List<ShapeOptionModel>>

    suspend fun getShapeOptions(): List<ShapeOptionModel>

    fun applyGridOption(gridKey: String)
+6 −3
Original line number Diff line number Diff line
@@ -25,8 +25,7 @@ import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext

@@ -44,7 +43,11 @@ constructor(
        gridOptions.map { gridOptions -> gridOptions.firstOrNull { it.isCurrent } }

    val isGridCustomizationAvailable =
        gridOptions.filterNotNull().map { it.size > 1 }.distinctUntilChanged()
        combine(manager.isCustomizationAvailable, gridOptions) { isCustomizationAvailable, _ ->
            // Call getGridOptions() instead of using gridOptions flow to avoid getting stale replay
            // value
            isCustomizationAvailable && manager.getGridOptions().size > 1
        }

    suspend fun applyGridOption(gridKey: String) =
        withContext(bgDispatcher) { manager.applyGridOption(gridKey) }
+12 −29
Original line number Diff line number Diff line
@@ -22,21 +22,14 @@ import android.content.res.Resources
import com.android.customization.model.ResourceConstants
import com.android.customization.model.grid.ShapeGridManager
import com.android.customization.model.grid.ShapeOptionModel
import com.android.wallpaper.R
import com.android.wallpaper.model.Screen
import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
import com.android.wallpaper.util.PreviewUtils
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@Singleton
@@ -45,15 +38,8 @@ class ShapeRepository
constructor(
    @ApplicationContext private val context: Context,
    private val shapeGridManager: ShapeGridManager,
    @BackgroundDispatcher private val bgScope: CoroutineScope,
    @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher,
) {
    private val authorityMetadataKey: String =
        context.getString(R.string.grid_control_metadata_name)
    private val previewUtils: PreviewUtils =
        PreviewUtils(context, authorityMetadataKey, Screen.HOME_SCREEN)

    private val _shapeOptions = MutableStateFlow<List<ShapeOptionModel>?>(null)

    val defaultShapePath =
        context.resources.getString(
@@ -65,23 +51,20 @@ constructor(
                )
        )

    init {
        bgScope.launch { refreshShapeOptions() }
    }
    val shapeOptions: Flow<List<ShapeOptionModel>> = shapeGridManager.shapeOptions

    val shapeOptions: StateFlow<List<ShapeOptionModel>?> = _shapeOptions.asStateFlow()
    val isShapeOptionsAvailable =
        combine(shapeGridManager.isCustomizationAvailable, shapeOptions) {
            isCustomizationAvailable,
            _ ->
            // Call getShapeOptions() instead of using shapeOptions flow to avoid getting stale
            // replay value
            isCustomizationAvailable && shapeGridManager.getShapeOptions().size > 1
        }

    val selectedShapeOption: Flow<ShapeOptionModel?> =
        shapeOptions.map { shapeOptions -> shapeOptions?.firstOrNull { it.isCurrent } }
        shapeOptions.map { shapeOptions -> shapeOptions.firstOrNull { it.isCurrent } }

    suspend fun applyShape(shapeKey: String) =
        withContext(bgDispatcher) {
            shapeGridManager.applyShapeOption(shapeKey)
            // After applying, we should query and update shape options again.
            refreshShapeOptions()
        }

    suspend fun refreshShapeOptions() {
        _shapeOptions.value = shapeGridManager.getShapeOptions()
    }
        withContext(bgDispatcher) { shapeGridManager.applyShapeOption(shapeKey) }
}
+1 −0
Original line number Diff line number Diff line
@@ -54,6 +54,7 @@ constructor(
    @BackgroundDispatcher private val backgroundScope: CoroutineScope,
) : IconStyleRepository {
    private val metadataKey = appContext.getString(R.string.themed_icon_metadata_key)
    // TODO (b/424856247): test the retry logic for getting PreviewUtils
    private var previewUtils: PreviewUtils? = null
    private val previewUtilsFlow = flow {
        // If PreviewUtils is created too early on start up, the provider (e.g. Launcher) may not be
Loading