summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Daniel Norman <danielnorman@google.com> 2025-02-26 17:45:26 -0800
committer Daniel Norman <danielnorman@google.com> 2025-02-26 17:45:26 -0800
commitfc89f1c79787b3c0bfa17e9bede6ae035862804e (patch)
treea170c7574e47fe30ebf126ee8c596116da4da6ab
parent85ca0f4c687a860d46bfd3300a04252410ffc094 (diff)
feat(force invert): force invert the entire light-themed app
Instead of forcing dark each rendernode individually, we force invert the entire app. This helps us avoid issues where some text or icons don't get detected as dark, and they get inverted, causing black-on- black text, etc. The lightness detection in this change uses a simple theme check that will return the wrong answer if the app theme is incorrect. This will be improved upon in future changes to check other heuristics like the main window background color, and to calculate the RenderNode color area polarity in real time. Note: this disables the old force dark behavior when the user has the new force invert dark theme toggle enabled. Bug: 372561761 Flag: android.view.accessibility.force_invert_color Test: manual only; observe inverted apps. See attachments in bug. Change-Id: I86e3e095af206368b5cd3d61330aa1d457103e7d
-rw-r--r--core/java/android/view/ViewRootImpl.java50
-rw-r--r--libs/hwui/CanvasTransform.cpp44
-rw-r--r--libs/hwui/RenderNode.cpp33
-rw-r--r--libs/hwui/hwui/Bitmap.cpp13
-rw-r--r--libs/hwui/hwui/Bitmap.h1
5 files changed, 91 insertions, 50 deletions
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index e157da72196a..9d0773f0a606 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2071,26 +2071,40 @@ public final class ViewRootImpl implements ViewParent,
*/
@VisibleForTesting
public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() {
- if (forceInvertColor()) {
- // Force invert ignores all developer opt-outs.
- // We also ignore dark theme, since the app developer can override the user's preference
- // for dark mode in configuration.uiMode. Instead, we assume that both force invert and
- // the system's dark theme are enabled.
- if (getUiModeManager().getForceInvertState() == UiModeManager.FORCE_INVERT_TYPE_DARK) {
- return ForceDarkType.FORCE_INVERT_COLOR_DARK;
- }
- }
-
- boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
- if (useAutoDark) {
- boolean forceDarkAllowedDefault =
- SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
- TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
- useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
- && a.getBoolean(R.styleable.Theme_forceDarkAllowed, forceDarkAllowedDefault);
+ TypedArray a = mContext.obtainStyledAttributes(R.styleable.Theme);
+ try {
+ if (forceInvertColor()) {
+ // Force invert ignores all developer opt-outs.
+ // We also ignore dark theme, since the app developer can override the user's
+ // preference for dark mode in configuration.uiMode. Instead, we assume that both
+ // force invert and the system's dark theme are enabled.
+ if (getUiModeManager().getForceInvertState() ==
+ UiModeManager.FORCE_INVERT_TYPE_DARK) {
+ final boolean isLightTheme =
+ a.getBoolean(R.styleable.Theme_isLightTheme, false);
+ // TODO: b/372558459 - Also check the background ColorDrawable color lightness
+ // TODO: b/368725782 - Use hwui color area detection instead of / in
+ // addition to these heuristics.
+ if (isLightTheme) {
+ return ForceDarkType.FORCE_INVERT_COLOR_DARK;
+ } else {
+ return ForceDarkType.NONE;
+ }
+ }
+ }
+
+ boolean useAutoDark = getNightMode() == Configuration.UI_MODE_NIGHT_YES;
+ if (useAutoDark) {
+ boolean forceDarkAllowedDefault =
+ SystemProperties.getBoolean(ThreadedRenderer.DEBUG_FORCE_DARK, false);
+ useAutoDark = a.getBoolean(R.styleable.Theme_isLightTheme, true)
+ && a.getBoolean(R.styleable.Theme_forceDarkAllowed,
+ forceDarkAllowedDefault);
+ }
+ return useAutoDark ? ForceDarkType.FORCE_DARK : ForceDarkType.NONE;
+ } finally {
a.recycle();
}
- return useAutoDark ? ForceDarkType.FORCE_DARK : ForceDarkType.NONE;
}
private void updateForceDarkMode() {
diff --git a/libs/hwui/CanvasTransform.cpp b/libs/hwui/CanvasTransform.cpp
index 30e7a628f1f6..6f60d01e4395 100644
--- a/libs/hwui/CanvasTransform.cpp
+++ b/libs/hwui/CanvasTransform.cpp
@@ -55,12 +55,20 @@ SkColor makeDark(SkColor color) {
}
}
+SkColor invert(SkColor color) {
+ Lab lab = sRGBToLab(color);
+ lab.L = 100 - lab.L;
+ return LabToSRGB(lab, SkColorGetA(color));
+}
+
SkColor transformColor(ColorTransform transform, SkColor color) {
switch (transform) {
case ColorTransform::Light:
return makeLight(color);
case ColorTransform::Dark:
return makeDark(color);
+ case ColorTransform::Invert:
+ return invert(color);
default:
return color;
}
@@ -80,19 +88,6 @@ SkColor transformColorInverse(ColorTransform transform, SkColor color) {
static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
if (transform == ColorTransform::None) return;
- if (transform == ColorTransform::Invert) {
- auto filter = SkHighContrastFilter::Make(
- {/* grayscale= */ false, SkHighContrastConfig::InvertStyle::kInvertLightness,
- /* contrast= */ 0.0f});
-
- if (paint.getColorFilter()) {
- paint.setColorFilter(SkColorFilters::Compose(filter, paint.refColorFilter()));
- } else {
- paint.setColorFilter(filter);
- }
- return;
- }
-
SkColor newColor = transformColor(transform, paint.getColor());
paint.setColor(newColor);
@@ -112,6 +107,22 @@ static void applyColorTransform(ColorTransform transform, SkPaint& paint) {
paint.setShader(SkGradientShader::MakeLinear(
info.fPoints, info.fColors, info.fColorOffsets, info.fColorCount,
info.fTileMode, info.fGradientFlags, nullptr));
+ } else {
+ if (transform == ColorTransform::Invert) {
+ // Since we're trying to invert every thing around this draw call, we invert
+ // the color of the draw call if we don't know what it is.
+ auto filter = SkHighContrastFilter::Make(
+ {/* grayscale= */ false,
+ SkHighContrastConfig::InvertStyle::kInvertLightness,
+ /* contrast= */ 0.0f});
+
+ if (paint.getColorFilter()) {
+ paint.setColorFilter(SkColorFilters::Compose(filter, paint.refColorFilter()));
+ } else {
+ paint.setColorFilter(filter);
+ }
+ return;
+ }
}
}
@@ -150,8 +161,13 @@ bool transformPaint(ColorTransform transform, SkPaint* paint) {
}
bool transformPaint(ColorTransform transform, SkPaint* paint, BitmapPalette palette) {
- palette = filterPalette(paint, palette);
bool shouldInvert = false;
+ if (transform == ColorTransform::Invert && palette != BitmapPalette::Colorful) {
+ // When the transform is Invert we invert any image that is not deemed "colorful",
+ // regardless of calculated image brightness.
+ shouldInvert = true;
+ }
+ palette = filterPalette(paint, palette);
if (palette == BitmapPalette::Light && transform == ColorTransform::Dark) {
shouldInvert = true;
}
diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp
index 4801bd1038a3..8b4e59aa73e2 100644
--- a/libs/hwui/RenderNode.cpp
+++ b/libs/hwui/RenderNode.cpp
@@ -27,6 +27,7 @@
#include "DamageAccumulator.h"
#include "Debug.h"
+#include "FeatureFlags.h"
#include "Properties.h"
#include "TreeInfo.h"
#include "VectorDrawable.h"
@@ -398,26 +399,32 @@ void RenderNode::syncDisplayList(TreeObserver& observer, TreeInfo* info) {
deleteDisplayList(observer, info);
mDisplayList = std::move(mStagingDisplayList);
if (mDisplayList) {
- WebViewSyncData syncData{.applyForceDark = shouldEnableForceDark(info)};
+ WebViewSyncData syncData{.applyForceDark = shouldEnableForceDark(info) ||
+ (info && isForceInvertDark(*info))};
mDisplayList.syncContents(syncData);
handleForceDark(info);
}
}
+// Return true if the tree should use the force invert feature that inverts
+// the entire tree to darken it.
inline bool RenderNode::isForceInvertDark(TreeInfo& info) {
- return CC_UNLIKELY(
- info.forceDarkType == android::uirenderer::ForceDarkType::FORCE_INVERT_COLOR_DARK);
+ return CC_UNLIKELY(info.forceDarkType ==
+ android::uirenderer::ForceDarkType::FORCE_INVERT_COLOR_DARK);
}
+// Return true if the tree should use the force dark feature that selectively
+// darkens light nodes on the tree.
inline bool RenderNode::shouldEnableForceDark(TreeInfo* info) {
- return CC_UNLIKELY(
- info &&
- (!info->disableForceDark || isForceInvertDark(*info)));
+ return CC_UNLIKELY(info && !info->disableForceDark);
}
-
-
-void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
+void RenderNode::handleForceDark(TreeInfo *info) {
+ if (CC_UNLIKELY(view_accessibility_flags::force_invert_color() && info &&
+ isForceInvertDark(*info))) {
+ mDisplayList.applyColorTransform(ColorTransform::Invert);
+ return;
+ }
if (!shouldEnableForceDark(info)) {
return;
}
@@ -427,13 +434,7 @@ void RenderNode::handleForceDark(android::uirenderer::TreeInfo *info) {
children.push_back(node);
});
if (mDisplayList.hasText()) {
- if (isForceInvertDark(*info) && mDisplayList.hasFill()) {
- // Handle a special case for custom views that draw both text and background in the
- // same RenderNode, which would otherwise be altered to white-on-white text.
- usage = UsageHint::Container;
- } else {
- usage = UsageHint::Foreground;
- }
+ usage = UsageHint::Foreground;
}
if (usage == UsageHint::Unknown) {
if (children.size() > 1) {
diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp
index 63a024b8e780..3ef970830dc4 100644
--- a/libs/hwui/hwui/Bitmap.cpp
+++ b/libs/hwui/hwui/Bitmap.cpp
@@ -16,6 +16,8 @@
#include "Bitmap.h"
#include <android-base/file.h>
+
+#include "FeatureFlags.h"
#include "HardwareBitmapUploader.h"
#include "Properties.h"
#ifdef __ANDROID__ // Layoutlib does not support render thread
@@ -547,9 +549,16 @@ BitmapPalette Bitmap::computePalette(const SkImageInfo& info, const void* addr,
}
ALOGV("samples = %d, hue [min = %f, max = %f, avg = %f]; saturation [min = %f, max = %f, avg = "
- "%f]",
+ "%f] %d x %d",
sampledCount, hue.min(), hue.max(), hue.average(), saturation.min(), saturation.max(),
- saturation.average());
+ saturation.average(), info.width(), info.height());
+
+ if (CC_UNLIKELY(view_accessibility_flags::force_invert_color())) {
+ if (saturation.delta() > 0.1f ||
+ (hue.delta() > 20 && saturation.average() > 0.2f && value.average() < 0.9f)) {
+ return BitmapPalette::Colorful;
+ }
+ }
if (hue.delta() <= 20 && saturation.delta() <= .1f) {
if (value.average() >= .5f) {
diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h
index 4e9bcf27c0ef..0fe5fe88f715 100644
--- a/libs/hwui/hwui/Bitmap.h
+++ b/libs/hwui/hwui/Bitmap.h
@@ -49,6 +49,7 @@ enum class BitmapPalette {
Unknown,
Light,
Dark,
+ Colorful,
};
namespace uirenderer {