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

Commit 1c8b2264 authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Merge cherrypicks of ['googleplex-android-review.googlesource.com/33616549',...

Merge cherrypicks of ['googleplex-android-review.googlesource.com/33616549', 'googleplex-android-review.googlesource.com/34009791'] into 25Q3-release.

Change-Id: Ie5fdedc2d40b628d1ce278d6921cc7e4f93a9fd5
parents 893e5118 286d8928
Loading
Loading
Loading
Loading
+156 −117
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,83 +149,118 @@ 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,
                                )
                            }
                        }
                    }
                    .let { list ->
                        val selectedOptionCount = list.count { it.isCurrent }
                        if (list.isEmpty()) {
                        throw IllegalStateException(
                            "Grid option list can not be empty. It needs to have at least one item."
                            Log.e(
                                TAG,
                                "Grid option list can not be empty. It needs to have at least one item.",
                            )
                    }
                    // In this list, exactly one item should have isCurrent true.
                    val isCurrentCount = list.count { it.isCurrent }
                    if (isCurrentCount != 1) {
                        throw IllegalStateException(
                            "Exactly one grid option should have isCurrent = true. Found $isCurrentCount."
                            emptyList()
                        } else if (selectedOptionCount != 1) {
                            Log.e(
                                TAG,
                                "Exactly one grid option should have isCurrent = true. Found $selectedOptionCount.",
                            )
                    }
                            emptyList()
                        } else {
                            list
                        }
                    }
                    .sortedByDescending { it.rows * it.cols }
            } ?: 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(),
                                )
                            )
                        }
                    }
                    .let { list ->
                        val selectedOptionCount = list.count { it.isCurrent }
                        if (list.isEmpty()) {
                                    throw IllegalStateException(
                                        "Shape option list can not be empty. It needs to have at least one item."
                            Log.e(
                                TAG,
                                "Shape option list can not be empty. It needs to have at least one item.",
                            )
                                }
                                // In this list, exactly one item should have isCurrent true.
                                val isCurrentCount = list.count { it.isCurrent }
                                if (isCurrentCount != 1) {
                                    throw IllegalStateException(
                                        "Exactly one shape option should have isCurrent = true. Found $isCurrentCount."
                            emptyList()
                        } else if (selectedOptionCount != 1) {
                            Log.e(
                                TAG,
                                "Exactly one shape option should have isCurrent = true. Found $selectedOptionCount.",
                            )
                                }
                            emptyList()
                        } else {
                            list
                        }
                    } ?: emptyList()
            } else {
                emptyList()
                    }
            } ?: 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