androidthemesandroid-9.0-pieandroid-8.1-oreoandroid-10.0

Is there an API to detect which theme the OS is using - dark or light (or other)?


Background

On recent Android versions, ever since Android 8.1, the OS got more and more support for themes. More specifically dark theme.

The problem

Even though there is a lot of talk about dark mode in the point-of-view for users, there is almost nothing that's written for developers.

What I've found

Starting from Android 8.1, Google provided some sort of dark theme . If the user chooses to have a dark wallpaper, some UI components of the OS would turn black (article here).

In addition, if you developed a live wallpaper app, you could tell the OS which colors it has (3 types of colors), which affected the OS colors too (at least on Vanilla based ROMs and on Google devices). That's why I even made an app that lets you have any wallpaper while still be able to choose the colors (here). This is done by calling notifyColorsChanged and then provide them using onComputeColors

Starting from Android 9.0, it is now possible to choose which theme to have: light, dark or automatic (based on wallpaper) :

enter image description here

And now on the near Android Q , it seems it went further, yet it's still unclear to what extent. Somehow a launcher called "Smart Launcher" has ridden on it, offering to use the theme right on itself (article here). So, if you enable dark mode (manually, as written here) , you get the app's settings screen as such:

enter image description here

The only thing I've found so far is the above articles, and that I'm following this kind of topic.

I also know how to request the OS to change color using the live wallpaper, but this seems to be changing on Android Q, at least according to what I've seen when trying it (I think it's more based on time-of-day, but not sure).

The questions

  1. Is there an API to get which colors the OS is set to use ?

  2. Is there any kind of API to get the theme of the OS ? From which version?

  3. Is the new API somehow related to night mode too? How do those work together?

  4. Is there a nice API for apps to handle the chosen theme? Meaning that if the OS is in certain theme, so will the current app?


Solution

  • OK so I got to know how this usually works, on both newest version of Android (Q) and before.

    It seems that when the OS creates the WallpaperColors , it also generates color-hints. In the function WallpaperColors.fromBitmap , there is a call to int hints = calculateDarkHints(bitmap); , and this is the code of calculateDarkHints :

    /**
     * Checks if image is bright and clean enough to support light text.
     *
     * @param source What to read.
     * @return Whether image supports dark text or not.
     */
    private static int calculateDarkHints(Bitmap source) {
        if (source == null) {
            return 0;
        }
    
        int[] pixels = new int[source.getWidth() * source.getHeight()];
        double totalLuminance = 0;
        final int maxDarkPixels = (int) (pixels.length * MAX_DARK_AREA);
        int darkPixels = 0;
        source.getPixels(pixels, 0 /* offset */, source.getWidth(), 0 /* x */, 0 /* y */,
                source.getWidth(), source.getHeight());
    
        // This bitmap was already resized to fit the maximum allowed area.
        // Let's just loop through the pixels, no sweat!
        float[] tmpHsl = new float[3];
        for (int i = 0; i < pixels.length; i++) {
            ColorUtils.colorToHSL(pixels[i], tmpHsl);
            final float luminance = tmpHsl[2];
            final int alpha = Color.alpha(pixels[i]);
            // Make sure we don't have a dark pixel mass that will
            // make text illegible.
            if (luminance < DARK_PIXEL_LUMINANCE && alpha != 0) {
                darkPixels++;
            }
            totalLuminance += luminance;
        }
    
        int hints = 0;
        double meanLuminance = totalLuminance / pixels.length;
        if (meanLuminance > BRIGHT_IMAGE_MEAN_LUMINANCE && darkPixels < maxDarkPixels) {
            hints |= HINT_SUPPORTS_DARK_TEXT;
        }
        if (meanLuminance < DARK_THEME_MEAN_LUMINANCE) {
            hints |= HINT_SUPPORTS_DARK_THEME;
        }
    
        return hints;
    }
    

    Then searching for getColorHints that the WallpaperColors.java has, I've found updateTheme function in StatusBar.java :

        WallpaperColors systemColors = mColorExtractor
                .getWallpaperColors(WallpaperManager.FLAG_SYSTEM);
        final boolean useDarkTheme = systemColors != null
                && (systemColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_THEME) != 0;
    

    This would work only on Android 8.1 , because then the theme was based on the colors of the wallpaper alone. On Android 9.0 , the user can set it without any connection to the wallpaper.

    Here's what I've made, according to what I've seen on Android :

    enum class DarkThemeCheckResult {
        DEFAULT_BEFORE_THEMES, LIGHT, DARK, PROBABLY_DARK, PROBABLY_LIGHT, USER_CHOSEN
    }
    
    @JvmStatic
    fun getIsOsDarkTheme(context: Context): DarkThemeCheckResult {
        when {
            Build.VERSION.SDK_INT <= Build.VERSION_CODES.O -> return DarkThemeCheckResult.DEFAULT_BEFORE_THEMES
            Build.VERSION.SDK_INT <= Build.VERSION_CODES.P -> {
                val wallpaperManager = WallpaperManager.getInstance(context)
                val wallpaperColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
                        ?: return DarkThemeCheckResult.UNKNOWN
                val primaryColor = wallpaperColors.primaryColor.toArgb()
                val secondaryColor = wallpaperColors.secondaryColor?.toArgb() ?: primaryColor
                val tertiaryColor = wallpaperColors.tertiaryColor?.toArgb() ?: secondaryColor
                val bitmap = generateBitmapFromColors(primaryColor, secondaryColor, tertiaryColor)
                val darkHints = calculateDarkHints(bitmap)
                //taken from StatusBar.java , in updateTheme :
                val HINT_SUPPORTS_DARK_THEME = 1 shl 1
                val useDarkTheme = darkHints and HINT_SUPPORTS_DARK_THEME != 0
                if (Build.VERSION.SDK_INT == VERSION_CODES.O_MR1)
                    return if (useDarkTheme)
                        DarkThemeCheckResult.UNKNOWN_MAYBE_DARK
                    else DarkThemeCheckResult.UNKNOWN_MAYBE_LIGHT
                return if (useDarkTheme)
                    DarkThemeCheckResult.MOST_PROBABLY_DARK
                else DarkThemeCheckResult.MOST_PROBABLY_LIGHT
            }
            else -> {
                return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
                    Configuration.UI_MODE_NIGHT_YES -> DarkThemeCheckResult.DARK
                    Configuration.UI_MODE_NIGHT_NO -> DarkThemeCheckResult.LIGHT
                    else -> DarkThemeCheckResult.MOST_PROBABLY_LIGHT
                }
            }
        }
    }
    
    fun generateBitmapFromColors(@ColorInt primaryColor: Int, @ColorInt secondaryColor: Int, @ColorInt tertiaryColor: Int): Bitmap {
        val colors = intArrayOf(primaryColor, secondaryColor, tertiaryColor)
        val imageSize = 6
        val bitmap = Bitmap.createBitmap(imageSize, 1, Bitmap.Config.ARGB_8888)
        for (i in 0 until imageSize / 2)
            bitmap.setPixel(i, 0, colors[0])
        for (i in imageSize / 2 until imageSize / 2 + imageSize / 3)
            bitmap.setPixel(i, 0, colors[1])
        for (i in imageSize / 2 + imageSize / 3 until imageSize)
            bitmap.setPixel(i, 0, colors[2])
        return bitmap
    }
    

    I've set the various possible values, because in most of those cases nothing is guaranteed.