Dark theme toggle (2/2)
Implement using clean architecture and preview & apply pattern on the
picker side. Create an activity-scoped DarkModeLifecycleUtil to update
the singleton-scoped DarkModeRepositoryImpl during lifecycle changes.
Does not include changes to preview dark theme colors on the
picker UI or on Launcher.
Flag: com.android.systemui.shared.new_customization_picker_ui
Test: manually verified with battery saver mode on and off
Test: DarkModeViewModelTest
Bug: 371985937
Change-Id: I7b9c8a5f3e550c8c15ed6e1d5728e8f897ee2e8e
diff --git a/res/layout/floating_sheet_colors.xml b/res/layout/floating_sheet_colors.xml
index a22b264..5834caa 100644
--- a/res/layout/floating_sheet_colors.xml
+++ b/res/layout/floating_sheet_colors.xml
@@ -68,7 +68,6 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@null"
- android:clickable="false"
android:focusable="false"
android:minHeight="0dp" />
</LinearLayout>
diff --git a/src/com/android/customization/picker/mode/data/repository/DarkModeRepository.kt b/src/com/android/customization/picker/mode/data/repository/DarkModeRepository.kt
new file mode 100644
index 0000000..48d5c7f
--- /dev/null
+++ b/src/com/android/customization/picker/mode/data/repository/DarkModeRepository.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.customization.picker.mode.data.repository
+
+import com.android.app.tracing.coroutines.flow.map
+import com.android.customization.picker.mode.shared.util.DarkModeUtil
+import com.android.wallpaper.system.PowerManagerWrapper
+import com.android.wallpaper.system.UiModeManagerWrapper
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOf
+
+@Singleton
+class DarkModeRepository
+@Inject
+constructor(
+ darkModeUtil: DarkModeUtil,
+ private val uiModeManager: UiModeManagerWrapper,
+ private val powerManager: PowerManagerWrapper,
+) {
+ private val isPowerSaveMode = MutableStateFlow(powerManager.getIsPowerSaveMode() ?: false)
+
+ private val isAvailable = darkModeUtil.isAvailable()
+
+ val isEnabled =
+ if (isAvailable) {
+ isPowerSaveMode.map { !it }
+ } else flowOf(false)
+
+ private val _isDarkMode = MutableStateFlow(uiModeManager.getIsNightModeActivated())
+ val isDarkMode = _isDarkMode.asStateFlow()
+
+ fun setDarkModeActivated(isActive: Boolean) {
+ uiModeManager.setNightModeActivated(isActive)
+ refreshIsDarkModeActivated()
+ }
+
+ fun refreshIsDarkModeActivated() {
+ _isDarkMode.value = uiModeManager.getIsNightModeActivated()
+ }
+
+ fun refreshIsPowerSaveModeActivated() {
+ powerManager.getIsPowerSaveMode()?.let { isPowerSaveMode.value = it }
+ }
+}
diff --git a/src/com/android/customization/picker/mode/domain/interactor/DarkModeInteractor.kt b/src/com/android/customization/picker/mode/domain/interactor/DarkModeInteractor.kt
new file mode 100644
index 0000000..1b74e33
--- /dev/null
+++ b/src/com/android/customization/picker/mode/domain/interactor/DarkModeInteractor.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.customization.picker.mode.domain.interactor
+
+import com.android.customization.picker.mode.data.repository.DarkModeRepository
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DarkModeInteractor @Inject constructor(private val repository: DarkModeRepository) {
+ val isEnabled = repository.isEnabled
+ val isDarkMode = repository.isDarkMode
+
+ fun setDarkModeActivated(isActive: Boolean) = repository.setDarkModeActivated(isActive)
+}
diff --git a/src/com/android/customization/picker/mode/shared/util/DarkModeLifecycleUtil.kt b/src/com/android/customization/picker/mode/shared/util/DarkModeLifecycleUtil.kt
new file mode 100644
index 0000000..749ac2e
--- /dev/null
+++ b/src/com/android/customization/picker/mode/shared/util/DarkModeLifecycleUtil.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.customization.picker.mode.shared.util
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.PowerManager
+import android.text.TextUtils
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import com.android.customization.picker.mode.data.repository.DarkModeRepository
+import dagger.hilt.android.qualifiers.ActivityContext
+import dagger.hilt.android.scopes.ActivityScoped
+import javax.inject.Inject
+
+/**
+ * This class observes the activity lifecycle and updates the DarkModeRepositoryImpl based on
+ * lifecycle phases.
+ */
+@ActivityScoped
+class DarkModeLifecycleUtil
+@Inject
+constructor(
+ @ActivityContext private val activityContext: Context,
+ private val darkModeRepository: DarkModeRepository,
+) {
+ private val lifecycleOwner = activityContext as LifecycleOwner
+
+ private val batterySaverStateReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (
+ intent != null &&
+ TextUtils.equals(intent.action, PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
+ ) {
+ darkModeRepository.refreshIsPowerSaveModeActivated()
+ }
+ }
+ }
+ private val lifecycleObserver =
+ object : DefaultLifecycleObserver {
+ @Synchronized
+ override fun onStart(owner: LifecycleOwner) {
+ super.onStart(owner)
+ darkModeRepository.refreshIsDarkModeActivated()
+ darkModeRepository.refreshIsPowerSaveModeActivated()
+ if (lifecycleOwner.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
+ activityContext.registerReceiver(
+ batterySaverStateReceiver,
+ IntentFilter(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED),
+ )
+ }
+ }
+
+ @Synchronized
+ override fun onStop(owner: LifecycleOwner) {
+ super.onStop(owner)
+ activityContext.unregisterReceiver(batterySaverStateReceiver)
+ }
+
+ @Synchronized
+ override fun onDestroy(owner: LifecycleOwner) {
+ super.onDestroy(owner)
+ lifecycleOwner.lifecycle.removeObserver(this)
+ }
+ }
+
+ init {
+ lifecycleOwner.lifecycle.addObserver(lifecycleObserver)
+ }
+}
diff --git a/src/com/android/customization/picker/mode/shared/util/DarkModeUtil.kt b/src/com/android/customization/picker/mode/shared/util/DarkModeUtil.kt
new file mode 100644
index 0000000..9ad514d
--- /dev/null
+++ b/src/com/android/customization/picker/mode/shared/util/DarkModeUtil.kt
@@ -0,0 +1,21 @@
+/*
+ * 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.customization.picker.mode.shared.util
+
+interface DarkModeUtil {
+ fun isAvailable(): Boolean
+}
diff --git a/src/com/android/customization/picker/mode/shared/util/DarkModeUtilImpl.kt b/src/com/android/customization/picker/mode/shared/util/DarkModeUtilImpl.kt
new file mode 100644
index 0000000..a8e8535
--- /dev/null
+++ b/src/com/android/customization/picker/mode/shared/util/DarkModeUtilImpl.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.customization.picker.mode.shared.util
+
+import android.Manifest
+import android.content.Context
+import android.content.pm.PackageManager
+import androidx.core.content.ContextCompat
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DarkModeUtilImpl @Inject constructor(@ApplicationContext private val context: Context) :
+ DarkModeUtil {
+ override fun isAvailable(): Boolean {
+ return (ContextCompat.checkSelfPermission(
+ context,
+ Manifest.permission.MODIFY_DAY_NIGHT_MODE,
+ ) == PackageManager.PERMISSION_GRANTED)
+ }
+}
diff --git a/src/com/android/customization/picker/mode/shared/util/FakeDarkModeUtil.kt b/src/com/android/customization/picker/mode/shared/util/FakeDarkModeUtil.kt
new file mode 100644
index 0000000..f0225ef
--- /dev/null
+++ b/src/com/android/customization/picker/mode/shared/util/FakeDarkModeUtil.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.customization.picker.mode.shared.util
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class FakeDarkModeUtil @Inject constructor() : DarkModeUtil {
+ override fun isAvailable(): Boolean {
+ return true
+ }
+}
diff --git a/src/com/android/customization/picker/mode/ui/binder/DarkModeBinder.kt b/src/com/android/customization/picker/mode/ui/binder/DarkModeBinder.kt
new file mode 100644
index 0000000..b9c7041
--- /dev/null
+++ b/src/com/android/customization/picker/mode/ui/binder/DarkModeBinder.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.customization.picker.mode.ui.binder
+
+import android.widget.Switch
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.customization.picker.mode.ui.viewmodel.DarkModeViewModel
+import kotlinx.coroutines.launch
+
+object DarkModeBinder {
+ fun bind(darkModeToggle: Switch, viewModel: DarkModeViewModel, lifecycleOwner: LifecycleOwner) {
+ lifecycleOwner.lifecycleScope.launch {
+ lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch { viewModel.isEnabled.collect { darkModeToggle.isEnabled = it } }
+ launch { viewModel.previewingIsDarkMode.collect { darkModeToggle.isChecked = it } }
+ launch {
+ viewModel.toggleDarkMode.collect {
+ darkModeToggle.setOnCheckedChangeListener { _, _ -> it.invoke() }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModel.kt b/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModel.kt
new file mode 100644
index 0000000..2bcb644
--- /dev/null
+++ b/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModel.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.customization.picker.mode.ui.viewmodel
+
+import com.android.customization.module.logging.ThemesUserEventLogger
+import com.android.customization.picker.mode.domain.interactor.DarkModeInteractor
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+
+@ViewModelScoped
+class DarkModeViewModel
+@Inject
+constructor(private val interactor: DarkModeInteractor, private val logger: ThemesUserEventLogger) {
+ private val isDarkMode = interactor.isDarkMode
+ val isEnabled = interactor.isEnabled
+
+ private val overridingIsDarkMode = MutableStateFlow<Boolean?>(null)
+ val previewingIsDarkMode =
+ combine(overridingIsDarkMode, isDarkMode, isEnabled) { override, current, isEnabled ->
+ if (isEnabled) {
+ override ?: current
+ } else current
+ }
+
+ val toggleDarkMode =
+ combine(overridingIsDarkMode, isDarkMode) { override, current ->
+ { overridingIsDarkMode.value = override?.not() ?: !current }
+ }
+
+ val onApply: Flow<(suspend () -> Unit)?> =
+ combine(overridingIsDarkMode, isDarkMode, isEnabled) { override, current, isEnabled ->
+ if (override != null && override != current && isEnabled) {
+ {
+ interactor.setDarkModeActivated(override)
+ logger.logDarkThemeApplied(override)
+ }
+ } else null
+ }
+
+ fun resetPreview() {
+ overridingIsDarkMode.value = null
+ }
+}
diff --git a/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt b/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
index b06748a..e2a8cf3 100644
--- a/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
+++ b/src/com/android/wallpaper/customization/ui/binder/ColorsFloatingSheetBinder.kt
@@ -32,6 +32,7 @@
import com.android.customization.picker.color.ui.view.ColorOptionIconView
import com.android.customization.picker.color.ui.viewmodel.ColorOptionIconViewModel
import com.android.customization.picker.common.ui.view.DoubleRowListItemSpacing
+import com.android.customization.picker.mode.ui.binder.DarkModeBinder
import com.android.themepicker.R
import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.COLORS
import com.android.wallpaper.customization.ui.viewmodel.ThemePickerCustomizationOptionsViewModel
@@ -65,10 +66,16 @@
val tabAdapter =
FloatingToolbarTabAdapter(
colorUpdateViewModel = WeakReference(colorUpdateViewModel),
- shouldAnimateColor = { optionsViewModel.selectedOption.value == COLORS }
+ shouldAnimateColor = { optionsViewModel.selectedOption.value == COLORS },
)
.also { tabs.setAdapter(it) }
+ DarkModeBinder.bind(
+ darkModeToggle = view.findViewById(R.id.dark_mode_toggle),
+ viewModel = optionsViewModel.darkModeViewModel,
+ lifecycleOwner = lifecycleOwner,
+ )
+
lifecycleOwner.lifecycleScope.launch {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.colorTypeTabs.collect { tabAdapter.submitList(it) } }
@@ -91,7 +98,7 @@
private fun createOptionItemAdapter(
uiMode: Int,
- lifecycleOwner: LifecycleOwner
+ lifecycleOwner: LifecycleOwner,
): OptionItemAdapter<ColorOptionIconViewModel> =
OptionItemAdapter(
layoutResourceId = R.layout.color_option,
@@ -100,7 +107,7 @@
val colorOptionIconView = foregroundView as? ColorOptionIconView
val night = uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES
colorOptionIconView?.let { ColorOptionIconBinder.bind(it, colorIcon, night) }
- }
+ },
)
private fun RecyclerView.initColorsList(
@@ -109,13 +116,7 @@
) {
apply {
this.adapter = adapter
- layoutManager =
- GridLayoutManager(
- context,
- 2,
- GridLayoutManager.HORIZONTAL,
- false,
- )
+ layoutManager = GridLayoutManager(context, 2, GridLayoutManager.HORIZONTAL, false)
addItemDecoration(
DoubleRowListItemSpacing(
context.resources.getDimensionPixelSize(
diff --git a/src/com/android/wallpaper/customization/ui/util/ThemePickerCustomizationOptionUtil.kt b/src/com/android/wallpaper/customization/ui/util/ThemePickerCustomizationOptionUtil.kt
index 7a73b7d..6789e2b 100644
--- a/src/com/android/wallpaper/customization/ui/util/ThemePickerCustomizationOptionUtil.kt
+++ b/src/com/android/wallpaper/customization/ui/util/ThemePickerCustomizationOptionUtil.kt
@@ -21,6 +21,7 @@
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
+import com.android.customization.picker.mode.shared.util.DarkModeLifecycleUtil
import com.android.themepicker.R
import com.android.wallpaper.model.Screen
import com.android.wallpaper.model.Screen.HOME_SCREEN
@@ -36,6 +37,9 @@
constructor(private val defaultCustomizationOptionUtil: DefaultCustomizationOptionUtil) :
CustomizationOptionUtil {
+ // Instantiate DarkModeLifecycleUtil for it to observe lifecycle and update DarkModeRepository
+ @Inject lateinit var darkModeLifecycleUtil: DarkModeLifecycleUtil
+
enum class ThemePickerLockCustomizationOption : CustomizationOptionUtil.CustomizationOption {
CLOCK,
SHORTCUTS,
diff --git a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
index d114ac8..4707f81 100644
--- a/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
+++ b/src/com/android/wallpaper/customization/ui/viewmodel/ThemePickerCustomizationOptionsViewModel.kt
@@ -16,6 +16,7 @@
package com.android.wallpaper.customization.ui.viewmodel
+import com.android.customization.picker.mode.ui.viewmodel.DarkModeViewModel
import com.android.wallpaper.customization.ui.util.ThemePickerCustomizationOptionUtil
import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModel
import com.android.wallpaper.picker.customization.ui.viewmodel.CustomizationOptionsViewModelFactory
@@ -28,6 +29,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
@@ -42,6 +44,7 @@
colorPickerViewModel2Factory: ColorPickerViewModel2.Factory,
clockPickerViewModelFactory: ClockPickerViewModel.Factory,
shapeAndGridPickerViewModelFactory: ShapeAndGridPickerViewModel.Factory,
+ val darkModeViewModel: DarkModeViewModel,
@Assisted private val viewModelScope: CoroutineScope,
) : CustomizationOptionsViewModel {
@@ -62,6 +65,7 @@
shapeAndGridPickerViewModel.resetPreview()
clockPickerViewModel.resetPreview()
colorPickerViewModel2.resetPreview()
+ darkModeViewModel.resetPreview()
return defaultCustomizationOptionsViewModel.deselectOption()
}
@@ -131,7 +135,14 @@
ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption
.APP_SHAPE_AND_GRID -> shapeAndGridPickerViewModel.onApply
ThemePickerCustomizationOptionUtil.ThemePickerHomeCustomizationOption.COLORS ->
- colorPickerViewModel2.onApply
+ combine(colorPickerViewModel2.onApply, darkModeViewModel.onApply) {
+ colorOnApply,
+ darkModeOnApply ->
+ {
+ colorOnApply?.invoke()
+ darkModeOnApply?.invoke()
+ }
+ }
else -> flow { emit(null) }
}
}
diff --git a/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt b/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
index 0b32196..22e5fe5 100644
--- a/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
+++ b/src/com/android/wallpaper/picker/di/modules/ThemePickerSharedAppModule.kt
@@ -18,6 +18,8 @@
import com.android.customization.model.grid.DefaultGridOptionsManager
import com.android.customization.model.grid.GridOptionsManager2
+import com.android.customization.picker.mode.shared.util.DarkModeUtil
+import com.android.customization.picker.mode.shared.util.DarkModeUtilImpl
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@@ -31,4 +33,6 @@
@Binds
@Singleton
abstract fun bindGridOptionsManager2(impl: DefaultGridOptionsManager): GridOptionsManager2
+
+ @Binds @Singleton abstract fun bindDarkModeUtil(impl: DarkModeUtilImpl): DarkModeUtil
}
diff --git a/tests/common/src/com/android/customization/module/logging/TestThemesUserEventLogger.kt b/tests/common/src/com/android/customization/module/logging/TestThemesUserEventLogger.kt
index 4651067..05c95b0 100644
--- a/tests/common/src/com/android/customization/module/logging/TestThemesUserEventLogger.kt
+++ b/tests/common/src/com/android/customization/module/logging/TestThemesUserEventLogger.kt
@@ -40,6 +40,9 @@
var shortcutLogs: List<Pair<String, String>> = emptyList()
+ var useDarkTheme: Boolean = false
+ private set
+
override fun logThemeColorApplied(@ColorSource source: Int, style: Int, seedColor: Int) {
this.themeColorSource = source
this.themeColorStyle = style
@@ -64,7 +67,9 @@
shortcutLogs = shortcutLogs.toMutableList().apply { add(shortcut to shortcutSlotId) }
}
- override fun logDarkThemeApplied(useDarkTheme: Boolean) {}
+ override fun logDarkThemeApplied(useDarkTheme: Boolean) {
+ this.useDarkTheme = useDarkTheme
+ }
@ClockSize
fun getLoggedClockSize(): Int {
diff --git a/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt b/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
index 7781d4e..3112f75 100644
--- a/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
+++ b/tests/common/src/com/android/wallpaper/di/modules/ThemePickerSharedAppTestModule.kt
@@ -18,6 +18,8 @@
import com.android.customization.model.grid.FakeGridOptionsManager
import com.android.customization.model.grid.GridOptionsManager2
+import com.android.customization.picker.mode.shared.util.DarkModeUtil
+import com.android.customization.picker.mode.shared.util.FakeDarkModeUtil
import com.android.wallpaper.picker.di.modules.ThemePickerSharedAppModule
import dagger.Binds
import dagger.Module
@@ -28,11 +30,13 @@
@Module
@TestInstallIn(
components = [SingletonComponent::class],
- replaces = [ThemePickerSharedAppModule::class]
+ replaces = [ThemePickerSharedAppModule::class],
)
abstract class ThemePickerSharedAppTestModule {
@Binds
@Singleton
abstract fun bindGridOptionsManager2(impl: FakeGridOptionsManager): GridOptionsManager2
+
+ @Binds @Singleton abstract fun bindDarkModeUtil(impl: FakeDarkModeUtil): DarkModeUtil
}
diff --git a/tests/robotests/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModelTest.kt b/tests/robotests/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModelTest.kt
new file mode 100644
index 0000000..4078803
--- /dev/null
+++ b/tests/robotests/src/com/android/customization/picker/mode/ui/viewmodel/DarkModeViewModelTest.kt
@@ -0,0 +1,137 @@
+/*
+ * 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.customization.picker.mode.ui.viewmodel
+
+import com.android.customization.module.logging.TestThemesUserEventLogger
+import com.android.customization.picker.mode.data.repository.DarkModeRepository
+import com.android.customization.picker.mode.domain.interactor.DarkModeInteractor
+import com.android.wallpaper.testing.FakePowerManager
+import com.android.wallpaper.testing.FakeUiModeManager
+import com.android.wallpaper.testing.collectLastValue
+import com.google.common.truth.Truth.assertThat
+import dagger.hilt.android.testing.HiltAndroidRule
+import dagger.hilt.android.testing.HiltAndroidTest
+import javax.inject.Inject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+
+@HiltAndroidTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(RobolectricTestRunner::class)
+class DarkModeViewModelTest {
+ @get:Rule var hiltRule = HiltAndroidRule(this)
+
+ @Inject lateinit var uiModeManager: FakeUiModeManager
+ @Inject lateinit var powerManager: FakePowerManager
+ @Inject lateinit var darkModeRepository: DarkModeRepository
+ @Inject lateinit var darkModeInteractor: DarkModeInteractor
+ @Inject lateinit var logger: TestThemesUserEventLogger
+ lateinit var darkModeViewModel: DarkModeViewModel
+
+ @Inject lateinit var testDispatcher: TestDispatcher
+ @Inject lateinit var testScope: TestScope
+
+ @Before
+ fun setUp() {
+ hiltRule.inject()
+ Dispatchers.setMain(testDispatcher)
+
+ darkModeViewModel = DarkModeViewModel(darkModeInteractor, logger)
+ }
+
+ @Test
+ fun isEnabled_powerSaveModeOn() {
+ testScope.runTest {
+ powerManager.setIsPowerSaveMode(true)
+ darkModeRepository.refreshIsPowerSaveModeActivated()
+
+ val isEnabled = collectLastValue(darkModeViewModel.isEnabled)()
+
+ assertThat(isEnabled).isFalse()
+ }
+ }
+
+ @Test
+ fun isEnabled_powerSaveModeOff() {
+ testScope.runTest {
+ powerManager.setIsPowerSaveMode(false)
+ darkModeRepository.refreshIsPowerSaveModeActivated()
+
+ val isEnabled = collectLastValue(darkModeViewModel.isEnabled)()
+
+ assertThat(isEnabled).isTrue()
+ }
+ }
+
+ @Test
+ fun toggleDarkMode() {
+ testScope.runTest {
+ uiModeManager.setNightModeActivated(false)
+ darkModeRepository.refreshIsDarkModeActivated()
+ val getPreviewingIsDarkMode = collectLastValue(darkModeViewModel.previewingIsDarkMode)
+ val getToggleDarkMode = collectLastValue(darkModeViewModel.toggleDarkMode)
+ assertThat(getPreviewingIsDarkMode()).isFalse()
+
+ getToggleDarkMode()?.invoke()
+
+ assertThat(getPreviewingIsDarkMode()).isTrue()
+
+ getToggleDarkMode()?.invoke()
+
+ assertThat(getPreviewingIsDarkMode()).isFalse()
+ }
+ }
+
+ @Test
+ fun onApply_shouldLogDarkTheme() {
+ testScope.runTest {
+ uiModeManager.setNightModeActivated(false)
+ darkModeRepository.refreshIsDarkModeActivated()
+ val getToggleDarkMode = collectLastValue(darkModeViewModel.toggleDarkMode)
+ val onApply = collectLastValue(darkModeViewModel.onApply)
+
+ getToggleDarkMode()?.invoke()
+ onApply()?.invoke()
+
+ assertThat(logger.useDarkTheme).isTrue()
+ }
+ }
+
+ @Test
+ fun onApply_shouldApplyDarkTheme() {
+ testScope.runTest {
+ uiModeManager.setNightModeActivated(false)
+ darkModeRepository.refreshIsDarkModeActivated()
+ val getToggleDarkMode = collectLastValue(darkModeViewModel.toggleDarkMode)
+ val onApply = collectLastValue(darkModeViewModel.onApply)
+
+ getToggleDarkMode()?.invoke()
+ onApply()?.invoke()
+
+ assertThat(uiModeManager.getIsNightModeActivated()).isTrue()
+ }
+ }
+}