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

Commit 17e8bbef authored by Alejandro Nijamkin's avatar Alejandro Nijamkin
Browse files

Reset support for dark theme.

Fix: 267804364
Test: unit tests added
Test: manually verified that toggling the dark theme button makes the
RESET button show up, toggling it back, make it disappear. If the RESET
button is clicked, the theme setting goes back to the original.

Change-Id: Ifad8023928f20718a798b24d7da77698bc9d1677
parent 47698608
Loading
Loading
Loading
Loading
+7 −1
Original line number Diff line number Diff line
@@ -59,12 +59,17 @@ public class DarkModeSectionController implements

    private Context mContext;
    private DarkModeSectionView mDarkModeSectionView;
    private final DarkModeSnapshotRestorer mSnapshotRestorer;

    public DarkModeSectionController(Context context, Lifecycle lifecycle) {
    public DarkModeSectionController(
            Context context,
            Lifecycle lifecycle,
            DarkModeSnapshotRestorer snapshotRestorer) {
        mContext = context;
        mLifecycle = lifecycle;
        mPowerManager = context.getSystemService(PowerManager.class);
        mLifecycle.addObserver(this);
        mSnapshotRestorer = snapshotRestorer;
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_START)
@@ -132,6 +137,7 @@ public class DarkModeSectionController implements
                    mDarkModeSectionView.announceForAccessibility(
                            context.getString(R.string.mode_changed));
                    uiModeManager.setNightModeActivated(viewActivated);
                    mSnapshotRestorer.store(viewActivated);
                },
                /* delayMillis= */ shortDelay);
    }
