Is there an API to detect which theme the OS is using - dark or light (or other)?
Google has just published the documentation on the dark theme at the end of I/O 2019, here.
In order to manage the dark theme, you must first use the latest version of the Material Components library: "com.google.android.material:material:1.1.0-alpha06"
.
Change the application theme according to the system theme
For the application to switch to the dark theme depending on the system, only one theme is required. To do this, the theme must have Theme.MaterialComponents.DayNight as a parent.
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight"> ...</style>
Determine the current system theme
To know if the system is currently in dark theme or not, you can implement the following code:
switch (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) { case Configuration.UI_MODE_NIGHT_YES: … break; case Configuration.UI_MODE_NIGHT_NO: … break; }
Be notified of a change in the theme
I don't think it's possible to implement a callback to be notified whenever the theme changes, but that's not a problem. Indeed, when the system changes theme, the activity is automatically recreated. Placing the previous code at the beginning of the activity is then sufficient.
From which version of the Android SDK does it work?
I couldn't get this to work on Android Pie with version 28 of the Android SDK. So I assume that this only works from the next version of the SDK, which will be launched with Q, version 29.
Result
A simpler Kotlin approach to Charles Annic's answer:
fun Context.isDarkThemeOn(): Boolean { return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES}
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}@JvmStaticfun 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.