Loading src/com/android/customization/model/grid/DefaultShapeGridManager.kt +142 −90 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -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. Loading @@ -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) } } } Loading @@ -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 -> Loading Loading @@ -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 = Loading src/com/android/customization/model/grid/ShapeGridManager.kt +9 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.customization.model.grid import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.Flow interface ShapeGridManager { /** Loading @@ -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? } src/com/android/customization/picker/grid/data/repository/GridRepository2.kt +3 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading src/com/android/customization/picker/themedicon/data/repository/ThemedIconRepositoryImpl.kt +44 −73 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -122,10 +96,9 @@ constructor( contentResolver.unregisterContentObserver(contentObserver) } } } } awaitClose { disposableHandle?.dispose() } } } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), Loading Loading @@ -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) Loading @@ -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" } } tests/common/src/com/android/customization/model/grid/FakeShapeGridManager.kt +11 −6 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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 Loading
src/com/android/customization/model/grid/DefaultShapeGridManager.kt +142 −90 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 { Loading @@ -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. Loading @@ -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) } } } Loading @@ -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 -> Loading Loading @@ -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 = Loading
src/com/android/customization/model/grid/ShapeGridManager.kt +9 −5 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.customization.model.grid import android.graphics.drawable.Drawable import kotlinx.coroutines.flow.Flow interface ShapeGridManager { /** Loading @@ -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? }
src/com/android/customization/picker/grid/data/repository/GridRepository2.kt +3 −19 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) Loading
src/com/android/customization/picker/themedicon/data/repository/ThemedIconRepositoryImpl.kt +44 −73 Original line number Diff line number Diff line Loading @@ -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) { Loading @@ -122,10 +96,9 @@ constructor( contentResolver.unregisterContentObserver(contentObserver) } } } } awaitClose { disposableHandle?.dispose() } } } .stateIn( scope = backgroundScope, started = SharingStarted.WhileSubscribed(), Loading Loading @@ -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) Loading @@ -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" } }
tests/common/src/com/android/customization/model/grid/FakeShapeGridManager.kt +11 −6 Original line number Diff line number Diff line Loading @@ -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 { Loading @@ -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