chromium/android_webview/java/src/org/chromium/android_webview/DarkModeHelper.java

// Copyright 2021 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.android_webview;

import static org.chromium.components.embedder_support.application.ClassLoaderContextWrapperFactory.getOriginalApplicationContext;

import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.os.Build;

import androidx.core.graphics.ColorUtils;

import org.chromium.base.Log;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/** This class provides the utility methods for dark mode. */
public class DarkModeHelper {
    private static final String TAG = "DarkModeHelper";

    @Retention(RetentionPolicy.SOURCE)
    public @interface LightTheme {
        int LIGHT_THEME_UNDEFINED = 0;
        int LIGHT_THEME_FALSE = 1;
        int LIGHT_THEME_TRUE = 2;
        int LIGHT_THEME_COUNT = 3;
    }

    @Retention(RetentionPolicy.SOURCE)
    public @interface TextLuminance {
        int TEXT_LUMINACE_UNDEFINED = 0;
        int TEXT_LUMINACE_LIGHT = 1;
        int TEXT_LUMINACE_DARK = 2;
        int TEXT_LUMINACE_COUNT = 3;
    }

    @Retention(RetentionPolicy.SOURCE)
    public @interface NightMode {
        int NGITH_MODE_UNDEFINED = 0;
        int NIGHT_MODE_ON = 1;
        int NIGHT_MODE_OFF = 2;
        int NIGHT_MODE_COUNT = 3;
    }

    private static Integer sLightThemeForTesting;

    @NightMode
    public static int getNightMode(Context context) {
        int nightMode =
                getOriginalApplicationContext(context).getResources().getConfiguration().uiMode
                        & Configuration.UI_MODE_NIGHT_MASK;
        switch (nightMode) {
            case Configuration.UI_MODE_NIGHT_NO:
                return NightMode.NIGHT_MODE_OFF;
            case Configuration.UI_MODE_NIGHT_YES:
                return NightMode.NIGHT_MODE_ON;
            case Configuration.UI_MODE_NIGHT_UNDEFINED:
            default:
                return NightMode.NGITH_MODE_UNDEFINED;
        }
    }

    // must use getIdentifier to access resources from another app
    @SuppressWarnings("DiscouragedApi")
    @LightTheme
    public static int getLightTheme(Context context) {
        if (sLightThemeForTesting != null) return sLightThemeForTesting;
        int lightTheme = LightTheme.LIGHT_THEME_UNDEFINED;
        try {
            int resId = android.R.attr.isLightTheme;
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
                // android.R.attr.isLightTheme is added in Q, for pre-Q platform, WebView
                // checks if app has isLightTheme attr which could be added by Android X
                // and wasn't stripped out.
                resId =
                        getOriginalApplicationContext(context)
                                .getResources()
                                .getIdentifier(
                                        "isLightTheme",
                                        "attr",
                                        context.getApplicationContext().getPackageName());
                if (resId == 0) return lightTheme;
            }
            TypedArray a =
                    getOriginalApplicationContext(context)
                            .getTheme()
                            .obtainStyledAttributes(new int[] {resId});
            // TODO: use try-with-resources once minSdkVersion>=31 instead of recycle
            try {
                if (a.hasValue(0)) {
                    lightTheme =
                            a.getBoolean(0, true)
                                    ? LightTheme.LIGHT_THEME_TRUE
                                    : LightTheme.LIGHT_THEME_FALSE;
                }
            } finally {
                a.recycle();
            }
        } catch (RuntimeException e) {
            // The AssetManager may have been shut down, possibly due to the WebView outliving the
            // Activity it was associated with, but this just throws a generic RuntimeException.
            // Check the message to be sure.
            if ("AssetManager has been destroyed".equals(e.getMessage())) {
                // just fall through so we return the default
            } else {
                // rethrow if the message doesn't match
                throw e;
            }
        }
        return lightTheme;
    }

    public static void setsLightThemeForTesting(@LightTheme int lightThemeForTesting) {
        sLightThemeForTesting = Integer.valueOf(lightThemeForTesting);
    }

    @TextLuminance
    public static int getPrimaryTextLuminace(Context context) {
        int textColor = TextLuminance.TEXT_LUMINACE_UNDEFINED;
        TypedArray a =
                getOriginalApplicationContext(context)
                        .getTheme()
                        .obtainStyledAttributes(new int[] {android.R.attr.textColorPrimary});
        if (a.hasValue(0)) {
            try {
                textColor =
                        ColorUtils.calculateLuminance(a.getColor(0, 0)) < 0.5
                                ? TextLuminance.TEXT_LUMINACE_DARK
                                : TextLuminance.TEXT_LUMINACE_LIGHT;
            } catch (UnsupportedOperationException e) {
                Log.e(TAG, "Wrong color format", e);
            }
        }
        a.recycle();
        return textColor;
    }
}