Loading src/com/android/customization/model/grid/DefaultShapeGridManager.kt +1 −0 Original line number Original line Diff line number Diff line Loading @@ -181,6 +181,7 @@ constructor( const val SHAPE_OPTIONS: String = "shape_options" const val SHAPE_OPTIONS: String = "shape_options" const val GRID_OPTIONS: String = "list_options" const val GRID_OPTIONS: String = "list_options" const val SHAPE_GRID: String = "default_grid" const val SHAPE_GRID: String = "default_grid" const val SET_SHAPE: String = "set_shape" const val COL_SHAPE_KEY: String = "shape_key" const val COL_SHAPE_KEY: String = "shape_key" const val COL_GRID_KEY: String = "name" const val COL_GRID_KEY: String = "name" const val COL_GRID_NAME: String = "grid_name" const val COL_GRID_NAME: String = "grid_name" Loading src/com/android/customization/picker/grid/data/repository/ShapeRepository.kt 0 → 100644 +77 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.customization.picker.grid.data.repository import android.content.ContentValues import android.content.Context import com.android.customization.model.grid.DefaultShapeGridManager.Companion.COL_SHAPE_KEY import com.android.customization.model.grid.DefaultShapeGridManager.Companion.SET_SHAPE import com.android.customization.model.grid.ShapeGridManager import com.android.customization.model.grid.ShapeOptionModel import com.android.wallpaper.R 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.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Singleton class ShapeRepository @Inject 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) private val _shapeOptions = MutableStateFlow<List<ShapeOptionModel>?>(null) init { bgScope.launch { _shapeOptions.value = shapeGridManager.getShapeOptions() } } val shapeOptions: StateFlow<List<ShapeOptionModel>?> = _shapeOptions.asStateFlow() val selectedShapeOption: Flow<ShapeOptionModel?> = shapeOptions.map { shapeOptions -> shapeOptions?.firstOrNull { it.isCurrent } } suspend fun applyShape(shapeKey: String) = withContext(bgDispatcher) { context.contentResolver.update( previewUtils.getUri(SET_SHAPE), ContentValues().apply { put(COL_SHAPE_KEY, shapeKey) }, null, null, ) // After applying, we should query and update shape and grid options again. _shapeOptions.value = shapeGridManager.getShapeOptions() } } src/com/android/customization/picker/grid/domain/interactor/AppIconInteractor.kt 0 → 100644 +46 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.customization.picker.grid.domain.interactor import com.android.customization.picker.grid.data.repository.ShapeRepository import com.android.customization.picker.themedicon.data.repository.ThemedIconRepository import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow @Singleton class AppIconInteractor @Inject constructor( private val shapeRepository: ShapeRepository, private val themedIconRepository: ThemedIconRepository, ) { val shapeOptions = shapeRepository.shapeOptions val selectedShapeOption = shapeRepository.selectedShapeOption val isThemedIconAvailable: Flow<Boolean> = themedIconRepository.isAvailable val isThemedIconEnabled: Flow<Boolean> = themedIconRepository.isActivated suspend fun applyThemedIconEnabled(enabled: Boolean) = themedIconRepository.setThemedIconEnabled(enabled) suspend fun applyShape(shapeKey: String) = shapeRepository.applyShape(shapeKey) } src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModel.kt 0 → 100644 +152 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.customization.ui.viewmodel import com.android.customization.model.grid.ShapeOptionModel import com.android.customization.picker.grid.domain.interactor.AppIconInteractor import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel import com.android.wallpaper.picker.common.text.ui.viewmodel.Text import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn class AppIconPickerViewModel @AssistedInject constructor(interactor: AppIconInteractor, @Assisted private val viewModelScope: CoroutineScope) { //// Shape // The currently-set system shape option val selectedShapeKey = interactor.selectedShapeOption .filterNotNull() .map { it.key } .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1) private val overridingShapeKey = MutableStateFlow<String?>(null) // If the overriding key is null, use the currently-set system shape option val previewingShapeKey = combine(overridingShapeKey, selectedShapeKey) { overridingShapeOptionKey, selectedShapeKey -> overridingShapeOptionKey ?: selectedShapeKey } val shapeOptions: Flow<List<OptionItemViewModel2<ShapeIconViewModel>>> = interactor.shapeOptions .filterNotNull() .map { shapeOptions -> shapeOptions.map { toShapeOptionItemViewModel(it) } } .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1) //// Themed icons enabled val isThemedIconAvailable = interactor.isThemedIconAvailable.shareIn( scope = viewModelScope, started = SharingStarted.Lazily, replay = 1, ) private val overridingIsThemedIconEnabled = MutableStateFlow<Boolean?>(null) val isThemedIconEnabled = interactor.isThemedIconEnabled.shareIn( scope = viewModelScope, started = SharingStarted.Lazily, replay = 1, ) val previewingIsThemeIconEnabled = combine(overridingIsThemedIconEnabled, isThemedIconEnabled) { overridingIsThemeIconEnabled, isThemeIconEnabled -> overridingIsThemeIconEnabled ?: isThemeIconEnabled } val toggleThemedIcon: Flow<suspend () -> Unit> = previewingIsThemeIconEnabled.map { { val newValue = !it overridingIsThemedIconEnabled.value = newValue } } val onApply: Flow<(suspend () -> Unit)?> = combine( overridingShapeKey, selectedShapeKey, overridingIsThemedIconEnabled, isThemedIconEnabled, ) { overridingShapeKey, selectedShapeKey, overridingIsThemeIconEnabled, isThemeIconEnabled -> if ( (overridingShapeKey != null && overridingShapeKey != selectedShapeKey) || (overridingIsThemeIconEnabled != null && overridingIsThemeIconEnabled != isThemeIconEnabled) ) { { overridingShapeKey?.let { interactor.applyShape(it) } overridingIsThemeIconEnabled?.let { interactor.applyThemedIconEnabled(it) } } } else { null } } fun resetPreview() { overridingShapeKey.value = null overridingIsThemedIconEnabled.value = null } private fun toShapeOptionItemViewModel( option: ShapeOptionModel ): OptionItemViewModel2<ShapeIconViewModel> { val isSelected = previewingShapeKey .map { it == option.key } .stateIn( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = false, ) return OptionItemViewModel2( key = MutableStateFlow(option.key), payload = ShapeIconViewModel(option.key, option.path), text = Text.Loaded(option.title), isSelected = isSelected, onClicked = isSelected.map { if (!it) { { overridingShapeKey.value = option.key } } else { null } }, ) } @ViewModelScoped @AssistedFactory interface Factory { fun create(viewModelScope: CoroutineScope): AppIconPickerViewModel } } src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -47,6 +47,7 @@ constructor( colorPickerViewModel2Factory: ColorPickerViewModel2.Factory, colorPickerViewModel2Factory: ColorPickerViewModel2.Factory, clockPickerViewModelFactory: ClockPickerViewModel.Factory, clockPickerViewModelFactory: ClockPickerViewModel.Factory, shapeGridPickerViewModelFactory: ShapeGridPickerViewModel.Factory, shapeGridPickerViewModelFactory: ShapeGridPickerViewModel.Factory, appIconPickerViewModelFactory: AppIconPickerViewModel.Factory, val colorContrastSectionViewModel: ColorContrastSectionViewModel2, val colorContrastSectionViewModel: ColorContrastSectionViewModel2, val darkModeViewModel: DarkModeViewModel, val darkModeViewModel: DarkModeViewModel, val themedIconViewModel: ThemedIconViewModel, val themedIconViewModel: ThemedIconViewModel, Loading @@ -65,6 +66,8 @@ constructor( val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope) val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope) val shapeGridPickerViewModel = val shapeGridPickerViewModel = shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) val appIconPickerViewModel = appIconPickerViewModelFactory.create(viewModelScope = viewModelScope) private var onApplyJob: Job? = null private var onApplyJob: Job? = null Loading Loading
src/com/android/customization/model/grid/DefaultShapeGridManager.kt +1 −0 Original line number Original line Diff line number Diff line Loading @@ -181,6 +181,7 @@ constructor( const val SHAPE_OPTIONS: String = "shape_options" const val SHAPE_OPTIONS: String = "shape_options" const val GRID_OPTIONS: String = "list_options" const val GRID_OPTIONS: String = "list_options" const val SHAPE_GRID: String = "default_grid" const val SHAPE_GRID: String = "default_grid" const val SET_SHAPE: String = "set_shape" const val COL_SHAPE_KEY: String = "shape_key" const val COL_SHAPE_KEY: String = "shape_key" const val COL_GRID_KEY: String = "name" const val COL_GRID_KEY: String = "name" const val COL_GRID_NAME: String = "grid_name" const val COL_GRID_NAME: String = "grid_name" Loading
src/com/android/customization/picker/grid/data/repository/ShapeRepository.kt 0 → 100644 +77 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.customization.picker.grid.data.repository import android.content.ContentValues import android.content.Context import com.android.customization.model.grid.DefaultShapeGridManager.Companion.COL_SHAPE_KEY import com.android.customization.model.grid.DefaultShapeGridManager.Companion.SET_SHAPE import com.android.customization.model.grid.ShapeGridManager import com.android.customization.model.grid.ShapeOptionModel import com.android.wallpaper.R 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.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @Singleton class ShapeRepository @Inject 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) private val _shapeOptions = MutableStateFlow<List<ShapeOptionModel>?>(null) init { bgScope.launch { _shapeOptions.value = shapeGridManager.getShapeOptions() } } val shapeOptions: StateFlow<List<ShapeOptionModel>?> = _shapeOptions.asStateFlow() val selectedShapeOption: Flow<ShapeOptionModel?> = shapeOptions.map { shapeOptions -> shapeOptions?.firstOrNull { it.isCurrent } } suspend fun applyShape(shapeKey: String) = withContext(bgDispatcher) { context.contentResolver.update( previewUtils.getUri(SET_SHAPE), ContentValues().apply { put(COL_SHAPE_KEY, shapeKey) }, null, null, ) // After applying, we should query and update shape and grid options again. _shapeOptions.value = shapeGridManager.getShapeOptions() } }
src/com/android/customization/picker/grid/domain/interactor/AppIconInteractor.kt 0 → 100644 +46 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.android.customization.picker.grid.domain.interactor import com.android.customization.picker.grid.data.repository.ShapeRepository import com.android.customization.picker.themedicon.data.repository.ThemedIconRepository import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.Flow @Singleton class AppIconInteractor @Inject constructor( private val shapeRepository: ShapeRepository, private val themedIconRepository: ThemedIconRepository, ) { val shapeOptions = shapeRepository.shapeOptions val selectedShapeOption = shapeRepository.selectedShapeOption val isThemedIconAvailable: Flow<Boolean> = themedIconRepository.isAvailable val isThemedIconEnabled: Flow<Boolean> = themedIconRepository.isActivated suspend fun applyThemedIconEnabled(enabled: Boolean) = themedIconRepository.setThemedIconEnabled(enabled) suspend fun applyShape(shapeKey: String) = shapeRepository.applyShape(shapeKey) }
src/com/android/wallpaper/customization/ui/viewmodel/AppIconPickerViewModel.kt 0 → 100644 +152 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.wallpaper.customization.ui.viewmodel import com.android.customization.model.grid.ShapeOptionModel import com.android.customization.picker.grid.domain.interactor.AppIconInteractor import com.android.customization.picker.grid.ui.viewmodel.ShapeIconViewModel import com.android.wallpaper.picker.common.text.ui.viewmodel.Text import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel2 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn class AppIconPickerViewModel @AssistedInject constructor(interactor: AppIconInteractor, @Assisted private val viewModelScope: CoroutineScope) { //// Shape // The currently-set system shape option val selectedShapeKey = interactor.selectedShapeOption .filterNotNull() .map { it.key } .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1) private val overridingShapeKey = MutableStateFlow<String?>(null) // If the overriding key is null, use the currently-set system shape option val previewingShapeKey = combine(overridingShapeKey, selectedShapeKey) { overridingShapeOptionKey, selectedShapeKey -> overridingShapeOptionKey ?: selectedShapeKey } val shapeOptions: Flow<List<OptionItemViewModel2<ShapeIconViewModel>>> = interactor.shapeOptions .filterNotNull() .map { shapeOptions -> shapeOptions.map { toShapeOptionItemViewModel(it) } } .shareIn(scope = viewModelScope, started = SharingStarted.Lazily, replay = 1) //// Themed icons enabled val isThemedIconAvailable = interactor.isThemedIconAvailable.shareIn( scope = viewModelScope, started = SharingStarted.Lazily, replay = 1, ) private val overridingIsThemedIconEnabled = MutableStateFlow<Boolean?>(null) val isThemedIconEnabled = interactor.isThemedIconEnabled.shareIn( scope = viewModelScope, started = SharingStarted.Lazily, replay = 1, ) val previewingIsThemeIconEnabled = combine(overridingIsThemedIconEnabled, isThemedIconEnabled) { overridingIsThemeIconEnabled, isThemeIconEnabled -> overridingIsThemeIconEnabled ?: isThemeIconEnabled } val toggleThemedIcon: Flow<suspend () -> Unit> = previewingIsThemeIconEnabled.map { { val newValue = !it overridingIsThemedIconEnabled.value = newValue } } val onApply: Flow<(suspend () -> Unit)?> = combine( overridingShapeKey, selectedShapeKey, overridingIsThemedIconEnabled, isThemedIconEnabled, ) { overridingShapeKey, selectedShapeKey, overridingIsThemeIconEnabled, isThemeIconEnabled -> if ( (overridingShapeKey != null && overridingShapeKey != selectedShapeKey) || (overridingIsThemeIconEnabled != null && overridingIsThemeIconEnabled != isThemeIconEnabled) ) { { overridingShapeKey?.let { interactor.applyShape(it) } overridingIsThemeIconEnabled?.let { interactor.applyThemedIconEnabled(it) } } } else { null } } fun resetPreview() { overridingShapeKey.value = null overridingIsThemedIconEnabled.value = null } private fun toShapeOptionItemViewModel( option: ShapeOptionModel ): OptionItemViewModel2<ShapeIconViewModel> { val isSelected = previewingShapeKey .map { it == option.key } .stateIn( scope = viewModelScope, started = SharingStarted.Lazily, initialValue = false, ) return OptionItemViewModel2( key = MutableStateFlow(option.key), payload = ShapeIconViewModel(option.key, option.path), text = Text.Loaded(option.title), isSelected = isSelected, onClicked = isSelected.map { if (!it) { { overridingShapeKey.value = option.key } } else { null } }, ) } @ViewModelScoped @AssistedFactory interface Factory { fun create(viewModelScope: CoroutineScope): AppIconPickerViewModel } }
src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt +3 −0 Original line number Original line Diff line number Diff line Loading @@ -47,6 +47,7 @@ constructor( colorPickerViewModel2Factory: ColorPickerViewModel2.Factory, colorPickerViewModel2Factory: ColorPickerViewModel2.Factory, clockPickerViewModelFactory: ClockPickerViewModel.Factory, clockPickerViewModelFactory: ClockPickerViewModel.Factory, shapeGridPickerViewModelFactory: ShapeGridPickerViewModel.Factory, shapeGridPickerViewModelFactory: ShapeGridPickerViewModel.Factory, appIconPickerViewModelFactory: AppIconPickerViewModel.Factory, val colorContrastSectionViewModel: ColorContrastSectionViewModel2, val colorContrastSectionViewModel: ColorContrastSectionViewModel2, val darkModeViewModel: DarkModeViewModel, val darkModeViewModel: DarkModeViewModel, val themedIconViewModel: ThemedIconViewModel, val themedIconViewModel: ThemedIconViewModel, Loading @@ -65,6 +66,8 @@ constructor( val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope) val colorPickerViewModel2 = colorPickerViewModel2Factory.create(viewModelScope = viewModelScope) val shapeGridPickerViewModel = val shapeGridPickerViewModel = shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) shapeGridPickerViewModelFactory.create(viewModelScope = viewModelScope) val appIconPickerViewModel = appIconPickerViewModelFactory.create(viewModelScope = viewModelScope) private var onApplyJob: Job? = null private var onApplyJob: Job? = null Loading