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

Commit 48129894 authored by George Lin's avatar George Lin
Browse files

App icon view model

Create app icon view model to handle icon shape and themed icon
configuration.

Test: Unit tests
Bug: 402161932
Flag: com.android.systemui.shared.new_customization_picker_ui
Change-Id: Id00282f0ef352d4acaaa4641cfacbc8885ba4703
parent 06e5e2b5
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -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"
+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()
        }
}
+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)
}
+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
    }
}
+3 −0
Original line number Original line Diff line number Diff line
@@ -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,
@@ -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