Color Picker Fragment Selection Panel (1/2)
Implemented the bottom color selection panel within the new color fragment using clean architecture
Bug: 262924623
Test: Manual Verified, screen recording - https://drive.google.com/file/d/1vozlF_PoasDEectXdVvyk-6rGH5pvXDV/view?usp=sharing&resourcekey=0-EUDWLiHyC9W7m2jmUAF4RA
Change-Id: Ib899a10baa24a882814396f77886747214591a71
diff --git a/res/drawable/color_option_section_selected.xml b/res/drawable/color_option_selected_no_background.xml
similarity index 100%
rename from res/drawable/color_option_section_selected.xml
rename to res/drawable/color_option_selected_no_background.xml
diff --git a/res/layout/color_option_section.xml b/res/layout/color_option_no_background.xml
similarity index 98%
rename from res/layout/color_option_section.xml
rename to res/layout/color_option_no_background.xml
index d10f8e5..b90b480 100644
--- a/res/layout/color_option_section.xml
+++ b/res/layout/color_option_no_background.xml
@@ -97,6 +97,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
- android:src="@drawable/color_option_section_selected"
+ android:src="@drawable/color_option_selected_no_background"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/color_option_section_overflow.xml b/res/layout/color_option_overflow_no_background.xml
similarity index 100%
rename from res/layout/color_option_section_overflow.xml
rename to res/layout/color_option_overflow_no_background.xml
diff --git a/res/layout/color_option_with_background.xml b/res/layout/color_option_with_background.xml
new file mode 100644
index 0000000..9d3be58
--- /dev/null
+++ b/res/layout/color_option_with_background.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2023 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.
+-->
+<!-- Content description is set programmatically on the parent FrameLayout -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/icon_container"
+ android:layout_width="@dimen/option_tile_width"
+ android:layout_height="@dimen/option_tile_width"
+ android:importantForAccessibility="yes">
+
+ <ImageView
+ android:id="@id/selection_border"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/option_item_border"
+ android:alpha="0"
+ android:importantForAccessibility="no" />
+
+ <ImageView
+ android:id="@id/background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@drawable/option_item_background"
+ android:importantForAccessibility="no" />
+
+ <ImageView
+ android:id="@+id/color_preview_0"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginRight="@dimen/color_seed_chip_margin"
+ android:layout_marginBottom="@dimen/color_seed_chip_margin"
+ android:src="@drawable/color_chip_seed_filled0"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/color_preview_1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginLeft="@dimen/color_seed_chip_margin"
+ android:layout_marginBottom="@dimen/color_seed_chip_margin"
+ android:src="@drawable/color_chip_seed_filled2"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/color_preview_2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginRight="@dimen/color_seed_chip_margin"
+ android:layout_marginTop="@dimen/color_seed_chip_margin"
+ android:src="@drawable/color_chip_seed_filled1"
+ android:importantForAccessibility="no"/>
+
+ <ImageView
+ android:id="@+id/color_preview_3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:layout_marginLeft="@dimen/color_seed_chip_margin"
+ android:layout_marginTop="@dimen/color_seed_chip_margin"
+ android:src="@drawable/color_chip_seed_filled3"
+ android:importantForAccessibility="no"/>
+</FrameLayout>
diff --git a/res/layout/color_section_view2.xml b/res/layout/color_section_view2.xml
index 1811641..0a3fc7f 100644
--- a/res/layout/color_section_view2.xml
+++ b/res/layout/color_section_view2.xml
@@ -14,7 +14,7 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<com.android.customization.picker.color.ColorSectionView2
+<com.android.customization.picker.color.ui.view.ColorSectionView2
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/color_section_option_container"
android:layout_width="match_parent"
@@ -39,7 +39,7 @@
<include
android:layout_width="0dp"
android:layout_height="wrap_content"
- layout="@layout/color_option_section_overflow"
+ layout="@layout/color_option_overflow_no_background"
android:visibility="invisible"
android:layout_weight="1"/>
-</com.android.customization.picker.color.ColorSectionView2>
+</com.android.customization.picker.color.ui.view.ColorSectionView2>
diff --git a/res/layout/fragment_color_picker.xml b/res/layout/fragment_color_picker.xml
index d91ac5e..bf0c414 100644
--- a/res/layout/fragment_color_picker.xml
+++ b/res/layout/fragment_color_picker.xml
@@ -17,7 +17,6 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@@ -94,13 +93,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content">
- <androidx.viewpager2.widget.ViewPager2
- android:id="@+id/color_section_view_pager"
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/color_options"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/color_options_container_top_margin"
- android:clipChildren="false"
- android:clipToPadding="false"/>
+ android:clipToPadding="false"
+ android:paddingHorizontal="16dp" />
<!--
This is just an invisible placeholder put in place so that the parent keeps its height
@@ -112,7 +110,7 @@
It's critical for any TextViews inside the included layout to have text.
-->
<include
- layout="@layout/color_option"
+ layout="@layout/color_option_with_background"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible" />
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 83367d4..f24ea28 100755
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -285,6 +285,12 @@
<!-- Title of a section of color selection option that obtains colors automatically from the
wallpaper instead of a set color [CHAR LIMIT=15] -->
<string name="adaptive_color_title">Dynamic</string>
+ <!--
+ Title for a screen where the user can configure the system colors by selecting from a list of
+ color options.
+ [CHAR LIMIT=32].
+ -->
+ <string name="color_picker_title">System Colors</string>
<!--
Name of the slot on the "start" side of the bottom of the lock screen, where lock screen
diff --git a/src/com/android/customization/model/color/ColorOption.java b/src/com/android/customization/model/color/ColorOption.java
index c8b28c2..26e025d 100644
--- a/src/com/android/customization/model/color/ColorOption.java
+++ b/src/com/android/customization/model/color/ColorOption.java
@@ -182,7 +182,8 @@
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
- protected CharSequence getContentDescription(Context context) {
+ /** */
+ public CharSequence getContentDescription(Context context) {
if (mContentDescription == null) {
CharSequence defaultName = context.getString(R.string.default_theme_title);
if (isDefault()) {
diff --git a/src/com/android/customization/model/color/ColorSectionController.java b/src/com/android/customization/model/color/ColorSectionController.java
index 3b8a927..2a067cb 100644
--- a/src/com/android/customization/model/color/ColorSectionController.java
+++ b/src/com/android/customization/model/color/ColorSectionController.java
@@ -174,13 +174,13 @@
// TODO(b/202145216): Use just 2 views when tapping either button on top.
mTabLayout.setViewPager(mColorSectionViewPager);
- mWallpaperColorsViewModel.getHomeWallpaperColors().observe(mLifecycleOwner,
+ mWallpaperColorsViewModel.getHomeWallpaperColorsLiveData().observe(mLifecycleOwner,
homeColors -> {
mHomeWallpaperColors = homeColors;
mHomeWallpaperColorsReady = true;
maybeLoadColors();
});
- mWallpaperColorsViewModel.getLockWallpaperColors().observe(mLifecycleOwner,
+ mWallpaperColorsViewModel.getLockWallpaperColorsLiveData().observe(mLifecycleOwner,
lockColors -> {
mLockWallpaperColors = lockColors;
mLockWallpaperColorsReady = true;
diff --git a/src/com/android/customization/model/color/ColorSectionController2.java b/src/com/android/customization/model/color/ColorSectionController2.java
index 257bb94..791a9a0 100644
--- a/src/com/android/customization/model/color/ColorSectionController2.java
+++ b/src/com/android/customization/model/color/ColorSectionController2.java
@@ -36,8 +36,8 @@
import com.android.customization.model.theme.OverlayManagerCompat;
import com.android.customization.module.CustomizationInjector;
import com.android.customization.module.ThemesUserEventLogger;
-import com.android.customization.picker.color.ColorPickerFragment;
-import com.android.customization.picker.color.ColorSectionView2;
+import com.android.customization.picker.color.ui.fragment.ColorPickerFragment;
+import com.android.customization.picker.color.ui.view.ColorSectionView2;
import com.android.wallpaper.R;
import com.android.wallpaper.model.CustomizationSectionController;
import com.android.wallpaper.model.WallpaperColorsViewModel;
@@ -51,6 +51,8 @@
/**
* Color section view's controller for the logic of color customization.
+ *
+ * TODO (b/262924584): Convert ColorSectionController2 into Kotlin & use new architecture
*/
public class ColorSectionController2 implements CustomizationSectionController<ColorSectionView2> {
@@ -97,13 +99,13 @@
mColorSectionView = (ColorSectionView2) LayoutInflater.from(context).inflate(
R.layout.color_section_view2, /* root= */ null);
- mWallpaperColorsViewModel.getHomeWallpaperColors().observe(mLifecycleOwner,
+ mWallpaperColorsViewModel.getHomeWallpaperColorsLiveData().observe(mLifecycleOwner,
homeColors -> {
mHomeWallpaperColors = homeColors;
mHomeWallpaperColorsReady = true;
maybeLoadColors();
});
- mWallpaperColorsViewModel.getLockWallpaperColors().observe(mLifecycleOwner,
+ mWallpaperColorsViewModel.getLockWallpaperColorsLiveData().observe(mLifecycleOwner,
lockColors -> {
mLockWallpaperColors = lockColors;
mLockWallpaperColorsReady = true;
diff --git a/src/com/android/customization/model/color/ColorSeedOption.java b/src/com/android/customization/model/color/ColorSeedOption.java
index 53d3954..ba61ed1 100644
--- a/src/com/android/customization/model/color/ColorSeedOption.java
+++ b/src/com/android/customization/model/color/ColorSeedOption.java
@@ -80,7 +80,7 @@
}
@Override
- protected CharSequence getContentDescription(Context context) {
+ public CharSequence getContentDescription(Context context) {
// Override because we want all options with the same description.
return context.getString(R.string.wallpaper_color_title);
}
diff --git a/src/com/android/customization/module/CustomizationInjector.kt b/src/com/android/customization/module/CustomizationInjector.kt
index 6194e11..1f0507e 100644
--- a/src/com/android/customization/module/CustomizationInjector.kt
+++ b/src/com/android/customization/module/CustomizationInjector.kt
@@ -23,8 +23,11 @@
import com.android.customization.picker.clock.data.repository.ClockRegistryProvider
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
import com.android.systemui.shared.clocks.ClockRegistry
+import com.android.wallpaper.model.WallpaperColorsViewModel
import com.android.wallpaper.module.Injector
interface CustomizationInjector : Injector {
@@ -52,4 +55,14 @@
context: Context,
clockRegistry: ClockRegistry
): ClockSectionViewModel
+
+ fun getColorPickerInteractor(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerInteractor
+
+ fun getColorPickerViewModelFactory(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerViewModel.Factory
}
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 9723326..581c233 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -29,6 +29,9 @@
import com.android.customization.picker.clock.data.repository.ClockRegistryProvider
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
import com.android.customization.picker.notifications.data.repository.NotificationsRepository
import com.android.customization.picker.notifications.domain.interactor.NotificationsInteractor
import com.android.customization.picker.notifications.ui.viewmodel.NotificationSectionViewModel
@@ -40,6 +43,7 @@
import com.android.systemui.shared.customization.data.content.CustomizationProviderClient
import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl
import com.android.wallpaper.model.LiveWallpaperInfo
+import com.android.wallpaper.model.WallpaperColorsViewModel
import com.android.wallpaper.model.WallpaperInfo
import com.android.wallpaper.module.CustomizationSections
import com.android.wallpaper.module.FragmentFactory
@@ -72,6 +76,8 @@
private var clockSectionViewModel: ClockSectionViewModel? = null
private var notificationsInteractor: NotificationsInteractor? = null
private var notificationSectionViewModelFactory: NotificationSectionViewModel.Factory? = null
+ private var colorPickerInteractor: ColorPickerInteractor? = null
+ private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
return customizationSections
@@ -259,6 +265,27 @@
.also { notificationsInteractor = it }
}
+ override fun getColorPickerInteractor(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerInteractor {
+ return colorPickerInteractor
+ ?: ColorPickerInteractor(ColorPickerRepositoryImpl(context, wallpaperColorsViewModel))
+ .also { colorPickerInteractor = it }
+ }
+
+ override fun getColorPickerViewModelFactory(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerViewModel.Factory {
+ return colorPickerViewModelFactory
+ ?: ColorPickerViewModel.Factory(
+ context,
+ getColorPickerInteractor(context, wallpaperColorsViewModel),
+ )
+ .also { colorPickerViewModelFactory = it }
+ }
+
companion object {
@JvmStatic
private val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER =
diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt
new file mode 100644
index 0000000..c375574
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.color.data.repository
+
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Abstracts access to application state related to functionality for selecting, picking, or setting
+ * system color.
+ */
+interface ColorPickerRepository {
+ val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>>
+
+ fun select(colorOptionModel: ColorOptionModel)
+}
diff --git a/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
new file mode 100644
index 0000000..e163c15
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/ColorPickerRepositoryImpl.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2023 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.color.data.repository
+
+import android.app.WallpaperColors
+import android.content.Context
+import android.util.Log
+import com.android.customization.model.CustomizationManager
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorCustomizationManager
+import com.android.customization.model.color.ColorOption
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.model.theme.OverlayManagerCompat
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.model.WallpaperColorsViewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.combine
+
+// TODO (b/262924623): refactor to remove dependency on ColorCustomizationManager & ColorOption
+class ColorPickerRepositoryImpl(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+) : ColorPickerRepository {
+
+ private val homeWallpaperColors: StateFlow<WallpaperColors?> =
+ wallpaperColorsViewModel.homeWallpaperColors
+ private val lockWallpaperColors: StateFlow<WallpaperColors?> =
+ wallpaperColorsViewModel.lockWallpaperColors
+ private val colorManager: ColorCustomizationManager =
+ ColorCustomizationManager.getInstance(context, OverlayManagerCompat(context))
+
+ /** List of wallpaper and preset color options on the device, categorized by Color Type */
+ override val colorOptions: Flow<Map<ColorType, List<ColorOptionModel>>> =
+ combine(homeWallpaperColors, lockWallpaperColors) { homeColors, lockColors ->
+ colorManager.setWallpaperColors(homeColors, lockColors)
+ val wallpaperColorOptions: MutableList<ColorOptionModel> = mutableListOf()
+ val presetColorOptions: MutableList<ColorOptionModel> = mutableListOf()
+ colorManager.fetchOptions(
+ object : CustomizationManager.OptionsFetchedListener<ColorOption?> {
+ override fun onOptionsLoaded(options: MutableList<ColorOption?>?) {
+ options?.forEach { option ->
+ when (option) {
+ is ColorSeedOption -> wallpaperColorOptions.add(option.toModel())
+ is ColorBundle -> presetColorOptions.add(option.toModel())
+ }
+ }
+ }
+
+ override fun onError(throwable: Throwable?) {
+ Log.e("ColorPickerRepository", "Error loading theme bundles", throwable)
+ }
+ },
+ /* reload= */ false
+ )
+ mapOf(
+ ColorType.WALLPAPER_COLOR to wallpaperColorOptions,
+ ColorType.BASIC_COLOR to presetColorOptions
+ )
+ }
+
+ override fun select(colorOptionModel: ColorOptionModel) {
+ val colorOption: ColorOption = colorOptionModel.colorOption
+ colorManager.apply(
+ colorOption,
+ object : CustomizationManager.Callback {
+ override fun onSuccess() = Unit
+
+ override fun onError(throwable: Throwable?) {
+ Log.w("ColorPickerRepository", "Apply theme with error", throwable)
+ }
+ }
+ )
+ }
+
+ private fun ColorOption.toModel(): ColorOptionModel {
+ return ColorOptionModel(
+ colorOption = this,
+ isSelected = isActive(colorManager),
+ )
+ }
+}
diff --git a/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
new file mode 100644
index 0000000..6d8b7dc
--- /dev/null
+++ b/src/com/android/customization/picker/color/data/repository/FakeColorPickerRepository.kt
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2023 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.color.data.repository
+
+import android.content.Context
+import android.graphics.Color
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+class FakeColorPickerRepository(context: Context) : ColorPickerRepository {
+ private val colorSeedOption0: ColorSeedOption =
+ ColorSeedOption.Builder()
+ .setLightColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setDarkColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setIndex(0)
+ .build()
+ private val colorSeedOption1: ColorSeedOption =
+ ColorSeedOption.Builder()
+ .setLightColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setDarkColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setIndex(1)
+ .build()
+ private val colorSeedOption2: ColorSeedOption =
+ ColorSeedOption.Builder()
+ .setLightColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setDarkColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setIndex(2)
+ .build()
+ private val colorSeedOption3: ColorSeedOption =
+ ColorSeedOption.Builder()
+ .setLightColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setDarkColors(
+ intArrayOf(
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT,
+ Color.TRANSPARENT
+ )
+ )
+ .setIndex(3)
+ .build()
+ private val colorBundle0: ColorBundle = ColorBundle.Builder().setIndex(0).build(context)
+ private val colorBundle1: ColorBundle = ColorBundle.Builder().setIndex(1).build(context)
+ private val colorBundle2: ColorBundle = ColorBundle.Builder().setIndex(2).build(context)
+ private val colorBundle3: ColorBundle = ColorBundle.Builder().setIndex(3).build(context)
+
+ private val _colorOptions =
+ MutableStateFlow(
+ mapOf(
+ ColorType.WALLPAPER_COLOR to
+ listOf(
+ ColorOptionModel(colorOption = colorSeedOption0, isSelected = true),
+ ColorOptionModel(colorOption = colorSeedOption1, isSelected = false),
+ ColorOptionModel(colorOption = colorSeedOption2, isSelected = false),
+ ColorOptionModel(colorOption = colorSeedOption3, isSelected = false)
+ ),
+ ColorType.BASIC_COLOR to
+ listOf(
+ ColorOptionModel(colorOption = colorBundle0, isSelected = false),
+ ColorOptionModel(colorOption = colorBundle1, isSelected = false),
+ ColorOptionModel(colorOption = colorBundle2, isSelected = false),
+ ColorOptionModel(colorOption = colorBundle3, isSelected = false)
+ )
+ )
+ )
+ override val colorOptions: StateFlow<Map<ColorType, List<ColorOptionModel>>> =
+ _colorOptions.asStateFlow()
+
+ override fun select(colorOptionModel: ColorOptionModel) {
+ val colorOptions = _colorOptions.value
+ colorOptions[ColorType.WALLPAPER_COLOR]?.forEach {
+ it.isSelected = (it.testEquals(colorOptionModel))
+ }
+ colorOptions[ColorType.BASIC_COLOR]?.forEach {
+ it.isSelected = (it.testEquals(colorOptionModel))
+ }
+ _colorOptions.value = colorOptions
+ }
+
+ private fun ColorOptionModel.testEquals(other: Any?): Boolean {
+ if (other == null) {
+ return false
+ }
+ return if (other is ColorOptionModel) {
+ val thisColorOptionIsWallpaperColor = this.colorOption is ColorSeedOption
+ val otherColorOptionIsWallpaperColor = other.colorOption is ColorSeedOption
+ (thisColorOptionIsWallpaperColor == otherColorOptionIsWallpaperColor) &&
+ (this.colorOption.index == other.colorOption.index)
+ } else {
+ false
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
new file mode 100644
index 0000000..ce453c3
--- /dev/null
+++ b/src/com/android/customization/picker/color/domain/interactor/ColorPickerInteractor.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2023 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.color.domain.interactor
+
+import com.android.customization.picker.color.data.repository.ColorPickerRepository
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+
+/** Single entry-point for all application state and business logic related to system color. */
+class ColorPickerInteractor(
+ private val repository: ColorPickerRepository,
+) {
+ /** List of wallpaper and preset color options on the device, categorized by Color Type */
+ val colorOptions = repository.colorOptions
+
+ fun select(colorOptionModel: ColorOptionModel) {
+ repository.select(colorOptionModel)
+ }
+}
diff --git a/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt b/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
new file mode 100644
index 0000000..69ef62a
--- /dev/null
+++ b/src/com/android/customization/picker/color/shared/model/ColorOptionModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 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.color.shared.model
+
+import com.android.customization.model.color.ColorOption
+
+/** Models application state for a color option in a picker experience. */
+data class ColorOptionModel(
+ /** Colors for the color option. */
+ val colorOption: ColorOption,
+
+ /** Whether this color option is selected. */
+ var isSelected: Boolean,
+)
diff --git a/src/com/android/customization/picker/color/shared/model/ColorType.kt b/src/com/android/customization/picker/color/shared/model/ColorType.kt
new file mode 100644
index 0000000..631bb3d
--- /dev/null
+++ b/src/com/android/customization/picker/color/shared/model/ColorType.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2023 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.color.shared.model
+
+enum class ColorType {
+ /** Colors generated based on the current wallpaper */
+ WALLPAPER_COLOR,
+
+ /** Preset colors */
+ BASIC_COLOR,
+}
diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
new file mode 100644
index 0000000..791811d
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/adapter/ColorOptionAdapter.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2023 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.color.ui.adapter
+
+import android.graphics.BlendMode
+import android.graphics.BlendModeColorFilter
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.wallpaper.R
+
+/**
+ * Adapts between color option items and views.
+ *
+ * TODO (b/262924623): Refactor color picker with animated option framework ag/21132368
+ */
+class ColorOptionAdapter : RecyclerView.Adapter<ColorOptionAdapter.ViewHolder>() {
+
+ private val items = mutableListOf<ColorOptionViewModel>()
+
+ fun setItems(items: List<ColorOptionViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val borderView: View = itemView.requireViewById(R.id.selection_border)
+ val backgroundView: View = itemView.requireViewById(R.id.background)
+ val color0View: ImageView = itemView.requireViewById(R.id.color_preview_0)
+ val color1View: ImageView = itemView.requireViewById(R.id.color_preview_1)
+ val color2View: ImageView = itemView.requireViewById(R.id.color_preview_2)
+ val color3View: ImageView = itemView.requireViewById(R.id.color_preview_3)
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(
+ R.layout.color_option_with_background,
+ parent,
+ false,
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+
+ holder.itemView.setOnClickListener(
+ if (item.onClick != null) {
+ View.OnClickListener { item.onClick.invoke() }
+ } else {
+ null
+ }
+ )
+ if (item.isSelected) {
+ holder.borderView.alpha = 1f
+ holder.borderView.scaleX = 1f
+ holder.borderView.scaleY = 1f
+ holder.backgroundView.scaleX = 0.86f
+ holder.backgroundView.scaleY = 0.86f
+ } else {
+ holder.borderView.alpha = 0f
+ holder.backgroundView.scaleX = 1f
+ holder.backgroundView.scaleY = 1f
+ }
+ holder.color0View.drawable.colorFilter = BlendModeColorFilter(item.color0, BlendMode.SRC)
+ holder.color1View.drawable.colorFilter = BlendModeColorFilter(item.color1, BlendMode.SRC)
+ holder.color2View.drawable.colorFilter = BlendModeColorFilter(item.color2, BlendMode.SRC)
+ holder.color3View.drawable.colorFilter = BlendModeColorFilter(item.color3, BlendMode.SRC)
+ holder.itemView.contentDescription = item.contentDescription
+ }
+}
diff --git a/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt
new file mode 100644
index 0000000..8f1340a
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/adapter/ColorTypeTabAdapter.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 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.color.ui.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.viewmodel.ColorTypeViewModel
+import com.android.wallpaper.R
+
+/** Adapts between color type items and views. */
+class ColorTypeTabAdapter : RecyclerView.Adapter<ColorTypeTabAdapter.ViewHolder>() {
+
+ private val items = mutableListOf<ColorTypeViewModel>()
+
+ fun setItems(items: List<ColorTypeViewModel>) {
+ this.items.clear()
+ this.items.addAll(items)
+ notifyDataSetChanged()
+ }
+
+ override fun getItemCount(): Int {
+ return items.size
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(parent.context)
+ .inflate(
+ R.layout.picker_fragment_tab,
+ parent,
+ false,
+ )
+ )
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ val item = items[position]
+ holder.itemView.isSelected = item.isSelected
+ holder.textView.text = item.name
+ holder.textView.setOnClickListener(
+ if (item.onClick != null) {
+ View.OnClickListener { item.onClick.invoke() }
+ } else {
+ null
+ }
+ )
+ }
+
+ class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val textView: TextView = itemView.requireViewById(R.id.text)
+ }
+}
diff --git a/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
new file mode 100644
index 0000000..887fef0
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/binder/ColorPickerBinder.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2023 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.color.ui.binder
+
+import android.graphics.Rect
+import android.view.View
+import androidx.core.view.ViewCompat
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.customization.picker.color.ui.adapter.ColorOptionAdapter
+import com.android.customization.picker.color.ui.adapter.ColorTypeTabAdapter
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+object ColorPickerBinder {
+
+ /**
+ * Binds view with view-model for a color picker experience. The view should include a Recycler
+ * View for color type tabs with id [R.id.color_type_tabs] and a Recycler View for color options
+ * with id [R.id.color_options]
+ */
+ @JvmStatic
+ fun bind(
+ view: View,
+ viewModel: ColorPickerViewModel,
+ lifecycleOwner: LifecycleOwner,
+ ) {
+ val colorTypeTabView: RecyclerView = view.requireViewById(R.id.color_type_tabs)
+ val colorOptionContainerView: RecyclerView = view.requireViewById(R.id.color_options)
+
+ val colorTypeTabAdapter = ColorTypeTabAdapter()
+ colorTypeTabView.adapter = colorTypeTabAdapter
+ colorTypeTabView.layoutManager =
+ LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ colorTypeTabView.addItemDecoration(ItemSpacing())
+ val colorOptionAdapter = ColorOptionAdapter()
+ colorOptionContainerView.adapter = colorOptionAdapter
+ colorOptionContainerView.layoutManager =
+ LinearLayoutManager(view.context, RecyclerView.HORIZONTAL, false)
+ colorOptionContainerView.addItemDecoration(ItemSpacing())
+
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.colorTypes
+ .map { colorTypeById -> colorTypeById.values }
+ .collect { colorTypes -> colorTypeTabAdapter.setItems(colorTypes.toList()) }
+ }
+
+ launch {
+ viewModel.colorOptions.collect { colorOptions ->
+ colorOptionAdapter.setItems(colorOptions)
+ }
+ }
+ }
+ }
+ }
+
+ // TODO (b/262924623): Remove function and use common ItemSpacing after ag/20929223 is merged
+ private class ItemSpacing : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(outRect: Rect, itemPosition: Int, parent: RecyclerView) {
+ val addSpacingToStart = itemPosition > 0
+ val addSpacingToEnd = itemPosition < (parent.adapter?.itemCount ?: 0) - 1
+ val isRtl = parent.layoutManager?.layoutDirection == ViewCompat.LAYOUT_DIRECTION_RTL
+ val density = parent.context.resources.displayMetrics.density
+ if (!isRtl) {
+ outRect.left = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+ outRect.right = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+ } else {
+ outRect.left = if (addSpacingToEnd) ITEM_SPACING_DP.toPx(density) else 0
+ outRect.right = if (addSpacingToStart) ITEM_SPACING_DP.toPx(density) else 0
+ }
+ }
+
+ private fun Int.toPx(density: Float): Int {
+ return (this * density).toInt()
+ }
+
+ companion object {
+ private const val ITEM_SPACING_DP = 8
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt
new file mode 100644
index 0000000..9b707a3
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/fragment/ColorPickerFragment.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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.color.ui.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.get
+import com.android.customization.module.ThemePickerInjector
+import com.android.customization.picker.color.ui.binder.ColorPickerBinder
+import com.android.wallpaper.R
+import com.android.wallpaper.model.WallpaperColorsViewModel
+import com.android.wallpaper.module.InjectorProvider
+import com.android.wallpaper.picker.AppbarFragment
+
+class ColorPickerFragment : AppbarFragment() {
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view =
+ inflater.inflate(
+ R.layout.fragment_color_picker,
+ container,
+ false,
+ )
+ setUpToolbar(view)
+ val injector = InjectorProvider.getInjector() as ThemePickerInjector
+ val wcViewModel = ViewModelProvider(requireActivity())[WallpaperColorsViewModel::class.java]
+ ColorPickerBinder.bind(
+ view = view,
+ viewModel =
+ ViewModelProvider(
+ requireActivity(),
+ injector.getColorPickerViewModelFactory(
+ context = requireContext(),
+ wallpaperColorsViewModel = wcViewModel,
+ ),
+ )
+ .get(),
+ lifecycleOwner = this,
+ )
+ return view
+ }
+
+ override fun getDefaultTitle(): CharSequence {
+ return requireContext().getString(R.string.color_picker_title)
+ }
+}
diff --git a/src/com/android/customization/picker/color/ColorSectionView2.kt b/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
similarity index 94%
rename from src/com/android/customization/picker/color/ColorSectionView2.kt
rename to src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
index 3e4e4bc..358514e 100644
--- a/src/com/android/customization/picker/color/ColorSectionView2.kt
+++ b/src/com/android/customization/picker/color/ui/view/ColorSectionView2.kt
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.customization.picker.color
+package com.android.customization.picker.color.ui.view
import android.content.Context
import android.util.AttributeSet
@@ -49,7 +49,7 @@
val item = items[position]
val itemView =
LayoutInflater.from(context)
- .inflate(R.layout.color_option_section, optionContainer, false)
+ .inflate(R.layout.color_option_no_background, optionContainer, false)
item.bindThumbnailTile(itemView.findViewById(R.id.option_tile))
if (item.isActive(manager)) {
val optionSelectedView = itemView.findViewById<ImageView>(R.id.option_selected)
@@ -62,7 +62,7 @@
// add overflow option
val itemView =
LayoutInflater.from(context)
- .inflate(R.layout.color_option_section_overflow, optionContainer, false)
+ .inflate(R.layout.color_option_overflow_no_background, optionContainer, false)
itemView.setOnClickListener { overflowOnClick?.invoke() }
optionContainer.addView(itemView)
}
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
new file mode 100644
index 0000000..0ebc74b
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorOptionViewModel.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 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.color.ui.viewmodel
+
+import android.annotation.ColorInt
+
+/** Models UI state for a color options in a picker experience. */
+data class ColorOptionViewModel(
+ /** Colors for the color option. */
+ @ColorInt val color0: Int,
+ @ColorInt val color1: Int,
+ @ColorInt val color2: Int,
+ @ColorInt val color3: Int,
+
+ /** A content description for the color. */
+ val contentDescription: String,
+
+ /** Whether this color is selected. */
+ val isSelected: Boolean,
+
+ /** Notifies that the color has been clicked by the user. */
+ val onClick: (() -> Unit)?,
+)
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
new file mode 100644
index 0000000..15445fa
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorPickerViewModel.kt
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 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.color.ui.viewmodel
+
+import android.content.Context
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.android.customization.model.color.ColorBundle
+import com.android.customization.model.color.ColorSeedOption
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorOptionModel
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.R
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+
+/** Models UI state for a color picker experience. */
+class ColorPickerViewModel
+private constructor(
+ context: Context,
+ private val interactor: ColorPickerInteractor,
+) : ViewModel() {
+
+ private val selectedColorTypeId = MutableStateFlow<ColorType?>(null)
+
+ /** View-models for each color type. */
+ val colorTypes: Flow<Map<ColorType, ColorTypeViewModel>> =
+ combine(
+ interactor.colorOptions,
+ selectedColorTypeId,
+ ) { colorOptions, selectedColorTypeIdOrNull ->
+ colorOptions.keys
+ .mapIndexed { index, colorType ->
+ val isSelected =
+ (selectedColorTypeIdOrNull == null && index == 0) ||
+ selectedColorTypeIdOrNull == colorType
+ colorType to
+ ColorTypeViewModel(
+ name =
+ when (colorType) {
+ ColorType.WALLPAPER_COLOR ->
+ context.resources.getString(R.string.wallpaper_color_tab)
+ ColorType.BASIC_COLOR ->
+ context.resources.getString(R.string.preset_color_tab)
+ },
+ isSelected = isSelected,
+ onClick =
+ if (isSelected) {
+ null
+ } else {
+ { this.selectedColorTypeId.value = colorType }
+ },
+ )
+ }
+ .toMap()
+ }
+
+ /** The list of all available color options for the selected Color Type. */
+ val colorOptions: Flow<List<ColorOptionViewModel>> =
+ combine(interactor.colorOptions, selectedColorTypeId) {
+ colorOptions,
+ selectedColorTypeIdOrNull ->
+ val selectedColorType: ColorType =
+ selectedColorTypeIdOrNull ?: ColorType.WALLPAPER_COLOR
+ val selectedColorOptions: List<ColorOptionModel> = colorOptions[selectedColorType]!!
+ selectedColorOptions.map { colorOptionModel ->
+ when (selectedColorType) {
+ ColorType.BASIC_COLOR -> {
+ val colorBundle: ColorBundle = colorOptionModel.colorOption as ColorBundle
+ val primaryColor =
+ colorBundle.previewInfo.resolvePrimaryColor(context.resources)
+ val secondaryColor =
+ colorBundle.previewInfo.resolveSecondaryColor(context.resources)
+ ColorOptionViewModel(
+ color0 = primaryColor,
+ color1 = secondaryColor,
+ color2 = primaryColor,
+ color3 = secondaryColor,
+ contentDescription =
+ colorBundle.getContentDescription(context).toString(),
+ isSelected = colorOptionModel.isSelected,
+ onClick =
+ if (colorOptionModel.isSelected) {
+ null
+ } else {
+ { interactor.select(colorOptionModel) }
+ },
+ )
+ }
+ ColorType.WALLPAPER_COLOR -> {
+ val colorSeedOption: ColorSeedOption =
+ colorOptionModel.colorOption as ColorSeedOption
+ val colors = colorSeedOption.previewInfo.resolveColors(context.resources)
+ ColorOptionViewModel(
+ color0 = colors[0],
+ color1 = colors[1],
+ color2 = colors[2],
+ color3 = colors[3],
+ contentDescription =
+ colorSeedOption.getContentDescription(context).toString(),
+ isSelected = colorOptionModel.isSelected,
+ onClick =
+ if (colorOptionModel.isSelected) {
+ null
+ } else {
+ { interactor.select(colorOptionModel) }
+ },
+ )
+ }
+ }
+ }
+ }
+
+ class Factory(
+ private val context: Context,
+ private val interactor: ColorPickerInteractor,
+ ) : ViewModelProvider.Factory {
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ @Suppress("UNCHECKED_CAST")
+ return ColorPickerViewModel(
+ context = context,
+ interactor = interactor,
+ )
+ as T
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt b/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt
new file mode 100644
index 0000000..7343748
--- /dev/null
+++ b/src/com/android/customization/picker/color/ui/viewmodel/ColorTypeViewModel.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2023 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.color.ui.viewmodel
+
+/** Models UI state for a single color type in a picker experience. */
+data class ColorTypeViewModel(
+ /** User-visible name for the color type. */
+ val name: String,
+
+ /** Whether this is the currently-selected color type in the picker. */
+ val isSelected: Boolean,
+
+ /** Notifies that the color type has been clicked by the user. */
+ val onClick: (() -> Unit)?,
+)
diff --git a/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt b/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt
new file mode 100644
index 0000000..81ef55f
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/color/domain/interactor/ColorPickerInteractorTest.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 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.model.picker.color.domain.interactor
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ColorPickerInteractorTest {
+ private lateinit var underTest: ColorPickerInteractor
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ underTest =
+ ColorPickerInteractor(
+ repository = FakeColorPickerRepository(context = context),
+ )
+ }
+
+ @Test
+ fun select() = runTest {
+ val colorOptions = collectLastValue(underTest.colorOptions)
+
+ val wallpaperColorOptionModelBefore = colorOptions()?.get(ColorType.WALLPAPER_COLOR)?.get(2)
+ assertThat(wallpaperColorOptionModelBefore?.isSelected).isFalse()
+
+ wallpaperColorOptionModelBefore?.let { underTest.select(colorOptionModel = it) }
+ val wallpaperColorOptionModelAfter = colorOptions()?.get(ColorType.WALLPAPER_COLOR)?.get(2)
+ assertThat(wallpaperColorOptionModelAfter?.isSelected).isTrue()
+
+ val presetColorOptionModelBefore = colorOptions()?.get(ColorType.BASIC_COLOR)?.get(1)
+ assertThat(presetColorOptionModelBefore?.isSelected).isFalse()
+
+ presetColorOptionModelBefore?.let { underTest.select(colorOptionModel = it) }
+ val presetColorOptionModelAfter = colorOptions()?.get(ColorType.BASIC_COLOR)?.get(1)
+ assertThat(presetColorOptionModelAfter?.isSelected).isTrue()
+ }
+}
diff --git a/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt b/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
new file mode 100644
index 0000000..19d5dd1
--- /dev/null
+++ b/tests/src/com/android/customization/model/picker/color/ui/viewmodel/ColorPickerViewModelTest.kt
@@ -0,0 +1,180 @@
+/*
+ * Copyright (C) 2023 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.model.picker.color.ui.viewmodel
+
+import android.content.Context
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorTypeViewModel
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class ColorPickerViewModelTest {
+ private lateinit var underTest: ColorPickerViewModel
+
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+
+ underTest =
+ ColorPickerViewModel.Factory(
+ context = context,
+ interactor =
+ ColorPickerInteractor(
+ repository = FakeColorPickerRepository(context = context),
+ ),
+ )
+ .create(ColorPickerViewModel::class.java)
+ }
+
+ @Test
+ fun `Select a preset color`() = runTest {
+ val colorTypes = collectLastValue(underTest.colorTypes)
+ val colorOptions = collectLastValue(underTest.colorOptions)
+
+ // Initially, the wallpaper color tab should be selected
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Wallpaper colors",
+ selectedColorOptionIndex = 0
+ )
+
+ // Select "Basic colors" tab
+ colorTypes()?.get(ColorType.BASIC_COLOR)?.onClick?.invoke()
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Basic colors",
+ selectedColorOptionIndex = -1
+ )
+
+ // Select a color option
+ colorOptions()?.get(2)?.onClick?.invoke()
+
+ // Check original option is no longer selected
+ colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Wallpaper colors",
+ selectedColorOptionIndex = -1
+ )
+
+ // Check new option is selected
+ colorTypes()?.get(ColorType.BASIC_COLOR)?.onClick?.invoke()
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Basic colors",
+ selectedColorOptionIndex = 2
+ )
+ }
+
+ /**
+ * Asserts the entire picker UI state is what is expected. This includes the color type tabs and
+ * the color options list.
+ *
+ * @param colorTypes The observed color type view-models, keyed by ColorType
+ * @param colorOptions The observed color options
+ * @param selectedColorTypeText The text of the color type that's expected to be selected
+ * @param selectedColorOptionIndex The index of the color option that's expected to be selected,
+ * -1 stands for no color option should be selected
+ */
+ private fun assertPickerUiState(
+ colorTypes: Map<ColorType, ColorTypeViewModel>?,
+ colorOptions: List<ColorOptionViewModel>?,
+ selectedColorTypeText: String,
+ selectedColorOptionIndex: Int,
+ ) {
+ assertColorTypeTabUiState(
+ colorTypes = colorTypes,
+ colorTypeId = ColorType.WALLPAPER_COLOR,
+ isSelected = "Wallpaper colors" == selectedColorTypeText,
+ )
+ assertColorTypeTabUiState(
+ colorTypes = colorTypes,
+ colorTypeId = ColorType.BASIC_COLOR,
+ isSelected = "Basic colors" == selectedColorTypeText,
+ )
+
+ var foundSelectedColorOption = false
+ assertThat(colorOptions).isNotNull()
+ if (colorOptions != null) {
+ for (i in colorOptions.indices) {
+ val colorOptionHasSelectedIndex = i == selectedColorOptionIndex
+ assertWithMessage(
+ "Expected color option with index \"${i}\" to have" +
+ " isSelected=$colorOptionHasSelectedIndex but it was" +
+ " ${colorOptions[i].isSelected}, num options: ${colorOptions.size}"
+ )
+ .that(colorOptions[i].isSelected)
+ .isEqualTo(colorOptionHasSelectedIndex)
+ foundSelectedColorOption = foundSelectedColorOption || colorOptionHasSelectedIndex
+ }
+ if (selectedColorOptionIndex == -1) {
+ assertWithMessage(
+ "Expected no color options to be selected, but a color option is" +
+ " selected"
+ )
+ .that(foundSelectedColorOption)
+ .isFalse()
+ } else {
+ assertWithMessage(
+ "Expected a color option to be selected, but no color option is" +
+ " selected"
+ )
+ .that(foundSelectedColorOption)
+ .isTrue()
+ }
+ }
+ }
+
+ /**
+ * Asserts that a color type tab has the correct UI state.
+ *
+ * @param colorTypes The observed color type view-models, keyed by ColorType enum
+ * @param colorTypeId the ID of the color type to assert
+ * @param isSelected Whether that color type should be selected
+ */
+ private fun assertColorTypeTabUiState(
+ colorTypes: Map<ColorType, ColorTypeViewModel>?,
+ colorTypeId: ColorType,
+ isSelected: Boolean,
+ ) {
+ val viewModel =
+ colorTypes?.get(colorTypeId) ?: error("No color type with ID \"$colorTypeId\"!")
+ assertThat(viewModel.isSelected).isEqualTo(isSelected)
+ }
+}
diff --git a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
index 735ba8e..fca4904 100644
--- a/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
+++ b/tests/src/com/android/customization/testing/TestCustomizationInjector.kt
@@ -12,6 +12,9 @@
import com.android.customization.picker.clock.data.repository.FakeClockPickerRepository
import com.android.customization.picker.clock.domain.interactor.ClockPickerInteractor
import com.android.customization.picker.clock.ui.viewmodel.ClockSectionViewModel
+import com.android.customization.picker.color.data.repository.ColorPickerRepositoryImpl
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.ui.viewmodel.ColorPickerViewModel
import com.android.customization.picker.quickaffordance.data.repository.KeyguardQuickAffordancePickerRepository
import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordancePickerInteractor
import com.android.customization.picker.quickaffordance.domain.interactor.KeyguardQuickAffordanceSnapshotRestorer
@@ -19,6 +22,7 @@
import com.android.systemui.shared.customization.data.content.CustomizationProviderClient
import com.android.systemui.shared.customization.data.content.CustomizationProviderClientImpl
import com.android.wallpaper.config.BaseFlags
+import com.android.wallpaper.model.WallpaperColorsViewModel
import com.android.wallpaper.module.DrawableLayerResolver
import com.android.wallpaper.module.PackageStatusNotifier
import com.android.wallpaper.module.UserEventLogger
@@ -42,6 +46,8 @@
private var clockRegistryProvider: ClockRegistryProvider? = null
private var clockPickerInteractor: ClockPickerInteractor? = null
private var clockSectionViewModel: ClockSectionViewModel? = null
+ private var colorPickerInteractor: ColorPickerInteractor? = null
+ private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
override fun getCustomizationPreferences(context: Context): CustomizationPreferences {
return customizationPreferences
@@ -152,6 +158,27 @@
}
}
+ override fun getColorPickerInteractor(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerInteractor {
+ return colorPickerInteractor
+ ?: ColorPickerInteractor(ColorPickerRepositoryImpl(context, wallpaperColorsViewModel))
+ .also { colorPickerInteractor = it }
+ }
+
+ override fun getColorPickerViewModelFactory(
+ context: Context,
+ wallpaperColorsViewModel: WallpaperColorsViewModel,
+ ): ColorPickerViewModel.Factory {
+ return colorPickerViewModelFactory
+ ?: ColorPickerViewModel.Factory(
+ context,
+ getColorPickerInteractor(context, wallpaperColorsViewModel),
+ )
+ .also { colorPickerViewModelFactory = it }
+ }
+
companion object {
private const val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER = 1
}