chromium/chrome/browser/ui/android/night_mode/java/src/org/chromium/chrome/browser/night_mode/NightModeUtils.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.night_mode;

import static org.chromium.chrome.browser.preferences.ChromePreferenceKeys.UI_THEME_SETTING;

import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.view.ContextThemeWrapper;

import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;

import org.chromium.base.ResettersForTesting;
import org.chromium.chrome.browser.preferences.ChromeSharedPreferences;

import java.util.LinkedHashSet;

/** Helper methods for supporting night mode. */
public class NightModeUtils {
    private static Boolean sNightModeSupportedForTest;

    /**
     * @return Whether night mode is supported.
     */
    public static boolean isNightModeSupported() {
        if (sNightModeSupportedForTest != null) return sNightModeSupportedForTest;
        return true;
    }

    /**
     * Updates configuration for night mode to ensure night mode settings are applied properly.
     * Should be called anytime the Activity's configuration changes (e.g. from
     * {@link Activity#onConfigurationChanged(Configuration)}) if uiMode was not overridden on
     * the configuration during activity initialization
     * (see {@link #applyOverridesForNightMode(NightModeStateProvider, Configuration)}).
     * @param activity The {@link Activity} that needs to be updated.
     * @param inNightMode Whether night mode should be set on the activity.
     * @param newConfig The new {@link Configuration} from
     *                  {@link Activity#onConfigurationChanged(Configuration)}.
     * @param themeResIds An ordered set of {@link StyleRes} of the themes applied to the activity.
     */
    public static void updateConfigurationForNightMode(
            Activity activity,
            boolean inNightMode,
            Configuration newConfig,
            LinkedHashSet<Integer> themeResIds) {
        final int uiNightMode =
                inNightMode ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;

        if (uiNightMode == (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK)) return;

        // Rebase the theme against the new configuration, so the attributes get resolved to the
        // correct colors based on the night mode setting. See https://crbug.com/1280540.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            activity.getTheme().rebase();
        } else {
            // Theme#rebase() is only available on APIs 29+ and the support library of the method
            // isn't guaranteed to succeed on older versions. So, we manually re-apply all the
            // cached styles.
            for (Integer themeResId : themeResIds) {
                activity.getTheme().applyStyle(themeResId, true);
            }
        }
    }

    /**
     * @param provider The {@link NightModeStateProvider} that provides the night mode state.
     * @param config The {@link Configuration} on which UI night mode should be overridden if
     *               necessary.
     * @return True if UI night mode is overridden on the provided {@code config}, and false
     *         otherwise.
     */
    public static boolean applyOverridesForNightMode(
            NightModeStateProvider provider, Configuration config) {
        if (!provider.shouldOverrideConfiguration()) return false;

        // Override uiMode so that UIs created by the DecorView (e.g. context menu, floating
        // action bar) get the correct theme. May check if this is needed on newer version
        // of support library. See https://crbug.com/935731.
        final int nightMode =
                provider.isInNightMode()
                        ? Configuration.UI_MODE_NIGHT_YES
                        : Configuration.UI_MODE_NIGHT_NO;
        config.uiMode = nightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
        return true;
    }

    /**
     * Wraps a {@link Context} into one having a resource configuration with the given night mode
     * setting.
     * @param context {@link Context} to wrap.
     * @param themeResId Theme resource to use with {@link ContextThemeWrapper}.
     * @param nightMode Whether to apply night mode.
     * @return Wrapped {@link Context}.
     */
    public static Context wrapContextWithNightModeConfig(
            Context context, @StyleRes int themeResId, boolean nightMode) {
        ContextThemeWrapper wrapper = new ContextThemeWrapper(context, themeResId);
        Configuration config = new Configuration();
        // Pre-Android O, fontScale gets initialized to 1 in the constructor. Set it to 0 so
        // that applyOverrideConfiguration() does not interpret it as an overridden value.
        config.fontScale = 0;
        int nightModeFlag =
                nightMode ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;
        config.uiMode = nightModeFlag | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
        wrapper.applyOverrideConfiguration(config);
        return wrapper;
    }

    /**
     * The current theme setting, reflecting either the user setting or the default if the user has
     * not explicitly set a preference.
     * @return The current theme setting. See {@link ThemeType}.
     */
    public static @ThemeType int getThemeSetting() {
        int userSetting = ChromeSharedPreferences.getInstance().readInt(UI_THEME_SETTING, -1);
        if (userSetting == -1) {
            return isNightModeDefaultToLight() ? ThemeType.LIGHT : ThemeType.SYSTEM_DEFAULT;
        } else {
            return userSetting;
        }
    }

    public static void setNightModeSupportedForTesting(@Nullable Boolean nightModeSupported) {
        sNightModeSupportedForTest = nightModeSupported;
        ResettersForTesting.register(() -> sNightModeSupportedForTest = null);
    }

    /**
     * @return Whether or not to default to the light theme.
     */
    public static boolean isNightModeDefaultToLight() {
        return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
    }
}