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

Commit b4be21c2 authored by Catherine Liang's avatar Catherine Liang Committed by Android (Google) Code Review
Browse files

Merge "Fix missing Themed Icons and Grid Layout entries on start up" into main

parents 71533ec8 838210c8
Loading
Loading
Loading
Loading
+142 −90
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package com.android.customization.model.grid
import android.content.ContentValues
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
@@ -30,27 +32,87 @@ 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.DisposableHandle
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.withContext

@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class DefaultShapeGridManager
@Inject
constructor(
    @ApplicationContext private val context: Context,
    @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher,
    @BackgroundDispatcher private val bgScope: CoroutineScope,
) : ShapeGridManager {

    private val authorityMetadataKey: String =
        context.getString(R.string.grid_control_metadata_name)
    private val previewUtils: PreviewUtils =
        PreviewUtils(context, authorityMetadataKey, Screen.HOME_SCREEN)
    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
        // ready, so PreviewUtils#supportsPreview would return false. Only cache previewUtils if it
        // supports previewing. Otherwise, retry when new flow consumers appear.
        if (previewUtils == null) {
            PreviewUtils(context, authorityMetadataKey, Screen.HOME_SCREEN).let {
                if (it.supportsPreview()) {
                    previewUtils = it
                }
            }
        }
        emit(previewUtils)
    }

    override val gridOptions: Flow<List<GridOptionModel>> =
        previewUtilsFlow
            .flatMapLatest {
                callbackFlow {
                    var disposableHandle: DisposableHandle? = null
                    if (it != null) {
                        val contentObserver =
                            object : ContentObserver(null) {
                                override fun onChange(selfChange: Boolean) {
                                    trySend(getGridOptions(it.getUri(GRID_OPTIONS)))
                                }
                            }
                        context.contentResolver.registerContentObserver(
                            it.getUri(SET_GRID),
                            /* notifyForDescendants= */ true,
                            contentObserver,
                        )

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

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

    override suspend fun getGridOptions(): List<GridOptionModel> =
        withContext(bgDispatcher) {
            if (previewUtils.supportsPreview()) {
                context.contentResolver
                    .query(previewUtils.getUri(GRID_OPTIONS), null, null, null, null)
                    ?.use { cursor ->
            val uri = previewUtilsFlow.first()?.getUri(GRID_OPTIONS)
            if (uri != null) {
                getGridOptions(uri)
            } else {
                emptyList()
            }
        }

    private fun getGridOptions(uri: Uri): List<GridOptionModel> {
        return context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
            buildList {
                    while (cursor.moveToNext()) {
                        try {
@@ -63,8 +125,7 @@ constructor(
                                    rows,
                                )
                            val title =
                                            cursor.getColumnIndex(COL_GRID_TITLE).let { titleIndex
                                                ->
                                cursor.getColumnIndex(COL_GRID_TITLE).let { titleIndex ->
                                    if (titleIndex != -1) {
                                        // Note that title can be null, even when the
                                        // column field exists.
@@ -76,31 +137,19 @@ constructor(

                            add(
                                GridOptionModel(
                                                key =
                                                    cursor.getString(
                                                        cursor.getColumnIndex(COL_GRID_KEY)
                                                    ),
                                    key = cursor.getString(cursor.getColumnIndex(COL_GRID_KEY)),
                                    title = title,
                                    isCurrent =
                                        cursor
                                                        .getString(
                                                            cursor.getColumnIndex(COL_IS_DEFAULT)
                                                        )
                                            .getString(cursor.getColumnIndex(COL_IS_DEFAULT))
                                            .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)
                        }
                    }
                }
@@ -121,14 +170,12 @@ constructor(
                }
                .sortedByDescending { it.rows * it.cols }
        } ?: emptyList()
            } else {
                emptyList()
            }
    }

    override suspend fun getShapeOptions(): List<ShapeOptionModel> =
        withContext(bgDispatcher) {
            if (previewUtils.supportsPreview()) {
            val previewUtils = previewUtilsFlow.first()
            if (previewUtils != null) {
                context.contentResolver
                    .query(previewUtils.getUri(SHAPE_OPTIONS), null, null, null, null)
                    ?.use { cursor ->
@@ -178,21 +225,26 @@ constructor(
        }

    override fun applyGridOption(gridKey: String) {
        previewUtils?.let {
            context.contentResolver.update(
            previewUtils.getUri(SET_GRID),
                it.getUri(SET_GRID),
                ContentValues().apply { put(COL_GRID_KEY, gridKey) },
                null,
                null,
            )
        }
    }

    override fun applyShapeOption(shapeKey: String) =
    override fun applyShapeOption(shapeKey: String) {
        previewUtils?.let {
            context.contentResolver.update(
            previewUtils.getUri(SET_SHAPE),
                it.getUri(SET_SHAPE),
                ContentValues().apply { put(COL_SHAPE_KEY, shapeKey) },
                null,
                null,
            )
        }
    }

    override fun getGridOptionDrawable(iconId: Int): Drawable? {
        val launcherPackageName =
+9 −5
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package com.android.customization.model.grid

import android.graphics.drawable.Drawable
import kotlinx.coroutines.flow.Flow

interface ShapeGridManager {
    /**
@@ -26,15 +27,18 @@ interface ShapeGridManager {
     */
    suspend fun getGridOptions(): List<GridOptionModel>

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

    suspend fun getShapeOptions(): List<ShapeOptionModel>

    fun applyGridOption(gridKey: String)

    /**
     * @return an integer representing whether the operation was successful, 1 for success and 0 for
     *   failure
     */
    fun applyShapeOption(shapeKey: String): Int
    fun applyShapeOption(shapeKey: String)

    fun getGridOptionDrawable(iconId: Int): Drawable?
}
+3 −19
Original line number Diff line number Diff line
@@ -24,15 +24,10 @@ import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
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.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

@Singleton
@@ -40,30 +35,19 @@ class GridRepository2
@Inject
constructor(
    private val manager: ShapeGridManager,
    @BackgroundDispatcher private val bgScope: CoroutineScope,
    @BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher,
) {

    private val _gridOptions = MutableStateFlow<List<GridOptionModel>?>(null)

    init {
        bgScope.launch { _gridOptions.value = manager.getGridOptions() }
    }

    val gridOptions: StateFlow<List<GridOptionModel>?> = _gridOptions.asStateFlow()
    val gridOptions: Flow<List<GridOptionModel>> = manager.gridOptions

    val selectedGridOption: Flow<GridOptionModel?> =
        gridOptions.map { gridOptions -> gridOptions?.firstOrNull { it.isCurrent } }
        gridOptions.map { gridOptions -> gridOptions.firstOrNull { it.isCurrent } }

    val isGridCustomizationAvailable =
        gridOptions.filterNotNull().map { it.size > 1 }.distinctUntilChanged()

    suspend fun applyGridOption(gridKey: String) =
        withContext(bgDispatcher) {
            manager.applyGridOption(gridKey)
            // After applying, we should query and update shape and grid options again.
            _gridOptions.value = manager.getGridOptions()
        }
        withContext(bgDispatcher) { manager.applyGridOption(gridKey) }

    fun getGridOptionDrawable(iconId: Int): Drawable? {
        return manager.getGridOptionDrawable(iconId)
+44 −73
Original line number Diff line number Diff line
@@ -19,90 +19,64 @@ package com.android.customization.picker.themedicon.data.repository
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.net.Uri
import android.util.Log
import com.android.customization.module.CustomizationPreferences
import com.android.themepicker.R
import com.android.wallpaper.model.Screen
import com.android.wallpaper.module.InjectorProvider
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.CoroutineScope
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class ThemedIconRepositoryImpl
@Inject
constructor(
    @ApplicationContext private val appContext: Context,
    private val contentResolver: ContentResolver,
    packageManager: PackageManager,
    @BackgroundDispatcher private val backgroundScope: CoroutineScope,
) : ThemedIconRepository {
    private val uri: MutableStateFlow<Uri?> = MutableStateFlow(null)
    private val _isAvailable: MutableStateFlow<Boolean?> = MutableStateFlow(null)
    private var getUriJob: Job =
        backgroundScope.launch {
            val homeIntent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)
            val resolveInfo =
                packageManager.resolveActivity(
                    homeIntent,
                    PackageManager.MATCH_DEFAULT_ONLY or PackageManager.GET_META_DATA,
                )
            val metadataKey = appContext.getString(R.string.themed_icon_metadata_key)
            val providerAuthority = resolveInfo?.activityInfo?.metaData?.getString(metadataKey)
            if (providerAuthority == null) {
                Log.i(TAG, "Couldn't resolve $metadataKey from $homeIntent")
            }
            val providerInfo =
                providerAuthority?.let { authority ->
                    val info = packageManager.resolveContentProvider(authority, 0)
                    val hasPermission =
                        info?.readPermission?.let {
                            if (it.isNotEmpty()) {
                                appContext.checkSelfPermission(it) ==
                                    PackageManager.PERMISSION_GRANTED
                            } else true
                        } ?: true
                    if (!hasPermission) {
                        Log.i(TAG, "No permission to query authority $authority")
                        null
                    } else {
                        info
                    }
                }
            uri.value =
                providerInfo?.let {
                    Uri.Builder()
                        .scheme(ContentResolver.SCHEME_CONTENT)
                        .authority(providerInfo.authority)
                        .appendPath(ICON_THEMED)
                        .build()
                }
            _isAvailable.value = uri.value != null
    private val metadataKey = appContext.getString(R.string.themed_icon_metadata_key)
    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
        // ready, so PreviewUtils#supportsPreview would return false. Only cache previewUtils if it
        // supports previewing. Otherwise, retry when new flow consumers appear.
        if (previewUtils == null) {
            PreviewUtils(appContext, metadataKey, Screen.HOME_SCREEN).let {
                if (it.supportsPreview()) {
                    previewUtils = it
                }
            }
        }
        emit(previewUtils)
    }
    private var uri: Uri? = null
    private val uriFlow: Flow<Uri?> =
        previewUtilsFlow.map { uri ?: it?.getUri(ICON_THEMED)?.also { result -> uri = result } }

    override val isAvailable: Flow<Boolean> = _isAvailable.filterNotNull()
    override val isAvailable: Flow<Boolean> = previewUtilsFlow.map { it != null }

    override val isActivated: Flow<Boolean> =
        uriFlow
            .flatMapLatest {
                callbackFlow {
                    var disposableHandle: DisposableHandle? = null
                launch {
                    uri.collect {
                        disposableHandle?.dispose()
                    if (it != null) {
                        val contentObserver =
                            object : ContentObserver(null) {
@@ -122,10 +96,9 @@ constructor(
                            contentResolver.unregisterContentObserver(contentObserver)
                        }
                    }
                    }
                }
                    awaitClose { disposableHandle?.dispose() }
                }
            }
            .stateIn(
                scope = backgroundScope,
                started = SharingStarted.WhileSubscribed(),
@@ -156,8 +129,7 @@ constructor(
    }

    override suspend fun setThemedIconEnabled(enabled: Boolean) {
        getUriJob.join()
        uri.value?.let {
        uri?.let {
            val values = ContentValues()
            values.put(COL_ICON_THEMED_VALUE, enabled)
            contentResolver.update(it, values, /* where= */ null, /* selectionArgs= */ null)
@@ -168,6 +140,5 @@ constructor(
        private const val ICON_THEMED = "icon_themed"
        private const val COL_ICON_THEMED_VALUE = "boolean_value"
        private const val ENABLED = 1
        private const val TAG = "ThemedIconRepositoryImpl"
    }
}
+11 −6
Original line number Diff line number Diff line
@@ -21,6 +21,9 @@ import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toDrawable
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow

@Singleton
class FakeShapeGridManager @Inject constructor() : ShapeGridManager {
@@ -28,29 +31,31 @@ class FakeShapeGridManager @Inject constructor() : ShapeGridManager {
    val gridOptionDrawable0: Drawable = Color.BLUE.toDrawable()
    val gridOptionDrawable1: Drawable = Color.GREEN.toDrawable()

    private var gridOptions: List<GridOptionModel> = DEFAULT_GRID_OPTION_LIST
    private var _gridOptions: MutableStateFlow<List<GridOptionModel>> =
        MutableStateFlow(DEFAULT_GRID_OPTION_LIST)

    private var shapeOptions: List<ShapeOptionModel> = DEFAULT_SHAPE_OPTION_LIST

    fun setGridOptions(gridOptions: List<GridOptionModel>) {
        this.gridOptions = gridOptions
        this._gridOptions.value = gridOptions
    }

    fun setShapeOptions(shapeOptions: List<ShapeOptionModel>) {
        this.shapeOptions = shapeOptions
    }

    override suspend fun getGridOptions(): List<GridOptionModel> = gridOptions
    override val gridOptions: Flow<List<GridOptionModel>> = _gridOptions.asStateFlow()

    override suspend fun getGridOptions(): List<GridOptionModel> = _gridOptions.value

    override suspend fun getShapeOptions(): List<ShapeOptionModel> = shapeOptions

    override fun applyGridOption(gridKey: String) {
        gridOptions = gridOptions.map { it.copy(isCurrent = it.key == gridKey) }
        _gridOptions.value = _gridOptions.value.map { it.copy(isCurrent = it.key == gridKey) }
    }

    override fun applyShapeOption(shapeKey: String): Int {
    override fun applyShapeOption(shapeKey: String) {
        shapeOptions = shapeOptions.map { it.copy(isCurrent = it.key == shapeKey) }
        return 0
    }

    override fun getGridOptionDrawable(iconId: Int): Drawable? {
Loading