Merge "Import translations. DO NOT MERGE ANYWHERE" into main
diff --git a/res/layout/floating_sheet_shortcut.xml b/res/layout/floating_sheet_shortcut.xml
index 44a9b6d..ce4665e 100644
--- a/res/layout/floating_sheet_shortcut.xml
+++ b/res/layout/floating_sheet_shortcut.xml
@@ -18,7 +18,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="@dimen/floating_sheet_horizontal_padding"
- android:paddingBottom="16dp"
android:orientation="vertical"
android:clipToPadding="false"
android:clipChildren="false">
@@ -45,4 +44,4 @@
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginVertical="@dimen/floating_sheet_tab_toolbar_vertical_margin" />
-</LinearLayout>
\ No newline at end of file
+</LinearLayout>
diff --git a/src/com/android/customization/module/DefaultCustomizationPreferences.kt b/src/com/android/customization/module/DefaultCustomizationPreferences.kt
index 49fd1a9..0ef4a1d 100644
--- a/src/com/android/customization/module/DefaultCustomizationPreferences.kt
+++ b/src/com/android/customization/module/DefaultCustomizationPreferences.kt
@@ -17,8 +17,14 @@
import android.content.Context
import com.android.wallpaper.module.DefaultWallpaperPreferences
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
-open class DefaultCustomizationPreferences(context: Context) :
+@Singleton
+open class DefaultCustomizationPreferences
+@Inject
+constructor(@ApplicationContext context: Context) :
DefaultWallpaperPreferences(context), CustomizationPreferences {
override fun getSerializedCustomThemes(): String? {
diff --git a/src/com/android/customization/module/ThemePickerInjector.kt b/src/com/android/customization/module/ThemePickerInjector.kt
index 08bb800..e62235c 100644
--- a/src/com/android/customization/module/ThemePickerInjector.kt
+++ b/src/com/android/customization/module/ThemePickerInjector.kt
@@ -67,7 +67,9 @@
import com.android.wallpaper.module.FragmentFactory
import com.android.wallpaper.module.WallpaperPicker2Injector
import com.android.wallpaper.picker.CustomizationPickerActivity
+import com.android.wallpaper.picker.customization.data.content.WallpaperClientImpl
import com.android.wallpaper.picker.customization.data.repository.WallpaperColorsRepository
+import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository
import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
import com.android.wallpaper.picker.di.modules.BackgroundDispatcher
import com.android.wallpaper.picker.di.modules.MainDispatcher
@@ -89,6 +91,7 @@
@BackgroundDispatcher private val bgDispatcher: CoroutineDispatcher,
) : WallpaperPicker2Injector(mainScope, bgDispatcher), CustomizationInjector {
private var customizationSections: CustomizationSections? = null
+ private var wallpaperInteractor: WallpaperInteractor? = null
private var keyguardQuickAffordancePickerViewModelFactory:
KeyguardQuickAffordancePickerViewModel.Factory? =
null
@@ -194,7 +197,27 @@
}
override fun getWallpaperInteractor(context: Context): WallpaperInteractor {
- return injectedWallpaperInteractor.get()
+ if (getFlags().isMultiCropEnabled()) {
+ return injectedWallpaperInteractor.get()
+ }
+
+ val appContext = context.applicationContext
+ return wallpaperInteractor
+ ?: WallpaperInteractor(
+ repository =
+ WallpaperRepository(
+ scope = getApplicationCoroutineScope(),
+ client =
+ WallpaperClientImpl(
+ context = appContext,
+ wallpaperManager = WallpaperManager.getInstance(appContext),
+ wallpaperPreferences = getPreferences(appContext),
+ ),
+ wallpaperPreferences = getPreferences(context = appContext),
+ backgroundDispatcher = bgDispatcher,
+ ),
+ )
+ .also { wallpaperInteractor = it }
}
override fun getKeyguardQuickAffordancePickerInteractor(
diff --git a/src_override/com/android/wallpaper/modules/ThemePickerAppModule.kt b/src_override/com/android/wallpaper/modules/ThemePickerAppModule.kt
index b700d2f..271f925 100644
--- a/src_override/com/android/wallpaper/modules/ThemePickerAppModule.kt
+++ b/src_override/com/android/wallpaper/modules/ThemePickerAppModule.kt
@@ -86,14 +86,13 @@
@Singleton
abstract fun bindColorPickerRepository(impl: ColorPickerRepositoryImpl): ColorPickerRepository
+ @Binds
+ @Singleton
+ abstract fun bindWallpaperPreferences(
+ impl: DefaultCustomizationPreferences
+ ): WallpaperPreferences
+
companion object {
- @Provides
- @Singleton
- fun provideWallpaperPreferences(
- @ApplicationContext context: Context
- ): WallpaperPreferences {
- return DefaultCustomizationPreferences(context)
- }
@Provides
@Singleton
diff --git a/src_override/com/android/wallpaper/picker/di/modules/InteractorModule.kt b/src_override/com/android/wallpaper/picker/di/modules/InteractorModule.kt
deleted file mode 100644
index 81edb2f..0000000
--- a/src_override/com/android/wallpaper/picker/di/modules/InteractorModule.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.wallpaper.picker.di.modules
-
-import android.text.TextUtils
-import com.android.customization.model.color.ColorCustomizationManager
-import com.android.customization.model.color.ColorOptionsProvider.COLOR_SOURCE_PRESET
-import com.android.wallpaper.picker.customization.data.repository.WallpaperRepository
-import com.android.wallpaper.picker.customization.domain.interactor.WallpaperInteractor
-import dagger.Module
-import dagger.Provides
-import dagger.hilt.InstallIn
-import dagger.hilt.components.SingletonComponent
-import javax.inject.Singleton
-
-/** This class provides the singleton scoped interactors for theme picker. */
-@InstallIn(SingletonComponent::class)
-@Module
-internal object InteractorModule {
-
- @Provides
- @Singleton
- fun provideWallpaperInteractor(
- wallpaperRepository: WallpaperRepository,
- colorCustomizationManager: ColorCustomizationManager,
- ): WallpaperInteractor {
- return WallpaperInteractor(wallpaperRepository) {
- TextUtils.equals(colorCustomizationManager.currentColorSource, COLOR_SOURCE_PRESET)
- }
- }
-}
diff --git a/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2Test.kt b/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2Test.kt
new file mode 100644
index 0000000..a43a7ce
--- /dev/null
+++ b/tests/robotests/src/com/android/wallpaper/customization/ui/viewmodel/ColorPickerViewModel2Test.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2024 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 android.content.Context
+import android.graphics.Color
+import android.stats.style.StyleEnums
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.customization.model.color.ColorOptionsProvider
+import com.android.customization.module.logging.TestThemesUserEventLogger
+import com.android.customization.picker.color.data.repository.FakeColorPickerRepository
+import com.android.customization.picker.color.domain.interactor.ColorPickerInteractor
+import com.android.customization.picker.color.domain.interactor.ColorPickerSnapshotRestorer
+import com.android.customization.picker.color.shared.model.ColorType
+import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
+import com.android.customization.picker.color.ui.viewmodel.ColorTypeTabViewModel
+import com.android.systemui.monet.Style
+import com.android.wallpaper.picker.option.ui.viewmodel.OptionItemViewModel
+import com.android.wallpaper.testing.FakeSnapshotStore
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(RobolectricTestRunner::class)
+class ColorPickerViewModel2Test {
+ private val logger = TestThemesUserEventLogger()
+ private lateinit var underTest: ColorPickerViewModel2
+ private lateinit var repository: FakeColorPickerRepository
+ private lateinit var interactor: ColorPickerInteractor
+ private lateinit var store: FakeSnapshotStore
+
+ private lateinit var context: Context
+ private lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ context = InstrumentationRegistry.getInstrumentation().targetContext
+ val testDispatcher = UnconfinedTestDispatcher()
+ Dispatchers.setMain(testDispatcher)
+ testScope = TestScope(testDispatcher)
+ repository = FakeColorPickerRepository(context = context)
+ store = FakeSnapshotStore()
+
+ interactor =
+ ColorPickerInteractor(
+ repository = repository,
+ snapshotRestorer =
+ ColorPickerSnapshotRestorer(repository = repository).apply {
+ runBlocking { setUpSnapshotRestorer(store = store) }
+ },
+ )
+
+ underTest =
+ ColorPickerViewModel2(
+ context = context,
+ interactor = interactor,
+ logger = logger,
+ viewModelScope = testScope.backgroundScope,
+ )
+
+ repository.setOptions(4, 4, ColorType.WALLPAPER_COLOR, 0)
+ }
+
+ @After
+ fun tearDown() {
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun `Log selected wallpaper color`() =
+ testScope.runTest {
+ repository.setOptions(
+ listOf(
+ repository.buildWallpaperOption(
+ ColorOptionsProvider.COLOR_SOURCE_LOCK,
+ Style.EXPRESSIVE,
+ "121212"
+ )
+ ),
+ listOf(repository.buildPresetOption(Style.FRUIT_SALAD, "#ABCDEF")),
+ ColorType.PRESET_COLOR,
+ 0
+ )
+
+ val colorTypes = collectLastValue(underTest.colorTypeTabs)
+ val colorOptions = collectLastValue(underTest.colorOptions)
+
+ // Select "Wallpaper colors" tab
+ colorTypes()?.get(ColorType.WALLPAPER_COLOR)?.onClick?.invoke()
+ // Select a color option
+ selectColorOption(colorOptions, 0)
+
+ assertThat(logger.themeColorSource)
+ .isEqualTo(StyleEnums.COLOR_SOURCE_LOCK_SCREEN_WALLPAPER)
+ assertThat(logger.themeColorStyle).isEqualTo(Style.EXPRESSIVE.toString().hashCode())
+ assertThat(logger.themeSeedColor).isEqualTo(Color.parseColor("#121212"))
+ }
+
+ @Test
+ fun `Log selected preset color`() =
+ testScope.runTest {
+ repository.setOptions(
+ listOf(
+ repository.buildWallpaperOption(
+ ColorOptionsProvider.COLOR_SOURCE_LOCK,
+ Style.EXPRESSIVE,
+ "121212"
+ )
+ ),
+ listOf(repository.buildPresetOption(Style.FRUIT_SALAD, "#ABCDEF")),
+ ColorType.WALLPAPER_COLOR,
+ 0
+ )
+
+ val colorTypes = collectLastValue(underTest.colorTypeTabs)
+ val colorOptions = collectLastValue(underTest.colorOptions)
+
+ // Select "Wallpaper colors" tab
+ colorTypes()?.get(ColorType.PRESET_COLOR)?.onClick?.invoke()
+ // Select a color option
+ selectColorOption(colorOptions, 0)
+
+ assertThat(logger.themeColorSource).isEqualTo(StyleEnums.COLOR_SOURCE_PRESET_COLOR)
+ assertThat(logger.themeColorStyle).isEqualTo(Style.FRUIT_SALAD.toString().hashCode())
+ assertThat(logger.themeSeedColor).isEqualTo(Color.parseColor("#ABCDEF"))
+ }
+
+ @Test
+ fun `Select a preset color`() =
+ testScope.runTest {
+ val colorTypes = collectLastValue(underTest.colorTypeTabs)
+ 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.PRESET_COLOR)?.onClick?.invoke()
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Basic colors",
+ selectedColorOptionIndex = -1
+ )
+
+ // Select a color option
+ selectColorOption(colorOptions, 2)
+
+ // 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.PRESET_COLOR)?.onClick?.invoke()
+ assertPickerUiState(
+ colorTypes = colorTypes(),
+ colorOptions = colorOptions(),
+ selectedColorTypeText = "Basic colors",
+ selectedColorOptionIndex = 2
+ )
+ }
+
+ /** Simulates a user selecting the affordance at the given index, if that is clickable. */
+ private fun TestScope.selectColorOption(
+ colorOptions: () -> List<OptionItemViewModel<ColorOptionIconViewModel>>?,
+ index: Int,
+ ) {
+ val onClickedFlow = colorOptions()?.get(index)?.onClicked
+ val onClickedLastValueOrNull: (() -> (() -> Unit)?)? =
+ onClickedFlow?.let { collectLastValue(it) }
+ onClickedLastValueOrNull?.let { onClickedLastValue ->
+ val onClickedOrNull: (() -> Unit)? = onClickedLastValue()
+ onClickedOrNull?.let { onClicked -> onClicked() }
+ }
+ }
+
+ /**
+ * 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 TestScope.assertPickerUiState(
+ colorTypes: Map<ColorType, ColorTypeTabViewModel>?,
+ colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
+ selectedColorTypeText: String,
+ selectedColorOptionIndex: Int,
+ ) {
+ assertColorTypeTabUiState(
+ colorTypes = colorTypes,
+ colorTypeId = ColorType.WALLPAPER_COLOR,
+ isSelected = "Wallpaper colors" == selectedColorTypeText,
+ )
+ assertColorTypeTabUiState(
+ colorTypes = colorTypes,
+ colorTypeId = ColorType.PRESET_COLOR,
+ isSelected = "Basic colors" == selectedColorTypeText,
+ )
+ assertColorOptionUiState(colorOptions, selectedColorOptionIndex)
+ }
+
+ /**
+ * Asserts the picker section UI state is what is expected.
+ *
+ * @param colorOptions The observed color options
+ * @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 TestScope.assertColorOptionUiState(
+ colorOptions: List<OptionItemViewModel<ColorOptionIconViewModel>>?,
+ selectedColorOptionIndex: Int,
+ ) {
+ var foundSelectedColorOption = false
+ assertThat(colorOptions).isNotNull()
+ if (colorOptions != null) {
+ for (i in colorOptions.indices) {
+ val colorOptionHasSelectedIndex = i == selectedColorOptionIndex
+ val isSelected: Boolean? = collectLastValue(colorOptions[i].isSelected).invoke()
+ assertWithMessage(
+ "Expected color option with index \"${i}\" to have" +
+ " isSelected=$colorOptionHasSelectedIndex but it was" +
+ " ${isSelected}, num options: ${colorOptions.size}"
+ )
+ .that(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, ColorTypeTabViewModel>?,
+ colorTypeId: ColorType,
+ isSelected: Boolean,
+ ) {
+ val viewModel =
+ colorTypes?.get(colorTypeId) ?: error("No color type with ID \"$colorTypeId\"!")
+ assertThat(viewModel.isSelected).isEqualTo(isSelected)
+ }
+}