+101 −0
Original line number Diff line number Diff line
/*
 * 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.mode

import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import androidx.annotation.VisibleForTesting
import com.android.wallpaper.picker.undo.domain.interactor.SnapshotRestorer
import com.android.wallpaper.picker.undo.domain.interactor.SnapshotStore
import com.android.wallpaper.picker.undo.shared.model.RestorableSnapshot
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext

class DarkModeSnapshotRestorer : SnapshotRestorer {

    private val backgroundDispatcher: CoroutineDispatcher
    private val isActive: () -> Boolean
    private val setActive: suspend (Boolean) -> Unit

    private lateinit var store: SnapshotStore

    constructor(
        context: Context,
        manager: UiModeManager,
        backgroundDispatcher: CoroutineDispatcher,
    ) : this(
        backgroundDispatcher = backgroundDispatcher,
        isActive = {
            context.applicationContext.resources.configuration.uiMode and
                Configuration.UI_MODE_NIGHT_YES != 0
        },
        setActive = { isActive -> manager.setNightModeActivated(isActive) },
    )

    @VisibleForTesting
    constructor(
        backgroundDispatcher: CoroutineDispatcher,
        isActive: () -> Boolean,
        setActive: suspend (Boolean) -> Unit,
    ) {
        this.backgroundDispatcher = backgroundDispatcher
        this.isActive = isActive
        this.setActive = setActive
    }

    override suspend fun setUpSnapshotRestorer(store: SnapshotStore): RestorableSnapshot {
        this.store = store
        return snapshot(
            isActivated = isActive(),
        )
    }

    override suspend fun restoreToSnapshot(snapshot: RestorableSnapshot) {
        val isActivated = snapshot.args[KEY]?.toBoolean() == true
        withContext(backgroundDispatcher) { setActive(isActivated) }
    }

    fun store(
        isActivated: Boolean,
    ) {
        store.store(
            snapshot(
                isActivated = isActivated,
            ),
        )
    }

    private fun snapshot(
        isActivated: Boolean,
    ): RestorableSnapshot {
        return RestorableSnapshot(
            args =
                buildMap {
                    put(
                        KEY,
                        isActivated.toString(),
                    )
                }
        )
    }

    companion object {
        private const val KEY = "is_activated"
    }
}
+13 −5
Original line number Diff line number Diff line
@@ -12,6 +12,7 @@ import com.android.customization.model.color.ColorSectionController2;
import com.android.customization.model.grid.GridOptionsManager;
import com.android.customization.model.grid.GridSectionController;
import com.android.customization.model.mode.DarkModeSectionController;
import com.android.customization.model.mode.DarkModeSnapshotRestorer;
import com.android.customization.model.themedicon.ThemedIconSectionController;
import com.android.customization.model.themedicon.ThemedIconSwitchProvider;
import com.android.customization.picker.clock.data.repository.ClockRegistryProvider;
@@ -56,6 +57,7 @@ public final class DefaultCustomizationSections implements CustomizationSections
            mClockCarouselViewModelProvider;
    private final PreviewWithClockCarouselSectionController.ClockViewFactoryProvider
            mClockViewFactoryProvider;
    private final DarkModeSnapshotRestorer mDarkModeSnapshotRestorer;

    public DefaultCustomizationSections(
            KeyguardQuickAffordancePickerInteractor keyguardQuickAffordancePickerInteractor,
@@ -65,7 +67,8 @@ public final class DefaultCustomizationSections implements CustomizationSections
            BaseFlags flags,
            ClockRegistryProvider clockRegistryProvider,
            ClockCarouselViewModelProvider clockCarouselViewModelProvider,
            ClockViewFactoryProvider clockViewFactoryProvider) {
            ClockViewFactoryProvider clockViewFactoryProvider,
            DarkModeSnapshotRestorer darkModeSnapshotRestorer) {
        mKeyguardQuickAffordancePickerInteractor = keyguardQuickAffordancePickerInteractor;
        mKeyguardQuickAffordancePickerViewModelFactory =
                keyguardQuickAffordancePickerViewModelFactory;
@@ -74,6 +77,7 @@ public final class DefaultCustomizationSections implements CustomizationSections
        mClockRegistryProvider = clockRegistryProvider;
        mClockCarouselViewModelProvider = clockCarouselViewModelProvider;
        mClockViewFactoryProvider = clockViewFactoryProvider;
        mDarkModeSnapshotRestorer = darkModeSnapshotRestorer;
    }

    @Override
@@ -157,8 +161,10 @@ public final class DefaultCustomizationSections implements CustomizationSections

            case HOME_SCREEN:
                // Dark/Light theme section.
                sectionControllers.add(new DarkModeSectionController(activity,
                        lifecycleOwner.getLifecycle()));
                sectionControllers.add(new DarkModeSectionController(
                        activity,
                        lifecycleOwner.getLifecycle(),
                        mDarkModeSnapshotRestorer));

                // Themed app icon section.
                sectionControllers.add(new ThemedIconSectionController(
@@ -198,8 +204,10 @@ public final class DefaultCustomizationSections implements CustomizationSections
                activity, wallpaperColorsViewModel, lifecycleOwner, savedInstanceState));

        // Dark/Light theme section.
        sectionControllers.add(new DarkModeSectionController(activity,
                lifecycleOwner.getLifecycle()));
        sectionControllers.add(new DarkModeSectionController(
                activity,
                lifecycleOwner.getLifecycle(),
                mDarkModeSnapshotRestorer));

        // Themed app icon section.
        sectionControllers.add(new ThemedIconSectionController(
+21 −2
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@
 */
package com.android.customization.module

import android.app.UiModeManager
import android.content.Context
import android.content.Intent
import android.net.Uri
@@ -22,6 +23,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import com.android.customization.model.mode.DarkModeSnapshotRestorer
import com.android.customization.model.theme.OverlayManagerCompat
import com.android.customization.model.theme.ThemeBundleProvider
import com.android.customization.model.theme.ThemeManager
@@ -86,6 +88,7 @@ open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInject
    private var notificationSectionViewModelFactory: NotificationSectionViewModel.Factory? = null
    private var colorPickerInteractor: ColorPickerInteractor? = null
    private var colorPickerViewModelFactory: ColorPickerViewModel.Factory? = null
    private var darkModeSnapshotRestorer: DarkModeSnapshotRestorer? = null

    override fun getCustomizationSections(activity: ComponentActivity): CustomizationSections {
        return customizationSections
@@ -112,7 +115,8 @@ open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInject
                                registry = registry,
                            )
                        }
                    }
                    },
                    getDarkModeSnapshotRestorer(activity),
                )
                .also { customizationSections = it }
    }
