Optimize statusbar rendering paths

This commit is contained in:
ajp_anton
2026-06-03 12:33:12 +00:00
parent 6d6ee64275
commit 46db70e067
11 changed files with 769 additions and 59 deletions
@@ -1,5 +1,6 @@
package se.ajpanton.statusbartweak.runtime.layout;
import android.app.Notification;
import android.app.KeyguardManager;
import android.content.Context;
import android.graphics.Bitmap;
@@ -22,6 +23,7 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
@@ -47,6 +49,7 @@ import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationStri
import se.ajpanton.statusbartweak.runtime.notifications.NotificationActiveKeyStore;
import se.ajpanton.statusbartweak.runtime.notifications.NotificationUnreadTracker;
import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager;
import se.ajpanton.statusbartweak.runtime.render.StatusBarTintTarget;
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController;
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult;
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
@@ -106,12 +109,19 @@ final class StockLayoutCanvasController {
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
private final Map<View, Integer> settingsIdentityByRoot = new WeakHashMap<>();
private final Map<View, Integer> lockscreenCardsRowFilterSignatures = new WeakHashMap<>();
private final Map<View, Integer> lockscreenCardsNotificationRenderGenerationByRoot =
new WeakHashMap<>();
private final Map<View, Long> clockTimeBucketByRoot = new WeakHashMap<>();
private final Map<View, Integer> aodCardsNotificationRenderGenerationByRoot = new WeakHashMap<>();
private final Map<View, AodSourceGeometry> aodSourceGeometryByView = new WeakHashMap<>();
private final Map<View, Integer> aodNotificationWidgetSteadyWidths = new WeakHashMap<>();
private final Map<View, Integer> aodNotificationIconOnlyAreaSteadyLefts = new WeakHashMap<>();
private final Map<View, Integer> originalAodNotificationWidgetLayoutWidths = new WeakHashMap<>();
private final Map<Class<?>, ArrayList<NotifCollectionAccessor>> notifCollectionAccessorsByClass =
new WeakHashMap<>();
private final Map<String, IconFingerprint> notificationFingerprintByCacheKey = new HashMap<>();
private final Map<ImageView, DrawableFingerprintCache> viewFingerprintCacheByImageView =
new WeakHashMap<>();
private final Set<View> confirmedAodPluginViews =
Collections.newSetFromMap(new WeakHashMap<>());
private final Map<View, Float> aodPluginVisualAlphaByView = new WeakHashMap<>();
@@ -440,6 +450,12 @@ final class StockLayoutCanvasController {
if (darkIconDispatcherClass != null) {
darkIconDispatcherGuard.install(darkIconDispatcherClass);
}
Class<?> lightBarTransitionsControllerClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.LightBarTransitionsController",
classLoader);
if (lightBarTransitionsControllerClass != null) {
hookLightBarTintTarget(lightBarTransitionsControllerClass);
}
Class<?> statusBarIconViewClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.StatusBarIconView",
@@ -540,6 +556,32 @@ final class StockLayoutCanvasController {
hookStatusChipContentInvalidation(ImageView.class, "setImageLevel");
}
private void hookLightBarTintTarget(Class<?> type) {
for (Method method : type.getDeclaredMethods()) {
if (!isSetIconsDarkMethod(method)) {
continue;
}
method.setAccessible(true);
runtimeContext.getFramework().hook(method).intercept(
chain -> {
List<Object> args = chain.getArgs();
Object result = chain.proceed();
if (args.size() >= 1
&& args.get(0) instanceof Boolean value
&& StatusBarTintTarget.setDarkIcons(value)) {
scheduleUnlockedAppearanceRefreshForAllRoots(true);
}
return result;
});
}
}
private boolean isSetIconsDarkMethod(Method method) {
return "setIconsDark".equals(method.getName())
&& method.getParameterCount() >= 1
&& method.getParameterTypes()[0] == boolean.class;
}
private void hookViewAppearanceInvalidation(Class<?> type, String methodName) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
@@ -857,6 +899,8 @@ final class StockLayoutCanvasController {
if (notifications.isEmpty()) {
latestAodNotifications = new ArrayList<>();
latestAodIconImageViews = new ArrayList<>();
notificationFingerprintByCacheKey.clear();
viewFingerprintCacheByImageView.clear();
NotificationActiveKeyStore.replace(Collections.emptySet());
NotificationUnreadTracker.retainActiveKeys(Collections.emptySet());
LockscreenCardsNotificationIconSourceStore.clearAodSources();
@@ -880,6 +924,7 @@ final class StockLayoutCanvasController {
changed |= NotificationUnreadTracker.retainActiveKeys(activeKeys);
LockscreenCardsNotificationIconSourceStore.putAodNotificationPackages(packages);
latestAodNotifications = notifications;
pruneNotificationFingerprintCache(notifications);
if (changed) {
markKnownAodNotificationRootsDirty();
}
@@ -891,38 +936,95 @@ final class StockLayoutCanvasController {
if (collection == null) {
return notifications;
}
for (String methodName : NOTIF_COLLECTION_METHOD_NAMES) {
Object value = ReflectionSupport.invokeMethod(collection, methodName);
appendNotificationsFromNotifCollectionValue(notifications, value);
for (NotifCollectionAccessor accessor : notifCollectionAccessors(collection.getClass())) {
appendNotificationsFromNotifCollectionValue(notifications, accessor.get(collection));
if (!notifications.isEmpty()) {
return notifications;
}
}
for (String fieldName : NOTIF_COLLECTION_FIELD_NAMES) {
Object value = ReflectionSupport.getFieldValue(collection, fieldName);
appendNotificationsFromNotifCollectionValue(notifications, value);
if (!notifications.isEmpty()) {
return notifications;
}
}
for (Field field : collection.getClass().getDeclaredFields()) {
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
field.setAccessible(true);
Object value = field.get(collection);
appendNotificationsFromNotifCollectionValue(notifications, value);
if (!notifications.isEmpty()) {
return notifications;
}
} catch (Throwable ignored) {
// Best-effort source matching; inaccessible fields just do not contribute.
}
}
return notifications;
}
private ArrayList<NotifCollectionAccessor> notifCollectionAccessors(Class<?> type) {
ArrayList<NotifCollectionAccessor> cached = notifCollectionAccessorsByClass.get(type);
if (cached != null) {
return cached;
}
ArrayList<NotifCollectionAccessor> accessors = buildNotifCollectionAccessors(type);
notifCollectionAccessorsByClass.put(type, accessors);
return accessors;
}
private ArrayList<NotifCollectionAccessor> buildNotifCollectionAccessors(Class<?> type) {
ArrayList<NotifCollectionAccessor> accessors = new ArrayList<>();
HashSet<String> fieldNames = new HashSet<>();
for (String methodName : NOTIF_COLLECTION_METHOD_NAMES) {
Method method = findNoArgMethod(type, methodName);
if (method != null) {
accessors.add(new MethodNotifCollectionAccessor(method));
}
}
for (String fieldName : NOTIF_COLLECTION_FIELD_NAMES) {
Field field = findField(type, fieldName);
if (field != null) {
fieldNames.add(field.getDeclaringClass().getName() + "#" + field.getName());
accessors.add(new FieldNotifCollectionAccessor(field));
}
}
Class<?> current = type;
while (current != null) {
for (Field field : current.getDeclaredFields()) {
if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) {
continue;
}
String key = field.getDeclaringClass().getName() + "#" + field.getName();
if (!fieldNames.add(key)) {
continue;
}
try {
field.setAccessible(true);
accessors.add(new FieldNotifCollectionAccessor(field));
} catch (Throwable ignored) {
// Best-effort source matching; inaccessible fields just do not contribute.
}
}
current = current.getSuperclass();
}
return accessors;
}
private Method findNoArgMethod(Class<?> type, String methodName) {
Class<?> current = type;
while (current != null) {
try {
Method method = current.getDeclaredMethod(methodName);
method.setAccessible(true);
return method;
} catch (NoSuchMethodException ignored) {
current = current.getSuperclass();
} catch (Throwable ignored) {
return null;
}
}
return null;
}
private Field findField(Class<?> type, String fieldName) {
Class<?> current = type;
while (current != null) {
try {
Field field = current.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
} catch (Throwable ignored) {
return null;
}
}
return null;
}
private void appendNotificationsFromNotifCollectionValue(
ArrayList<StatusBarNotification> notifications,
Object value
@@ -1185,7 +1287,7 @@ final class StockLayoutCanvasController {
if (hasVisibleConfirmedAodPluginView()) {
hideConfirmedAodPluginViews();
}
removeLockscreenCardsNotificationHost(root);
hideLockscreenCardsNotificationHost(root);
if (activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& isAodLayoutRoot(root)) {
renderAodCardsNotificationStripIfNeeded(root, settings, activeScene);
@@ -1229,8 +1331,8 @@ final class StockLayoutCanvasController {
if (activeScene != null && activeScene.isUnlocked()) {
if (isKnownRootGeometryChanged(root)) {
ownedIconHostManager.setUnlockedHostsVisible(root, false);
removeDebugOverlay(root);
}
removeDebugOverlay(root);
}
}
@@ -1515,8 +1617,12 @@ final class StockLayoutCanvasController {
}
private void scheduleUnlockedAppearanceRefreshForAllRoots() {
scheduleUnlockedAppearanceRefreshForAllRoots(false);
}
private void scheduleUnlockedAppearanceRefreshForAllRoots(boolean highPriority) {
for (View root : new ArrayList<>(unlockedHostsByRoot.keySet())) {
scheduleUnlockedAppearanceRefreshForRoot(root);
scheduleUnlockedAppearanceRefreshForRoot(root, highPriority);
}
}
@@ -1586,16 +1692,29 @@ final class StockLayoutCanvasController {
}
private void scheduleUnlockedAppearanceRefreshForRoot(View root) {
scheduleUnlockedAppearanceRefreshForRoot(root, false);
}
private void scheduleUnlockedAppearanceRefreshForRoot(View root, boolean highPriority) {
if (root == null
|| !root.isAttachedToWindow()
|| !isUnlockedStatusBarRoot(root)
|| !pendingUnlockedAppearanceRefreshRoots.add(root)) {
|| !isUnlockedStatusBarRoot(root)) {
return;
}
root.post(() -> {
pendingUnlockedAppearanceRefreshRoots.remove(root);
if (!pendingUnlockedAppearanceRefreshRoots.add(root) && !highPriority) {
return;
}
Runnable refresh = () -> {
if (!pendingUnlockedAppearanceRefreshRoots.remove(root)) {
return;
}
refreshUnlockedAppearanceNow(root);
});
};
if (highPriority && root.getHandler() != null) {
root.getHandler().postAtFrontOfQueue(refresh);
} else {
root.post(refresh);
}
}
private boolean clockTextRefreshEnabledForScene(SbtSettings settings, SceneKey scene) {
@@ -1762,13 +1881,15 @@ final class StockLayoutCanvasController {
return;
}
if (!layoutRoot) {
removeSharedLayoutHosts(root);
if (!unlockedScene && canContainLockedStatusBarIconSurfaces(root)) {
if (activeScene != null
&& activeScene.isLockscreen()
&& canContainLockedStatusBarIconSurfaces(root)) {
removeUnlockedLayoutHosts(root);
removeAodCardsNotificationHost(root);
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
hideOrDiscoverLockedStatusBarIconSurfaces(root);
} else {
removeLockscreenCardsNotificationHost(root);
removeAodCardsNotificationHost(root);
removeSharedLayoutHosts(root);
}
removeDebugOverlay(root);
return;
@@ -2562,8 +2683,7 @@ final class StockLayoutCanvasController {
// writes conflicting values both while tracking expansion and when it clears
// its state after collapse.
return !runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
&& (stackContains("LegacyQsExpandAnimator", "setQsExpansionPosition")
|| stackContains("LegacyQsExpandAnimator", "clearAnimationState"));
&& stackContainsConflictingShadeHeaderAnimator();
}
private boolean isSplitShadeStatusBarCandidate(View view) {
@@ -2579,16 +2699,18 @@ final class StockLayoutCanvasController {
return candidate;
}
private boolean stackContains(String classNamePart, String methodName) {
private boolean stackContainsConflictingShadeHeaderAnimator() {
StackTraceElement[] stack = Thread.currentThread().getStackTrace();
for (StackTraceElement element : stack) {
if (element == null) {
continue;
}
String className = element.getClassName();
String methodName = element.getMethodName();
if (className != null
&& className.contains(classNamePart)
&& methodName.equals(element.getMethodName())) {
&& className.contains("LegacyQsExpandAnimator")
&& ("setQsExpansionPosition".equals(methodName)
|| "clearAnimationState".equals(methodName))) {
return true;
}
}
@@ -2966,6 +3088,7 @@ final class StockLayoutCanvasController {
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
lockscreenCardsRowFilterSignatures.remove(root);
lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
removeDebugOverlay(root);
root.requestLayout();
@@ -3190,6 +3313,7 @@ final class StockLayoutCanvasController {
ArrayList<ImageView> imageViews = imageViewsFromArgs(args);
if (!imageViews.isEmpty()) {
latestAodIconImageViews = imageViews;
pruneViewFingerprintCache(imageViews);
if (!runtimeContext.getModeStateRepository().isLayoutEnabled()) {
return;
}
@@ -3201,6 +3325,10 @@ final class StockLayoutCanvasController {
if (!(containerView instanceof ViewGroup container)) {
return;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene == null || activeScene.isUnlocked()) {
return;
}
if (!isLayoutEnabled(container.getContext())) {
knownAodNotificationContainers.remove(container);
LockscreenCardsNotificationIconSourceStore.clearAodSources(container);
@@ -3218,6 +3346,10 @@ final class StockLayoutCanvasController {
if (knownAodNotificationContainers.isEmpty()) {
return;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene == null || activeScene.isUnlocked()) {
return;
}
for (ViewGroup container : knownAodNotificationContainers) {
if (container == null || !container.isAttachedToWindow()) {
continue;
@@ -3345,9 +3477,18 @@ final class StockLayoutCanvasController {
|| !AppIconRules.isValidPackageName(sbn.getPackageName())) {
return null;
}
String cacheKey = notificationFingerprintCacheKey(sbn);
IconFingerprint cached = notificationFingerprintByCacheKey.get(cacheKey);
if (cached != null) {
return cached;
}
try {
Drawable drawable = sbn.getNotification().getSmallIcon().loadDrawable(context);
return fingerprintForDrawable(drawable, sbn.getPackageName());
IconFingerprint fingerprint = fingerprintForDrawable(drawable, sbn.getPackageName());
if (fingerprint != null) {
notificationFingerprintByCacheKey.put(cacheKey, fingerprint);
}
return fingerprint;
} catch (Throwable ignored) {
return null;
}
@@ -3357,7 +3498,57 @@ final class StockLayoutCanvasController {
if (imageView == null || imageView.getDrawable() == null) {
return null;
}
return fingerprintForDrawable(imageView.getDrawable(), null);
Drawable drawable = imageView.getDrawable();
DrawableFingerprintCache cached = viewFingerprintCacheByImageView.get(imageView);
if (cached != null && cached.drawable == drawable) {
return cached.fingerprint;
}
IconFingerprint fingerprint = fingerprintForDrawable(drawable, null);
if (fingerprint != null) {
viewFingerprintCacheByImageView.put(imageView, new DrawableFingerprintCache(drawable, fingerprint));
} else {
viewFingerprintCacheByImageView.remove(imageView);
}
return fingerprint;
}
private String notificationFingerprintCacheKey(StatusBarNotification sbn) {
Notification notification = sbn != null ? sbn.getNotification() : null;
Object smallIcon = notification != null ? notification.getSmallIcon() : null;
return sbn.getKey()
+ "|"
+ sbn.getPackageName()
+ "|"
+ sbn.getId()
+ "|"
+ sbn.getPostTime()
+ "|"
+ (notification != null ? notification.when : 0L)
+ "|"
+ System.identityHashCode(smallIcon);
}
private void pruneNotificationFingerprintCache(ArrayList<StatusBarNotification> notifications) {
if (notificationFingerprintByCacheKey.isEmpty()) {
return;
}
HashSet<String> activeCacheKeys = new HashSet<>();
if (notifications != null) {
for (StatusBarNotification sbn : notifications) {
if (sbn != null && sbn.getNotification() != null) {
activeCacheKeys.add(notificationFingerprintCacheKey(sbn));
}
}
}
notificationFingerprintByCacheKey.keySet().removeIf(key -> !activeCacheKeys.contains(key));
}
private void pruneViewFingerprintCache(ArrayList<ImageView> imageViews) {
if (viewFingerprintCacheByImageView.isEmpty()) {
return;
}
HashSet<ImageView> activeViews = new HashSet<>(imageViews);
viewFingerprintCacheByImageView.keySet().removeIf(view -> !activeViews.contains(view));
}
private IconFingerprint fingerprintForDrawable(Drawable drawable, String packageName) {
@@ -3708,6 +3899,19 @@ final class StockLayoutCanvasController {
clockTimeBucketByRoot.remove(root);
}
private void removeUnlockedLayoutHosts(View root) {
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST);
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST);
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST);
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_UNLOCKED_CARRIER_HOST);
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST);
unlockedHostsByRoot.remove(root);
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
}
private boolean isDescendantOfNotificationShadeWindow(View view) {
View current = view;
while (current != null) {
@@ -3768,13 +3972,37 @@ final class StockLayoutCanvasController {
|| activeScene == null
|| !activeScene.isLockscreen()
|| activeScene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS
|| !isNotificationShadeWindowRoot(root)
|| suppressForTransition) {
removeLockscreenCardsNotificationHost(root);
lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
return;
}
View host = ownedIconHostManager.ensureLockscreenCardsNotificationHost(root);
if (host instanceof ViewGroup hostGroup) {
lockscreenCardsNotificationStripRenderer.render(root, hostGroup, settings, activeScene);
if (host.getVisibility() != View.VISIBLE) {
host.setVisibility(View.VISIBLE);
}
setHostAlpha(host, 1f);
int settingsId = System.identityHashCode(settings);
int rootWidth = root.getWidth();
int rootHeight = root.getHeight();
int activeKeyGeneration = NotificationActiveKeyStore.generation();
int sourceSignature =
LockscreenCardsNotificationIconSourceStore.snapshotSourceSignatureForRoot(root);
int generation = settingsId;
generation = 31 * generation + rootWidth;
generation = 31 * generation + rootHeight;
generation = 31 * generation + activeKeyGeneration;
generation = 31 * generation + sourceSignature;
Integer previous = lockscreenCardsNotificationRenderGenerationByRoot.get(root);
boolean shouldRender = previous == null
|| previous != generation
|| hostGroup.getChildCount() <= 0;
if (shouldRender) {
lockscreenCardsNotificationRenderGenerationByRoot.put(root, generation);
lockscreenCardsNotificationStripRenderer.render(root, hostGroup, settings, activeScene);
}
host.bringToFront();
}
}
@@ -3933,10 +4161,27 @@ final class StockLayoutCanvasController {
|| className.contains("keyguardpattern");
}
private void hideLockscreenCardsNotificationHost(View root) {
View host = root instanceof ViewGroup group
? findDirectOwnedHost(group, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST)
: null;
if (host == null) {
return;
}
if (host.getVisibility() != View.INVISIBLE) {
host.setVisibility(View.INVISIBLE);
}
setHostAlpha(host, 0f);
}
private void removeLockscreenCardsNotificationHost(View root) {
View host = root instanceof ViewGroup group
? findDirectOwnedHost(group, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST)
: null;
if (host == null) {
return;
}
lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
if (host instanceof ViewGroup hostGroup) {
lockscreenCardsNotificationStripRenderer.clear(hostGroup);
}
@@ -4136,6 +4381,33 @@ final class StockLayoutCanvasController {
void visit(View view);
}
@FunctionalInterface
private interface NotifCollectionAccessor {
Object get(Object collection);
}
private record MethodNotifCollectionAccessor(Method method) implements NotifCollectionAccessor {
@Override
public Object get(Object collection) {
try {
return method.invoke(collection);
} catch (Throwable ignored) {
return null;
}
}
}
private record FieldNotifCollectionAccessor(Field field) implements NotifCollectionAccessor {
@Override
public Object get(Object collection) {
try {
return field.get(collection);
} catch (Throwable ignored) {
return null;
}
}
}
private static final class HiddenViewState {
final int visibility;
final float alpha;
@@ -4146,6 +4418,9 @@ final class StockLayoutCanvasController {
}
}
private record DrawableFingerprintCache(Drawable drawable, IconFingerprint fingerprint) {
}
private record IconFingerprint(String packageName, boolean[] mask, int filled) {
int distance(IconFingerprint other) {
if (other == null || other.mask == null || mask == null || other.mask.length != mask.length) {
@@ -128,6 +128,15 @@ final class ClockLayout {
TextView source,
LayoutPosition position,
int sourceBaselineY
) {
return simpleText(source, position, sourceBaselineY, Integer.MAX_VALUE);
}
static ClockLayout simpleText(
TextView source,
LayoutPosition position,
int sourceBaselineY,
int maxWidth
) {
if (source == null || source.getText() == null || source.getText().length() == 0) {
return null;
@@ -141,12 +150,13 @@ final class ClockLayout {
if (measured.width <= 0 || measured.height <= 0) {
return null;
}
int width = Math.max(1, Math.min(measured.width, maxWidth));
int top = sourceBaselineY - measured.baseline;
ClockRow row = new ClockRow(
source.getText(),
0,
0,
measured.width,
width,
measured.height,
0,
0,
@@ -161,7 +171,7 @@ final class ClockLayout {
top,
0,
false,
measured.width,
width,
measured.height);
}
@@ -1,5 +1,6 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.text.TextUtils;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.view.View;
@@ -8,9 +9,12 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Objects;
import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
final class ClockProxyRenderer {
private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>();
@@ -50,6 +54,12 @@ final class ClockProxyRenderer {
if (copyTextStyleIfChanged(placement.source, view)) {
hostChanged = true;
}
if (applyTextColorOverrideIfNeeded(view, placement.textColorOverride)) {
hostChanged = true;
}
if (applyCarrierMarqueeIfNeeded(placement.source, view)) {
hostChanged = true;
}
if (!sameTextAndColorSpans(view.getText(), row.text)) {
view.setText(row.text);
hostChanged = true;
@@ -90,6 +100,35 @@ final class ClockProxyRenderer {
}
}
private static boolean applyCarrierMarqueeIfNeeded(TextView source, TextView target) {
boolean carrier = isCarrierTextView(source);
boolean changed = false;
TextUtils.TruncateAt desiredEllipsize = carrier ? TextUtils.TruncateAt.MARQUEE : null;
if (target.getEllipsize() != desiredEllipsize) {
target.setEllipsize(desiredEllipsize);
changed = true;
}
int desiredRepeatLimit = carrier ? -1 : 0;
if (target.getMarqueeRepeatLimit() != desiredRepeatLimit) {
target.setMarqueeRepeatLimit(desiredRepeatLimit);
changed = true;
}
if (target.isSelected() != carrier) {
target.setSelected(carrier);
changed = true;
}
return changed;
}
private static boolean isCarrierTextView(TextView view) {
if (view == null) {
return false;
}
String idName = ViewIdNames.idName(view).toLowerCase(Locale.ROOT);
String className = view.getClass().getName().toLowerCase(Locale.ROOT);
return idName.contains("carrier") || className.contains("carrier");
}
private static boolean sameTextAndColorSpans(CharSequence current, CharSequence desired) {
if (!String.valueOf(current).contentEquals(String.valueOf(desired))) {
return false;
@@ -189,6 +228,17 @@ final class ClockProxyRenderer {
return changed;
}
private static boolean applyTextColorOverrideIfNeeded(TextView view, int color) {
if (view == null || (color >>> 24) == 0) {
return false;
}
if (view.getCurrentTextColor() == color) {
return false;
}
view.setTextColor(color);
return true;
}
private static boolean applyTextLayoutParams(
ViewGroup host,
TextView view,
@@ -11,17 +11,29 @@ final class ClockPlacement {
final String contentDescription;
final ArrayList<ClockRow> rows;
final Bounds bounds;
final int textColorOverride;
ClockPlacement(
TextView source,
String contentDescription,
ArrayList<ClockRow> rows,
Bounds bounds
) {
this(source, contentDescription, rows, bounds, android.graphics.Color.TRANSPARENT);
}
ClockPlacement(
TextView source,
String contentDescription,
ArrayList<ClockRow> rows,
Bounds bounds,
int textColorOverride
) {
this.source = source;
this.contentDescription = contentDescription;
this.rows = rows;
this.bounds = bounds;
this.textColorOverride = textColorOverride;
}
}
@@ -257,6 +257,35 @@ public final class LockscreenCardsNotificationIconSourceStore {
return result;
}
public static int snapshotSourceSignatureForRoot(View root) {
ArrayList<SnapshotSource> sources = snapshotSourcesForRoot(root);
int result = 17;
for (SnapshotSource source : sources) {
result = 31 * result + snapshotSourceSignature(root, source);
}
return result;
}
private static int snapshotSourceSignature(View root, SnapshotSource source) {
if (source == null) {
return 0;
}
int result = 17;
View view = source.view();
result = 31 * result + System.identityHashCode(view);
result = 31 * result + (source.keyHint() != null ? source.keyHint().hashCode() : 0);
result = 31 * result + (source.mode() != null ? source.mode().hashCode() : 0);
Bounds override = source.boundsOverride();
result = 31 * result + (override != null ? override.signature() : 0);
Bounds bounds = ViewGeometry.boundsRelativeTo(root, view);
result = 31 * result + (bounds != null ? bounds.signature() : 0);
if (view != null) {
result = 31 * result + view.getVisibility();
result = 31 * result + Float.floatToIntBits(view.getAlpha());
}
return result;
}
static ArrayList<SnapshotSource> snapshotSourcesForAodIconsRoot(View root) {
ArrayList<SnapshotSource> result = new ArrayList<>();
ViewGroup container = sourceContainerForAodIconsRoot(root);
@@ -180,7 +180,8 @@ record SnapshotRenderState(
String heldSignalKey,
int heldBitmapId,
SnapshotMode sourceMode,
int sourceSignature
int sourceSignature,
int tintSignature
) {
}
@@ -6,6 +6,7 @@ import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
@@ -45,9 +46,12 @@ final class SnapshotRenderer {
new WeakHashMap<>();
private final WeakHashMap<View, SourceSignatureCache> sourceSignaturesByView =
new WeakHashMap<>();
private final WeakHashMap<View, ContentBoundsCache> contentBoundsByView =
new WeakHashMap<>();
private final LinkedHashMap<String, HeldSignal> heldSignals = new LinkedHashMap<>();
private long holdSignalsUntilElapsedMs;
private boolean statusBarTintTargetEnabled;
SnapshotRenderer(
boolean holdSignalsDuringTransition,
@@ -65,6 +69,14 @@ final class SnapshotRenderer {
}
}
void setStatusBarTintTargetEnabled(boolean enabled) {
statusBarTintTargetEnabled = enabled;
}
int statusBarTintColor() {
return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : Color.TRANSPARENT;
}
ArrayList<SnapshotPlacement> rawPlacements(
View root,
ArrayList<SnapshotSource> sources
@@ -78,6 +90,7 @@ final class SnapshotRenderer {
boolean sortByX
) {
ArrayList<SnapshotPlacement> placements = new ArrayList<>();
LinkedHashMap<String, Integer> keyCounts = new LinkedHashMap<>();
int order = 0;
for (SnapshotSource source : sources) {
Bounds bounds = source.boundsOverride() != null
@@ -87,7 +100,7 @@ final class SnapshotRenderer {
if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE) {
bounds = notificationDrawableBounds(bounds);
}
String key = normalizedKeyFor(source, order);
String key = uniqueKey(normalizedKeyFor(source, order), keyCounts);
boolean signal = isSignalKey(key);
boolean trimContent = !signal && shouldTrimContent(source);
Bounds contentBounds = trimContent ? snapshotContentBounds(source, bounds) : null;
@@ -138,14 +151,44 @@ final class SnapshotRenderer {
return placements;
}
private String uniqueKey(String key, LinkedHashMap<String, Integer> keyCounts) {
if (key == null || keyCounts == null) {
return key;
}
int count = keyCounts.getOrDefault(key, 0);
keyCounts.put(key, count + 1);
return count == 0 ? key : key + "#" + count;
}
Bounds snapshotContentBounds(SnapshotSource source, Bounds bounds) {
if (source == null || bounds == null || bounds.width <= 0 || bounds.height <= 0) {
return null;
}
int signature = sourceSignature(source);
View sourceView = source.view();
if (sourceView != null) {
ContentBoundsCache cached = contentBoundsByView.get(sourceView);
if (cached != null
&& cached.signature == signature
&& cached.width == bounds.width
&& cached.height == bounds.height
&& cached.mode == source.mode()) {
return cached.bounds;
}
}
Bitmap bitmap = Bitmap.createBitmap(bounds.width, bounds.height, Bitmap.Config.ARGB_8888);
try {
drawSnapshot(source, bitmap);
return bitmapContentBounds(bitmap);
Bounds contentBounds = bitmapContentBounds(bitmap);
if (sourceView != null && contentBounds != null) {
contentBoundsByView.put(sourceView, new ContentBoundsCache(
signature,
bounds.width,
bounds.height,
source.mode(),
contentBounds));
}
return contentBounds;
} finally {
bitmap.recycle();
}
@@ -204,6 +247,8 @@ final class SnapshotRenderer {
drawAppIcon(host.getContext(), placement.source(), bitmap);
} else if (placement.source() != null) {
drawSnapshot(placement, bitmap);
applyStatusBarTintTarget(placement.source(), bitmap);
normalizeSemiTransparentDarkIcon(bitmap);
if (placement.signal()) {
saveHeldSignal(key, bounds, bitmap);
}
@@ -516,7 +561,8 @@ final class SnapshotRenderer {
placement.heldSignal() != null ? placement.heldSignal().key : "",
placement.heldSignal() != null ? System.identityHashCode(placement.heldSignal().bitmap) : 0,
placement.source() != null ? placement.source().mode() : null,
placement.source() != null ? sourceSignature(placement.source()) : 0);
placement.source() != null ? sourceSignature(placement.source()) : 0,
tintSignatureFor(placement.source()));
}
private int sourceSignature(SnapshotSource source) {
@@ -527,9 +573,14 @@ final class SnapshotRenderer {
result = 31 * result + sourceTreeSignature(source.view());
Bounds boundsOverride = source.boundsOverride();
result = 31 * result + (boundsOverride != null ? boundsOverride.signature() : 0);
result = 31 * result + tintSignatureFor(source);
return result;
}
private int tintSignatureFor(SnapshotSource source) {
return shouldApplyStatusBarTintTarget(source) ? StatusBarTintTarget.signature() : 0;
}
private boolean isStatusBarIconView(View view) {
return view != null && view.getClass().getName().contains("StatusBarIconView");
}
@@ -626,6 +677,15 @@ final class SnapshotRenderer {
private record SourceSignatureCache(int stamp, int signature) {
}
private record ContentBoundsCache(
int signature,
int width,
int height,
SnapshotMode mode,
Bounds bounds
) {
}
private void drawSnapshot(SnapshotSource source, Bitmap bitmap) {
drawSnapshot(source, bitmap, 0);
}
@@ -724,6 +784,100 @@ final class SnapshotRenderer {
}
}
private void applyStatusBarTintTarget(SnapshotSource source, Bitmap bitmap) {
if (!shouldApplyStatusBarTintTarget(source)
|| bitmap == null
|| bitmap.isRecycled()
|| bitmap.getWidth() <= 0
|| bitmap.getHeight() <= 0) {
return;
}
if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE && !isTintableMonochrome(bitmap)) {
return;
}
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(StatusBarTintTarget.color(), PorterDuff.Mode.SRC_IN);
}
private boolean shouldApplyStatusBarTintTarget(SnapshotSource source) {
return statusBarTintTargetEnabled
&& source != null
&& source.mode() != SnapshotMode.APP_ICON
&& source.mode() != SnapshotMode.STATUS_CHIP;
}
private boolean isTintableMonochrome(Bitmap bitmap) {
int coloredPixels = 0;
int monochromePixels = 0;
for (int y = 0; y < bitmap.getHeight(); y++) {
for (int x = 0; x < bitmap.getWidth(); x++) {
int color = bitmap.getPixel(x, y);
int alpha = Color.alpha(color);
if (alpha == 0) {
continue;
}
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
int spread = Math.max(red, Math.max(green, blue)) - Math.min(red, Math.min(green, blue));
if (spread <= 18) {
monochromePixels++;
} else {
coloredPixels++;
}
}
}
int total = monochromePixels + coloredPixels;
if (total == 0) {
return false;
}
// Anti-aliasing and compression can introduce tiny color noise, but real
// app/debug icons contain enough colored pixels to fail this threshold.
return coloredPixels <= Math.max(2, total / 20);
}
private void normalizeSemiTransparentDarkIcon(Bitmap bitmap) {
if (bitmap == null || bitmap.isRecycled()) {
return;
}
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int maxAlpha = 0;
int darkPixels = 0;
int nonDarkPixels = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int color = bitmap.getPixel(x, y);
int alpha = Color.alpha(color);
if (alpha == 0) {
continue;
}
if (Color.red(color) <= 48 && Color.green(color) <= 48 && Color.blue(color) <= 48) {
darkPixels++;
maxAlpha = Math.max(maxAlpha, alpha);
} else {
nonDarkPixels++;
}
}
}
if (darkPixels == 0 || nonDarkPixels > darkPixels / 8 || maxAlpha < 64 || maxAlpha >= 250) {
return;
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int color = bitmap.getPixel(x, y);
int alpha = Color.alpha(color);
if (alpha == 0) {
continue;
}
if (Color.red(color) <= 48 && Color.green(color) <= 48 && Color.blue(color) <= 48) {
int scaledAlpha = Math.min(255, Math.round(alpha * 255f / maxAlpha));
bitmap.setPixel(x, y, Color.argb(scaledAlpha, 0, 0, 0));
}
}
}
}
private Bounds effectiveSourceRenderBounds(
SnapshotPlacement placement,
Bounds requestedBounds,
@@ -0,0 +1,30 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.graphics.Color;
public final class StatusBarTintTarget {
private static volatile int color = Color.WHITE;
private static volatile int generation;
private StatusBarTintTarget() {
}
public static boolean setDarkIcons(boolean darkIcons) {
int nextColor = darkIcons ? Color.BLACK : Color.WHITE;
if (color == nextColor) {
return false;
}
color = nextColor;
generation++;
return true;
}
static int color() {
return color;
}
static int signature() {
return 31 * generation + color;
}
}
@@ -42,6 +42,7 @@ public final class UnlockedIconSnapshotRenderController {
private final WeakHashMap<View, Integer> lastClockSourceGeometryByRoot = new WeakHashMap<>();
private final WeakHashMap<View, Integer> lastClockAppearanceByRoot = new WeakHashMap<>();
private final WeakHashMap<View, Boolean> forcedChipRefreshByRoot = new WeakHashMap<>();
private boolean statusBarTintTargetEnabled;
public UnlockedIconSnapshotRenderController() {
sourceCollector = new UnlockedStockSourceCollector();
@@ -75,6 +76,7 @@ public final class UnlockedIconSnapshotRenderController {
return new RenderResult(true, true, true);
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
setStatusBarTintTargetEnabled(scene.isUnlocked());
layoutHostToRoot(root, clockHost);
layoutHostToRoot(root, carrierHost);
layoutHostToRoot(root, chipHost);
@@ -209,6 +211,7 @@ public final class UnlockedIconSnapshotRenderController {
lastClockGeometryByRoot.put(root, currentClockGeometry != null ? currentClockGeometry : 0);
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons);
layoutPlan = withCurrentClockAppearance(root, layoutPlan, collectedIcons, settings);
lastLayoutPlanByRoot.put(root, layoutPlan);
if (clockEnabledForScene(settings, scene) && layoutPlan.clockPlacement != null) {
clockRenderer.render(clockHost, layoutPlan.clockPlacement);
@@ -257,6 +260,7 @@ public final class UnlockedIconSnapshotRenderController {
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
setStatusBarTintTargetEnabled(true);
CollectedIcons collectedIcons = lastCollectedIconsByRoot.get(root);
LayoutPlan updatedPlan = withCurrentDotColors(layoutPlan, collectedIcons);
if (updatedPlan != layoutPlan) {
@@ -304,6 +308,7 @@ public final class UnlockedIconSnapshotRenderController {
if (!settings.layoutChipEnabledUnlocked) {
return false;
}
setStatusBarTintTargetEnabled(true);
chipRenderer.renderPlacements(chipHost, layoutPlan.chipPlacements);
return true;
}
@@ -325,6 +330,7 @@ public final class UnlockedIconSnapshotRenderController {
return false;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
setStatusBarTintTargetEnabled(scene.isUnlocked());
if (!clockEnabledForScene(settings, scene)) {
return false;
}
@@ -504,6 +510,10 @@ public final class UnlockedIconSnapshotRenderController {
}
private int sourceColor(SnapshotSource source) {
int targetColor = statusBarTintTargetColor();
if (hasAlpha(targetColor)) {
return targetColor;
}
if (source == null || source.view() == null) {
return 0;
}
@@ -545,6 +555,10 @@ public final class UnlockedIconSnapshotRenderController {
}
private int clockReferenceColor(ClockPlacement placement, CollectedIcons icons) {
int targetColor = statusBarTintTargetColor();
if (hasAlpha(targetColor)) {
return targetColor;
}
if (icons != null && icons.statusIcons != null) {
for (SnapshotSource source : icons.statusIcons) {
int color = sourceColor(source);
@@ -567,6 +581,21 @@ public final class UnlockedIconSnapshotRenderController {
return android.graphics.Color.WHITE;
}
private void setStatusBarTintTargetEnabled(boolean enabled) {
statusBarTintTargetEnabled = enabled;
statusRenderer.setStatusBarTintTargetEnabled(enabled);
notificationRenderer.setStatusBarTintTargetEnabled(enabled);
chipRenderer.setStatusBarTintTargetEnabled(false);
}
private int statusBarTintTargetColor() {
return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : android.graphics.Color.TRANSPARENT;
}
private boolean hasAlpha(int color) {
return (color >>> 24) != 0;
}
private int clockAppearanceSignature(
ClockPlacement placement,
int referenceColor,
@@ -574,6 +603,7 @@ public final class UnlockedIconSnapshotRenderController {
) {
int result = 17;
result = 31 * result + referenceColor;
result = 31 * result + (placement != null ? placement.textColorOverride : 0);
result = 31 * result + clockTextSettingsSignature(settings);
if (placement != null && placement.source != null) {
result = 31 * result + placement.source.getCurrentTextColor();
@@ -839,7 +869,8 @@ public final class UnlockedIconSnapshotRenderController {
source,
model.contentDescription,
rows,
previousPlacement.bounds);
previousPlacement.bounds,
statusBarTintTargetColor());
}
private Integer clockSourceGeometrySignature(TextView source) {
@@ -894,7 +925,12 @@ public final class UnlockedIconSnapshotRenderController {
previousRow.rowIndex,
previousRow.segment));
}
return new ClockPlacement(previous.source, currentText.contentDescription, rows, previous.bounds);
return new ClockPlacement(
previous.source,
currentText.contentDescription,
rows,
previous.bounds,
currentText.textColorOverride);
}
private ClockRow matchingClockRow(ClockRow previousRow, ArrayList<ClockRow> currentRows) {
@@ -558,6 +558,10 @@ final class UnlockedLayoutBand {
}
private int sourceColor(SnapshotPlacement placement) {
int targetColor = renderer != null ? renderer.statusBarTintColor() : android.graphics.Color.TRANSPARENT;
if (hasAlpha(targetColor)) {
return targetColor;
}
if (placement == null || placement.source == null || placement.source.view() == null) {
return 0;
}
@@ -567,6 +571,10 @@ final class UnlockedLayoutBand {
return android.graphics.Color.alpha(color) != 0 ? color : 0;
}
private boolean hasAlpha(int color) {
return (color >>> 24) != 0;
}
private SnapshotPlacement emptyIconContainerPlacement() {
int height = Math.max(1, dotHeight(activePlacements()));
return new SnapshotPlacement(
@@ -699,7 +707,8 @@ final class UnlockedLayoutBand {
placement.source,
placement.contentDescription,
rows,
new Bounds(clipLeft, placement.bounds.top, Math.max(0, clipRight - clipLeft), placement.bounds.height));
new Bounds(clipLeft, placement.bounds.top, Math.max(0, clipRight - clipLeft), placement.bounds.height),
placement.textColorOverride);
}
private boolean isTextKind() {
@@ -87,6 +87,7 @@ final class UnlockedLayoutPlanner {
Bounds aodStatusbarBounds = scene != null && scene.isAod()
? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds)
: null;
LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds);
int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds);
if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) {
bands.add(UnlockedLayoutBand.clock(
@@ -95,7 +96,14 @@ final class UnlockedLayoutPlanner {
positionFor(settings.clockPosition),
sideFor(settings.clockMiddleCutoutSide)));
}
ClockLayout carrierLayout = carrierLayout(root, collectedIcons, settings, scene);
ClockLayout carrierLayout = carrierLayout(
root,
collectedIcons,
settings,
scene,
edges,
layoutCutoutBounds,
clockLayout);
if (carrierLayout != null && carrierLayout.width() > 0) {
bands.add(UnlockedLayoutBand.carrier(
carrierHost,
@@ -257,7 +265,6 @@ final class UnlockedLayoutPlanner {
ArrayList<LayoutItemKind> itemOrder = orderedKinds(settings.layoutItemOrder);
splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings);
sortBandsForItemOrder(bands, itemOrder);
LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds);
int itemPadding = Math.max(0, settings.layoutItemPaddingPx);
solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding, scene);
@@ -634,7 +641,10 @@ final class UnlockedLayoutPlanner {
View root,
CollectedIcons collectedIcons,
SbtSettings settings,
SceneKey scene
SceneKey scene,
LayoutEdges edges,
Bounds cutoutBounds,
ClockLayout clockLayout
) {
if (root == null
|| collectedIcons == null
@@ -666,7 +676,96 @@ final class UnlockedLayoutPlanner {
return ClockLayout.simpleText(
collectedIcons.carrierView,
positionFor(settings.layoutCarrierPosition),
anchorBounds.top + baseline - verticalOffset);
anchorBounds.top + baseline - verticalOffset,
carrierMaxWidth(
root,
settings,
edges,
cutoutBounds,
positionFor(settings.layoutCarrierPosition),
sideFor(settings.layoutCarrierMiddleSide),
clockLayout));
}
private int carrierMaxWidth(
View root,
SbtSettings settings,
LayoutEdges edges,
Bounds cutoutBounds,
LayoutPosition position,
AnchorSide middleSide,
ClockLayout clockLayout
) {
int rootWidth = root != null ? root.getWidth() : 0;
if (rootWidth <= 0 || edges == null) {
return Integer.MAX_VALUE;
}
int left = Math.max(0, edges.left());
int right = Math.min(rootWidth, edges.right());
if (right <= left) {
return Integer.MAX_VALUE;
}
boolean useRightSide = position == LayoutPosition.RIGHT
|| position == LayoutPosition.MIDDLE && middleSide == AnchorSide.RIGHT;
boolean usableCutout = cutoutBounds != null
&& cutoutBounds.width > 0
&& cutoutBounds.left > left
&& cutoutBounds.left < right;
if (useRightSide) {
int minLeft = usableCutout
? Math.min(right, cutoutBounds.left + cutoutBounds.width)
: left;
return carrierMaxWidthWithClockReserve(
Math.max(1, right - minLeft),
settings,
position,
middleSide,
clockLayout,
AnchorSide.RIGHT);
}
int maxRight = usableCutout ? Math.max(left, cutoutBounds.left) : right;
return carrierMaxWidthWithClockReserve(
Math.max(1, maxRight - left),
settings,
position,
middleSide,
clockLayout,
AnchorSide.LEFT);
}
private int carrierMaxWidthWithClockReserve(
int sideWidth,
SbtSettings settings,
LayoutPosition carrierPosition,
AnchorSide carrierMiddleSide,
ClockLayout clockLayout,
AnchorSide side
) {
if (sideWidth <= 0 || settings == null || clockLayout == null || clockLayout.width() <= 0) {
return Math.max(1, sideWidth);
}
LayoutPosition clockPosition = positionFor(settings.clockPosition);
AnchorSide clockMiddleSide = sideFor(settings.clockMiddleCutoutSide);
if (!occupiesSide(clockPosition, clockMiddleSide, side)
|| !occupiesSide(carrierPosition, carrierMiddleSide, side)) {
return Math.max(1, sideWidth);
}
int reserved = clockLayout.width() + Math.max(0, settings.layoutItemPaddingPx);
return Math.max(1, sideWidth - reserved);
}
private boolean occupiesSide(
LayoutPosition position,
AnchorSide middleSide,
AnchorSide side
) {
if (position == LayoutPosition.LEFT) {
return side == AnchorSide.LEFT;
}
if (position == LayoutPosition.RIGHT) {
return side == AnchorSide.RIGHT;
}
return middleSide == side;
}
private int statusCountForScene(SbtSettings settings, SceneKey scene) {
@@ -1539,7 +1638,12 @@ final class UnlockedLayoutPlanner {
row.segment));
}
}
return new ClockPlacement(first.source, first.contentDescription, rows, union);
return new ClockPlacement(
first.source,
first.contentDescription,
rows,
union,
first.textColorOverride);
}
static Bounds cutoutBounds(Rect cutout, int cutoutGap) {