diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java index 08fcc3e..adb6aa0 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java @@ -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 debugOverlaySignatures = new WeakHashMap<>(); private final Map settingsIdentityByRoot = new WeakHashMap<>(); private final Map lockscreenCardsRowFilterSignatures = new WeakHashMap<>(); + private final Map lockscreenCardsNotificationRenderGenerationByRoot = + new WeakHashMap<>(); private final Map clockTimeBucketByRoot = new WeakHashMap<>(); private final Map aodCardsNotificationRenderGenerationByRoot = new WeakHashMap<>(); private final Map aodSourceGeometryByView = new WeakHashMap<>(); private final Map aodNotificationWidgetSteadyWidths = new WeakHashMap<>(); private final Map aodNotificationIconOnlyAreaSteadyLefts = new WeakHashMap<>(); private final Map originalAodNotificationWidgetLayoutWidths = new WeakHashMap<>(); + private final Map, ArrayList> notifCollectionAccessorsByClass = + new WeakHashMap<>(); + private final Map notificationFingerprintByCacheKey = new HashMap<>(); + private final Map viewFingerprintCacheByImageView = + new WeakHashMap<>(); private final Set confirmedAodPluginViews = Collections.newSetFromMap(new WeakHashMap<>()); private final Map 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 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 notifCollectionAccessors(Class type) { + ArrayList cached = notifCollectionAccessorsByClass.get(type); + if (cached != null) { + return cached; + } + ArrayList accessors = buildNotifCollectionAccessors(type); + notifCollectionAccessorsByClass.put(type, accessors); + return accessors; + } + + private ArrayList buildNotifCollectionAccessors(Class type) { + ArrayList accessors = new ArrayList<>(); + HashSet 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 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 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 notifications) { + if (notificationFingerprintByCacheKey.isEmpty()) { + return; + } + HashSet 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 imageViews) { + if (viewFingerprintCacheByImageView.isEmpty()) { + return; + } + HashSet 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) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java index 1a8ed23..6cefe72 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockLayout.java @@ -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); } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockProxyRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockProxyRenderer.java index 8a7020f..9075696 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockProxyRenderer.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockProxyRenderer.java @@ -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> 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, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockRenderModels.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockRenderModels.java index 246af63..1e297ff 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockRenderModels.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/ClockRenderModels.java @@ -11,17 +11,29 @@ final class ClockPlacement { final String contentDescription; final ArrayList rows; final Bounds bounds; + final int textColorOverride; ClockPlacement( TextView source, String contentDescription, ArrayList rows, Bounds bounds + ) { + this(source, contentDescription, rows, bounds, android.graphics.Color.TRANSPARENT); + } + + ClockPlacement( + TextView source, + String contentDescription, + ArrayList rows, + Bounds bounds, + int textColorOverride ) { this.source = source; this.contentDescription = contentDescription; this.rows = rows; this.bounds = bounds; + this.textColorOverride = textColorOverride; } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationIconSourceStore.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationIconSourceStore.java index a7ca057..ada268b 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationIconSourceStore.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationIconSourceStore.java @@ -257,6 +257,35 @@ public final class LockscreenCardsNotificationIconSourceStore { return result; } + public static int snapshotSourceSignatureForRoot(View root) { + ArrayList 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 snapshotSourcesForAodIconsRoot(View root) { ArrayList result = new ArrayList<>(); ViewGroup container = sourceContainerForAodIconsRoot(root); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotModels.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotModels.java index c9400f7..31b8dc2 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotModels.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotModels.java @@ -180,7 +180,8 @@ record SnapshotRenderState( String heldSignalKey, int heldBitmapId, SnapshotMode sourceMode, - int sourceSignature + int sourceSignature, + int tintSignature ) { } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java index b49d7ae..886cdc5 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java @@ -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 sourceSignaturesByView = new WeakHashMap<>(); + private final WeakHashMap contentBoundsByView = + new WeakHashMap<>(); private final LinkedHashMap 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 rawPlacements( View root, ArrayList sources @@ -78,6 +90,7 @@ final class SnapshotRenderer { boolean sortByX ) { ArrayList placements = new ArrayList<>(); + LinkedHashMap 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 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, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusBarTintTarget.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusBarTintTarget.java new file mode 100644 index 0000000..37dc12c --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusBarTintTarget.java @@ -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; + } + +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java index c27305a..fbae374 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java @@ -42,6 +42,7 @@ public final class UnlockedIconSnapshotRenderController { private final WeakHashMap lastClockSourceGeometryByRoot = new WeakHashMap<>(); private final WeakHashMap lastClockAppearanceByRoot = new WeakHashMap<>(); private final WeakHashMap 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 currentRows) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java index e6f08cd..3d917b7 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java @@ -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() { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java index 8007cc8..bdb9a72 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java @@ -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 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) {