@@ -173,6 +177,7 @@ open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInject
                getKeyguardQuickAffordanceSnapshotRestorer(context)
            this[KEY_WALLPAPER_SNAPSHOT_RESTORER] = getWallpaperSnapshotRestorer(context)
            this[KEY_NOTIFICATIONS_SNAPSHOT_RESTORER] = getNotificationsSnapshotRestorer(context)
            this[KEY_DARK_MODE_SNAPSHOT_RESTORER] = getDarkModeSnapshotRestorer(context)
        }
    }

@@ -348,6 +353,18 @@ open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInject
                .also { colorPickerViewModelFactory = it }
    }

    protected fun getDarkModeSnapshotRestorer(
        context: Context,
    ): DarkModeSnapshotRestorer {
        return darkModeSnapshotRestorer
            ?: DarkModeSnapshotRestorer(
                    context = context,
                    manager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager,
                    backgroundDispatcher = Dispatchers.IO,
                )
                .also { darkModeSnapshotRestorer = it }
    }

    companion object {
        @JvmStatic
        private val KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER =
@@ -356,11 +373,13 @@ open class ThemePickerInjector : WallpaperPicker2Injector(), CustomizationInject
        private val KEY_WALLPAPER_SNAPSHOT_RESTORER = KEY_QUICK_AFFORDANCE_SNAPSHOT_RESTORER + 1
        @JvmStatic
        private val KEY_NOTIFICATIONS_SNAPSHOT_RESTORER = KEY_WALLPAPER_SNAPSHOT_RESTORER + 1
        @JvmStatic
        private val KEY_DARK_MODE_SNAPSHOT_RESTORER = KEY_NOTIFICATIONS_SNAPSHOT_RESTORER + 1

        /**
         * When this injector is overridden, this is the minimal value that should be used by
         * restorers returns in [getSnapshotRestorers].
         */
        @JvmStatic protected val MIN_SNAPSHOT_RESTORER_KEY = KEY_NOTIFICATIONS_SNAPSHOT_RESTORER + 1
        @JvmStatic protected val MIN_SNAPSHOT_RESTORER_KEY = KEY_DARK_MODE_SNAPSHOT_RESTORER + 1
    }
}
+107 −0
Original line number Diff line number Diff line
/*
 * 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.mode

import androidx.test.filters.SmallTest
import com.android.wallpaper.testing.FakeSnapshotStore
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
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 DarkModeSnapshotRestorerTest {

    private lateinit var underTest: DarkModeSnapshotRestorer
    private lateinit var testScope: TestScope

    private var isActive = false

    @Before
    fun setUp() {
        val testDispatcher = StandardTestDispatcher()
        testScope = TestScope(testDispatcher)
        underTest =
            DarkModeSnapshotRestorer(
                backgroundDispatcher = testDispatcher,
                isActive = { isActive },
                setActive = { isActive = it },
            )
    }

    @Test
    fun `set up and restore - active`() =
        testScope.runTest {
            isActive = true

            val store = FakeSnapshotStore()
            store.store(underTest.setUpSnapshotRestorer(store = store))
            val storedSnapshot = store.retrieve()

            underTest.restoreToSnapshot(snapshot = storedSnapshot)
            assertThat(isActive).isTrue()
        }

    @Test
    fun `set up and restore - inactive`() =
        testScope.runTest {
            isActive = false

            val store = FakeSnapshotStore()
            store.store(underTest.setUpSnapshotRestorer(store = store))
            val storedSnapshot = store.retrieve()

            underTest.restoreToSnapshot(snapshot = storedSnapshot)
            assertThat(isActive).isFalse()
        }

    @Test
    fun `set up - deactivate - restore to active`() =
        testScope.runTest {
            isActive = true
            val store = FakeSnapshotStore()
            store.store(underTest.setUpSnapshotRestorer(store = store))
            val initialSnapshot = store.retrieve()

            underTest.store(isActivated = false)

            underTest.restoreToSnapshot(snapshot = initialSnapshot)
            assertThat(isActive).isTrue()
        }

    @Test
    fun `set up - activate - restore to inactive`() =
        testScope.runTest {
            isActive = false
            val store = FakeSnapshotStore()
            store.store(underTest.setUpSnapshotRestorer(store = store))
            val initialSnapshot = store.retrieve()

            underTest.store(isActivated = true)

            underTest.restoreToSnapshot(snapshot = initialSnapshot)
            assertThat(isActive).isFalse()
        }
}