diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/notifications/StockUnlockedNotificationIconsController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/notifications/StockUnlockedNotificationIconsController.java index 38db56c..a5763f0 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/notifications/StockUnlockedNotificationIconsController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/notifications/StockUnlockedNotificationIconsController.java @@ -17,6 +17,7 @@ import java.util.WeakHashMap; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport; +import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; @@ -46,25 +47,31 @@ final class StockUnlockedNotificationIconsController { Class containerClass = ReflectionSupport.requireClass(CONTAINER_CLASS_NAME, classLoader); XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> { Object result = chain.proceed(); - if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) { - trackContainer(view); - view.post(() -> refreshContainer(view)); + if (chain.getThisObject() instanceof View view) { + if (shouldManageContainer(view)) { + trackContainer(view); + view.post(() -> refreshContainer(view)); + } } return result; }); XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> { - if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) { - trackContainer(view); - applyLimitIfNeeded(view); + if (chain.getThisObject() instanceof View view) { + if (shouldManageContainer(view)) { + trackContainer(view); + applyLimitIfNeeded(view); + } } return chain.proceed(); }); XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onLayout", chain -> { Object result = chain.proceed(); - if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) { - trackContainer(view); - if (isFeatureEnabled(view.getContext())) { - enforceParentWidth(view); + if (chain.getThisObject() instanceof View view) { + if (shouldManageContainer(view)) { + trackContainer(view); + if (isFeatureEnabled(view.getContext())) { + enforceParentWidth(view); + } } } return result; @@ -236,6 +243,9 @@ final class StockUnlockedNotificationIconsController { } private boolean shouldManageContainer(View view) { + if (!isUnlockedScene()) { + return false; + } if (isKeyguardContainer(view)) { return false; } @@ -260,6 +270,11 @@ final class StockUnlockedNotificationIconsController { return inUnlockedStatusBarIconArea; } + private boolean isUnlockedScene() { + SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene(); + return scene != null && scene.isUnlocked(); + } + private String resolveIdName(View view) { if (view == null || view.getId() == View.NO_ID) { return ""; 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 f7d89ff..cc1996c 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,15 +1,23 @@ package se.ajpanton.statusbartweak.runtime.layout; import android.app.KeyguardManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.content.Context; import android.content.res.Resources; import android.os.Looper; import android.os.PowerManager; +import android.service.notification.StatusBarNotification; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Proxy; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -24,16 +32,20 @@ import java.util.function.Predicate; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; import se.ajpanton.statusbartweak.runtime.mode.SystemNotificationDisplayStyleDetector; +import se.ajpanton.statusbartweak.runtime.render.AodStatusbarSourceStore; import se.ajpanton.statusbartweak.runtime.render.DebugBoundsOverlayController; import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationIconSourceStore; import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationStripRenderer; import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager; +import se.ajpanton.statusbartweak.runtime.render.StatusIconSlotVisibilityStore; import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController; import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; +import se.ajpanton.statusbartweak.shell.settings.AppIconRules; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class StockLayoutCanvasController { @@ -72,12 +84,21 @@ final class StockLayoutCanvasController { private final Map settingsIdentityByRoot = new WeakHashMap<>(); private final Map lockscreenCardsRowFilterSignatures = 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 Set confirmedAodPluginViews = Collections.newSetFromMap(new WeakHashMap<>()); + private final Map aodPluginVisualAlphaByView = new WeakHashMap<>(); private final Set layoutOffCleanedRoots = Collections.newSetFromMap(new WeakHashMap<>()); private final Set trackedStatusChipSources = Collections.newSetFromMap(new WeakHashMap<>()); + private final Map statusChipContentSignatureByRoot = new WeakHashMap<>(); + private final Map statusChipLayoutSignatureByRoot = new WeakHashMap<>(); + private final Map statusChipLifecycleLayoutSignatureByView = new WeakHashMap<>(); private final Set trackedUnlockedStockSurfaces = Collections.newSetFromMap(new WeakHashMap<>()); private final Set trackedLockedStatusBarIconSurfaces = @@ -88,12 +109,35 @@ final class StockLayoutCanvasController { Collections.newSetFromMap(new WeakHashMap<>()); private final Set hookedPluginClassLoaders = Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set pendingPluginClassLoaders = + Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set> hookedAodNotificationListenerClasses = + Collections.newSetFromMap(new IdentityHashMap<>()); + private final Set darkIconDispatchers = + Collections.newSetFromMap(new WeakHashMap<>()); + private final Set knownStatusIconModelObjects = + Collections.newSetFromMap(new WeakHashMap<>()); + private final Map forcedDarkIconDispatcherStates = + new WeakHashMap<>(); + private final Map splitShadeStatusBarCandidateByView = new WeakHashMap<>(); + private boolean statusIconModelDirty = true; + private SceneKey lastStatusIconModelScene; + private boolean lastStatusIconModelLayoutEnabled; + private final Set capturingAodWrapperPackages = + Collections.newSetFromMap(new WeakHashMap<>()); + private final Set knownAodNotificationContainers = + Collections.newSetFromMap(new WeakHashMap<>()); + private final Set knownAodNotificationIconOnlyAreas = + Collections.newSetFromMap(new WeakHashMap<>()); + private ArrayList latestAodIconImageViews = new ArrayList<>(); + private ArrayList latestAodNotifications = new ArrayList<>(); + private final Map stockPreparedAodIconDrawableByView = new WeakHashMap<>(); + private int aodNotificationSourceGeneration; private final Map unlockedHostsByRoot = new WeakHashMap<>(); private final OwnedIconHostManager ownedIconHostManager = new OwnedIconHostManager(); private final DebugBoundsOverlayController debugBoundsOverlayController = new DebugBoundsOverlayController(); - private final LockscreenCardsNotificationStripRenderer lockscreenCardsNotificationStripRenderer = - new LockscreenCardsNotificationStripRenderer(); + private final LockscreenCardsNotificationStripRenderer lockscreenCardsNotificationStripRenderer; private final LockscreenCardsNotificationRowFilter lockscreenCardsNotificationRowFilter = new LockscreenCardsNotificationRowFilter(); private final UnlockedIconSnapshotRenderController unlockedIconRenderController; @@ -103,10 +147,14 @@ final class StockLayoutCanvasController { private WeakReference lockedCarrierTextSource = new WeakReference<>(null); private boolean hooksInstalled; + private int ownHiddenViewAlphaWriteDepth; StockLayoutCanvasController(RuntimeContext runtimeContext) { this.runtimeContext = runtimeContext; - unlockedIconRenderController = new UnlockedIconSnapshotRenderController(); + lockscreenCardsNotificationStripRenderer = + new LockscreenCardsNotificationStripRenderer(); + unlockedIconRenderController = new UnlockedIconSnapshotRenderController(runtimeContext); + runtimeContext.getModeStateRepository().addSceneListener(this::scheduleSceneRefresh); } void install(ClassLoader classLoader) { @@ -117,11 +165,277 @@ final class StockLayoutCanvasController { installCardsNotificationIconSourceHook(classLoader); installLockscreenCardsNotificationRowFilterHook(classLoader); installUnlockedAppearanceInvalidationHooks(classLoader); + installStatusIconModelHooks(classLoader); installShadeHeaderAnimationGuardHook(classLoader); installViewRootHook(classLoader); hooksInstalled = true; } + private void installStatusIconModelHooks(ClassLoader classLoader) { + String[] controllerClassNames = { + "com.android.systemui.statusbar.phone.StatusBarIconControllerImpl", + "com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl" + }; + for (String className : controllerClassNames) { + Class controllerClass = ReflectionSupport.findClassIfExists(className, classLoader); + if (controllerClass == null) { + continue; + } + hookStatusIconModelConstructors(controllerClass); + hookStatusIconModelMethod(controllerClass, "setIcon"); + hookStatusIconModelMethod(controllerClass, "setIconVisibility"); + hookStatusIconModelMethod(controllerClass, "removeIcon"); + hookStatusIconModelMethod(controllerClass, "addIconGroup"); + hookStatusIconModelMethod(controllerClass, "removeIconGroup"); + hookStatusIconModelMethod(controllerClass, "refreshIconGroup"); + } + + String[] listClassNames = { + "com.android.systemui.statusbar.phone.StatusBarIconList", + "com.android.systemui.statusbar.phone.ui.StatusBarIconList" + }; + for (String className : listClassNames) { + Class listClass = ReflectionSupport.findClassIfExists(className, classLoader); + if (listClass == null) { + continue; + } + hookStatusIconModelConstructors(listClass); + hookStatusIconModelMethod(listClass, "setIcon"); + hookStatusIconModelMethod(listClass, "removeIcon"); + } + + String[] iconManagerClassNames = { + "com.android.systemui.statusbar.phone.StatusBarIconController$IconManager", + "com.android.systemui.statusbar.phone.ui.StatusBarIconController$IconManager", + "com.android.systemui.statusbar.phone.StatusBarIconControllerImpl$IconManager", + "com.android.systemui.statusbar.phone.ui.StatusBarIconControllerImpl$IconManager" + }; + for (String className : iconManagerClassNames) { + Class managerClass = ReflectionSupport.findClassIfExists(className, classLoader); + if (managerClass == null) { + continue; + } + hookStatusIconModelConstructors(managerClass); + hookStatusIconModelMethod(managerClass, "onIconAdded"); + hookStatusIconModelMethod(managerClass, "onSetIcon"); + hookStatusIconModelMethod(managerClass, "onRemoveIcon"); + hookStatusIconModelMethod(managerClass, "onIconExternal"); + } + } + + private void hookStatusIconModelConstructors(Class type) { + try { + XposedHookSupport.hookAllConstructors( + runtimeContext.getFramework(), + type, + chain -> { + Object result = chain.proceed(); + rememberStatusIconModelObject(chain.getThisObject()); + return result; + }); + } catch (Throwable throwable) { + if (runtimeContext.isLogEnabled()) { + runtimeContext.logWarning( + "Status icon model constructor hook failed for " + type.getName(), + throwable); + } + } + } + + private void hookStatusIconModelMethod(Class type, String methodName) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + type, + methodName, + chain -> { + rememberStatusIconModelObject(chain.getThisObject()); + Object result = chain.proceed(); + rememberStatusIconModelObject(chain.getThisObject()); + return result; + }); + } + + private void rememberStatusIconModelObject(Object object) { + if (object == null) { + return; + } + knownStatusIconModelObjects.add(object); + rememberStatusIconModelField(object, "mStatusBarIconList"); + rememberStatusIconModelField(object, "mIconList"); + rememberStatusIconModelField(object, "mIconGroups"); + rememberStatusIconModelField(object, "mIconGroup"); + rememberStatusIconModelField(object, "mSlots"); + rememberStatusIconModelField(object, "mStatusIcons"); + statusIconModelDirty = true; + } + + private void rememberStatusIconModelField(Object target, String fieldName) { + Object value = ReflectionSupport.getFieldValue(target, fieldName); + if (value == null) { + return; + } + knownStatusIconModelObjects.add(value); + if (value instanceof Iterable iterable) { + for (Object item : iterable) { + if (item != null) { + knownStatusIconModelObjects.add(item); + } + } + } + } + + private boolean updateActiveSystemIconSlotsForLayout(boolean layoutEnabled, SceneKey scene) { + if (!statusIconModelDirty + && layoutEnabled == lastStatusIconModelLayoutEnabled + && java.util.Objects.equals(scene, lastStatusIconModelScene)) { + return false; + } + lastStatusIconModelLayoutEnabled = layoutEnabled; + lastStatusIconModelScene = scene; + if (!layoutEnabled || scene == null || scene.isAod()) { + statusIconModelDirty = false; + return StatusIconSlotVisibilityStore.setActiveSlots(new ArrayList<>()); + } + if (knownStatusIconModelObjects.isEmpty()) { + statusIconModelDirty = false; + return false; + } + ArrayList mergedSlots = new ArrayList<>(); + for (Object object : new ArrayList<>(knownStatusIconModelObjects)) { + rememberStatusIconModelObject(object); + Object iconList = ReflectionSupport.getFieldValue(object, "mStatusBarIconList"); + if (iconList == null) { + continue; + } + ArrayList hiddenSlots = copyStringList( + ReflectionSupport.getFieldValue(object, "mIconHideList")); + for (String slot : activeSystemIconSlots(iconList, hiddenSlots)) { + if (!mergedSlots.contains(slot)) { + mergedSlots.add(slot); + } + } + } + boolean changed = StatusIconSlotVisibilityStore.setActiveSlots(mergedSlots); + statusIconModelDirty = false; + return changed; + } + + private ArrayList activeSystemIconSlots(Object iconList, ArrayList hiddenSlots) { + ArrayList result = new ArrayList<>(); + addActiveSystemIconSlots( + result, + ReflectionSupport.getFieldValue(iconList, "mSlots"), + hiddenSlots); + addActiveSystemIconSlots( + result, + ReflectionSupport.getFieldValue(iconList, "mViewOnlySlots"), + hiddenSlots); + return result; + } + + private void addActiveSystemIconSlots( + ArrayList out, + Object slotsObj, + ArrayList hiddenSlots + ) { + if (!(slotsObj instanceof Iterable slots)) { + return; + } + for (Object slotObj : slots) { + String name = firstNonEmpty( + reflectiveFieldString(slotObj, "mName"), + reflectiveFieldString(slotObj, "name"), + reflectiveString(slotObj, "getName")); + if (name.isEmpty() + || out.contains(name) + || (hiddenSlots != null && hiddenSlots.contains(name))) { + continue; + } + Object holder = firstNonNull( + ReflectionSupport.getFieldValue(slotObj, "mHolder"), + ReflectionSupport.getFieldValue(slotObj, "holder")); + Object subSlots = ReflectionSupport.getFieldValue(slotObj, "mSubSlots"); + boolean hasSubSlots = subSlots instanceof Iterable iterable && iterable.iterator().hasNext(); + if ((holder != null && isStatusIconHolderVisible(holder)) || hasSubSlots) { + out.add(name); + } + } + } + + private boolean isStatusIconHolderVisible(Object holder) { + if (holder == null) { + return false; + } + Object methodVisible = ReflectionSupport.invokeMethod(holder, "isVisible", new Class[0]); + if (methodVisible instanceof Boolean visible) { + return visible; + } + Object visibleField = firstNonNull( + ReflectionSupport.getFieldValue(holder, "mVisible"), + ReflectionSupport.getFieldValue(holder, "visible")); + if (visibleField instanceof Boolean visible) { + return visible; + } + Object icon = firstNonNull( + ReflectionSupport.getFieldValue(holder, "mIcon"), + ReflectionSupport.getFieldValue(holder, "icon"), + ReflectionSupport.invokeMethod(holder, "getIcon", new Class[0])); + Object iconVisible = firstNonNull( + ReflectionSupport.getFieldValue(icon, "visible"), + ReflectionSupport.getFieldValue(icon, "mVisible")); + return !(iconVisible instanceof Boolean) || (Boolean) iconVisible; + } + + private ArrayList copyStringList(Object value) { + ArrayList result = new ArrayList<>(); + if (!(value instanceof Iterable iterable)) { + return result; + } + for (Object item : iterable) { + if (item != null) { + String text = String.valueOf(item); + if (!text.isEmpty() && !result.contains(text)) { + result.add(text); + } + } + } + return result; + } + + private String reflectiveString(Object target, String methodName) { + Object value = ReflectionSupport.invokeMethod(target, methodName, new Class[0]); + return value != null ? String.valueOf(value) : ""; + } + + private String reflectiveFieldString(Object target, String fieldName) { + Object value = ReflectionSupport.getFieldValue(target, fieldName); + return value != null ? String.valueOf(value) : ""; + } + + private Object firstNonNull(Object... values) { + if (values == null) { + return null; + } + for (Object value : values) { + if (value != null) { + return value; + } + } + return null; + } + + private String firstNonEmpty(String... values) { + if (values == null) { + return ""; + } + for (String value : values) { + if (value != null && !value.isEmpty() && !"null".equals(value)) { + return value; + } + } + return ""; + } + private void installShadeHeaderAnimationGuardHook(ClassLoader classLoader) { Class viewClass = ReflectionSupport.requireClass("android.view.View", classLoader); XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setAlpha", chain -> { @@ -131,9 +445,32 @@ final class StockLayoutCanvasController { if (shouldSuppressConflictingShadeHeaderAlpha(view, alpha)) { return null; } + if (shouldSuppressStatusChipShellAlpha(view, alpha)) { + return null; + } + if (shouldSuppressTrackedStatusChipAlpha(view, alpha)) { + return null; + } + if (shouldCaptureAodVisualAlpha(view)) { + rememberAodPluginVisualAlpha(view, alpha); + } } return chain.proceed(); }); + XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setVisibility", chain -> { + Object target = chain.getThisObject(); + Object visibilityArg = firstArg(chain.getArgs()); + if (target instanceof View view && visibilityArg instanceof Integer visibility) { + if (shouldSuppressTrackedStatusChipVisibility(view, visibility)) { + return null; + } + } + Object result = chain.proceed(); + if (target instanceof View view && visibilityArg instanceof Integer visibility) { + scheduleStatusChipRefreshIfContentChanged(view); + } + return result; + }); XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setTranslationY", chain -> { Object target = chain.getThisObject(); Object translationArg = firstArg(chain.getArgs()); @@ -144,6 +481,31 @@ final class StockLayoutCanvasController { } return chain.proceed(); }); + XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "layout", chain -> { + Object target = chain.getThisObject(); + List args = chain.getArgs(); + if (target instanceof View view && args.size() == 4 + && args.get(0) instanceof Integer left + && args.get(1) instanceof Integer top + && args.get(2) instanceof Integer right + && args.get(3) instanceof Integer bottom) { + AodLayoutBounds override = carrierlessAodIconOnlyAreaLayoutOverride( + view, + left, + top, + right, + bottom); + if (override != null) { + return chain.proceed(new Object[]{ + override.left, + override.top, + override.right, + override.bottom + }); + } + } + return chain.proceed(); + }); } private void installLockscreenCardsNotificationRowFilterHook(ClassLoader classLoader) { @@ -182,15 +544,7 @@ final class StockLayoutCanvasController { "com.android.systemui.statusbar.phone.DarkIconDispatcherImpl", classLoader); if (darkIconDispatcherClass != null) { - XposedHookSupport.hookAllMethodsIfExists( - runtimeContext.getFramework(), - darkIconDispatcherClass, - "applyIconTint", - chain -> { - Object result = chain.proceed(); - scheduleUnlockedAppearanceRefreshForAllRoots(); - return result; - }); + installDarkIconDispatcherLockscreenGuard(darkIconDispatcherClass); } Class statusBarIconViewClass = ReflectionSupport.findClassIfExists( @@ -203,7 +557,7 @@ final class StockLayoutCanvasController { hookViewAppearanceInvalidation(statusBarIconViewClass, "setIconColor"); hookViewAppearanceInvalidation(statusBarIconViewClass, "updateIconColor"); hookViewLayoutInvalidation(statusBarIconViewClass, "onLayout"); - hookViewLayoutInvalidation(statusBarIconViewClass, "setVisibleState"); + hookViewAppearanceInvalidation(statusBarIconViewClass, "setVisibleState"); } Class notificationIconAreaClass = ReflectionSupport.findClassIfExists( @@ -245,7 +599,6 @@ final class StockLayoutCanvasController { hookClockTextInvalidation(clockClass, "setText"); hookViewLayoutInvalidation(clockClass, "onLayout"); } - Class notificationContainerClass = ReflectionSupport.findClassIfExists( "com.android.systemui.statusbar.phone.NotificationIconContainer", classLoader); @@ -261,12 +614,134 @@ final class StockLayoutCanvasController { hookViewLayoutInvalidation(statusIconContainerClass, "onLayout"); } + Class touchInterceptClass = ReflectionSupport.findClassIfExists( + "com.android.systemui.statusbar.phone.TouchInterceptFrameLayout", + classLoader); + if (touchInterceptClass != null) { + hookStatusChipLifecycle(touchInterceptClass, "onAttachedToWindow"); + hookStatusChipLifecycle(touchInterceptClass, "onDetachedFromWindow"); + hookStatusChipLifecycle(touchInterceptClass, "onLayout"); + hookStatusChipLifecycle(touchInterceptClass, "setVisibility"); + hookStatusChipLifecycle(touchInterceptClass, "setAlpha"); + hookViewLayoutInvalidation(touchInterceptClass, "onLayout"); + } + Class batteryClass = ReflectionSupport.findClassIfExists( "com.android.systemui.battery.BatteryMeterView", classLoader); if (batteryClass != null) { + hookViewAppearanceInvalidation(batteryClass, "onDarkChanged"); hookViewLayoutInvalidation(batteryClass, "onLayout"); } + + hookStatusChipChildLifecycle(ViewGroup.class, "onViewAdded"); + hookStatusChipChildLifecycle(ViewGroup.class, "onViewRemoved"); + hookStatusChipContentInvalidation(TextView.class, "setText"); + hookStatusChipContentInvalidation(TextView.class, "setTextColor"); + hookStatusChipContentInvalidation(TextView.class, "setTextSize"); + hookStatusChipContentInvalidation(ImageView.class, "setImageDrawable"); + hookStatusChipContentInvalidation(ImageView.class, "setImageBitmap"); + hookStatusChipContentInvalidation(ImageView.class, "setImageIcon"); + hookStatusChipContentInvalidation(ImageView.class, "setImageResource"); + hookStatusChipContentInvalidation(ImageView.class, "setImageLevel"); + } + + private void installDarkIconDispatcherLockscreenGuard(Class darkIconDispatcherClass) { + XposedHookSupport.hookAllConstructors( + runtimeContext.getFramework(), + darkIconDispatcherClass, + chain -> { + Object result = chain.proceed(); + trackDarkIconDispatcher(chain.getThisObject()); + return result; + }); + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + darkIconDispatcherClass, + "applyIconTint", + chain -> { + Object dispatcher = chain.getThisObject(); + trackDarkIconDispatcher(dispatcher); + if (isLockscreenScene()) { + forceDarkIconDispatcherLightState(dispatcher); + } + Object result = chain.proceed(); + scheduleUnlockedAppearanceRefreshForAllRoots(); + return result; + }); + } + + private void trackDarkIconDispatcher(Object dispatcher) { + if (dispatcher != null) { + darkIconDispatchers.add(dispatcher); + } + } + + private DarkIconDispatcherState captureDarkIconDispatcherState(Object dispatcher) { + return dispatcher != null + ? new DarkIconDispatcherState( + ReflectionSupport.getFieldValue(dispatcher, "mDarkIntensity"), + ReflectionSupport.getFieldValue(dispatcher, "mIconTint"), + ReflectionSupport.getFieldValue(dispatcher, "mTint"), + ReflectionSupport.getFieldValue(dispatcher, "mDarkAreas"), + ReflectionSupport.getFieldValue(dispatcher, "mTintAreas")) + : null; + } + + private void setDarkIconDispatcherLightState(Object dispatcher) { + if (dispatcher == null) { + return; + } + ReflectionSupport.setFieldValue(dispatcher, "mDarkIntensity", 0f); + ReflectionSupport.setFieldValue(dispatcher, "mIconTint", Color.WHITE); + ReflectionSupport.setFieldValue(dispatcher, "mTint", Color.WHITE); + ReflectionSupport.setFieldValue(dispatcher, "mDarkAreas", new ArrayList<>()); + ReflectionSupport.setFieldValue(dispatcher, "mTintAreas", new ArrayList<>()); + } + + private void forceDarkIconDispatcherLightState(Object dispatcher) { + if (dispatcher == null || !isLockscreenScene()) { + return; + } + forcedDarkIconDispatcherStates.computeIfAbsent( + dispatcher, + this::captureDarkIconDispatcherState); + setDarkIconDispatcherLightState(dispatcher); + } + + private void forceDarkIconDispatchersLightState() { + if (!isLockscreenScene()) { + return; + } + for (Object dispatcher : new ArrayList<>(darkIconDispatchers)) { + forceDarkIconDispatcherLightState(dispatcher); + ReflectionSupport.invokeMethod(dispatcher, "applyIconTint", new Class[0]); + } + } + + private void restoreForcedDarkIconDispatcherStates() { + if (forcedDarkIconDispatcherStates.isEmpty()) { + return; + } + for (Map.Entry entry : + new ArrayList<>(forcedDarkIconDispatcherStates.entrySet())) { + Object dispatcher = entry.getKey(); + DarkIconDispatcherState state = entry.getValue(); + restoreDarkIconDispatcherState(dispatcher, state); + ReflectionSupport.invokeMethod(dispatcher, "applyIconTint", new Class[0]); + } + forcedDarkIconDispatcherStates.clear(); + } + + private void restoreDarkIconDispatcherState(Object dispatcher, DarkIconDispatcherState state) { + if (dispatcher == null || state == null) { + return; + } + ReflectionSupport.setFieldValue(dispatcher, "mDarkIntensity", state.darkIntensity); + ReflectionSupport.setFieldValue(dispatcher, "mIconTint", state.iconTint); + ReflectionSupport.setFieldValue(dispatcher, "mTint", state.tint); + ReflectionSupport.setFieldValue(dispatcher, "mDarkAreas", state.darkAreas); + ReflectionSupport.setFieldValue(dispatcher, "mTintAreas", state.tintAreas); } private void hookViewAppearanceInvalidation(Class type, String methodName) { @@ -313,6 +788,59 @@ final class StockLayoutCanvasController { }); } + private void hookStatusChipContentInvalidation(Class type, String methodName) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + type, + methodName, + chain -> { + Object result = chain.proceed(); + if (chain.getThisObject() instanceof View view) { + scheduleStatusChipRefreshIfContentChanged(view); + } + return result; + }); + } + + private void hookStatusChipLifecycle(Class type, String methodName) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + type, + methodName, + chain -> { + View view = chain.getThisObject() instanceof View candidate ? candidate : null; + View rootBefore = view != null ? unlockedStatusBarRootFor(view) : null; + Object result = chain.proceed(); + if (view != null && ("onLayout".equals(methodName) + || "setVisibility".equals(methodName) + || "onAttachedToWindow".equals(methodName) + || "onDetachedFromWindow".equals(methodName))) { + markStatusChipLifecycleDirtyIfNeeded(view, rootBefore); + } + return result; + }); + } + + private void hookStatusChipChildLifecycle(Class type, String methodName) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + type, + methodName, + chain -> { + ViewGroup parent = chain.getThisObject() instanceof ViewGroup group ? group : null; + View child = firstArg(chain.getArgs()) instanceof View view ? view : null; + View rootBefore = parent != null ? unlockedStatusBarRootFor(parent) : null; + boolean affectedBefore = isStatusChipTreeMutation(parent, child); + Object result = chain.proceed(); + boolean affectedAfter = isStatusChipTreeMutation(parent, child); + if (affectedBefore || affectedAfter) { + View root = rootBefore != null ? rootBefore : unlockedStatusBarRootFor(parent); + requestStatusChipRefresh(root); + } + return result; + }); + } + private void installCardsNotificationIconSourceHook(ClassLoader classLoader) { Class controllerClass = ReflectionSupport.findClassIfExists( CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER, @@ -346,6 +874,11 @@ final class StockLayoutCanvasController { if (!(containerObj instanceof ViewGroup container)) { return chain.proceed(); } + if (!isLayoutEnabled(container.getContext())) { + Object result = chain.proceed(); + LockscreenCardsNotificationIconSourceStore.put(container, new ArrayList<>()); + return result; + } if (!isCardsNotificationIconSourceContainer(container)) { Object result = chain.proceed(); markUnlockedLayoutDirty(container); @@ -395,13 +928,34 @@ final class StockLayoutCanvasController { return result; } ClassLoader pluginClassLoader = plugin.getClass().getClassLoader(); - if (pluginClassLoader != null && hookedPluginClassLoaders.add(pluginClassLoader)) { - installPluginClassLoaderHooks(pluginClassLoader); + if (pluginClassLoader == null) { + return result; + } + if (runtimeContext.getModeStateRepository().isLayoutEnabled()) { + installPluginClassLoaderHooksIfNeeded(pluginClassLoader); + } else if (!hookedPluginClassLoaders.contains(pluginClassLoader)) { + pendingPluginClassLoaders.add(pluginClassLoader); } return result; }); } + private void installPendingPluginClassLoaderHooks() { + if (pendingPluginClassLoaders.isEmpty()) { + return; + } + for (ClassLoader pluginClassLoader : new ArrayList<>(pendingPluginClassLoaders)) { + installPluginClassLoaderHooksIfNeeded(pluginClassLoader); + } + } + + private void installPluginClassLoaderHooksIfNeeded(ClassLoader pluginClassLoader) { + if (pluginClassLoader != null && hookedPluginClassLoaders.add(pluginClassLoader)) { + pendingPluginClassLoaders.remove(pluginClassLoader); + installPluginClassLoaderHooks(pluginClassLoader); + } + } + private void installPluginClassLoaderHooks(ClassLoader pluginClassLoader) { Class qiClass = ReflectionSupport.findClassIfExists(CLASS_AOD_DIRECT_CONTROLLER, pluginClassLoader); if (qiClass != null) { @@ -433,6 +987,10 @@ final class StockLayoutCanvasController { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), iconsOnlyClass, "onLayout", chain -> { Object result = chain.proceed(); if (chain.getThisObject() instanceof View view) { + if (!isLayoutEnabled(view.getContext())) { + return result; + } + updateAodNotificationPackageSources(view); applyConfirmedAodPluginPolicy(view); } return result; @@ -440,6 +998,156 @@ final class StockLayoutCanvasController { } } + private void captureAodPackagesFromFaceWidgetWrapper(Object wrapper) { + if (wrapper == null || capturingAodWrapperPackages.contains(wrapper)) { + return; + } + capturingAodWrapperPackages.add(wrapper); + try { + captureAodPackagesFromNotifCollection(wrapper); + } finally { + capturingAodWrapperPackages.remove(wrapper); + } + } + + private void captureAodPackagesFromNotificationCallback(Object callback) { + Object target = targetInstanceFromNotificationCallback(callback); + if (target != null) { + captureAodPackagesFromFaceWidgetWrapper(target); + } + } + + private Object targetInstanceFromNotificationCallback(Object callback) { + if (callback == null) { + return null; + } + if (callback.getClass().getName().contains("FaceWidgetNotificationControllerWrapper")) { + return callback; + } + Object target = ReflectionSupport.getFieldValue(callback, "mTargetInstance"); + if (target != null) { + return target; + } + if (Proxy.isProxyClass(callback.getClass())) { + try { + Object handler = Proxy.getInvocationHandler(callback); + target = ReflectionSupport.getFieldValue(handler, "mTargetInstance"); + if (target != null) { + return target; + } + return handler; + } catch (Throwable ignored) { + return null; + } + } + return null; + } + + private void captureAodPackagesFromNotifCollection(Object wrapper) { + Object collection = ReflectionSupport.getFieldValue(wrapper, "mNotifCollection"); + ArrayList notifications = notificationsFromNotifCollection(collection); + if (notifications.isEmpty()) { + return; + } + latestAodNotifications = notifications; + updateKnownAodNotificationSources(); + } + + private ArrayList notificationsFromNotifCollection(Object collection) { + ArrayList notifications = new ArrayList<>(); + if (collection == null) { + return notifications; + } + for (String methodName : new String[]{"getAllNotifs", "getAllNotifications"}) { + Object value = ReflectionSupport.invokeMethod(collection, methodName, new Class[0]); + appendNotificationsFromNotifCollectionValue(notifications, value); + if (!notifications.isEmpty()) { + return notifications; + } + } + for (String fieldName : new String[]{ + "mReadOnlyNotificationSet", + "mNotificationSet", + "mEntryMap", + "mEntries", + "entries" + }) { + 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 void appendNotificationsFromNotifCollectionValue( + ArrayList notifications, + Object value + ) { + if (value == null || !notifications.isEmpty()) { + return; + } + if (value instanceof Map map) { + for (Object entry : map.values()) { + appendNotificationFromNotifEntry(notifications, entry); + } + return; + } + if (value instanceof Iterable iterable) { + for (Object entry : iterable) { + appendNotificationFromNotifEntry(notifications, entry); + } + } + } + + private void appendNotificationFromNotifEntry( + ArrayList notifications, + Object entry + ) { + StatusBarNotification sbn = statusBarNotificationFromNotifEntry(entry); + if (sbn == null || !AppIconRules.isValidPackageName(sbn.getPackageName())) { + return; + } + notifications.add(sbn); + } + + private StatusBarNotification statusBarNotificationFromNotifEntry(Object entry) { + if (entry instanceof StatusBarNotification sbn) { + return sbn; + } + Object value = ReflectionSupport.invokeMethod(entry, "getSbn", new Class[0]); + if (value instanceof StatusBarNotification sbn) { + return sbn; + } + value = ReflectionSupport.invokeMethod(entry, "getStatusBarNotification", new Class[0]); + if (value instanceof StatusBarNotification sbn) { + return sbn; + } + for (String fieldName : new String[]{"mSbn", "sbn", "mStatusBarNotification", "statusBarNotification"}) { + value = ReflectionSupport.getFieldValue(entry, fieldName); + if (value instanceof StatusBarNotification sbn) { + return sbn; + } + } + return null; + } + private void installViewRootHook(ClassLoader classLoader) { Class viewRootClass = ReflectionSupport.requireClass("android.view.ViewRootImpl", classLoader); XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), viewRootClass, "performDraw", chain -> { @@ -475,17 +1183,29 @@ final class StockLayoutCanvasController { if (!isViewThread(root)) { return; } + applyBeforeTraversalInternal(root); + } + + private void applyBeforeTraversalInternal(View root) { if (root == null) { return; } SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); if (!settings.clockEnabled) { + lockscreenCardsNotificationRowFilter.restoreAll(); return; } SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); if (activeScene == null) { return; } + if (isNotificationShadeWindowRoot(root)) { + if (shouldPrepareCarrierlessAodNotificationTransition(settings, activeScene)) { + prepareCarrierlessAodNotificationTransition(root); + } else if (!activeScene.isAod()) { + restorePreparedAodNotificationWidgetWidths(root); + } + } if (activeScene.isLockscreen() && activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS && isNotificationShadeWindowRoot(root)) { @@ -513,9 +1233,22 @@ final class StockLayoutCanvasController { } SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); if (activeScene != null && activeScene.isAod()) { - hideConfirmedAodPluginViews(); + if (hasVisibleConfirmedAodPluginView()) { + hideConfirmedAodPluginViews(); + } + removeLockscreenCardsNotificationHost(root); + if (activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS + && isAodLayoutRoot(root)) { + renderAodCardsNotificationStripIfNeeded(root, settings, activeScene); + } else { + removeAodCardsNotificationHost(root); + } + syncAodOwnedHostVisualState(root); + return; } - if (activeScene != null && !activeScene.isUnlocked()) { + removeAodCardsNotificationHost(root); + if (activeScene != null + && !activeScene.isUnlocked()) { hideTrackedLockedStatusBarIconSurfaces(null); } if (activeScene != null @@ -573,6 +1306,191 @@ final class StockLayoutCanvasController { } } + private void markStatusChipLifecycleDirtyIfNeeded(View view) { + markStatusChipLifecycleDirtyIfNeeded(view, null); + } + + private void markStatusChipLifecycleDirtyIfNeeded(View view, View fallbackRoot) { + if (view == null || !isStatusChipCandidate(view)) { + View root = view != null ? unlockedStatusBarRootFor(view) : fallbackRoot; + requestUnlockedLayoutRefresh(root); + return; + } + int signature = statusChipLayoutSignature(view); + Integer previous = statusChipLifecycleLayoutSignatureByView.put(view, signature); + if (previous == null || previous != signature) { + View root = unlockedStatusBarRootFor(view); + requestUnlockedLayoutRefresh(root != null ? root : fallbackRoot); + } + } + + private void scheduleStatusChipRefreshIfContentChanged(View changedView) { + if (changedView == null || trackedStatusChipSources.isEmpty()) { + return; + } + View chipSource = trackedStatusChipSourceFor(changedView); + if (chipSource == null) { + return; + } + View root = unlockedStatusBarRootFor(chipSource); + if (root == null || !root.isAttachedToWindow()) { + return; + } + int signature = statusChipContentSignature(chipSource); + Integer previous = statusChipContentSignatureByRoot.put(root, signature); + if (previous != null && previous == signature) { + return; + } + int layoutSignature = statusChipLayoutSignature(chipSource); + Integer previousLayoutSignature = statusChipLayoutSignatureByRoot.put(root, layoutSignature); + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root); + if (previous != null + && previousLayoutSignature != null + && previousLayoutSignature == layoutSignature + && activeScene != null + && activeScene.isUnlocked() + && areUnlockedHostsAttached(root, unlockedHosts) + && unlockedIconRenderController.refreshUnlockedChipContent( + root, + unlockedHosts.chipHost, + activeScene)) { + bringUnlockedHostsAndDebugOverlayToFront(root); + return; + } + unlockedIconRenderController.forceChipRefresh(root); + requestUnlockedLayoutRefresh(root); + } + + private void requestStatusChipRefresh(View root) { + if (root == null || !root.isAttachedToWindow()) { + return; + } + unlockedIconRenderController.forceChipRefresh(root); + requestUnlockedLayoutRefresh(root); + } + + private void requestUnlockedLayoutRefresh(View root) { + if (root == null || !root.isAttachedToWindow()) { + return; + } + dirtyUnlockedLayoutRoots.add(root); + root.requestLayout(); + root.invalidate(); + } + + private View trackedStatusChipSourceFor(View view) { + ArrayDeque staleViews = new ArrayDeque<>(); + View result = null; + for (View source : trackedStatusChipSources) { + if (source == null || !source.isAttachedToWindow()) { + staleViews.add(source); + continue; + } + if (view == source || isDescendantOf(view, source)) { + result = source; + break; + } + } + while (!staleViews.isEmpty()) { + trackedStatusChipSources.remove(staleViews.removeFirst()); + } + return result; + } + + private boolean isStatusChipTreeMutation(View parent, View child) { + if (parent == null + || !isLayoutEnabled(parent.getContext()) + || unlockedStatusBarRootFor(parent) == null) { + return false; + } + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + if (activeScene == null || !activeScene.isUnlocked()) { + return false; + } + if (isStatusChipCandidate(parent)) { + return true; + } + if (trackedStatusChipSources.isEmpty()) { + return false; + } + for (View source : trackedStatusChipSources) { + if (source == null || !source.isAttachedToWindow()) { + continue; + } + if (parent == source + || child == source + || isDescendantOf(parent, source) + || isDescendantOf(child, source)) { + return true; + } + } + return false; + } + + private int statusChipLayoutSignature(View view) { + if (view == null) { + return 0; + } + int result = 17; + result = 31 * result + System.identityHashCode(view); + result = 31 * result + view.getVisibility(); + result = 31 * result + view.getWidth(); + result = 31 * result + view.getHeight(); + result = 31 * result + view.getMeasuredWidth(); + result = 31 * result + view.getMeasuredHeight(); + if (view instanceof TextView textView) { + result = 31 * result + Float.floatToIntBits(textView.getTextSize()); + } + if (view instanceof ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + result = 31 * result + System.identityHashCode(drawable); + if (drawable != null) { + result = 31 * result + drawable.getBounds().hashCode(); + } + } + if (view instanceof ViewGroup group) { + result = 31 * result + group.getChildCount(); + for (int i = 0; i < group.getChildCount(); i++) { + result = 31 * result + statusChipLayoutSignature(group.getChildAt(i)); + } + } + return result; + } + + private int statusChipContentSignature(View view) { + if (view == null) { + return 0; + } + int result = 17; + result = 31 * result + view.getVisibility(); + result = 31 * result + view.getWidth(); + result = 31 * result + view.getHeight(); + result = 31 * result + view.getMeasuredWidth(); + result = 31 * result + view.getMeasuredHeight(); + result = 31 * result + String.valueOf(view.getContentDescription()).hashCode(); + if (view instanceof TextView textView) { + result = 31 * result + String.valueOf(textView.getText()).hashCode(); + result = 31 * result + textView.getCurrentTextColor(); + } + if (view instanceof ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + result = 31 * result + System.identityHashCode(drawable); + if (drawable != null) { + result = 31 * result + drawable.getAlpha(); + result = 31 * result + drawable.getLevel(); + result = 31 * result + drawable.getBounds().hashCode(); + } + } + if (view instanceof ViewGroup group) { + result = 31 * result + group.getChildCount(); + for (int i = 0; i < group.getChildCount(); i++) { + result = 31 * result + statusChipContentSignature(group.getChildAt(i)); + } + } + return result; + } + private void markDirtyIfSettingsChanged(View root, SbtSettings settings, boolean statusBarRoot) { if (!statusBarRoot || root == null || settings == null) { return; @@ -586,7 +1504,11 @@ final class StockLayoutCanvasController { } private void markDirtyIfClockTimeChanged(View root, SbtSettings settings, boolean statusBarRoot) { - if (!statusBarRoot || root == null || settings == null || !settings.clockEnabledUnlocked) { + if (!statusBarRoot || root == null || settings == null || !settings.clockEnabled) { + return; + } + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + if (!clockTextRefreshEnabledForScene(settings, activeScene)) { return; } long bucket = clockTimeBucket(settings); @@ -616,6 +1538,59 @@ final class StockLayoutCanvasController { } } + private void scheduleSceneRefresh(SceneKey previous, SceneKey current) { + if (previous == null || current == null || previous.equals(current)) { + return; + } + if (previous.isLockscreen() && !current.isLockscreen()) { + restoreForcedDarkIconDispatcherStates(); + } + boolean lockscreenEntry = current.isLockscreen(); + if (lockscreenEntry) { + forceDarkIconDispatchersLightState(); + } + ArrayList roots = new ArrayList<>(rootGeometries.keySet()); + for (View root : new ArrayList<>(unlockedHostsByRoot.keySet())) { + if (!roots.contains(root)) { + roots.add(root); + } + } + for (View root : roots) { + if (root == null || !root.isAttachedToWindow()) { + rootGeometries.remove(root); + unlockedHostsByRoot.remove(root); + continue; + } + Runnable refresh = () -> refreshSceneRoot(root); + if (isViewThread(root)) { + refresh.run(); + } else if (root.getHandler() != null) { + root.getHandler().post(refresh); + } + if (lockscreenEntry) { + root.postDelayed(() -> refreshSceneRoot(root), 80L); + root.postDelayed(() -> refreshSceneRoot(root), 180L); + } + } + } + + private void refreshSceneRoot(View root) { + if (root == null || !root.isAttachedToWindow()) { + rootGeometries.remove(root); + unlockedHostsByRoot.remove(root); + return; + } + unlockedIconRenderController.clearRoot(root); + dirtyUnlockedLayoutRoots.add(root); + root.requestLayout(); + root.invalidate(); + } + + private boolean isLockscreenScene() { + SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene(); + return scene != null && scene.isLockscreen(); + } + private void scheduleUnlockedAppearanceRefreshForRoot(View root) { if (root == null || !root.isAttachedToWindow() @@ -629,6 +1604,19 @@ final class StockLayoutCanvasController { }); } + private boolean clockTextRefreshEnabledForScene(SbtSettings settings, SceneKey scene) { + if (settings == null || scene == null) { + return false; + } + if (scene.isUnlocked()) { + return settings.clockEnabledUnlocked; + } + if (scene.isLockscreen()) { + return settings.clockEnabledLockscreen; + } + return false; + } + private void scheduleUnlockedClockTextRefreshForRoot(View root) { if (root == null || !root.isAttachedToWindow() @@ -666,7 +1654,7 @@ final class StockLayoutCanvasController { unlockedHosts.statusHost, unlockedHosts.notificationHost, activeScene); - ownedIconHostManager.bringUnlockedHostsToFront(root); + bringUnlockedHostsAndDebugOverlayToFront(root); } private View unlockedStatusBarRootFor(View view) { @@ -693,11 +1681,11 @@ final class StockLayoutCanvasController { return; } SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); - if (!settings.clockEnabled || !settings.clockEnabledUnlocked) { + if (!settings.clockEnabled) { return; } SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); - if (activeScene == null || !activeScene.isUnlocked()) { + if (!clockTextRefreshEnabledForScene(settings, activeScene)) { return; } UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root); @@ -708,10 +1696,15 @@ final class StockLayoutCanvasController { root, unlockedHosts.clockHost, activeScene)) { - ownedIconHostManager.bringUnlockedHostsToFront(root); + bringUnlockedHostsAndDebugOverlayToFront(root); } } + private void bringUnlockedHostsAndDebugOverlayToFront(View root) { + ownedIconHostManager.bringUnlockedHostsToFront(root); + debugBoundsOverlayController.bringToFront(root); + } + private void applyToRootInternal(View root) { if (root == null) { return; @@ -725,37 +1718,81 @@ final class StockLayoutCanvasController { runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled); if (!layoutEnabled) { updateActiveScene(root, context); + updateActiveSystemIconSlotsForLayout(false, runtimeContext.getModeStateRepository().getActiveScene()); applyLayoutOffRoot(root, settings, statusBarRoot, runtimeContext.getModeStateRepository().getActiveScene()); return; } + installPendingPluginClassLoaderHooks(); layoutOffCleanedRoots.remove(root); updateActiveScene(root, context); + if (updateActiveSystemIconSlotsForLayout( + true, + runtimeContext.getModeStateRepository().getActiveScene())) { + unlockedIconRenderController.clearRoot(root); + dirtyUnlockedLayoutRoots.add(root); + } SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); boolean unlockedScene = activeScene != null && activeScene.isUnlocked(); - if (!statusBarRoot) { + boolean aodScene = activeScene != null && activeScene.isAod(); + boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root); + boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root); + boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root); + boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot; + if (aodScene + && aodLayoutRoot + && hasAodLayoutPlan + && !rootDirty + && !isKnownRootGeometryChanged(root)) { + return; + } + if (isNotificationShadeWindowRoot(root)) { + if (aodScene) { + cacheAodNotificationWidgetSteadyWidth(root); + } else if (shouldPrepareCarrierlessAodNotificationTransition(settings, activeScene)) { + prepareCarrierlessAodNotificationTransition(root); + } else { + restorePreparedAodNotificationWidgetWidths(root); + } + } + if (aodScene && statusBarRoot) { + discoverAodStatusbarSourceContainers(root, settings); + } + if (aodScene && statusBarRoot && !aodLayoutRoot) { + removeSharedLayoutHosts(root); + unlockedIconRenderController.clearRoot(root); + removeLockscreenCardsNotificationHost(root); + removeAodCardsNotificationHost(root); + removeDebugOverlay(root); + return; + } + if (!layoutRoot) { + removeSharedLayoutHosts(root); if (!unlockedScene && canContainLockedStatusBarIconSurfaces(root)) { renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene); hideOrDiscoverLockedStatusBarIconSurfaces(root); } else { removeLockscreenCardsNotificationHost(root); + removeAodCardsNotificationHost(root); } removeDebugOverlay(root); return; } if (activeScene != null - && (activeScene.isUnlocked() || activeScene.isLockscreen()) + && (activeScene.isUnlocked() || activeScene.isLockscreen() || activeScene.isAod()) && resetUnlockedHostsForRootGeometryChange(root)) { return; } UnlockedHosts unlockedHosts = updateUnlockedOwnedHosts(root, activeScene); RenderResult renderResult = new RenderResult(false, false); - if (unlockedHosts != null && (unlockedScene || activeScene != null && activeScene.isLockscreen())) { + if (unlockedHosts != null + && (unlockedScene + || activeScene != null && activeScene.isLockscreen() + || aodScene)) { if (!unlockedScene) { restoreTrackedLockedStatusBarIconSurfaces(root); } boolean shouldRender = !unlockedIconRenderController.hasLayoutPlan(root) - || dirtyUnlockedLayoutRoots.remove(root) - || !unlockedScene; + || rootDirty; if (shouldRender) { markRootGeometryBeforeDraw(root); renderResult = unlockedIconRenderController.renderUnlocked( @@ -767,24 +1804,41 @@ final class StockLayoutCanvasController { unlockedHosts.notificationHost, activeScene, lockedCarrierTextSource()); - boolean shouldShowUnlockedHosts = unlockedScene || renderResult.shouldOwnLockscreen(); + boolean shouldShowUnlockedHosts = unlockedScene || aodScene || renderResult.shouldOwnLockscreen(); ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts); if (shouldShowUnlockedHosts) { - ownedIconHostManager.bringUnlockedHostsToFront(root); + bringUnlockedHostsAndDebugOverlayToFront(root); } + syncAodOwnedHostVisualState(root, unlockedHosts); } } if (!unlockedScene) { trackedUnlockedStockSurfaces.clear(); restoreTransientUnlockedSources(); - if (renderResult.shouldOwnLockscreen()) { + if (!aodScene && renderResult.shouldOwnLockscreen()) { hideOrDiscoverLockedStatusBarIconSurfaces(root); } - removeDebugOverlay(root); + if (aodScene) { + applyDebugOverlayIfNeeded( + root, + true, + settings, + renderResult.layoutChanged(), + true, + activeScene); + } else { + removeDebugOverlay(root); + } return; } boolean scanTree = renderResult.stockSourcesChanged() || !hasTrackedUnlockedStockSurfaces(root); - applyDebugOverlayIfNeeded(root, true, settings, renderResult.layoutChanged()); + applyDebugOverlayIfNeeded( + root, + true, + settings, + renderResult.layoutChanged(), + false, + activeScene); hideTrackedStatusChipSources(root); if (renderResult.chipSourcesChanged() && !scanTree @@ -795,14 +1849,18 @@ final class StockLayoutCanvasController { return; } walkTree(root, view -> { - boolean statusChipSource = isStatusChipSource(view); - if (statusChipSource) { + boolean statusChipSurface = isTrackableStatusChipSurface(view); + if (statusChipSurface) { trackedStatusChipSources.add(view); } - boolean shouldHide = statusChipSource || shouldHideNonChipView(view); + boolean shouldHide = statusChipSurface || shouldHideNonChipView(view); if (shouldHide) { trackedUnlockedStockSurfaces.add(view); - hideView(view, true); + if (statusChipSurface) { + hideStatusChipSurface(root, view); + } else { + hideView(view, true); + } } else { trackedUnlockedStockSurfaces.remove(view); restoreView(view); @@ -817,64 +1875,53 @@ final class StockLayoutCanvasController { SceneKey activeScene ) { if (statusBarRoot) { - applyDebugOverlayIfNeeded(root, false, settings, false); + applyDebugOverlayIfNeeded(root, false, settings, false, false, activeScene); } else { removeDebugOverlay(root); } cleanupLayoutOffRoot(root, activeScene); - suppressLayoutOffShadeSurfaces(root, statusBarRoot, activeScene); - } - - private void suppressLayoutOffShadeSurfaces( - View root, - boolean statusBarRoot, - SceneKey activeScene - ) { - if (statusBarRoot - || root == null - || activeScene == null - || !canContainLockedStatusBarIconSurfaces(root)) { - return; - } - boolean shadeSurfaceShouldStayHidden = activeScene.isUnlocked() - || activeScene.isLockscreen() - && runtimeContext.getModeStateRepository().isSystemUiShadeLocked(); - if (shadeSurfaceShouldStayHidden) { - hideOrDiscoverLockedStatusBarIconSurfaces(root); - } } private void applyDebugOverlay( View root, boolean layoutEnabled, - SbtSettings settings + SbtSettings settings, + boolean aodStockVisualizersEnabled, + SceneKey scene ) { - if (!debugBoundsOverlayController.hasEnabledVisualizer(layoutEnabled, settings)) { + if (!debugBoundsOverlayController.hasEnabledVisualizer( + layoutEnabled, + settings, + aodStockVisualizersEnabled)) { debugBoundsOverlayController.remove(root); return; } - debugBoundsOverlayController.apply(root, layoutEnabled, settings); + debugBoundsOverlayController.apply(root, layoutEnabled, settings, aodStockVisualizersEnabled, scene); } private void applyDebugOverlayIfNeeded( View root, boolean layoutEnabled, SbtSettings settings, - boolean force + boolean force, + boolean aodStockVisualizersEnabled, + SceneKey scene ) { - int signature = debugOverlaySignature(root, layoutEnabled, settings); + int signature = debugOverlaySignature(root, layoutEnabled, settings, aodStockVisualizersEnabled, scene); Integer previous = debugOverlaySignatures.get(root); if (!force && previous != null && previous == signature) { return; } debugOverlaySignatures.put(root, signature); - applyDebugOverlay(root, layoutEnabled, settings); + applyDebugOverlay(root, layoutEnabled, settings, aodStockVisualizersEnabled, scene); } private int debugOverlaySignature( View root, boolean layoutEnabled, - SbtSettings settings + SbtSettings settings, + boolean aodStockVisualizersEnabled, + SceneKey scene ) { int result = 17; result = 31 * result + (layoutEnabled ? 1 : 0); @@ -886,7 +1933,14 @@ final class StockLayoutCanvasController { result = 31 * result + (settings.debugVisualizeStatusContainer ? 1 : 0); result = 31 * result + (settings.debugVisualizeClockContainer ? 1 : 0); result = 31 * result + (settings.debugVisualizeChipContainer ? 1 : 0); + result = 31 * result + (settings.debugVisualizeAodStockContainers ? 1 : 0); } + result = 31 * result + debugBoundsOverlayController.sourceSignature( + root, + layoutEnabled, + settings, + aodStockVisualizersEnabled, + scene); return result; } @@ -902,6 +1956,9 @@ final class StockLayoutCanvasController { ArrayDeque views = new ArrayDeque<>(hiddenViewStates.keySet()); while (!views.isEmpty()) { View view = views.removeFirst(); + if (forgetInactiveTrackedStatusChip(view)) { + continue; + } if (confirmedAodPluginViews.contains(view) || trackedLockedStatusBarIconSurfaces.contains(view)) { continue; @@ -923,7 +1980,14 @@ final class StockLayoutCanvasController { staleViews.add(view); continue; } - hideView(view, true); + if (!isTrackableStatusChipSurface(view) + && !isTrackedHiddenStatusChipSource(root, view)) { + hiddenViewStates.remove(view); + trackedUnlockedStockSurfaces.remove(view); + staleViews.add(view); + continue; + } + hideStatusChipSurface(root, view); } while (!staleViews.isEmpty()) { trackedStatusChipSources.remove(staleViews.removeFirst()); @@ -932,9 +1996,10 @@ final class StockLayoutCanvasController { private void trackAndHideStatusChipSources(View root) { walkTree(root, view -> { - if (isStatusChipSource(view)) { + if (isTrackableStatusChipSurface(view)) { trackedStatusChipSources.add(view); - hideView(view, true); + trackedUnlockedStockSurfaces.add(view); + hideStatusChipSurface(root, view); } }); } @@ -972,7 +2037,395 @@ final class StockLayoutCanvasController { } private void hideConfirmedAodPluginViews() { - hideTrackedViews(confirmedAodPluginViews, view -> true, false); + if (confirmedAodPluginViews.isEmpty()) { + return; + } + ArrayDeque staleViews = new ArrayDeque<>(); + for (View view : confirmedAodPluginViews) { + if (view == null || !view.isAttachedToWindow()) { + staleViews.add(view); + continue; + } + hideView(view, keepConfirmedAodPluginViewLaidOut(view)); + } + while (!staleViews.isEmpty()) { + confirmedAodPluginViews.remove(staleViews.removeFirst()); + } + } + + private boolean hasVisibleConfirmedAodPluginView() { + if (confirmedAodPluginViews.isEmpty()) { + return false; + } + for (View view : confirmedAodPluginViews) { + if (view == null || !view.isAttachedToWindow()) { + continue; + } + int targetVisibility = keepConfirmedAodPluginViewLaidOut(view) ? View.VISIBLE : View.GONE; + if (view.getVisibility() != targetVisibility || view.getAlpha() != 0f) { + return true; + } + } + return false; + } + + private boolean shouldCaptureAodVisualAlpha(View view) { + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + return ownHiddenViewAlphaWriteDepth <= 0 + && activeScene != null + && activeScene.isAod() + && runtimeContext.getModeStateRepository().isLayoutEnabled() + && isRelatedToConfirmedAodPluginView(view); + } + + private void rememberAodPluginVisualAlpha(View view, float alpha) { + if (view == null || Float.isNaN(alpha)) { + return; + } + aodPluginVisualAlphaByView.put(view, clampAlpha(alpha)); + View root = view.getRootView(); + if (root != null && root.isAttachedToWindow()) { + root.invalidate(); + } + } + + private void rememberAodPluginVisualAlphaChain(View view) { + View current = view; + while (current != null) { + aodPluginVisualAlphaByView.put(current, currentAodVisualAlpha(current)); + Object parent = current.getParent(); + current = parent instanceof View parentView ? parentView : null; + } + } + + private float currentAodVisualAlpha(View view) { + if (view == null) { + return 1f; + } + HiddenViewState hiddenState = hiddenViewStates.get(view); + if (hiddenState != null && view.getAlpha() == 0f) { + Float remembered = aodPluginVisualAlphaByView.get(view); + return remembered != null ? remembered : clampAlpha(hiddenState.alpha); + } + return clampAlpha(view.getAlpha()); + } + + private void syncAodOwnedHostVisualState(View root) { + syncAodOwnedHostVisualState(root, unlockedHostsByRoot.get(root)); + } + + private void syncAodOwnedHostVisualState(View root, UnlockedHosts hosts) { + if (root == null || hosts == null) { + return; + } + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + float alpha = activeScene != null && activeScene.isAod() + ? aodVisualAlphaForRoot(root) + : 1f; + setHostAlpha(hosts.clockHost, alpha); + setHostAlpha(hosts.carrierHost, alpha); + setHostAlpha(hosts.chipHost, alpha); + setHostAlpha(hosts.statusHost, alpha); + setHostAlpha(hosts.notificationHost, alpha); + } + + private float aodVisualAlphaForRoot(View root) { + float alpha = 1f; + boolean found = false; + boolean foundPositive = false; + float positiveAlpha = 1f; + float zeroInclusiveAlpha = 1f; + ArrayDeque staleViews = new ArrayDeque<>(); + for (View view : confirmedAodPluginViews) { + if (view == null || !view.isAttachedToWindow()) { + staleViews.add(view); + continue; + } + if (root != null && !isDescendantOf(view, root) && !isDescendantOf(root, view)) { + continue; + } + found = true; + alpha = effectiveAodVisualAlpha(view); + zeroInclusiveAlpha = Math.min(zeroInclusiveAlpha, alpha); + if (alpha > 0.001f) { + foundPositive = true; + positiveAlpha = Math.min(positiveAlpha, alpha); + } + } + while (!staleViews.isEmpty()) { + View stale = staleViews.removeFirst(); + confirmedAodPluginViews.remove(stale); + aodPluginVisualAlphaByView.remove(stale); + } + if (!found) { + return 1f; + } + return clampAlpha(foundPositive ? positiveAlpha : zeroInclusiveAlpha); + } + + private float effectiveAodVisualAlpha(View view) { + float alpha = 1f; + View current = view; + while (current != null) { + Float captured = aodPluginVisualAlphaByView.get(current); + if (captured != null) { + alpha *= captured; + } else { + alpha *= current.getAlpha(); + } + Object parent = current.getParent(); + current = parent instanceof View parentView ? parentView : null; + } + return clampAlpha(alpha); + } + + private void setHostAlpha(View host, float alpha) { + if (host != null && host.getAlpha() != alpha) { + host.setAlpha(alpha); + } + } + + private float clampAlpha(float alpha) { + if (Float.isNaN(alpha)) { + return 1f; + } + return Math.max(0f, Math.min(1f, alpha)); + } + + private boolean shouldPrepareCarrierlessAodNotificationTransition( + SbtSettings settings, + SceneKey scene + ) { + return settings != null + && scene != null + && scene.isLockscreen() + && scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS + && !settings.layoutCarrierEnabledLockscreen; + } + + private void cacheAodNotificationWidgetSteadyWidth(View root) { + ViewGroup iconContainer = findKeyguardIconOnlyContainer(root); + if (iconContainer == null || iconContainer.getChildCount() <= 0) { + return; + } + View widget = commonNotificationWidgetFor(iconContainer); + if (widget == null || widget.getWidth() <= 0) { + return; + } + View current = iconContainer; + while (current != null) { + int width = current == widget + ? rightEdgeRelativeTo(widget, iconContainer) + : current.getWidth(); + cacheAodNotificationSteadyWidth(current, width); + if (current == widget) { + break; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + cacheAodNotificationIconOnlyAreaSteadyLeft(notificationIconOnlyAreaFor(iconContainer)); + } + + private void cacheAodNotificationSteadyWidth(View view, int width) { + if (view == null || width <= 0) { + return; + } + Integer previous = aodNotificationWidgetSteadyWidths.get(view); + if (previous == null || width < previous) { + aodNotificationWidgetSteadyWidths.put(view, width); + } + } + + private void cacheAodNotificationIconOnlyAreaSteadyLeft(View view) { + if (view == null || view.getWidth() <= 0) { + return; + } + knownAodNotificationIconOnlyAreas.add(view); + int left = view.getLeft(); + if (left <= 0) { + return; + } + Integer previous = aodNotificationIconOnlyAreaSteadyLefts.get(view); + if (previous == null || left < previous) { + aodNotificationIconOnlyAreaSteadyLefts.put(view, left); + } + } + + private void prepareCarrierlessAodNotificationTransition(View root) { + ViewGroup iconContainer = findKeyguardIconOnlyContainer(root); + if (iconContainer == null) { + return; + } + View widget = commonNotificationWidgetFor(iconContainer); + if (widget == null) { + return; + } + View current = iconContainer; + while (current != null) { + Integer desiredWidth = aodNotificationWidgetSteadyWidths.get(current); + if (desiredWidth != null && desiredWidth > 0) { + applyPreparedAodNotificationWidth(current, desiredWidth); + } + if (current == widget) { + break; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + } + + private void applyPreparedAodNotificationWidth(View view, int desiredWidth) { + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params == null || params.width == desiredWidth) { + return; + } + originalAodNotificationWidgetLayoutWidths.putIfAbsent(view, params.width); + params.width = desiredWidth; + view.setLayoutParams(params); + } + + private void restorePreparedAodNotificationWidgetWidths(View root) { + if (originalAodNotificationWidgetLayoutWidths.isEmpty()) { + return; + } + ArrayDeque staleViews = new ArrayDeque<>(); + for (Map.Entry entry : originalAodNotificationWidgetLayoutWidths.entrySet()) { + View view = entry.getKey(); + if (view == null || !view.isAttachedToWindow()) { + staleViews.add(view); + continue; + } + if (root != null && !isDescendantOf(view, root)) { + continue; + } + ViewGroup.LayoutParams params = view.getLayoutParams(); + if (params != null && params.width != entry.getValue()) { + params.width = entry.getValue(); + view.setLayoutParams(params); + } + staleViews.add(view); + } + while (!staleViews.isEmpty()) { + originalAodNotificationWidgetLayoutWidths.remove(staleViews.removeFirst()); + } + } + + private AodLayoutBounds carrierlessAodIconOnlyAreaLayoutOverride( + View view, + int left, + int top, + int right, + int bottom + ) { + if (view == null) { + return null; + } + if (!knownAodNotificationIconOnlyAreas.contains(view)) { + if (!(view instanceof android.widget.LinearLayout) + || !"notification_icononly_area".equals(resolveIdName(view))) { + return null; + } + knownAodNotificationIconOnlyAreas.add(view); + } + if (!isDescendantOfNotificationShadeWindow(view)) { + return null; + } + SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene(); + SbtSettings settings = RuntimeSettingsCache.get(view.getContext()); + if (settings == null + || scene == null + || !scene.isAod() + || scene.notificationDisplayStyle() != NotificationDisplayStyle.ICONS + || !settings.clockEnabled + || !settings.layoutNotifEnabledAod + || settings.layoutCarrierEnabledLockscreen) { + return null; + } + Integer desiredLeft = aodNotificationIconOnlyAreaSteadyLefts.get(view); + int width = right - left; + if (desiredLeft == null || desiredLeft <= 0 || width <= 0 || left == desiredLeft) { + return null; + } + return new AodLayoutBounds(desiredLeft, top, desiredLeft + width, bottom); + } + + private ViewGroup findKeyguardIconOnlyContainer(View root) { + if (!(root instanceof ViewGroup rootGroup)) { + return null; + } + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootGroup); + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + if (view instanceof ViewGroup group + && "keyguard_icononly_container_view".equals(resolveIdName(group))) { + return group; + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child != null) { + queue.addLast(child); + } + } + } + } + return null; + } + + private View commonNotificationWidgetFor(View view) { + View current = view; + while (current != null) { + if ("common_notification_widget".equals(resolveIdName(current))) { + return current; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return null; + } + + private View notificationIconOnlyAreaFor(View view) { + View current = view; + while (current != null) { + if ("notification_icononly_area".equals(resolveIdName(current))) { + return current; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return null; + } + + private int rightEdgeRelativeTo(View root, View view) { + if (root == null || view == null) { + return 0; + } + int right = view.getRight(); + Object parent = view.getParent(); + while (parent instanceof View parentView && parentView != root) { + right += parentView.getLeft(); + parent = parentView.getParent(); + } + return parent == root ? right : 0; + } + + private boolean isRelatedToConfirmedAodPluginView(View view) { + if (view == null || confirmedAodPluginViews.isEmpty()) { + return false; + } + for (View confirmed : confirmedAodPluginViews) { + if (confirmed == null || !confirmed.isAttachedToWindow()) { + continue; + } + if (view == confirmed + || isDescendantOf(view, confirmed) + || isDescendantOf(confirmed, view)) { + return true; + } + } + return false; } private void trackAndHideLockedStatusBarIconSurfaces(View root) { @@ -999,7 +2452,12 @@ final class StockLayoutCanvasController { boolean foundInRoot = false; ArrayDeque staleViews = new ArrayDeque<>(); for (View view : trackedLockedStatusBarIconSurfaces) { - if (view == null || !view.isAttachedToWindow() || !isLockedStatusBarIconSurface(view)) { + if (view == null || !view.isAttachedToWindow()) { + staleViews.add(view); + continue; + } + if (!isLockedStatusBarIconSurface(view)) { + restoreView(view); staleViews.add(view); continue; } @@ -1020,6 +2478,78 @@ final class StockLayoutCanvasController { && isConflictingShadeHeaderAnimatorWrite(view); } + private boolean shouldSuppressStatusChipShellAlpha(View view, float alpha) { + if (alpha <= 0f || !isStatusChipShellWriteTarget(view)) { + return false; + } + trackAndHideStatusChipShell(view); + return true; + } + + private boolean shouldSuppressTrackedStatusChipAlpha(View view, float alpha) { + return alpha > 0f && isTrackedStatusChipWriteTarget(view); + } + + private boolean shouldSuppressTrackedStatusChipVisibility(View view, int visibility) { + return visibility != View.VISIBLE + && isTrackedStatusChipWriteTarget(view) + && hasVisibleStatusChipContent(view); + } + + private boolean isTrackedStatusChipWriteTarget(View view) { + if (view == null + || ownHiddenViewAlphaWriteDepth > 0 + || !trackedStatusChipSources.contains(view) + || !hiddenViewStates.containsKey(view) + || !view.isAttachedToWindow() + || !isLayoutEnabled(view.getContext()) + || !isStatusChipCandidate(view) + || (!hasVisibleStatusChipContent(view) && !isStatusChipShell(view))) { + return false; + } + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + if (activeScene == null || !activeScene.isUnlocked()) { + return false; + } + View root = unlockedStatusBarRootFor(view); + return root != null && isDescendantOf(view, root); + } + + private boolean isStatusChipShellWriteTarget(View view) { + if (view == null + || ownHiddenViewAlphaWriteDepth > 0 + || !view.isAttachedToWindow() + || !isLayoutEnabled(view.getContext()) + || !isStatusChipShell(view)) { + return false; + } + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); + if (activeScene == null || !activeScene.isUnlocked()) { + return false; + } + View root = unlockedStatusBarRootFor(view); + return root != null && isDescendantOf(view, root); + } + + private void trackAndHideStatusChipShell(View view) { + View root = unlockedStatusBarRootFor(view); + if (root == null) { + return; + } + boolean alreadyTracked = trackedStatusChipSources.contains(view) + && hiddenViewStates.containsKey(view); + trackedStatusChipSources.add(view); + trackedUnlockedStockSurfaces.add(view); + boolean hiddenChanged = hideStatusChipSurface(root, view); + if (alreadyTracked && !hiddenChanged) { + return; + } + unlockedIconRenderController.forceChipRefresh(root); + dirtyUnlockedLayoutRoots.add(root); + root.requestLayout(); + root.invalidate(); + } + private boolean shouldSuppressConflictingShadeHeaderTranslationY(View view, float translationY) { return view != null && translationY != view.getTranslationY() @@ -1027,24 +2557,37 @@ final class StockLayoutCanvasController { } private boolean isConflictingShadeHeaderAnimatorWrite(View view) { + SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); if (view == null - || !"split_shade_status_bar".equals(resolveIdName(view)) - || !isLayoutEnabled(view.getContext())) { + || activeScene == null + || !activeScene.isLockscreen() + || activeScene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS + || !isLayoutEnabled(view.getContext()) + || !isSplitShadeStatusBarCandidate(view)) { return false; } - SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene(); // Samsung runs two QS header animators during lockscreen cards shade pulls. // LegacyQsOpenAnimator provides the visible fade. LegacyQsExpandAnimator // writes conflicting values both while tracking expansion and when it clears // its state after collapse. - return activeScene != null - && activeScene.isLockscreen() - && activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS - && !runtimeContext.getModeStateRepository().isSystemUiShadeLocked() + return !runtimeContext.getModeStateRepository().isSystemUiShadeLocked() && (stackContains("LegacyQsExpandAnimator", "setQsExpansionPosition") || stackContains("LegacyQsExpandAnimator", "clearAnimationState")); } + private boolean isSplitShadeStatusBarCandidate(View view) { + if (view == null || view.getId() == View.NO_ID) { + return false; + } + Boolean cached = splitShadeStatusBarCandidateByView.get(view); + if (cached != null) { + return cached; + } + boolean candidate = "split_shade_status_bar".equals(resolveIdName(view)); + splitShadeStatusBarCandidateByView.put(view, candidate); + return candidate; + } + private boolean stackContains(String classNamePart, String methodName) { StackTraceElement[] stack = Thread.currentThread().getStackTrace(); for (StackTraceElement element : stack) { @@ -1080,7 +2623,8 @@ final class StockLayoutCanvasController { if (view != null && view.isAttachedToWindow() && isDescendantOf(view, root) - && isStatusChipSource(view)) { + && (isStatusChipSource(view) + || isTrackedHiddenStatusChipSource(root, view))) { return true; } } @@ -1117,10 +2661,8 @@ final class StockLayoutCanvasController { boolean carrierSurface = idName.toLowerCase(java.util.Locale.ROOT).contains("carrier") || viewClassName.toLowerCase(java.util.Locale.ROOT).contains("carrier"); boolean shelfSurface = isCardsNotificationIconSurface(view); - boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName) - || "notification_icononly_area".equals(idName) - || "keyguard_icononly_container_view".equals(idName) - || "common_notification_widget".equals(idName); + boolean shelfBackgroundSurface = isCardsNotificationShelfBackgroundSurface(view); + boolean shadeIconOnlySurface = "keyguard_icononly_container_view".equals(idName); if (!("system_icons".equals(idName) || carrierSurface || "system_icon_area".equals(idName) @@ -1129,11 +2671,9 @@ final class StockLayoutCanvasController { || "notification_icon_area".equals(idName) || "notification_icon_area_inner".equals(idName) || "notificationIcons".equals(idName) - || "notification_icononly_container".equals(idName) - || "notification_icononly_area".equals(idName) || "keyguard_icononly_container_view".equals(idName) - || "common_notification_widget".equals(idName) - || shelfSurface)) { + || shelfSurface + || shelfBackgroundSurface)) { return false; } Object current = view; @@ -1153,6 +2693,9 @@ final class StockLayoutCanvasController { if (shelfSurface && className.contains("NotificationShadeWindowView")) { return true; } + if (shelfBackgroundSurface && className.contains("NotificationShadeWindowView")) { + return true; + } if (shadeIconOnlySurface && className.contains("NotificationShadeWindowView")) { return true; } @@ -1162,7 +2705,7 @@ final class StockLayoutCanvasController { } private boolean keepLockedSurfaceLaidOut(View view) { - return isCarrierView(view) || isCardsNotificationIconSurface(view); + return isCarrierView(view); } private boolean isCardsNotificationIconSurface(View view) { @@ -1172,11 +2715,7 @@ final class StockLayoutCanvasController { String idName = resolveIdName(view); String className = view.getClass().getName(); return className.contains("SecShelfNotificationIconContainer") - || className.contains("NotificationShelfBackground") - || "notification_icononly_container".equals(idName) - || "notification_icononly_area".equals(idName) - || "keyguard_icononly_container_view".equals(idName) - || "common_notification_widget".equals(idName); + || "keyguard_icononly_container_view".equals(idName); } private boolean isCardsNotificationShelfSurface(View view) { @@ -1184,8 +2723,27 @@ final class StockLayoutCanvasController { return false; } String className = view.getClass().getName(); - return className.contains("SecShelfNotificationIconContainer") - || className.contains("NotificationShelfBackground"); + return className.contains("SecShelfNotificationIconContainer"); + } + + private boolean isCardsNotificationShelfBackgroundSurface(View view) { + if (view == null) { + return false; + } + String className = view.getClass().getName(); + return className.contains("NotificationShelfBackground") + && hasAncestorClass(view, "NotificationShelf"); + } + + private boolean hasAncestorClass(View view, String classNamePart) { + Object current = view != null ? view.getParent() : null; + while (current instanceof View currentView) { + if (currentView.getClass().getName().contains(classNamePart)) { + return true; + } + current = currentView.getParent(); + } + return false; } private boolean isCardsNotificationIconSourceContainer(View view) { @@ -1307,6 +2865,9 @@ final class StockLayoutCanvasController { if (!isStatusChipCandidate(view)) { return false; } + if (view instanceof ViewGroup && !hasVisibleStatusChipContent(view)) { + return false; + } Object parent = view.getParent(); while (parent instanceof View parentView) { if (isStatusChipCandidate(parentView)) { @@ -1317,6 +2878,74 @@ final class StockLayoutCanvasController { return true; } + private boolean isTrackableStatusChipSurface(View view) { + return isStatusChipSource(view) || isStatusChipShell(view); + } + + private boolean isStatusChipShell(View view) { + if (view == null || ownedIconHostManager.isOwnedHost(view)) { + return false; + } + String idName = resolveIdName(view); + if (!"ongoing_activity_capsule".equals(idName)) { + return false; + } + String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT); + return className.contains("touchinterceptframelayout"); + } + + private boolean hideStatusChipSurface(View root, View view) { + boolean keepLaidOut = isStatusChipShell(view) + || isStatusChipSource(view) + || isTrackedHiddenStatusChipSource(root, view); + if (keepLaidOut) { + hiddenViewStates.put(view, new HiddenViewState(View.VISIBLE, 1f)); + } + return hideView(view, keepLaidOut); + } + + private boolean isTrackedHiddenStatusChipSource(View root, View view) { + if (view == null + || !trackedStatusChipSources.contains(view) + || !hiddenViewStates.containsKey(view) + || !view.isAttachedToWindow() + || root == null + || !isDescendantOf(view, root) + || !isStatusChipCandidate(view)) { + return false; + } + return hasVisibleStatusChipContent(view); + } + + private boolean hasVisibleStatusChipContent(View source) { + if (!(source instanceof ViewGroup group)) { + return false; + } + ArrayDeque queue = new ArrayDeque<>(); + for (int i = 0; i < group.getChildCount(); i++) { + queue.addLast(group.getChildAt(i)); + } + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + if (view == null) { + continue; + } + if (view.getVisibility() == View.VISIBLE && view.getAlpha() > 0f) { + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + if (width > 0 && height > 0) { + return true; + } + } + if (view instanceof ViewGroup childGroup) { + for (int i = 0; i < childGroup.getChildCount(); i++) { + queue.addLast(childGroup.getChildAt(i)); + } + } + } + return false; + } + private boolean isStatusChipCandidate(View view) { String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT); String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT); @@ -1385,36 +3014,501 @@ final class StockLayoutCanvasController { if (controller == null) { return; } - applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "d"))); - applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "l"))); - applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "k"))); - applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "m"))); - applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "s"))); + View statusContainer = castView(ReflectionSupport.getFieldValue(controller, "c")); + View statusContainerEnd = castView(ReflectionSupport.getFieldValue(controller, "e")); + View notificationContainer = castView(ReflectionSupport.getFieldValue(controller, "d")); + if (!runtimeContext.getModeStateRepository().isLayoutEnabled()) { + return; + } + if (!shouldApplyLayoutPolicy(notificationContainer, statusContainer, statusContainerEnd)) { + return; + } + markAodLayoutDirty(statusContainer); + markAodLayoutDirty(statusContainerEnd); + applyConfirmedAodPluginPolicy(notificationContainer); + applyConfirmedAodPluginPolicy(statusContainer); + applyConfirmedAodPluginPolicy(statusContainerEnd); + } + + private void discoverAodStatusbarSourceContainers(View root, SbtSettings settings) { + if (!(root instanceof ViewGroup) || !runtimeContext.getModeStateRepository().isLayoutEnabled()) { + return; + } + View movingAodStatusSource = null; + View best = null; + View systemIconsSource = null; + View statusIconsSource = null; + View batterySource = null; + int bestPriority = Integer.MAX_VALUE; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(root); + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + if (view == null || ownedIconHostManager.isOwnedHost(view)) { + continue; + } + String idName = resolveIdName(view); + String className = view.getClass().getName(); + if (movingAodStatusSource == null && isMovingAodStatusbarSource(view)) { + movingAodStatusSource = view; + } + int priority = aodStatusbarSourcePriority(idName, className); + if ("system_icons".equals(idName)) { + systemIconsSource = view; + } else if ("statusIcons".equals(idName) || className.contains("StatusIconContainer")) { + statusIconsSource = view; + } else if ("battery".equals(idName) || className.contains("BatteryMeterView")) { + batterySource = view; + } + if (priority < bestPriority) { + best = view; + bestPriority = priority; + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + queue.addLast(group.getChildAt(i)); + } + } + } + if (movingAodStatusSource != null) { + AodStatusbarSourceStore.replaceStatusContainer(movingAodStatusSource); + return; + } + ArrayList statusSources = aodStatusbarSources( + systemIconsSource, + statusIconsSource, + batterySource, + best); + if (statusIconsSource != null || batterySource != null || systemIconsSource != null) { + AodStatusbarSourceStore.replaceStatusSources( + systemIconsSource, + statusIconsSource, + batterySource); + } else if (!statusSources.isEmpty()) { + AodStatusbarSourceStore.replaceStatusSources(statusSources); + } + } + + private ArrayList aodStatusbarSources( + View systemIcons, + View statusIcons, + View battery, + View selected + ) { + ArrayList result = new ArrayList<>(); + addIfAttached(result, statusIcons); + addIfAttached(result, battery); + if (result.isEmpty() && systemIcons instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + String idName = resolveIdName(child); + String className = child != null ? child.getClass().getName() : ""; + if ("statusIcons".equals(idName) + || "battery".equals(idName) + || className.contains("StatusIconContainer") + || className.contains("BatteryMeterView")) { + addIfAttached(result, child); + } + } + } + if (result.isEmpty()) { + addIfAttached(result, selected); + } + return result; + } + + private void addIfAttached(ArrayList out, View view) { + if (out == null || view == null || !view.isAttachedToWindow() || out.contains(view)) { + return; + } + out.add(view); + } + + private int aodStatusbarSourcePriority(String idName, String className) { + if ("system_icon_area".equals(idName)) { + return 0; + } + if ("status_bar_end_side_content".equals(idName) || "system_icons".equals(idName)) { + return 1; + } + if ("statusIcons".equals(idName) || (className != null && className.contains("StatusIconContainer"))) { + return 2; + } + return Integer.MAX_VALUE; } private void enforceIconsOnlyControllerPolicy(Object controller) { if (controller == null) { return; } - View container = castView(ReflectionSupport.requireFieldValue(controller, "mNotificationContainer")); + View container = castView(ReflectionSupport.getFieldValue(controller, "mNotificationContainer")); + installAodNotificationListenerHooks(controller); + if (!runtimeContext.getModeStateRepository().isLayoutEnabled()) { + captureAodPackagesFromNotificationCallback( + ReflectionSupport.getFieldValue(controller, "notificationCallback")); + return; + } + enforceDirectAodControllerPolicy(ReflectionSupport.getFieldValue( + controller, + "keyguardNIOStatusBarController")); + enforceDirectAodControllerPolicy(ReflectionSupport.getFieldValue( + controller, + "keyguardNioController")); + if (!shouldApplyLayoutPolicy(container)) { + return; + } + captureAodPackagesFromNotificationCallback( + ReflectionSupport.getFieldValue(controller, "notificationCallback")); + updateAodNotificationPackageSources(container); applyConfirmedAodPluginPolicy(container); } + private void installAodNotificationListenerHooks(Object controller) { + Object listener = ReflectionSupport.getFieldValue(controller, "mNotificationListener"); + if (listener == null) { + return; + } + Class listenerClass = listener.getClass(); + if (!hookedAodNotificationListenerClasses.add(listenerClass)) { + return; + } + hookAodNotificationListenerMethod(listenerClass, "updateNotification"); + hookAodNotificationListenerMethod(listenerClass, "a"); + } + + private void hookAodNotificationListenerMethod(Class listenerClass, String methodName) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + listenerClass, + methodName, + chain -> { + captureAodNotificationListenerCall( + chain.getThisObject(), + methodName, + chain.getArgs()); + return chain.proceed(); + }); + } + + private void captureAodNotificationListenerCall(Object listener, String methodName, List args) { + if (!runtimeContext.getModeStateRepository().isLayoutEnabled()) { + return; + } + Object controller = ReflectionSupport.getFieldValue(listener, "a"); + captureAodPackagesFromNotificationCallback( + ReflectionSupport.getFieldValue(controller, "notificationCallback")); + ArrayList imageViews = imageViewsFromArgs(args); + if (!imageViews.isEmpty()) { + latestAodIconImageViews = imageViews; + if (!runtimeContext.getModeStateRepository().isLayoutEnabled()) { + return; + } + updateKnownAodNotificationSources(); + } + } + + private void updateAodNotificationPackageSources(View containerView) { + if (!(containerView instanceof ViewGroup container)) { + return; + } + if (!isLayoutEnabled(container.getContext())) { + knownAodNotificationContainers.remove(container); + LockscreenCardsNotificationIconSourceStore.clearAodSources(container); + return; + } + boolean changed = false; + ArrayList packages = alignedAodIconPackages(container.getContext()); + if (!packages.isEmpty()) { + changed |= LockscreenCardsNotificationIconSourceStore.putPackages(container, packages); + } + boolean prepared = prepareAodNotificationIconSources(container); + changed |= prepared; + changed |= LockscreenCardsNotificationIconSourceStore.putAodDrawables( + container, + latestAodIconImageViews); + knownAodNotificationContainers.add(container); + if (changed) { + View root = container.getRootView(); + invalidateAodNotificationRoot(root); + } + } + + private void updateKnownAodNotificationSources() { + if (knownAodNotificationContainers.isEmpty()) { + return; + } + for (ViewGroup container : knownAodNotificationContainers) { + if (container == null || !container.isAttachedToWindow()) { + continue; + } + if (!isLayoutEnabled(container.getContext())) { + LockscreenCardsNotificationIconSourceStore.clearAodSources(container); + continue; + } + ArrayList packages = alignedAodIconPackages(container.getContext()); + boolean changed = false; + if (!packages.isEmpty()) { + changed |= LockscreenCardsNotificationIconSourceStore.putPackages(container, packages); + } + boolean prepared = prepareAodNotificationIconSources(container); + changed |= prepared; + changed |= LockscreenCardsNotificationIconSourceStore.putAodDrawables( + container, + latestAodIconImageViews); + if (changed) { + View root = container.getRootView(); + invalidateAodNotificationRoot(root); + } + } + } + + private boolean prepareAodNotificationIconSources(ViewGroup container) { + if (container == null + || latestAodIconImageViews == null + || latestAodIconImageViews.isEmpty()) { + return false; + } + boolean invoked = false; + for (ImageView imageView : new ArrayList<>(latestAodIconImageViews)) { + Drawable drawable = imageView != null ? imageView.getDrawable() : null; + if (drawable == null || stockPreparedAodIconDrawableByView.get(imageView) == drawable) { + continue; + } + ReflectionSupport.invokeMethod( + container, + "setChildProperty", + new Class[]{View.class}, + imageView); + stockPreparedAodIconDrawableByView.put(imageView, imageView.getDrawable()); + invoked = true; + } + return invoked; + } + + private ArrayList alignedAodIconPackages(Context context) { + int imageCount = latestAodIconImageViews != null ? latestAodIconImageViews.size() : 0; + if (imageCount <= 0) { + return new ArrayList<>(); + } + ArrayList aligned = alignAodPackagesByDrawable(context); + if (!aligned.isEmpty()) { + return aligned; + } + return new ArrayList<>(); + } + + private ArrayList alignAodPackagesByDrawable(Context context) { + ArrayList aligned = new ArrayList<>(); + if (context == null + || latestAodIconImageViews == null + || latestAodIconImageViews.isEmpty() + || latestAodNotifications == null + || latestAodNotifications.isEmpty()) { + return aligned; + } + ArrayList candidates = new ArrayList<>(); + for (StatusBarNotification sbn : latestAodNotifications) { + IconFingerprint fingerprint = notificationFingerprint(context, sbn); + if (fingerprint != null) { + candidates.add(fingerprint); + } + } + int matched = 0; + for (ImageView imageView : latestAodIconImageViews) { + IconFingerprint source = viewFingerprint(imageView); + String pkg = null; + if (source != null) { + int bestDistance = Integer.MAX_VALUE; + for (IconFingerprint candidate : candidates) { + int distance = source.distance(candidate); + if (distance < bestDistance) { + bestDistance = distance; + pkg = candidate.packageName; + } + } + if (bestDistance <= source.maxAcceptableDistance()) { + matched++; + } else { + pkg = null; + } + } + aligned.add(pkg); + } + return matched > 0 ? aligned : new ArrayList<>(); + } + + private IconFingerprint notificationFingerprint(Context context, StatusBarNotification sbn) { + if (context == null + || sbn == null + || sbn.getNotification() == null + || sbn.getNotification().getSmallIcon() == null + || !AppIconRules.isValidPackageName(sbn.getPackageName())) { + return null; + } + try { + Drawable drawable = sbn.getNotification().getSmallIcon().loadDrawable(context); + return fingerprintForDrawable(drawable, sbn.getPackageName()); + } catch (Throwable ignored) { + return null; + } + } + + private IconFingerprint viewFingerprint(ImageView imageView) { + if (imageView == null || imageView.getDrawable() == null) { + return null; + } + return fingerprintForDrawable(imageView.getDrawable(), null); + } + + private IconFingerprint fingerprintForDrawable(Drawable drawable, String packageName) { + if (drawable == null) { + return null; + } + int size = 32; + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + int oldLeft = drawable.getBounds().left; + int oldTop = drawable.getBounds().top; + int oldRight = drawable.getBounds().right; + int oldBottom = drawable.getBounds().bottom; + try { + drawable.setBounds(0, 0, size, size); + drawable.draw(canvas); + } catch (Throwable ignored) { + return null; + } finally { + drawable.setBounds(oldLeft, oldTop, oldRight, oldBottom); + } + boolean[] mask = new boolean[size * size]; + int filled = 0; + for (int y = 0; y < size; y++) { + for (int x = 0; x < size; x++) { + int color = bitmap.getPixel(x, y); + boolean on = Color.alpha(color) > 32; + mask[y * size + x] = on; + if (on) { + filled++; + } + } + } + bitmap.recycle(); + if (filled <= 0) { + return null; + } + return new IconFingerprint(packageName, mask, filled); + } + + private void invalidateAodNotificationRoot(View root) { + if (root == null) { + return; + } + aodNotificationSourceGeneration++; + unlockedIconRenderController.clearRoot(root); + dirtyUnlockedLayoutRoots.add(root); + } + + private void markAodLayoutDirty(View view) { + if (view == null || !view.isAttachedToWindow()) { + return; + } + AodSourceGeometry current = aodSourceGeometry(view); + AodSourceGeometry previous = aodSourceGeometryByView.put(view, current); + if (current != null && current.equals(previous)) { + return; + } + View root = view.getRootView(); + if (root != null) { + dirtyUnlockedLayoutRoots.add(root); + } + } + + private AodSourceGeometry aodSourceGeometry(View view) { + if (view == null) { + return null; + } + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + int[] location = new int[2]; + view.getLocationOnScreen(location); + return new AodSourceGeometry(location[0], location[1], width, height); + } + + private boolean shouldApplyLayoutPolicy(View... views) { + if (views != null) { + for (View view : views) { + if (view != null) { + return isLayoutEnabled(view.getContext()); + } + } + } + return runtimeContext.getModeStateRepository().isLayoutEnabled(); + } + private void applyConfirmedAodPluginPolicy(View view) { if (view == null) { return; } - applyConditionalAodPluginPolicy(view, isLayoutEnabled(view.getContext())); - } - - private void applyConditionalAodPluginPolicy(View view, boolean shouldHide) { - if (shouldHide) { - confirmedAodPluginViews.add(view); - hideView(view); - } else { + if (isKeyguardIconOnlyLayoutAncestor(view)) { confirmedAodPluginViews.remove(view); restoreView(view); + return; } + if (!isLayoutEnabled(view.getContext())) { + confirmedAodPluginViews.remove(view); + restoreView(view); + return; + } + confirmedAodPluginViews.add(view); + rememberAodPluginVisualAlphaChain(view); + hideView(view, keepConfirmedAodPluginViewLaidOut(view)); + } + + private boolean keepConfirmedAodPluginViewLaidOut(View view) { + return isKeyguardIconOnlyContainerView(view) || isAodStatusbarPluginContainerView(view); + } + + private boolean isKeyguardIconOnlyLayoutAncestor(View view) { + if (view == null) { + return false; + } + String idName = resolveIdName(view); + return "common_notification_widget".equals(idName) + || "status_bar_area".equals(idName) + || "notification_icononly_area".equals(idName) + || "notification_icononly_container".equals(idName); + } + + private boolean isKeyguardIconOnlyContainerView(View view) { + if (view == null || !"keyguard_icononly_container_view".equals(resolveIdName(view))) { + return false; + } + Object parent = view.getParent(); + while (parent instanceof View parentView) { + String idName = resolveIdName(parentView); + if ("notification_icononly_container".equals(idName) + || "notification_icononly_area".equals(idName) + || "status_bar_area".equals(idName) + || "common_notification_widget".equals(idName)) { + return true; + } + parent = parentView.getParent(); + } + return false; + } + + private boolean isAodStatusbarPluginContainerView(View view) { + if (view == null) { + return false; + } + String idName = resolveIdName(view); + return "common_statusbar_battery_container".equals(idName) + || "common_battery_statusbar_container".equals(idName) + || "common_statusbar_battery_container_internal".equals(idName); + } + + private boolean isMovingAodStatusbarSource(View view) { + return view != null + && "common_statusbar_battery_container_internal".equals(resolveIdName(view)); } private boolean hideView(View view) { @@ -1431,14 +3525,38 @@ final class StockLayoutCanvasController { && view.getAlpha() == 0f) { return false; } - hiddenViewStates.computeIfAbsent(view, ignored -> new HiddenViewState( - view.getVisibility(), - view.getAlpha())); - view.setAlpha(0f); - view.setVisibility(targetVisibility); + hiddenViewStates.computeIfAbsent(view, ignored -> hiddenStateFor(view, keepLaidOut)); + ownHiddenViewAlphaWriteDepth++; + try { + view.setAlpha(0f); + view.setVisibility(targetVisibility); + } finally { + ownHiddenViewAlphaWriteDepth--; + } return true; } + private HiddenViewState hiddenStateFor(View view, boolean keepLaidOut) { + int visibility = view.getVisibility(); + float alpha = view.getAlpha(); + if (keepLaidOut && isRecoverableStockStatusBarSurface(view)) { + visibility = View.VISIBLE; + if (alpha <= 0f) { + alpha = 1f; + } + } + return new HiddenViewState(visibility, alpha); + } + + private boolean isRecoverableStockStatusBarSurface(View view) { + return trackedUnlockedStockSurfaces.contains(view) + || trackedLockedStatusBarIconSurfaces.contains(view) + || "left_clock_container".equals(resolveIdName(view)) + || "notification_icon_area".equals(resolveIdName(view)) + || "notification_icon_area_inner".equals(resolveIdName(view)) + || "notificationIcons".equals(resolveIdName(view)); + } + private boolean restoreView(View view) { if (view == null || !isViewThread(view)) { return false; @@ -1447,94 +3565,148 @@ final class StockLayoutCanvasController { if (state == null) { return false; } - view.setAlpha(state.alpha); - view.setVisibility(state.visibility); + ownHiddenViewAlphaWriteDepth++; + try { + view.setAlpha(state.alpha); + view.setVisibility(state.visibility); + } finally { + ownHiddenViewAlphaWriteDepth--; + } return true; } - private boolean releaseHiddenViews(SceneKey activeScene) { - boolean unlockedScene = activeScene != null && activeScene.isUnlocked(); - boolean steadyLockscreenScene = activeScene != null - && activeScene.isLockscreen() - && !runtimeContext.getModeStateRepository().isSystemUiShadeLocked(); - boolean lockscreenShadeScene = activeScene != null - && activeScene.isLockscreen() - && runtimeContext.getModeStateRepository().isSystemUiShadeLocked(); - boolean deferredLockedShadeSurfaces = false; + private void restoreAllHiddenViews() { ArrayDeque views = new ArrayDeque<>(hiddenViewStates.keySet()); while (!views.isEmpty()) { View view = views.removeFirst(); - boolean wasUnlockedStockSurface = trackedUnlockedStockSurfaces.contains(view); - boolean wasLockedShadeSurface = trackedLockedStatusBarIconSurfaces.contains(view) - && isDescendantOfNotificationShadeWindow(view); - if (unlockedScene && wasLockedShadeSurface) { - deferredLockedShadeSurfaces = true; - view.setAlpha(0f); - view.setVisibility(View.GONE); - requestViewAndParentLayout(view); - continue; - } - if (lockscreenShadeScene && wasLockedShadeSurface) { - deferredLockedShadeSurfaces = true; - view.setAlpha(0f); - view.setVisibility(View.GONE); - requestViewAndParentLayout(view); - continue; - } - if (steadyLockscreenScene - && wasLockedShadeSurface - && !isCardsNotificationShelfSurface(view)) { - deferredLockedShadeSurfaces = true; - view.setAlpha(0f); - view.setVisibility(View.GONE); - requestViewAndParentLayout(view); + if (forgetInactiveTrackedStatusChip(view)) { continue; } restoreView(view); - if ((unlockedScene && wasUnlockedStockSurface) - || (steadyLockscreenScene && isCardsNotificationShelfSurface(view))) { - forceViewVisible(view); - } } trackedStatusChipSources.clear(); - if (!deferredLockedShadeSurfaces) { - trackedLockedStatusBarIconSurfaces.clear(); - } + trackedLockedStatusBarIconSurfaces.clear(); trackedUnlockedStockSurfaces.clear(); - return deferredLockedShadeSurfaces; + aodPluginVisualAlphaByView.clear(); + } + + private boolean forgetInactiveTrackedStatusChip(View view) { + if (view == null + || !trackedStatusChipSources.contains(view) + || isStatusChipSource(view) + || isStatusChipShell(view)) { + return false; + } + hiddenViewStates.remove(view); + trackedStatusChipSources.remove(view); + trackedUnlockedStockSurfaces.remove(view); + return true; } private void cleanupLayoutOffRoot(View root, SceneKey activeScene) { - boolean deferredLockedShadeSurfaces = false; + lockscreenCardsNotificationRowFilter.restoreAll(); if (!hiddenViewStates.isEmpty()) { - deferredLockedShadeSurfaces = releaseHiddenViews(activeScene); + restoreAllHiddenViews(); } + restoreLayoutOffStockSurfaces(root, activeScene); trackedStatusChipSources.clear(); - if (!deferredLockedShadeSurfaces) { - trackedLockedStatusBarIconSurfaces.clear(); - } + trackedLockedStatusBarIconSurfaces.clear(); trackedUnlockedStockSurfaces.clear(); + confirmedAodPluginViews.clear(); + aodPluginVisualAlphaByView.clear(); + knownAodNotificationContainers.clear(); + knownAodNotificationIconOnlyAreas.clear(); + latestAodIconImageViews = new ArrayList<>(); + latestAodNotifications = new ArrayList<>(); + AodStatusbarSourceStore.clear(); + LockscreenCardsNotificationIconSourceStore.clearAodSources(); + aodNotificationWidgetSteadyWidths.clear(); + aodNotificationIconOnlyAreaSteadyLefts.clear(); + originalAodNotificationWidgetLayoutWidths.clear(); pendingUnlockedAppearanceRefreshRoots.remove(root); dirtyUnlockedLayoutRoots.remove(root); settingsIdentityByRoot.remove(root); lockscreenCardsRowFilterSignatures.remove(root); clockTimeBucketByRoot.remove(root); if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) { + unlockedHostsByRoot.remove(root); + rootGeometries.remove(root); + unlockedIconRenderController.clearRoot(root); removeLockscreenCardsNotificationHost(root); + removeAodCardsNotificationHost(root); + ownedIconHostManager.removeUnlockedHosts(root); return; } rootGeometries.remove(root); unlockedIconRenderController.clearAll(); + removeAodCardsNotificationHost(root); ownedIconHostManager.removeUnlockedHosts(root); } - private void forceViewVisible(View view) { - if (view == null || !isViewThread(view)) { + private void restoreLayoutOffStockSurfaces(View root, SceneKey activeScene) { + if (root == null || !isViewThread(root)) { return; } - view.setAlpha(1f); - view.setVisibility(View.VISIBLE); - requestViewAndParentLayout(view); + boolean unlockedScene = activeScene == null || activeScene.isUnlocked(); + walkTree(root, view -> { + if (!isLayoutOffRecoverableStockSurface(view)) { + return; + } + restoreView(view); + if (!unlockedScene && isUnlockedOnlyStatusbarSurface(view)) { + if (view.getAlpha() != 0f) { + view.setAlpha(0f); + } + if (view.getVisibility() != View.INVISIBLE) { + view.setVisibility(View.INVISIBLE); + } + return; + } + if (unlockedScene && view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + } + if (unlockedScene && view.getAlpha() <= 0f) { + view.setAlpha(1f); + } + }); + } + + private boolean isUnlockedOnlyStatusbarSurface(View view) { + String idName = resolveIdName(view); + return "left_clock_container".equals(idName) + || "notification_icon_area".equals(idName) + || "notification_icon_area_inner".equals(idName) + || "notificationIcons".equals(idName); + } + + private boolean isLayoutOffRecoverableStockSurface(View view) { + if (view == null || ownedIconHostManager.isOwnedHost(view)) { + return false; + } + String idName = resolveIdName(view); + String className = view.getClass().getName(); + return "left_clock_container".equals(idName) + || "notification_icon_area".equals(idName) + || "notification_icon_area_inner".equals(idName) + || "notificationIcons".equals(idName) + || "system_icon_area".equals(idName) + || "statusIcons".equals(idName) + || "battery".equals(idName) + || className.contains("NotificationIconContainer") + || className.contains("StatusIconContainer") + || className.contains("BatteryMeterView") + || className.contains("QSClockIndicatorView"); + } + + private void removeSharedLayoutHosts(View root) { + removeLockscreenCardsNotificationHost(root); + removeAodCardsNotificationHost(root); + ownedIconHostManager.removeUnlockedHosts(root); + unlockedHostsByRoot.remove(root); + pendingUnlockedAppearanceRefreshRoots.remove(root); + dirtyUnlockedLayoutRoots.remove(root); + settingsIdentityByRoot.remove(root); + clockTimeBucketByRoot.remove(root); } private void requestViewAndParentLayout(View view) { @@ -1621,6 +3793,55 @@ final class StockLayoutCanvasController { } } + private void renderAodCardsNotificationStripIfNeeded( + View root, + SbtSettings settings, + SceneKey activeScene + ) { + if (root == null + || settings == null + || activeScene == null + || !activeScene.isAod() + || activeScene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS) { + removeAodCardsNotificationHost(root); + return; + } + View host = ownedIconHostManager.ensureAodCardsNotificationHost(root); + if (host instanceof ViewGroup hostGroup) { + int generation = System.identityHashCode(settings); + generation = 31 * generation + root.getWidth(); + generation = 31 * generation + root.getHeight(); + generation = 31 * generation + aodNotificationSourceGeneration; + generation = 31 * generation + latestAodIconImageViews.size(); + generation = 31 * generation + latestAodNotifications.size(); + generation = 31 * generation + boundsSignature( + LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForRoot(root)); + Integer previous = aodCardsNotificationRenderGenerationByRoot.get(root); + boolean shouldRender = previous == null + || previous != generation + || dirtyUnlockedLayoutRoots.contains(root) + || hostGroup.getChildCount() <= 0; + if (shouldRender) { + aodCardsNotificationRenderGenerationByRoot.put(root, generation); + lockscreenCardsNotificationStripRenderer.render(root, hostGroup, settings, activeScene); + } + setHostAlpha(host, aodVisualAlphaForRoot(root)); + host.bringToFront(); + } + } + + private int boundsSignature(Bounds bounds) { + if (bounds == null) { + return 0; + } + int result = 17; + result = 31 * result + bounds.left; + result = 31 * result + bounds.top; + result = 31 * result + bounds.width; + result = 31 * result + bounds.height; + return result; + } + private void applyLockscreenCardsNotificationRowFilter(ViewGroup stack) { applyLockscreenCardsNotificationRowFilter(stack, false); } @@ -1741,6 +3962,17 @@ final class StockLayoutCanvasController { ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST); } + private void removeAodCardsNotificationHost(View root) { + View host = root instanceof ViewGroup group + ? findDirectOwnedHost(group, OwnedIconHostManager.TAG_AOD_CARDS_NOTIFICATION_HOST) + : null; + if (host instanceof ViewGroup hostGroup) { + lockscreenCardsNotificationStripRenderer.clear(hostGroup); + } + aodCardsNotificationRenderGenerationByRoot.remove(root); + ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_AOD_CARDS_NOTIFICATION_HOST); + } + private View findDirectOwnedHost(ViewGroup root, String tag) { if (root == null || tag == null) { return null; @@ -1758,10 +3990,12 @@ final class StockLayoutCanvasController { View root, SceneKey activeScene ) { - if (!isUnlockedStatusBarRoot(root)) { + if (!isUnlockedStatusBarRoot(root) + && !(activeScene != null && activeScene.isAod() && isAodLayoutRoot(root))) { return null; } - if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) { + if (activeScene != null + && (activeScene.isUnlocked() || activeScene.isLockscreen() || activeScene.isAod())) { UnlockedHosts cached = unlockedHostsByRoot.get(root); if (areUnlockedHostsAttached(root, cached)) { return cached; @@ -1837,6 +4071,42 @@ final class StockLayoutCanvasController { return root != null && root.getClass().getName().toLowerCase(java.util.Locale.ROOT).contains("aod"); } + private boolean isAodLayoutRoot(View root) { + return isAodRoot(root) || hasConfirmedAodPluginDescendant(root); + } + + private boolean hasConfirmedAodPluginDescendant(View root) { + if (root == null || confirmedAodPluginViews.isEmpty()) { + return false; + } + for (View view : confirmedAodPluginViews) { + if (view != null && view.isAttachedToWindow() && isDescendantOf(view, root)) { + return true; + } + } + return false; + } + + private ArrayList imageViewsFromArgs(List args) { + ArrayList imageViews = new ArrayList<>(); + if (args == null) { + return imageViews; + } + for (Object arg : args) { + if (arg instanceof ImageView imageView && imageView.getDrawable() != null) { + imageViews.add(imageView); + } + if (arg instanceof Iterable iterable) { + for (Object item : iterable) { + if (item instanceof ImageView imageView && imageView.getDrawable() != null) { + imageViews.add(imageView); + } + } + } + } + return imageViews; + } + private View castView(Object value) { return value instanceof View ? (View) value : null; } @@ -1908,6 +4178,25 @@ final class StockLayoutCanvasController { } } + 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) { + return Integer.MAX_VALUE; + } + int distance = 0; + for (int i = 0; i < mask.length; i++) { + if (mask[i] != other.mask[i]) { + distance++; + } + } + return distance; + } + + int maxAcceptableDistance() { + return Math.max(18, Math.max(1, filled) / 4); + } + } + private record UnlockedHosts( ViewGroup clockHost, ViewGroup carrierHost, @@ -1920,4 +4209,19 @@ final class StockLayoutCanvasController { private record RootGeometry(int width, int height, int rotation) { } + private record AodSourceGeometry(int left, int top, int width, int height) { + } + + private record AodLayoutBounds(int left, int top, int right, int bottom) { + } + + private record DarkIconDispatcherState( + Object darkIntensity, + Object iconTint, + Object tint, + Object darkAreas, + Object tintAreas + ) { + } + } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java index bc56a11..c9d6c4a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java @@ -265,15 +265,17 @@ public final class PackedStatusBarLayoutSolver { neededForItem = Math.max(neededForItem, constraint.required - assumedAbsorberCapacity); } + ArrayList groupItems = item.isIconItem() + ? iconGroups.get(item.iconGroup) + : null; double amount = Math.min(Math.max(0, neededForItem), contextualShrinkCapacity(item, item)); double directActual = item.isIconItem() - ? truncateIconItemWithoutDot(item, amount) + ? truncateIconItemWithoutDot(item, amount, groupItems != null && groupItems.size() > 1) : item.truncateAtLeast(amount); if (directActual != 0) { adjustConstraintsForItemWidthDelta(constraints, item, directActual); } if (item.isIconItem()) { - ArrayList groupItems = iconGroups.get(item.iconGroup); ArrayList remaining = remainingIconWidths.get(item.iconGroup); int remainingStart = remainingIconStarts.getOrDefault(item.iconGroup, 0); ArrayList remainingAfterItem = allocateCurrentIconItem( @@ -336,7 +338,14 @@ public final class PackedStatusBarLayoutSolver { return item.canShrinkBy(); } - private static double truncateIconItemWithoutDot(LayoutItem item, double amount) { + private static double truncateIconItemWithoutDot( + LayoutItem item, + double amount, + boolean allowZeroWithoutDot + ) { + if (!allowZeroWithoutDot) { + return item.truncateAtLeast(amount); + } boolean oldAllowZero = item.allowZero; boolean oldAllowDot = item.allowDot; item.allowZero = true; @@ -403,8 +412,9 @@ public final class PackedStatusBarLayoutSolver { } ArrayList remaining = new ArrayList<>(groupItems.get(0).iconWidths()); int start = 0; + boolean allowZeroWithoutDot = groupItems.size() > 1; for (LayoutItem item : groupItems) { - setIconItemPool(item, remaining, start); + setIconItemPool(item, remaining, start, allowZeroWithoutDot); int used = item.visibleIcons; remaining = sliceFrom(remaining, used); start += used; @@ -420,12 +430,13 @@ public final class PackedStatusBarLayoutSolver { ArrayList constraints ) { HashMap oldWidths = widthsByItem(groupItems); - setIconItemPool(item, remainingWidths, remainingStart); + boolean allowZeroWithoutDot = groupItems != null && groupItems.size() > 1; + setIconItemPool(item, remainingWidths, remainingStart, allowZeroWithoutDot); ArrayList remainingAfterItem = sliceFrom(remainingWidths, item.visibleIcons); int remainingAfterStart = remainingStart + item.visibleIcons; int itemIndex = groupItems.indexOf(item); for (int i = itemIndex + 1; i < groupItems.size(); i++) { - setIconItemPool(groupItems.get(i), remainingAfterItem, remainingAfterStart); + setIconItemPool(groupItems.get(i), remainingAfterItem, remainingAfterStart, true); } adjustGroupConstraintDeltas(groupItems, oldWidths, constraints); return remainingAfterItem; @@ -451,7 +462,17 @@ public final class PackedStatusBarLayoutSolver { adjustGroupConstraintDeltas(groupItems, oldWidths, constraints); } - private static void setIconItemPool(LayoutItem item, ArrayList widths, int startIndex) { + private static void setIconItemPool( + LayoutItem item, + ArrayList widths, + int startIndex, + boolean allowZeroWithoutDot + ) { + if (!allowZeroWithoutDot) { + item.widthLimit = Math.min(item.currentWidthLimit(), item.width()); + item.setPoolIcons(widths, startIndex); + return; + } boolean oldAllowZero = item.allowZero; boolean oldAllowDot = item.allowDot; item.allowZero = true; @@ -494,6 +515,12 @@ public final class PackedStatusBarLayoutSolver { } LayoutItem item = groupItems.get(lastIcons); if (item.visibleIconWidths.isEmpty()) { + if (item.allowDot && item.poolRemaining > 0) { + item.dot = true; + item.visibleIcons = 0; + item.setPrimaryWidth(dotWidthForFinalPlacement(groupItems, item)); + item.widthLimit = item.width(); + } return; } item.visibleIconWidths.remove(item.visibleIconWidths.size() - 1); @@ -515,7 +542,7 @@ public final class PackedStatusBarLayoutSolver { break; } } - if (allLaterZero) { + if (allLaterZero && item.noCollisionsOnSide(false)) { return Math.min(dotWidth, item.dotWidth - item.dotWidth / 3.0); } } else { @@ -526,7 +553,7 @@ public final class PackedStatusBarLayoutSolver { break; } } - if (allEarlierZero) { + if (allEarlierZero && item.noCollisionsOnSide(true)) { return Math.min(dotWidth, item.dotWidth - item.dotWidth / 3.0); } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/mode/ModeStateRepository.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/mode/ModeStateRepository.java index da30646..24684b9 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/mode/ModeStateRepository.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/mode/ModeStateRepository.java @@ -13,12 +13,12 @@ import java.util.Objects; * runtime API. */ public final class ModeStateRepository { - private SceneKey activeScene = SceneKey.unlocked(); private boolean systemUiStatusBarStateKnown; private int systemUiStatusBarState; private boolean layoutEnabled; private final List layoutEnabledListeners = new ArrayList<>(); + private final List sceneListeners = new ArrayList<>(); public synchronized SceneKey getActiveScene() { return activeScene; @@ -56,39 +56,71 @@ public final class ModeStateRepository { } } - public synchronized void setUnlocked() { - activeScene = SceneKey.unlocked(); + public void setUnlocked() { + setActiveScene(SceneKey.unlocked()); } - public synchronized void setLockscreen(NotificationDisplayStyle style) { - activeScene = SceneKey.lockscreen(requireDisplayStyle(style)); + public void setLockscreen(NotificationDisplayStyle style) { + setActiveScene(SceneKey.lockscreen(requireDisplayStyle(style))); } - public synchronized void setAod(NotificationDisplayStyle style) { - activeScene = SceneKey.aod(requireDisplayStyle(style)); + public void setAod(NotificationDisplayStyle style) { + setActiveScene(SceneKey.aod(requireDisplayStyle(style))); } - public synchronized void setScene(SceneKey sceneKey) { - activeScene = Objects.requireNonNull(sceneKey, "sceneKey"); + public void setScene(SceneKey sceneKey) { + setActiveScene(Objects.requireNonNull(sceneKey, "sceneKey")); } - public synchronized void setSystemUiStatusBarState( + public void setSystemUiStatusBarState( int state, boolean dozing, NotificationDisplayStyle notificationDisplayStyle ) { - systemUiStatusBarStateKnown = true; - systemUiStatusBarState = state; NotificationDisplayStyle style = notificationDisplayStyle; if (style == null || style == NotificationDisplayStyle.NONE) { style = NotificationDisplayStyle.UNKNOWN; } - if (dozing) { - activeScene = SceneKey.aod(style); - } else if (state == 0) { - activeScene = SceneKey.unlocked(); - } else { - activeScene = SceneKey.lockscreen(style); + SceneKey scene = dozing + ? SceneKey.aod(style) + : state == 0 ? SceneKey.unlocked() : SceneKey.lockscreen(style); + synchronized (this) { + systemUiStatusBarStateKnown = true; + systemUiStatusBarState = state; + } + setActiveScene(scene); + } + + public synchronized void addSceneListener(SceneListener listener) { + if (listener != null && !sceneListeners.contains(listener)) { + sceneListeners.add(listener); + } + } + + private void setActiveScene(SceneKey scene) { + SceneKey previous; + List listeners; + synchronized (this) { + previous = activeScene; + if (previous.equals(scene)) { + return; + } + activeScene = scene; + listeners = new ArrayList<>(sceneListeners); + } + notifySceneChanged(previous, scene, listeners); + } + + private void notifySceneChanged( + SceneKey previous, + SceneKey current, + List listeners + ) { + if (listeners == null || listeners.isEmpty()) { + return; + } + for (SceneListener listener : listeners) { + listener.onSceneChanged(previous, current); } } @@ -102,4 +134,8 @@ public final class ModeStateRepository { public interface LayoutEnabledListener { void onLayoutEnabledChanged(boolean enabled); } + + public interface SceneListener { + void onSceneChanged(SceneKey previous, SceneKey current); + } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodLiveStatusbarSource.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodLiveStatusbarSource.java new file mode 100644 index 0000000..88d64fa --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodLiveStatusbarSource.java @@ -0,0 +1,151 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayDeque; + +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; + +final class AodLiveStatusbarSource { + private AodLiveStatusbarSource() { + } + + static Bounds movingBoundsForRoot(View root) { + View source = sourceForRoot(root, true); + return usableBounds(root, source); + } + + static Bounds movingIconContentBoundsForRoot(View root) { + View source = sourceForRoot(root, true); + Bounds sourceBounds = usableBounds(root, source); + if (!(source instanceof ViewGroup group)) { + return sourceBounds; + } + Bounds union = null; + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (!isIconContentCandidate(child)) { + continue; + } + Bounds childBounds = usableBounds(root, child); + if (childBounds == null) { + continue; + } + union = union == null ? childBounds : union(union, childBounds); + } + return union != null ? union : sourceBounds; + } + + static Bounds boundsForRoot(View root) { + View source = sourceForRoot(root, false); + return usableBounds(root, source); + } + + private static View sourceForRoot(View root, boolean movingOnly) { + if (!(root instanceof ViewGroup rootGroup)) { + return null; + } + View best = null; + int bestPriority = Integer.MAX_VALUE; + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootGroup); + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + int priority = sourcePriority(view, movingOnly); + if (priority < bestPriority && usableBounds(root, view) != null) { + best = view; + bestPriority = priority; + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child != null) { + queue.addLast(child); + } + } + } + } + return best; + } + + private static int sourcePriority(View view, boolean movingOnly) { + String idName = resolveIdName(view); + if ("common_statusbar_battery_container_internal".equals(idName) + && view instanceof ViewGroup group + && group.getChildCount() > 0 + && shortEnough(view)) { + return 0; + } + if (!movingOnly + && "system_icons".equals(idName) + && view instanceof ViewGroup group + && group.getChildCount() > 0 + && hasAncestorId(view, "system_icons_container") + && hasAncestorId(view, "status_icon_area") + && shortEnough(view)) { + return 1; + } + return Integer.MAX_VALUE; + } + + private static boolean shortEnough(View view) { + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + return height > 0 && height <= 80; + } + + private static boolean isIconContentCandidate(View view) { + if (view == null) { + return false; + } + String className = view.getClass().getName(); + if (className.contains("BatteryMeterView")) { + return false; + } + return className.contains("ImageView") || className.contains("StatusBarIcon"); + } + + private static Bounds union(Bounds first, Bounds second) { + int left = Math.min(first.left, second.left); + int top = Math.min(first.top, second.top); + int right = Math.max(first.left + first.width, second.left + second.width); + int bottom = Math.max(first.top + first.height, second.top + second.height); + return new Bounds(left, top, right - left, bottom - top); + } + + private static Bounds usableBounds(View root, View view) { + Bounds bounds = ViewGeometry.boundsRelativeTo(root, view); + if (root == null || bounds == null || bounds.width <= 0 || bounds.height <= 0) { + return null; + } + int right = bounds.left + bounds.width; + int bottom = bounds.top + bounds.height; + if (bounds.left < 0 || bounds.top < 0 || right > root.getWidth() || bottom > root.getHeight()) { + return null; + } + return bounds; + } + + private static boolean hasAncestorId(View view, String idName) { + Object parent = view != null ? view.getParent() : null; + while (parent instanceof View parentView) { + if (idName.equals(resolveIdName(parentView))) { + return true; + } + parent = parentView.getParent(); + } + return false; + } + + private static String resolveIdName(View view) { + if (view == null || view.getId() == View.NO_ID) { + return ""; + } + try { + return view.getResources().getResourceEntryName(view.getId()); + } catch (Resources.NotFoundException ignored) { + return ""; + } + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodStatusbarSourceStore.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodStatusbarSourceStore.java new file mode 100644 index 0000000..0536a27 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/AodStatusbarSourceStore.java @@ -0,0 +1,249 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import android.view.View; + +import java.util.ArrayList; +import java.util.List; +import java.util.WeakHashMap; + +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; + +public final class AodStatusbarSourceStore { + private static final WeakHashMap STATUS_SOURCES = new WeakHashMap<>(); + + public enum StatusSourceRole { + SYSTEM_ICONS, + STATUS_ICONS, + BATTERY, + UNKNOWN + } + + private AodStatusbarSourceStore() { + } + + public static void putStatusContainer(View view) { + putStatusSource(view); + } + + public static void putStatusSource(View view) { + if (view != null) { + STATUS_SOURCES.put(view, StatusSourceRole.UNKNOWN); + } + } + + public static void putStatusSource(View view, StatusSourceRole role) { + if (view != null) { + STATUS_SOURCES.put(view, role != null ? role : StatusSourceRole.UNKNOWN); + } + } + + public static void replaceStatusSources(List views) { + STATUS_SOURCES.clear(); + if (views == null) { + return; + } + for (View view : views) { + putStatusSource(view); + } + } + + public static void replaceStatusSources( + View systemIcons, + View statusIcons, + View battery + ) { + STATUS_SOURCES.clear(); + putStatusSource(systemIcons, StatusSourceRole.SYSTEM_ICONS); + putStatusSource(statusIcons, StatusSourceRole.STATUS_ICONS); + putStatusSource(battery, StatusSourceRole.BATTERY); + } + + public static void replaceStatusContainer(View view) { + STATUS_SOURCES.clear(); + putStatusSource(view); + } + + public static void clear() { + STATUS_SOURCES.clear(); + } + + public static Bounds statusBoundsForRoot(View root, View excludedContainer) { + if (root == null) { + return null; + } + Bounds union = null; + for (View source : STATUS_SOURCES.keySet()) { + if (!isUsableSource(root, source, excludedContainer)) { + continue; + } + if (!isStatusAnchorSource(STATUS_SOURCES.get(source))) { + continue; + } + Bounds bounds = boundsForRoot(root, source); + if (!isUsableRightAnchor(root, bounds)) { + continue; + } + union = union == null ? bounds : union(union, bounds); + } + return union; + } + + public static ArrayList statusBoundsListForRoot(View root, View excludedContainer) { + ArrayList result = new ArrayList<>(); + if (root == null) { + return result; + } + for (View source : STATUS_SOURCES.keySet()) { + if (!isUsableSource(root, source, excludedContainer)) { + continue; + } + if (!isStatusAnchorSource(STATUS_SOURCES.get(source))) { + continue; + } + Bounds bounds = boundsForRoot(root, source); + if (isUsableRightAnchor(root, bounds)) { + result.add(bounds); + } + } + return result; + } + + private static boolean isStatusAnchorSource(StatusSourceRole role) { + return role != StatusSourceRole.SYSTEM_ICONS; + } + + private static Bounds union(Bounds first, Bounds second) { + if (first == null) { + return second; + } + if (second == null) { + return first; + } + int left = Math.min(first.left, second.left); + int top = Math.min(first.top, second.top); + int right = Math.max(right(first), right(second)); + int bottom = Math.max(first.top + first.height, second.top + second.height); + return new Bounds(left, top, right - left, bottom - top); + } + + private static boolean isUsableSource(View root, View source, View excludedContainer) { + return source != null + && source != excludedContainer + && !isRelated(source, excludedContainer) + && !isNotificationContainer(source) + && !isOwnedRuntimeView(source) + && source.isAttachedToWindow(); + } + + private static Bounds boundsForRoot(View root, View view) { + Bounds localBounds = ViewGeometry.boundsRelativeTo(root, view); + if (localBounds != null) { + return clampToRoot(root, localBounds); + } + return screenBoundsRelativeTo(root, view); + } + + private static Bounds screenBoundsRelativeTo(View root, View view) { + if (root == null || view == null || root.getWidth() <= 0 || root.getHeight() <= 0) { + return null; + } + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + if (width <= 0 || height <= 0) { + return null; + } + int[] rootLocation = new int[2]; + int[] viewLocation = new int[2]; + root.getLocationOnScreen(rootLocation); + view.getLocationOnScreen(viewLocation); + int left = viewLocation[0] - rootLocation[0]; + int top = viewLocation[1] - rootLocation[1]; + return clampToRoot(root, new Bounds(left, top, width, height)); + } + + private static Bounds clampToRoot(View root, Bounds bounds) { + if (root == null || bounds == null || root.getWidth() <= 0 || root.getHeight() <= 0) { + return null; + } + int left = Math.max(0, bounds.left); + int top = Math.max(0, bounds.top); + int right = Math.min(root.getWidth(), bounds.left + bounds.width); + int bottom = Math.min(root.getHeight(), bounds.top + bounds.height); + if (right <= left || bottom <= top) { + return null; + } + return new Bounds(left, top, right - left, bottom - top); + } + + private static boolean isUsableRightAnchor(View root, Bounds bounds) { + if (root == null + || bounds == null + || bounds.width <= 0 + || bounds.height <= 0) { + return false; + } + int right = right(bounds); + int bottom = bounds.top + bounds.height; + return bounds.left >= 0 + && right <= root.getWidth() + && bounds.top >= 0 + && bottom <= root.getHeight(); + } + + private static int right(Bounds bounds) { + return bounds.left + bounds.width; + } + + static ArrayList statusSourcesForRoot(View root, View excludedContainer) { + ArrayList result = new ArrayList<>(); + for (View source : STATUS_SOURCES.keySet()) { + if (!isUsableSource(root, source, excludedContainer) + || !isDescendantOf(source, root)) { + continue; + } + if (!isStatusAnchorSource(STATUS_SOURCES.get(source))) { + continue; + } + result.add(source); + } + return result; + } + + private static boolean isDescendantOf(View view, View root) { + View current = view; + while (current != null) { + if (current == root) { + return true; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return false; + } + + private static boolean isRelated(View view, View other) { + return view != null + && other != null + && (isDescendantOf(view, other) || isDescendantOf(other, view)); + } + + private static boolean isNotificationContainer(View view) { + String className = view != null ? view.getClass().getName() : ""; + return className.contains("NotificationIconsOnlyContainer") + || className.contains("NotificationIconContainer"); + } + + private static boolean isOwnedRuntimeView(View view) { + View current = view; + while (current != null) { + Object tag = current.getTag(); + if (tag instanceof String value && value.startsWith("statusbartweak.")) { + return true; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return false; + } + +} 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 f986bc4..1a8ed23 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 @@ -165,6 +165,21 @@ final class ClockLayout { measured.height); } + static int centeredTextBaseline(TextView source, int containerHeight) { + if (source == null || source.getText() == null || source.getText().length() == 0) { + return 0; + } + TextView probe = new TextView(source.getContext()); + probe.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + ClockProxyRenderer.copyTextStyleIfChanged(source, probe); + MeasuredText measured = measureText(probe, source.getText()); + int availableHeight = containerHeight > 0 ? containerHeight : measured.height; + int extraHeight = Math.max(0, availableHeight - measured.height); + return (extraHeight / 2) + measured.baseline; + } + private static ArrayList positionRowsFromBaseline( ArrayList measuredRows, int sourceBaselineY, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java index 1bc45e5..c7f6e07 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java @@ -19,6 +19,8 @@ import java.util.Objects; import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver; +import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; +import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; public final class DebugBoundsOverlayController { @@ -31,8 +33,17 @@ public final class DebugBoundsOverlayController { private static final int COLOR_CLOCK = Color.argb(96, 64, 220, 96); private static final int COLOR_CHIP = Color.argb(112, 255, 0, 220); private static final int COLOR_CUTOUT = Color.argb(112, 255, 180, 0); + private static final int COLOR_AOD_DERIVED_STATUSBAR = Color.argb(80, 255, 220, 0); public boolean hasEnabledVisualizer(boolean layoutEnabled, SbtSettings settings) { + return hasEnabledVisualizer(layoutEnabled, settings, true); + } + + public boolean hasEnabledVisualizer( + boolean layoutEnabled, + SbtSettings settings, + boolean aodStockVisualizersEnabled + ) { if (settings == null) { return false; } @@ -41,21 +52,44 @@ public final class DebugBoundsOverlayController { && (settings.debugVisualizeNotificationContainer || settings.debugVisualizeStatusContainer || settings.debugVisualizeClockContainer - || settings.debugVisualizeChipContainer)); + || settings.debugVisualizeChipContainer + || (aodStockVisualizersEnabled && settings.debugVisualizeAodStockContainers))); } public void apply(View root, boolean layoutEnabled, SbtSettings settings) { + apply(root, layoutEnabled, settings, true); + } + + public void apply( + View root, + boolean layoutEnabled, + SbtSettings settings, + boolean aodStockVisualizersEnabled + ) { + apply(root, layoutEnabled, settings, aodStockVisualizersEnabled, null); + } + + public void apply( + View root, + boolean layoutEnabled, + SbtSettings settings, + boolean aodStockVisualizersEnabled, + SceneKey scene + ) { if (!(root instanceof ViewGroup parent) || settings == null) { return; } - if (!hasEnabledVisualizer(layoutEnabled, settings)) { - removeOverlay(parent); + if (!hasEnabledVisualizer(layoutEnabled, settings, aodStockVisualizersEnabled)) { + removeOverlays(parent); return; } ArrayList specs = new ArrayList<>(); if (settings.debugVisualizeCameraCutout) { addCutoutSpecs(root, specs); } + if (layoutEnabled && aodStockVisualizersEnabled && settings.debugVisualizeAodStockContainers) { + addAodSourceSpecs(root, settings, specs, scene); + } if (layoutEnabled && settings.debugVisualizeNotificationContainer) { addHostContainerSpecs( root, @@ -84,10 +118,12 @@ public final class DebugBoundsOverlayController { specs); } if (specs.isEmpty()) { - removeOverlay(parent); + removeOverlays(parent); return; } - OverlayView host = ensureOverlay(parent); + ViewGroup overlayParent = overlayParent(root, parent, layoutEnabled, aodStockVisualizersEnabled, settings); + removeOverlayFromOtherParent(parent, overlayParent); + OverlayView host = ensureOverlay(overlayParent); if (host == null) { return; } @@ -100,17 +136,183 @@ public final class DebugBoundsOverlayController { host.layout(0, 0, width, height); } host.setSpecs(specs); - if (parent.getChildAt(parent.getChildCount() - 1) != host) { + if (overlayParent.getChildAt(overlayParent.getChildCount() - 1) != host) { host.bringToFront(); } } + public int sourceSignature(View root, boolean layoutEnabled, SbtSettings settings) { + return sourceSignature(root, layoutEnabled, settings, true); + } + + public int sourceSignature( + View root, + boolean layoutEnabled, + SbtSettings settings, + boolean aodStockVisualizersEnabled + ) { + return sourceSignature(root, layoutEnabled, settings, aodStockVisualizersEnabled, null); + } + + public int sourceSignature( + View root, + boolean layoutEnabled, + SbtSettings settings, + boolean aodStockVisualizersEnabled, + SceneKey scene + ) { + if (!layoutEnabled || root == null || settings == null + || !aodStockVisualizersEnabled + || !settings.debugVisualizeAodStockContainers) { + return 0; + } + int result = 17; + ViewGroup notificationContainer = aodNotificationContainer(root, scene); + result = 31 * result + boundsSignature( + aodNotificationBounds(root, scene)); + result = 31 * result + boundsSignature(liveAodStatusIconsBounds(root)); + result = 31 * result + boundsSignature( + derivedAodStatusbarBounds(root, notificationContainer, settings, scene)); + return result; + } + public void remove(View root) { if (root instanceof ViewGroup parent) { - removeOverlay(parent); + removeOverlays(parent); } } + public void bringToFront(View root) { + if (!(root instanceof ViewGroup parent)) { + return; + } + bringOverlayToFront(parent); + bringOverlayToFront(directTaggedViewGroup(parent, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST)); + bringOverlayToFront(directTaggedViewGroup(parent, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST)); + bringOverlayToFront(directTaggedViewGroup(parent, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST)); + bringOverlayToFront(directTaggedViewGroup(parent, OwnedIconHostManager.TAG_UNLOCKED_CARRIER_HOST)); + bringOverlayToFront(directTaggedViewGroup(parent, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST)); + } + + private boolean addAodSourceSpecs( + View root, + SbtSettings settings, + ArrayList specs, + SceneKey scene + ) { + if (root == null || settings == null || specs == null) { + return false; + } + boolean added = false; + ViewGroup notificationContainer = aodNotificationContainer(root, scene); + Bounds derivedStatusbar = derivedAodStatusbarBounds(root, notificationContainer, settings, scene); + if (derivedStatusbar != null) { + specs.add(new OverlaySpec(derivedStatusbar, COLOR_AOD_DERIVED_STATUSBAR, false)); + added = true; + } + added |= addBoundsSpec( + aodNotificationBounds(root, scene), + COLOR_NOTIFICATION, + true, + specs); + added |= addBoundsSpec(liveAodStatusIconsBounds(root), COLOR_STATUS, true, specs); + return added; + } + + private boolean addBoundsSpec(Bounds bounds, int color, boolean fill, ArrayList specs) { + if (bounds == null || bounds.width <= 0 || bounds.height <= 0 || specs == null) { + return false; + } + specs.add(new OverlaySpec(bounds, color, fill)); + return true; + } + + private Bounds derivedAodStatusbarBounds( + View root, + View notificationContainer, + SbtSettings settings, + SceneKey scene + ) { + Bounds notificationBounds = aodNotificationBounds(root, scene); + Bounds statusBounds = liveAodStatusIconsBounds(root); + if (!isUsableAodAnchor(root, statusBounds)) { + statusBounds = overlayBounds(AodStatusbarSourceStore.statusBoundsForRoot( + root, + notificationContainer)); + } + boolean hasNotification = isUsableAodAnchor(root, notificationBounds); + boolean hasStatus = isUsableAodAnchor(root, statusBounds); + if (!hasNotification && !hasStatus) { + return null; + } + int left = hasNotification ? notificationBounds.left : 0; + int right = hasStatus ? statusBounds.left + statusBounds.width : root.getWidth(); + left += settings != null ? settings.layoutPaddingLeftPx : 0; + right -= settings != null ? settings.layoutPaddingRightPx : 0; + int notificationBottom = hasNotification + ? notificationBounds.top + notificationBounds.height + : 0; + int statusBottom = hasStatus ? statusBounds.top + statusBounds.height : 0; + int bottom = Math.max(notificationBottom, statusBottom); + if (right <= left || bottom <= 0) { + return null; + } + return new Bounds(left, 0, right - left, bottom); + } + + private Bounds aodNotificationBounds(View root, SceneKey scene) { + if (scene != null + && scene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) { + return null; + } + return overlayBounds( + LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForAodIconsRoot(root)); + } + + private ViewGroup aodNotificationContainer(View root, SceneKey scene) { + if (scene != null + && scene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) { + return LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); + } + return LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root); + } + + private boolean isUsableAodAnchor(View root, Bounds bounds) { + if (root == null || bounds == null || bounds.width <= 0 || bounds.height <= 0) { + return false; + } + int right = bounds.left + bounds.width; + int bottom = bounds.top + bounds.height; + return bounds.left >= 0 + && bounds.top >= 0 + && right <= root.getWidth() + && bottom <= root.getHeight(); + } + + private int boundsSignature(Bounds bounds) { + if (bounds == null) { + return 0; + } + int result = 17; + result = 31 * result + bounds.left; + result = 31 * result + bounds.top; + result = 31 * result + bounds.width; + result = 31 * result + bounds.height; + return result; + } + + private Bounds overlayBounds(StatusBarLayoutSolver.Bounds bounds) { + if (bounds == null) { + return null; + } + return new Bounds(bounds.left, bounds.top, bounds.width, bounds.height); + } + + private Bounds liveAodStatusIconsBounds(View root) { + StatusBarLayoutSolver.Bounds bounds = AodLiveStatusbarSource.boundsForRoot(root); + return bounds != null ? new Bounds(bounds.left, bounds.top, bounds.width, bounds.height) : null; + } + private void addCutoutSpecs(View root, ArrayList specs) { WindowInsets insets = root.getRootWindowInsets(); DisplayCutout cutout = insets != null ? insets.getDisplayCutout() : null; @@ -252,13 +454,94 @@ public final class DebugBoundsOverlayController { return host; } + private ViewGroup overlayParent( + View root, + ViewGroup rootParent, + boolean layoutEnabled, + boolean aodStockVisualizersEnabled, + SbtSettings settings + ) { + if (layoutEnabled + && aodStockVisualizersEnabled + && settings != null + && settings.debugVisualizeAodStockContainers) { + View chipHost = findDirectTaggedChild(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST); + if (chipHost instanceof ViewGroup chipHostGroup) { + return chipHostGroup; + } + View statusHost = findDirectTaggedChild(rootParent, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST); + if (statusHost instanceof ViewGroup statusHostGroup) { + return statusHostGroup; + } + } + return rootParent; + } + + private void removeOverlayFromOtherParent(ViewGroup rootParent, ViewGroup overlayParent) { + if (rootParent == null) { + return; + } + removeOverlayIfParentDiffers(rootParent, overlayParent); + removeOverlayIfParentDiffers( + directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST), + overlayParent); + removeOverlayIfParentDiffers( + directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST), + overlayParent); + removeOverlayIfParentDiffers( + directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST), + overlayParent); + removeOverlayIfParentDiffers( + directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CARRIER_HOST), + overlayParent); + removeOverlayIfParentDiffers( + directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST), + overlayParent); + } + + private void removeOverlays(ViewGroup rootParent) { + if (rootParent == null) { + return; + } + removeOverlay(rootParent); + removeOverlay(directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST)); + removeOverlay(directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST)); + removeOverlay(directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST)); + removeOverlay(directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CARRIER_HOST)); + removeOverlay(directTaggedViewGroup(rootParent, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST)); + } + + private void removeOverlayIfParentDiffers(ViewGroup parent, ViewGroup expectedParent) { + if (parent != null && parent != expectedParent) { + removeOverlay(parent); + } + } + private void removeOverlay(ViewGroup parent) { + if (parent == null) { + return; + } View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST); if (existing != null) { parent.removeView(existing); } } + private void bringOverlayToFront(ViewGroup parent) { + if (parent == null || parent.getChildCount() == 0) { + return; + } + View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST); + if (existing != null && parent.getChildAt(parent.getChildCount() - 1) != existing) { + existing.bringToFront(); + } + } + + private ViewGroup directTaggedViewGroup(ViewGroup parent, String tag) { + View view = findDirectTaggedChild(parent, tag); + return view instanceof ViewGroup group ? group : null; + } + private View findDirectTaggedChild(ViewGroup parent, String tag) { if (parent == null || tag == null) { return null; @@ -352,7 +635,10 @@ public final class DebugBoundsOverlayController { return Math.max(min, Math.min(max, value)); } - private record OverlaySpec(Bounds bounds, int color) { + private record OverlaySpec(Bounds bounds, int color, boolean fill) { + OverlaySpec(Bounds bounds, int color) { + this(bounds, color, true); + } } private record Bounds(int left, int top, int width, int height) { @@ -389,6 +675,8 @@ public final class DebugBoundsOverlayController { for (OverlaySpec spec : specs) { Bounds bounds = spec.bounds; paint.setColor(spec.color); + paint.setStyle(spec.fill ? Paint.Style.FILL : Paint.Style.STROKE); + paint.setStrokeWidth(Math.max(1f, getResources().getDisplayMetrics().density)); canvas.drawRect( bounds.left, bounds.top, 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 c020051..c45d999 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 @@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.runtime.render; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import java.util.ArrayList; import java.util.Collections; @@ -11,10 +12,17 @@ import java.util.Set; import java.util.WeakHashMap; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; +import se.ajpanton.statusbartweak.shell.settings.AppIconRules; public final class LockscreenCardsNotificationIconSourceStore { private static final WeakHashMap> SOURCES_BY_CONTAINER = new WeakHashMap<>(); + private static final WeakHashMap> PACKAGES_BY_CONTAINER = + new WeakHashMap<>(); + private static final WeakHashMap> AOD_DRAWABLES_BY_CONTAINER = + new WeakHashMap<>(); + private static final int VIRTUAL_ICON_SIZE_PX = 39; private LockscreenCardsNotificationIconSourceStore() { } @@ -41,36 +49,332 @@ public final class LockscreenCardsNotificationIconSourceStore { } } + public static boolean putPackages(ViewGroup container, ArrayList packages) { + if (container == null || packages == null || packages.isEmpty()) { + if (container != null) { + return PACKAGES_BY_CONTAINER.remove(container) != null; + } + return false; + } + ArrayList valid = new ArrayList<>(); + boolean hasValid = false; + for (String pkg : packages) { + if (AppIconRules.isValidPackageName(pkg)) { + valid.add(pkg); + hasValid = true; + } else { + valid.add(null); + } + } + if (!hasValid) { + return PACKAGES_BY_CONTAINER.remove(container) != null; + } else { + ArrayList previous = PACKAGES_BY_CONTAINER.get(container); + if (previous != null && previous.equals(valid)) { + return false; + } + PACKAGES_BY_CONTAINER.put(container, valid); + return true; + } + } + + public static boolean putAodDrawables(ViewGroup container, ArrayList views) { + if (container == null || views == null || views.isEmpty()) { + if (container != null) { + return AOD_DRAWABLES_BY_CONTAINER.remove(container) != null; + } + return false; + } + ArrayList valid = new ArrayList<>(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (View view : views) { + if (!(view instanceof ImageView imageView) + || imageView.getDrawable() == null + || !seen.add(view)) { + continue; + } + valid.add(view); + } + if (valid.isEmpty()) { + return AOD_DRAWABLES_BY_CONTAINER.remove(container) != null; + } else { + ArrayList previous = AOD_DRAWABLES_BY_CONTAINER.get(container); + if (sameViews(previous, valid)) { + return false; + } + AOD_DRAWABLES_BY_CONTAINER.put(container, valid); + return true; + } + } + + public static void clearAodSources() { + PACKAGES_BY_CONTAINER.clear(); + AOD_DRAWABLES_BY_CONTAINER.clear(); + } + + public static void clearAodSources(ViewGroup container) { + if (container == null) { + return; + } + PACKAGES_BY_CONTAINER.remove(container); + AOD_DRAWABLES_BY_CONTAINER.remove(container); + } + + private static boolean sameViews(ArrayList previous, ArrayList current) { + if (previous == current) { + return true; + } + if (previous == null || current == null || previous.size() != current.size()) { + return false; + } + for (int i = 0; i < previous.size(); i++) { + if (previous.get(i) != current.get(i)) { + return false; + } + } + return true; + } + static ArrayList snapshotSources(ViewGroup container, View root) { - ArrayList views = SOURCES_BY_CONTAINER.get(container); ArrayList result = new ArrayList<>(); - if (views == null || views.isEmpty()) { + ArrayList aodDrawables = AOD_DRAWABLES_BY_CONTAINER.get(container); + if (aodDrawables != null && !aodDrawables.isEmpty()) { + ArrayList packages = PACKAGES_BY_CONTAINER.get(container); + Bounds containerBounds = sourceBoundsRelativeTo(root, container); + int top = containerBounds != null ? containerBounds.top : 0; + int size = VIRTUAL_ICON_SIZE_PX; + for (int i = 0; i < aodDrawables.size(); i++) { + View view = aodDrawables.get(i); + result.add(new SnapshotSource( + view, + SnapshotMode.NOTIFICATION_DRAWABLE, + aodDrawableKey(packages, i, view), + new Bounds(0, top, size, size))); + } + if (!result.isEmpty()) { + return result; + } + } + ArrayList views = SOURCES_BY_CONTAINER.get(container); + if (views != null && !views.isEmpty()) { + int skipCards = visibleShelfStartIndex(container, views); + HashSet visibleCardKeys = visibleCardKeys(root, container); + for (int i = 0; i < views.size(); i++) { + if (i < skipCards) { + continue; + } + SourceEntry entry = views.get(i); + if (entry.key != null && visibleCardKeys.contains(entry.key)) { + continue; + } + View view = entry.view; + if (view == null || !view.isAttachedToWindow()) { + continue; + } + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + if (width <= 0 || height <= 0) { + continue; + } + result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE, entry.key)); + } + } + if (!result.isEmpty()) { return result; } - int skipCards = visibleShelfStartIndex(container, views); - HashSet visibleCardKeys = visibleCardKeys(root, container); - for (int i = 0; i < views.size(); i++) { - if (i < skipCards) { - continue; + ArrayList packages = PACKAGES_BY_CONTAINER.get(container); + if (packages != null) { + Bounds containerBounds = sourceBoundsRelativeTo(root, container); + int top = containerBounds != null ? containerBounds.top : 0; + int size = VIRTUAL_ICON_SIZE_PX; + for (int i = 0; i < packages.size(); i++) { + String pkg = packages.get(i); + if (!AppIconRules.isValidPackageName(pkg)) { + continue; + } + result.add(new SnapshotSource( + null, + SnapshotMode.APP_ICON, + pkg + "@aod" + i, + new Bounds(0, top, size, size))); } - SourceEntry entry = views.get(i); - if (entry.key != null && visibleCardKeys.contains(entry.key)) { - continue; - } - View view = entry.view; - if (view == null || !view.isAttachedToWindow()) { - continue; - } - int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); - int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); - if (width <= 0 || height <= 0) { - continue; - } - result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE, entry.key)); } return result; } + private static String aodDrawableKey(ArrayList packages, int index, View view) { + if (packages != null && index >= 0 && index < packages.size()) { + String pkg = packages.get(index); + if (AppIconRules.isValidPackageName(pkg)) { + return pkg + "@aod" + index; + } + } + return "statusbartweak:aod_drawable@" + index + "@0x" + + Integer.toHexString(System.identityHashCode(view)); + } + + static ArrayList snapshotSourcesForRoot(View root) { + ArrayList result = new ArrayList<>(); + if (!(root instanceof ViewGroup rootGroup)) { + return result; + } + ArrayList stack = new ArrayList<>(); + stack.add(rootGroup); + for (int i = 0; i < stack.size(); i++) { + View view = stack.get(i); + if (view instanceof ViewGroup group) { + if (hasSources(group)) { + result = snapshotSources(group, root); + if (!result.isEmpty()) { + return result; + } + } + for (int child = 0; child < group.getChildCount(); child++) { + View childView = group.getChildAt(child); + if (childView != null) { + stack.add(childView); + } + } + } + } + return result; + } + + static ArrayList snapshotSourcesForAodIconsRoot(View root) { + ArrayList result = new ArrayList<>(); + ViewGroup container = sourceContainerForAodIconsRoot(root); + if (container != null) { + result = snapshotSources(container, root); + } + return result; + } + + static ViewGroup sourceContainerForRoot(View root) { + ViewGroup container = sourceContainerForRoot(root, AOD_DRAWABLES_BY_CONTAINER.keySet()); + if (container != null) { + return container; + } + container = sourceContainerForRoot(root, SOURCES_BY_CONTAINER.keySet()); + if (container != null) { + return container; + } + return sourceContainerForRoot(root, PACKAGES_BY_CONTAINER.keySet()); + } + + static ViewGroup sourceContainerForAodIconsRoot(View root) { + ViewGroup container = sourceContainerForRoot( + root, + AOD_DRAWABLES_BY_CONTAINER.keySet(), + true); + if (container != null) { + return container; + } + return sourceContainerForRoot(root, PACKAGES_BY_CONTAINER.keySet(), true); + } + + public static Bounds sourceContainerBoundsForRoot(View root) { + ViewGroup container = sourceContainerForRoot(root); + Bounds bounds = sourceBoundsRelativeTo(root, container); + if (isZeroSizeDotContainer(container, bounds)) { + return new Bounds(bounds.left, bounds.top, VIRTUAL_ICON_SIZE_PX, VIRTUAL_ICON_SIZE_PX); + } + return bounds; + } + + public static Bounds sourceContainerBoundsForAodIconsRoot(View root) { + ViewGroup container = sourceContainerForAodIconsRoot(root); + Bounds bounds = sourceBoundsRelativeTo(root, container); + if (isZeroSizeDotContainer(container, bounds)) { + return new Bounds(bounds.left, bounds.top, VIRTUAL_ICON_SIZE_PX, VIRTUAL_ICON_SIZE_PX); + } + return bounds; + } + + private static Bounds sourceBoundsRelativeTo(View root, ViewGroup container) { + Bounds bounds = container != null ? ViewGeometry.boundsRelativeTo(root, container) : null; + if (bounds == null || isZeroSizeDotContainer(container, bounds)) { + return bounds; + } + if (!hasAodSources(container) || bounds.height <= 0) { + return bounds; + } + // Samsung's AOD icon-only container can temporarily expand vertically; + // downstream layout cares about the icon row, not that outer transition box. + int centerY = bounds.top + bounds.height / 2; + int top = centerY - VIRTUAL_ICON_SIZE_PX / 2; + return new Bounds(bounds.left, top, bounds.width, VIRTUAL_ICON_SIZE_PX); + } + + private static boolean hasAodSources(ViewGroup container) { + return container != null + && (AOD_DRAWABLES_BY_CONTAINER.containsKey(container) + || PACKAGES_BY_CONTAINER.containsKey(container)); + } + + private static boolean isZeroSizeDotContainer(ViewGroup container, Bounds bounds) { + if (container == null + || bounds == null + || bounds.width > 0 + || bounds.height > 0) { + return false; + } + return "notification_dot_container".equals(resolveIdName(container)); + } + + private static ViewGroup sourceContainerForRoot( + View root, + Set containers + ) { + return sourceContainerForRoot(root, containers, false); + } + + private static ViewGroup sourceContainerForRoot( + View root, + Set containers, + boolean iconOnlyContainerRequired + ) { + if (root == null || containers == null || containers.isEmpty()) { + return null; + } + for (ViewGroup container : containers) { + if (container != null + && container.isAttachedToWindow() + && isDescendantOf(container, root) + && (!iconOnlyContainerRequired || isAodIconOnlyContainer(container))) { + return container; + } + } + return null; + } + + private static boolean isAodIconOnlyContainer(View view) { + if (view == null) { + return false; + } + String idName = resolveIdName(view); + return "keyguard_icononly_container_view".equals(idName) + || "notification_icononly_container".equals(idName) + || "notification_icononly_area".equals(idName); + } + + private static boolean isDescendantOf(View view, View root) { + View current = view; + while (current != null) { + if (current == root) { + return true; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return false; + } + + private static boolean hasSources(ViewGroup container) { + return SOURCES_BY_CONTAINER.containsKey(container) + || AOD_DRAWABLES_BY_CONTAINER.containsKey(container) + || PACKAGES_BY_CONTAINER.containsKey(container); + } + private static HashSet visibleCardKeys(View root, View shelf) { HashSet result = new HashSet<>(); if (!(root instanceof ViewGroup group)) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationStripRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationStripRenderer.java index fcd5631..ec0f006 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationStripRenderer.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/LockscreenCardsNotificationStripRenderer.java @@ -30,6 +30,7 @@ public final class LockscreenCardsNotificationStripRenderer { private final SnapshotRenderer renderer = new SnapshotRenderer(false, false, false); private final WeakHashMap> pillViewsByHost = new WeakHashMap<>(); + private final WeakHashMap aodInitialStatusbarHeightByRoot = new WeakHashMap<>(); private final NotificationSeenAppReporter seenAppReporter = new NotificationSeenAppReporter(); public RenderResult render( @@ -38,28 +39,43 @@ public final class LockscreenCardsNotificationStripRenderer { SbtSettings settings, SceneKey scene ) { + if (root != null && (scene == null || !scene.isAod())) { + aodInitialStatusbarHeightByRoot.remove(root); + } if (root == null || host == null || settings == null || scene == null - || !scene.isLockscreen() || scene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS - || settings.cardsLockMaxRows <= 0) { + || !(scene.isLockscreen() || scene.isAod()) + || maxRows(settings, scene) <= 0) { clear(host); return RenderResult.empty(); } - ViewGroup sourceRoot = findSourceRoot(root); - if (sourceRoot == null) { + ViewGroup sourceRoot = scene.isAod() + ? LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root) + : findSourceRoot(root); + if (sourceRoot == null && scene.isAod()) { + sourceRoot = findSourceRoot(root); + } + if (sourceRoot == null && !scene.isAod()) { clear(host); return RenderResult.empty(); } - if (hasHiddenShelfAncestor(sourceRoot, root)) { + if (!scene.isAod() && hasHiddenShelfAncestor(sourceRoot, root)) { clear(host); return RenderResult.empty(); } - syncHostAnimation(host, sourceRoot); - ArrayList sources = LockscreenCardsNotificationIconSourceStore.snapshotSources(sourceRoot, root); - if (sources.isEmpty()) { + if (sourceRoot != null) { + syncHostAnimation(host, sourceRoot); + } + ArrayList sources = scene.isAod() + ? LockscreenCardsNotificationIconSourceStore.snapshotSourcesForRoot(root) + : new ArrayList<>(); + if (sources.isEmpty() && sourceRoot != null) { + sources = LockscreenCardsNotificationIconSourceStore.snapshotSources(sourceRoot, root); + } + if (sources.isEmpty() && sourceRoot != null) { sources = collectSources(sourceRoot); } if (sources.isEmpty()) { @@ -72,8 +88,24 @@ public final class LockscreenCardsNotificationStripRenderer { unfilteredPlacements, settings, scene); - StripLayout layout = layoutPlacements(root, sourceRoot, placements, settings); - renderPills(host, layout.rowBounds, resolvePillColor(sourceRoot)); + Bounds sourceBounds = scene.isAod() + ? LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForRoot(root) + : null; + if (sourceBounds == null && sourceRoot != null) { + sourceBounds = ViewGeometry.boundsRelativeTo(root, sourceRoot); + } + int statusbarHeight = statusbarHeight(root, sourceBounds, scene); + int iconHeight = iconHeightPx( + statusbarHeight, + cardsIconHeightSteps(settings, scene), + representativeHeight(placements)); + StripLayout layout = layoutPlacements(root, sourceBounds, placements, settings, scene, iconHeight); + if (scene.isAod()) { + clearPills(host); + } else { + renderPills(host, layout.rowBounds, + sourceRoot != null ? resolvePillColor(sourceRoot) : DEFAULT_PILL_COLOR); + } Bounds renderedBounds = renderer.renderPlacements(host, layout.placements); return new RenderResult(sourceRoot, renderedBounds); } @@ -85,6 +117,12 @@ public final class LockscreenCardsNotificationStripRenderer { public void clearAll() { renderer.clearAll(); + ArrayList hosts = new ArrayList<>(pillViewsByHost.keySet()); + for (ViewGroup host : hosts) { + clearPills(host); + } + pillViewsByHost.clear(); + aodInitialStatusbarHeightByRoot.clear(); } private ViewGroup findSourceRoot(View root) { @@ -172,17 +210,19 @@ public final class LockscreenCardsNotificationStripRenderer { private StripLayout layoutPlacements( View root, - View sourceRoot, + Bounds sourceBounds, ArrayList placements, - SbtSettings settings + SbtSettings settings, + SceneKey scene, + int iconHeight ) { ArrayList result = new ArrayList<>(); ArrayList rowBounds = new ArrayList<>(); if (placements == null || placements.isEmpty()) { return new StripLayout(result, rowBounds); } - int maxPerRow = Math.max(1, settings.cardsLockMaxIconsPerRow); - int maxRows = Math.max(0, settings.cardsLockMaxRows); + int maxPerRow = Math.max(1, maxIconsPerRow(settings, scene)); + int maxRows = Math.max(0, maxRows(settings, scene)); int capacity = maxPerRow * maxRows; if (capacity <= 0) { return new StripLayout(result, rowBounds); @@ -190,18 +230,16 @@ public final class LockscreenCardsNotificationStripRenderer { boolean showDot = placements.size() > capacity; int iconSlots = showDot ? Math.max(0, capacity - 1) : Math.min(capacity, placements.size()); int visible = iconSlots + (showDot ? 1 : 0); - int iconHeight = representativeHeight(placements); - int iconWidth = representativeWidth(placements, iconHeight); - Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, sourceRoot); + int iconWidth = iconHeight; int top = sourceBounds != null ? sourceBounds.top : placements.get(0).bounds.top; int centerX = sourceBounds != null ? sourceBounds.left + sourceBounds.width / 2 : root.getWidth() / 2; - int availableWidth = settings.cardsLockLimitToWidth + int availableWidth = limitToWidth(settings, scene) ? Math.max(0, root.getWidth()) : Integer.MAX_VALUE; int index = 0; - int[] rowCounts = rowCounts(visible, maxPerRow, maxRows, settings.cardsLockEvenDistribution); + int[] rowCounts = rowCounts(visible, maxPerRow, maxRows, evenDistribution(settings, scene)); int dotColor = dotColor(placements); int rowGap = dp(root, 8); for (int row = 0; row < rowCounts.length && index < visible; row++) { @@ -236,6 +274,30 @@ public final class LockscreenCardsNotificationStripRenderer { return new StripLayout(result, rowBounds); } + private int maxIconsPerRow(SbtSettings settings, SceneKey scene) { + return scene != null && scene.isAod() + ? settings.cardsAodMaxIconsPerRow + : settings.cardsLockMaxIconsPerRow; + } + + private int maxRows(SbtSettings settings, SceneKey scene) { + return scene != null && scene.isAod() + ? settings.cardsAodMaxRows + : settings.cardsLockMaxRows; + } + + private boolean evenDistribution(SbtSettings settings, SceneKey scene) { + return scene != null && scene.isAod() + ? settings.cardsAodEvenDistribution + : settings.cardsLockEvenDistribution; + } + + private boolean limitToWidth(SbtSettings settings, SceneKey scene) { + return scene != null && scene.isAod() + ? settings.cardsAodLimitToWidth + : settings.cardsLockLimitToWidth; + } + private int[] rowCounts( int slots, int maxPerRow, @@ -459,15 +521,6 @@ public final class LockscreenCardsNotificationStripRenderer { return params; } - private int representativeWidth(ArrayList placements, int fallback) { - for (SnapshotPlacement placement : placements) { - if (placement != null && placement.bounds != null && placement.bounds.width > 0) { - return placement.bounds.width; - } - } - return Math.max(1, fallback); - } - private int representativeHeight(ArrayList placements) { for (SnapshotPlacement placement : placements) { if (placement != null && placement.bounds != null && placement.bounds.height > 0) { @@ -477,6 +530,50 @@ public final class LockscreenCardsNotificationStripRenderer { return 1; } + private int cardsIconHeightSteps(SbtSettings settings, SceneKey scene) { + return scene != null && scene.isAod() + ? settings.cardsAodIconHeightSteps + : settings.cardsLockIconHeightSteps; + } + + private int iconHeightPx(int statusbarHeight, int steps, int fallbackHeight) { + if (statusbarHeight <= 0) { + return Math.max(1, fallbackHeight); + } + return Math.max(1, Math.round((float) statusbarHeight * Math.max(1, steps) / 100f)); + } + + private int statusbarHeight(View root, Bounds sourceBounds, SceneKey scene) { + int sharedHeight = StatusbarHeightStore.get(root); + if (sharedHeight > 0) { + return sharedHeight; + } + Bounds statusBounds = StatusbarHeightSource.bounds(root, scene, null); + if (statusBounds != null && statusBounds.top >= 0 && statusBounds.height > 0) { + int height = Math.max(1, statusBounds.top + statusBounds.height); + if (scene != null && scene.isAod() && root != null) { + Integer stored = aodInitialStatusbarHeightByRoot.get(root); + if (stored != null && stored > 0) { + return stored; + } + aodInitialStatusbarHeightByRoot.put(root, height); + } else { + StatusbarHeightStore.put(root, height); + } + return height; + } + if (scene != null && scene.isAod() && root != null) { + Integer stored = aodInitialStatusbarHeightByRoot.get(root); + if (stored != null && stored > 0) { + return stored; + } + } + if (sourceBounds != null && sourceBounds.top >= 0 && sourceBounds.height > 0) { + return Math.max(1, sourceBounds.top + sourceBounds.height); + } + return Math.max(1, root != null ? root.getHeight() : 1); + } + private boolean isShelfIconContainer(View view) { String className = view.getClass().getName(); return className.contains("SecShelfNotificationIconContainer"); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/NotificationSeenAppReporter.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/NotificationSeenAppReporter.java index 460513f..367218a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/NotificationSeenAppReporter.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/NotificationSeenAppReporter.java @@ -6,24 +6,34 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; +import java.util.Set; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class NotificationSeenAppReporter { - private static final long MIN_REPORT_INTERVAL_MS = 500L; + private static final long MIN_REPORT_INTERVAL_MS = 60_000L; private final Map lastReportedMs = new HashMap<>(); + private final Map> activeKeysByPackage = new HashMap<>(); void report(Context context, ArrayList placements) { if (context == null || placements == null || placements.isEmpty()) { + activeKeysByPackage.clear(); return; } long now = System.currentTimeMillis(); + HashMap> currentKeysByPackage = new HashMap<>(); HashSet reportedThisPass = new HashSet<>(); for (SnapshotPlacement placement : placements) { - String packageName = SnapshotIconFilter.notificationPackage( - placement != null ? placement.key : null); - if (packageName == null || !reportedThisPass.add(packageName)) { + String key = placement != null ? placement.key : null; + String packageName = SnapshotIconFilter.notificationPackage(key); + if (packageName == null || key == null) { + continue; + } + currentKeysByPackage + .computeIfAbsent(packageName, ignored -> new HashSet<>()) + .add(key); + if (wasAlreadyActive(packageName, key) || !reportedThisPass.add(packageName)) { continue; } Long lastReported = lastReportedMs.get(packageName); @@ -33,5 +43,12 @@ final class NotificationSeenAppReporter { lastReportedMs.put(packageName, now); SbtSettings.noteSeenIconApp(context, packageName, now); } + activeKeysByPackage.clear(); + activeKeysByPackage.putAll(currentKeysByPackage); + } + + private boolean wasAlreadyActive(String packageName, String key) { + Set activeKeys = activeKeysByPackage.get(packageName); + return activeKeys != null && activeKeys.contains(key); } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/OwnedIconHostManager.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/OwnedIconHostManager.java index bd5352c..45b67c0 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/OwnedIconHostManager.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/OwnedIconHostManager.java @@ -28,6 +28,8 @@ public final class OwnedIconHostManager { "statusbartweak.unlocked.chip.host"; public static final String TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST = "statusbartweak.lockscreen.cards.notification.host"; + public static final String TAG_AOD_CARDS_NOTIFICATION_HOST = + "statusbartweak.aod.cards.notification.host"; private static final String[] UNLOCKED_HOST_ORDER = { TAG_UNLOCKED_STATUS_HOST, TAG_UNLOCKED_NOTIFICATION_HOST, @@ -60,6 +62,10 @@ public final class OwnedIconHostManager { return ensureHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST); } + public FrameLayout ensureAodCardsNotificationHost(View root) { + return ensureHost(root, TAG_AOD_CARDS_NOTIFICATION_HOST); + } + public FrameLayout ensureHost(View root, String tag) { Objects.requireNonNull(tag, "tag"); if (!(root instanceof ViewGroup parent)) { @@ -99,6 +105,7 @@ public final class OwnedIconHostManager { removeHost(root, TAG_UNLOCKED_CARRIER_HOST); removeHost(root, TAG_UNLOCKED_CHIP_HOST); removeHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST); + removeHost(root, TAG_AOD_CARDS_NOTIFICATION_HOST); } public void setUnlockedHostsVisible(View root, boolean visible) { @@ -130,7 +137,8 @@ public final class OwnedIconHostManager { || TAG_UNLOCKED_CLOCK_HOST.equals(tag) || TAG_UNLOCKED_CARRIER_HOST.equals(tag) || TAG_UNLOCKED_CHIP_HOST.equals(tag) - || TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST.equals(tag); + || TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST.equals(tag) + || TAG_AOD_CARDS_NOTIFICATION_HOST.equals(tag); } private FrameLayout findDirectHost(ViewGroup parent, String tag) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotIconFilter.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotIconFilter.java index f7e6b2b..4f7f482 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotIconFilter.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotIconFilter.java @@ -156,6 +156,7 @@ final class SnapshotIconFilter { append(sb, placement.key); View view = placement.source != null ? placement.source.view() : null; appendViewText(sb, view); + appendAncestorViewText(sb, view); return sb.toString().toLowerCase(Locale.ROOT); } @@ -169,6 +170,15 @@ final class SnapshotIconFilter { append(sb, contentDescription != null ? contentDescription.toString() : null); } + private static void appendAncestorViewText(StringBuilder sb, View view) { + Object parent = view != null ? view.getParent() : null; + int depth = 0; + while (parent instanceof View parentView && depth++ < 4) { + appendViewText(sb, parentView); + parent = parentView.getParent(); + } + } + private static ArrayList statusSlots(String matchText) { ArrayList slots = new ArrayList<>(); add(slots, normalizedSlot(matchText)); 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 dae5335..eca4ec4 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 @@ -12,16 +12,23 @@ import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bou record SnapshotSource( View view, SnapshotMode mode, - String keyHint + String keyHint, + Bounds boundsOverride ) { + SnapshotSource(View view, SnapshotMode mode, String keyHint) { + this(view, mode, keyHint, null); + } + SnapshotSource(View view, SnapshotMode mode) { - this(view, mode, null); + this(view, mode, null, null); } } enum SnapshotMode { VIEW, - NOTIFICATION_DRAWABLE + STATUS_CHIP, + NOTIFICATION_DRAWABLE, + APP_ICON } final class SnapshotKeys { @@ -43,6 +50,7 @@ final class SnapshotPlacement { final HeldSignal heldSignal; final Bounds contentBounds; final int sourceOffsetX; + final Bounds sourceRenderBounds; SnapshotPlacement( SnapshotSource source, @@ -63,7 +71,8 @@ final class SnapshotPlacement { Color.WHITE, heldSignal, contentBounds, - sourceOffsetX); + sourceOffsetX, + null); } SnapshotPlacement( @@ -76,6 +85,31 @@ final class SnapshotPlacement { HeldSignal heldSignal, Bounds contentBounds, int sourceOffsetX + ) { + this( + source, + bounds, + key, + signal, + overflowDot, + overflowDotColor, + heldSignal, + contentBounds, + sourceOffsetX, + null); + } + + SnapshotPlacement( + SnapshotSource source, + Bounds bounds, + String key, + boolean signal, + boolean overflowDot, + int overflowDotColor, + HeldSignal heldSignal, + Bounds contentBounds, + int sourceOffsetX, + Bounds sourceRenderBounds ) { this.source = source; this.bounds = bounds; @@ -86,6 +120,7 @@ final class SnapshotPlacement { this.heldSignal = heldSignal; this.contentBounds = contentBounds; this.sourceOffsetX = sourceOffsetX; + this.sourceRenderBounds = sourceRenderBounds; } SnapshotSource source() { @@ -123,6 +158,10 @@ final class SnapshotPlacement { int sourceOffsetX() { return sourceOffsetX; } + + Bounds sourceRenderBounds() { + return sourceRenderBounds; + } } record BitmapResult(Bitmap bitmap, boolean created) { @@ -130,14 +169,13 @@ record BitmapResult(Bitmap bitmap, boolean created) { record SnapshotRenderState( String key, - int left, - int top, int width, int height, boolean signal, boolean overflowDot, int overflowDotColor, int sourceOffsetX, + int sourceRenderBoundsSignature, String heldSignalKey, int heldBitmapId, SnapshotMode sourceMode, 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 d53f49e..09b0451 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 @@ -1,5 +1,7 @@ package se.ajpanton.statusbartweak.runtime.render; +import android.content.Context; +import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -20,6 +22,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; +import java.util.IdentityHashMap; import java.util.WeakHashMap; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; @@ -35,6 +38,8 @@ final class SnapshotRenderer { new WeakHashMap<>(); private final WeakHashMap statesByProxy = new WeakHashMap<>(); + private final WeakHashMap sourceSignaturesByView = + new WeakHashMap<>(); private final LinkedHashMap heldSignals = new LinkedHashMap<>(); private long holdSignalsUntilElapsedMs; @@ -58,11 +63,21 @@ final class SnapshotRenderer { ArrayList rawPlacements( View root, ArrayList sources + ) { + return rawPlacements(root, sources, sortPlacementsByX); + } + + ArrayList rawPlacements( + View root, + ArrayList sources, + boolean sortByX ) { ArrayList placements = new ArrayList<>(); int order = 0; for (SnapshotSource source : sources) { - Bounds bounds = visualBoundsRelativeTo(root, source.view()); + Bounds bounds = source.boundsOverride() != null + ? source.boundsOverride() + : visualBoundsRelativeTo(root, source.view()); if (bounds != null && bounds.width > 0 && bounds.height > 0) { if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE) { bounds = notificationDrawableBounds(bounds); @@ -112,7 +127,7 @@ final class SnapshotRenderer { } order++; } - if (sortPlacementsByX) { + if (sortByX) { placements.sort(Comparator.comparingInt(placement -> placement.bounds().left)); } return placements; @@ -176,11 +191,14 @@ final class SnapshotRenderer { || !desiredState.equals(statesByProxy.get(proxy)); if (bitmapChanged) { if (placement.overflowDot()) { - drawOverflowDot(bitmap, placement.overflowDotColor()); + drawOverflowDot(bitmap, placement.overflowDotColor(), placement.sourceOffsetX()); } else if (placement.heldSignal() != null) { drawHeldSignal(placement.heldSignal(), bitmap); + } else if (placement.source() != null + && placement.source().mode() == SnapshotMode.APP_ICON) { + drawAppIcon(host.getContext(), placement.source(), bitmap); } else if (placement.source() != null) { - drawSnapshot(placement.source(), bitmap, placement.sourceOffsetX()); + drawSnapshot(placement, bitmap); if (placement.signal()) { saveHeldSignal(key, bounds, bitmap); } @@ -265,7 +283,9 @@ final class SnapshotRenderer { } private boolean shouldTrimContent(SnapshotSource source) { - return trimViewContentForCollision && source != null && source.mode() == SnapshotMode.VIEW; + return trimViewContentForCollision + && source != null + && (source.mode() == SnapshotMode.VIEW || source.mode() == SnapshotMode.STATUS_CHIP); } private Bounds bitmapContentBounds(Bitmap bitmap) { @@ -351,6 +371,9 @@ final class SnapshotRenderer { if (keyHint != null && !keyHint.isEmpty()) { return keyHint; } + if (view == null) { + return source.mode().name().toLowerCase(java.util.Locale.ROOT) + "#" + order; + } String slot = reflectString(view, "getSlot", "mSlot", "slot"); if (!slot.isEmpty()) { return slot; @@ -363,6 +386,9 @@ final class SnapshotRenderer { } private String signalKind(View view, String key) { + if (view == null || key == null) { + return null; + } String lowerKey = key.toLowerCase(java.util.Locale.ROOT); if (lowerKey.contains("wifi")) { return "wifi"; @@ -475,24 +501,60 @@ final class SnapshotRenderer { Bounds bounds = placement.bounds(); return new SnapshotRenderState( placement.key(), - bounds.left, - bounds.top, bounds.width, bounds.height, placement.signal(), placement.overflowDot(), placement.overflowDotColor(), placement.sourceOffsetX(), + boundsSignature(placement.sourceRenderBounds()), 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 ? sourceTreeSignature(placement.source().view()) : 0); + placement.source() != null ? sourceSignature(placement.source()) : 0); + } + + private int sourceSignature(SnapshotSource source) { + if (source == null) { + return 0; + } + int result = 17; + result = 31 * result + sourceTreeSignature(source.view()); + result = 31 * result + boundsSignature(source.boundsOverride()); + return result; + } + + private int boundsSignature(Bounds bounds) { + if (bounds == null) { + return 0; + } + int result = 17; + result = 31 * result + bounds.left; + result = 31 * result + bounds.top; + result = 31 * result + bounds.width; + result = 31 * result + bounds.height; + return result; + } + + private boolean isStatusBarIconView(View view) { + return view != null && view.getClass().getName().contains("StatusBarIconView"); } private int sourceTreeSignature(View view) { if (view == null) { return 0; } + int stamp = sourceSignatureStamp(view); + SourceSignatureCache cached = sourceSignaturesByView.get(view); + if (cached != null && cached.stamp == stamp) { + return cached.signature; + } + int signature = computeSourceTreeSignature(view); + sourceSignaturesByView.put(view, new SourceSignatureCache(stamp, signature)); + return signature; + } + + private int computeSourceTreeSignature(View view) { int result = 17; result = 31 * result + System.identityHashCode(view); result = 31 * result + view.getVisibility(); @@ -523,6 +585,53 @@ final class SnapshotRenderer { return result; } + private int sourceSignatureStamp(View view) { + if (view == null) { + return 0; + } + int result = 17; + result = 31 * result + System.identityHashCode(view); + result = 31 * result + view.getVisibility(); + result = 31 * result + Float.floatToIntBits(view.getAlpha()); + result = 31 * result + view.getWidth(); + result = 31 * result + view.getHeight(); + result = 31 * result + view.getScrollX(); + result = 31 * result + view.getScrollY(); + result = 31 * result + java.util.Arrays.hashCode(view.getDrawableState()); + result = 31 * result + SnapshotAppearanceSignature.view(view); + if (view instanceof TextView textView) { + CharSequence text = textView.getText(); + result = 31 * result + (text != null ? text.toString().hashCode() : 0); + result = 31 * result + textView.getCurrentTextColor(); + result = 31 * result + Float.floatToIntBits(textView.getTextSize()); + } + if (view instanceof ImageView imageView) { + Drawable drawable = imageView.getDrawable(); + result = 31 * result + System.identityHashCode(drawable); + if (drawable != null) { + result = 31 * result + drawable.getLevel(); + result = 31 * result + drawable.getAlpha(); + result = 31 * result + java.util.Arrays.hashCode(drawable.getState()); + } + result = 31 * result + (imageView.getImageTintList() != null + ? imageView.getImageTintList().hashCode() + : 0); + result = 31 * result + (imageView.getColorFilter() != null + ? System.identityHashCode(imageView.getColorFilter()) + : 0); + } + if (view instanceof ViewGroup group) { + result = 31 * result + group.getChildCount(); + for (int i = 0; i < group.getChildCount(); i++) { + result = 31 * result + sourceSignatureStamp(group.getChildAt(i)); + } + } + return result; + } + + private record SourceSignatureCache(int stamp, int signature) { + } + private int drawableSignature(Drawable drawable) { if (drawable == null) { return 0; @@ -548,26 +657,313 @@ final class SnapshotRenderer { canvas.translate(-offsetX, 0); } View sourceView = source.view(); + if (sourceView == null) { + return; + } int oldVisibility = sourceView.getVisibility(); float oldAlpha = sourceView.getAlpha(); try { + // StatusBarIconView may be mid-transition while its drawable is valid. + if (source.mode() == SnapshotMode.VIEW + && isStatusBarIconView(sourceView) + && drawNotificationDrawable( + sourceView, + canvas, + bitmap.getWidth(), + bitmap.getHeight())) { + return; + } if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE && drawNotificationDrawable( - sourceView, - canvas, - bitmap.getWidth(), - bitmap.getHeight())) { + sourceView, + canvas, + bitmap.getWidth(), + bitmap.getHeight())) { return; } sourceView.setVisibility(View.VISIBLE); sourceView.setAlpha(1f); - sourceView.draw(canvas); + if (source.mode() == SnapshotMode.STATUS_CHIP) { + drawTemporarilySizedView(sourceView, canvas, bitmap.getWidth(), bitmap.getHeight()); + } else { + sourceView.draw(canvas); + } } finally { sourceView.setAlpha(oldAlpha); sourceView.setVisibility(oldVisibility); } } + private void drawSnapshot(SnapshotPlacement placement, Bitmap bitmap) { + SnapshotSource source = placement.source(); + if (source == null) { + bitmap.eraseColor(Color.TRANSPARENT); + return; + } + Bounds sourceRenderBounds = placement.sourceRenderBounds(); + if ((source.mode() != SnapshotMode.STATUS_CHIP && source.mode() != SnapshotMode.VIEW) + || sourceRenderBounds == null + || sourceRenderBounds.width <= 0 + || sourceRenderBounds.height <= 0 + || bitmap.getWidth() <= 0 + || bitmap.getHeight() <= 0) { + drawSnapshot(source, bitmap, placement.sourceOffsetX()); + return; + } + sourceRenderBounds = effectiveSourceRenderBounds( + placement, + sourceRenderBounds, + bitmap.getWidth(), + bitmap.getHeight()); + if (sourceRenderBounds.width == bitmap.getWidth() + && sourceRenderBounds.height == bitmap.getHeight()) { + drawSnapshot(source, bitmap, placement.sourceOffsetX()); + return; + } + Bitmap sourceBitmap = Bitmap.createBitmap( + sourceRenderBounds.width, + sourceRenderBounds.height, + Bitmap.Config.ARGB_8888); + try { + drawSnapshot(source, sourceBitmap, 0); + bitmap.eraseColor(Color.TRANSPARENT); + Canvas canvas = new Canvas(bitmap); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG + | Paint.FILTER_BITMAP_FLAG + | Paint.DITHER_FLAG); + float scale = (float) bitmap.getHeight() / sourceRenderBounds.height; + int scaledWidth = Math.max(1, Math.round(sourceRenderBounds.width * scale)); + canvas.translate(-placement.sourceOffsetX(), 0); + canvas.drawBitmap( + sourceBitmap, + null, + new Rect(0, 0, scaledWidth, bitmap.getHeight()), + paint); + } finally { + sourceBitmap.recycle(); + } + } + + private Bounds effectiveSourceRenderBounds( + SnapshotPlacement placement, + Bounds requestedBounds, + int outputWidth, + int outputHeight + ) { + if (placement == null + || requestedBounds == null + || placement.source() == null + || placement.source().mode() != SnapshotMode.STATUS_CHIP + || placement.contentBounds() == null + || placement.contentBounds().width <= 0 + || outputWidth <= 0 + || outputHeight <= 0 + || requestedBounds.width <= 0 + || requestedBounds.height <= 0) { + return requestedBounds; + } + float scale = (float) outputHeight / requestedBounds.height; + if (scale <= 0f) { + return requestedBounds; + } + int targetContentWidth = Math.max(1, Math.round(placement.contentBounds().width / scale)); + int maxWidth = Math.max(1, Math.round(outputWidth / scale)); + int width = Math.max(1, Math.min(requestedBounds.width, maxWidth)); + for (int i = 0; i < 3; i++) { + Bounds content = sourceContentBoundsAtWidth(placement.source(), width, requestedBounds.height); + if (content == null || content.width <= 0) { + break; + } + int delta = targetContentWidth - content.width; + if (Math.abs(delta) <= 1) { + break; + } + int nextWidth = Math.max(1, Math.min(maxWidth, width + delta)); + if (nextWidth == width) { + break; + } + width = nextWidth; + } + return new Bounds( + requestedBounds.left, + requestedBounds.top, + width, + requestedBounds.height); + } + + private Bounds sourceContentBoundsAtWidth(SnapshotSource source, int width, int height) { + if (source == null || width <= 0 || height <= 0) { + return null; + } + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + try { + drawSnapshot(source, bitmap, 0); + return bitmapContentBounds(bitmap); + } finally { + bitmap.recycle(); + } + } + + private void drawTemporarilySizedView(View view, Canvas canvas, int width, int height) { + int oldLeft = view.getLeft(); + int oldTop = view.getTop(); + int oldRight = view.getRight(); + int oldBottom = view.getBottom(); + int oldMeasuredWidth = view.getMeasuredWidth(); + int oldMeasuredHeight = view.getMeasuredHeight(); + ViewFrameState frameState = captureFrameState(view); + IdentityHashMap oldMaxWidths = clearTextMaxWidths(view); + try { + int widthSpec = View.MeasureSpec.makeMeasureSpec(Math.max(1, width), View.MeasureSpec.EXACTLY); + int heightSpec = View.MeasureSpec.makeMeasureSpec(Math.max(1, height), View.MeasureSpec.EXACTLY); + view.measure(widthSpec, heightSpec); + view.layout(oldLeft, oldTop, oldLeft + Math.max(1, width), oldTop + Math.max(1, height)); + clampDescendantsToParent(view); + view.draw(canvas); + } finally { + restoreTextMaxWidths(oldMaxWidths); + restoreFrameState(frameState); + if (oldMeasuredWidth > 0 && oldMeasuredHeight > 0) { + view.measure( + View.MeasureSpec.makeMeasureSpec(oldMeasuredWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(oldMeasuredHeight, View.MeasureSpec.EXACTLY)); + } + view.layout(oldLeft, oldTop, oldRight, oldBottom); + } + } + + private void clampDescendantsToParent(View view) { + if (!(view instanceof ViewGroup group)) { + return; + } + int parentRight = Math.max(0, group.getWidth() - group.getPaddingRight()); + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + int availableWidth = Math.max(0, parentRight - child.getLeft()); + int currentWidth = child.getWidth() > 0 ? child.getWidth() : child.getMeasuredWidth(); + int currentHeight = child.getHeight() > 0 ? child.getHeight() : child.getMeasuredHeight(); + if (availableWidth > 0 && currentWidth > availableWidth && currentHeight > 0) { + child.measure( + View.MeasureSpec.makeMeasureSpec(availableWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(currentHeight, View.MeasureSpec.EXACTLY)); + child.layout( + child.getLeft(), + child.getTop(), + child.getLeft() + availableWidth, + child.getTop() + currentHeight); + } + clampDescendantsToParent(child); + } + } + + private ViewFrameState captureFrameState(View root) { + ViewFrameState state = new ViewFrameState(); + captureFrameState(root, state); + return state; + } + + private void captureFrameState(View view, ViewFrameState state) { + if (view == null) { + return; + } + state.views.add(view); + state.frames.put(view, new ViewFrame( + view.getLeft(), + view.getTop(), + view.getRight(), + view.getBottom(), + view.getMeasuredWidth(), + view.getMeasuredHeight())); + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + captureFrameState(group.getChildAt(i), state); + } + } + } + + private void restoreFrameState(ViewFrameState state) { + if (state == null) { + return; + } + for (int i = state.views.size() - 1; i >= 0; i--) { + View view = state.views.get(i); + ViewFrame frame = state.frames.get(view); + if (view == null || frame == null) { + continue; + } + if (frame.measuredWidth > 0 && frame.measuredHeight > 0) { + view.measure( + View.MeasureSpec.makeMeasureSpec(frame.measuredWidth, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(frame.measuredHeight, View.MeasureSpec.EXACTLY)); + } + view.layout(frame.left, frame.top, frame.right, frame.bottom); + } + } + + private static final class ViewFrameState { + final ArrayList views = new ArrayList<>(); + final IdentityHashMap frames = new IdentityHashMap<>(); + } + + private record ViewFrame( + int left, + int top, + int right, + int bottom, + int measuredWidth, + int measuredHeight + ) { + } + + private IdentityHashMap clearTextMaxWidths(View view) { + IdentityHashMap oldMaxWidths = new IdentityHashMap<>(); + collectTextMaxWidths(view, oldMaxWidths); + for (Map.Entry entry : oldMaxWidths.entrySet()) { + entry.getKey().setMaxWidth(Integer.MAX_VALUE); + } + return oldMaxWidths; + } + + private void collectTextMaxWidths(View view, IdentityHashMap out) { + if (view instanceof TextView textView) { + out.put(textView, textView.getMaxWidth()); + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + collectTextMaxWidths(group.getChildAt(i), out); + } + } + } + + private void restoreTextMaxWidths(IdentityHashMap oldMaxWidths) { + for (Map.Entry entry : oldMaxWidths.entrySet()) { + entry.getKey().setMaxWidth(entry.getValue()); + } + } + + private void drawAppIcon(Context context, SnapshotSource source, Bitmap bitmap) { + bitmap.eraseColor(Color.TRANSPARENT); + if (context == null || source == null || source.keyHint() == null) { + return; + } + try { + String packageName = source.keyHint(); + int suffixIndex = packageName.indexOf('@'); + if (suffixIndex > 0) { + packageName = packageName.substring(0, suffixIndex); + } + Drawable drawable = context.getPackageManager().getApplicationIcon(packageName); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight()); + drawable.draw(canvas); + } catch (PackageManager.NameNotFoundException ignored) { + // Unknown packages fail closed: keep the icon transparent rather than guessing. + } + } + private void drawHeldSignal(HeldSignal heldSignal, Bitmap bitmap) { bitmap.eraseColor(Color.TRANSPARENT); if (heldSignal.bitmap == null || heldSignal.bitmap.isRecycled()) { @@ -579,16 +975,18 @@ final class SnapshotRenderer { canvas.drawBitmap(heldSignal.bitmap, src, dst, null); } - private void drawOverflowDot(Bitmap bitmap, int color) { + private void drawOverflowDot(Bitmap bitmap, int color, int centerOffsetX) { bitmap.eraseColor(Color.TRANSPARENT); if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { return; } - float radius = Math.max(1f, Math.min(bitmap.getWidth() / 2f, bitmap.getHeight() * 0.16f)); + float radius = Math.max(1f, Math.min(bitmap.getWidth() / 2f, bitmap.getHeight() * 0.125f)); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(color); Canvas canvas = new Canvas(bitmap); - canvas.drawCircle(bitmap.getWidth() / 2f, bitmap.getHeight() / 2f, radius, paint); + float centerX = bitmap.getWidth() / 2f + centerOffsetX; + centerX = Math.max(radius, Math.min(bitmap.getWidth() - radius, centerX)); + canvas.drawCircle(centerX, bitmap.getHeight() / 2f, radius, paint); } private boolean drawNotificationDrawable(View source, Canvas canvas, int width, int height) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusIconSlotVisibilityStore.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusIconSlotVisibilityStore.java new file mode 100644 index 0000000..00a840f --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusIconSlotVisibilityStore.java @@ -0,0 +1,42 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import java.util.ArrayList; + +public final class StatusIconSlotVisibilityStore { + private static final Object LOCK = new Object(); + private static ArrayList activeSlots = new ArrayList<>(); + + private StatusIconSlotVisibilityStore() { + } + + public static boolean setActiveSlots(ArrayList slots) { + ArrayList normalized = normalize(slots); + synchronized (LOCK) { + if (activeSlots.equals(normalized)) { + return false; + } + activeSlots = normalized; + return true; + } + } + + static ArrayList activeSlotList() { + synchronized (LOCK) { + return new ArrayList<>(activeSlots); + } + } + + private static ArrayList normalize(ArrayList slots) { + ArrayList normalized = new ArrayList<>(); + if (slots == null) { + return normalized; + } + for (String slot : slots) { + if (slot == null || slot.isEmpty() || normalized.contains(slot)) { + continue; + } + normalized.add(slot); + } + return normalized; + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightSource.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightSource.java new file mode 100644 index 0000000..dd284c2 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightSource.java @@ -0,0 +1,105 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import android.content.res.Resources; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayDeque; + +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; +import se.ajpanton.statusbartweak.runtime.mode.SceneKey; + +final class StatusbarHeightSource { + private StatusbarHeightSource() { + } + + static Bounds bounds(View root, SceneKey scene, RootAnchors anchors) { + if (root == null) { + return null; + } + if (scene != null && scene.isAod()) { + Bounds aodBounds = firstBoundsForIds(root, + "common_battery_statusbar_container", + "keyguard_status_bar_container", + "keyguard_header"); + if (isUsable(root, aodBounds)) { + return aodBounds; + } + return null; + } + Bounds anchorBounds = ViewGeometry.boundsRelativeTo(root, anchors != null ? anchors.statusRoot : null); + if (isUsable(root, anchorBounds)) { + return anchorBounds; + } + Bounds scannedBounds = firstBoundsForIds(root, + "system_icon_area", + "status_bar_contents", + "status_bar"); + if (isUsable(root, scannedBounds)) { + return scannedBounds; + } + return null; + } + + static int height(View root, SceneKey scene, RootAnchors anchors, Bounds fallbackBounds) { + Bounds bounds = bounds(root, scene, anchors); + if (isUsable(root, bounds)) { + return bounds.top + bounds.height; + } + if (isUsable(root, fallbackBounds)) { + return fallbackBounds.top + fallbackBounds.height; + } + return Math.max(1, root != null ? root.getHeight() : 1); + } + + private static Bounds firstBoundsForIds(View root, String... idNames) { + if (!(root instanceof ViewGroup rootGroup) || idNames == null || idNames.length == 0) { + return null; + } + for (String candidateId : idNames) { + ArrayDeque queue = new ArrayDeque<>(); + queue.add(rootGroup); + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + if (candidateId.equals(resolveIdName(view))) { + Bounds bounds = ViewGeometry.boundsRelativeTo(root, view); + if (isUsable(root, bounds)) { + return bounds; + } + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child != null) { + queue.addLast(child); + } + } + } + } + } + return null; + } + + private static boolean isUsable(View root, Bounds bounds) { + if (root == null || bounds == null || bounds.width <= 0 || bounds.height <= 0) { + return false; + } + int right = bounds.left + bounds.width; + int bottom = bounds.top + bounds.height; + return bounds.left >= 0 + && bounds.top >= 0 + && right <= root.getWidth() + && bottom <= root.getHeight(); + } + + private static String resolveIdName(View view) { + if (view == null || view.getId() == View.NO_ID) { + return ""; + } + try { + return view.getResources().getResourceEntryName(view.getId()); + } catch (Resources.NotFoundException ignored) { + return ""; + } + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightStore.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightStore.java new file mode 100644 index 0000000..01ee9ea --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/StatusbarHeightStore.java @@ -0,0 +1,33 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import android.view.View; + +import java.util.LinkedHashMap; +import java.util.Map; + +final class StatusbarHeightStore { + private static final int MAX_ENTRIES = 4; + private static final Map HEIGHT_BY_ROOT_WIDTH = new LinkedHashMap<>(); + + private StatusbarHeightStore() { + } + + static void put(View root, int height) { + if (root == null || root.getWidth() <= 0 || height <= 0) { + return; + } + HEIGHT_BY_ROOT_WIDTH.put(root.getWidth(), height); + while (HEIGHT_BY_ROOT_WIDTH.size() > MAX_ENTRIES) { + Integer oldest = HEIGHT_BY_ROOT_WIDTH.keySet().iterator().next(); + HEIGHT_BY_ROOT_WIDTH.remove(oldest); + } + } + + static int get(View root) { + if (root == null || root.getWidth() <= 0) { + return 0; + } + Integer height = HEIGHT_BY_ROOT_WIDTH.get(root.getWidth()); + return height != null && height > 0 ? height : 0; + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedChipPlacementController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedChipPlacementController.java index 5adc8a8..edde4ad 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedChipPlacementController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedChipPlacementController.java @@ -1,6 +1,8 @@ package se.ajpanton.statusbartweak.runtime.render; import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; import java.util.ArrayList; @@ -9,15 +11,13 @@ import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class UnlockedChipPlacementController { private final SnapshotRenderer renderer; - private final UnlockedVerticalOffsetScaler offsetScaler; private ChipHoldState heldPlacements; private Bounds lastCollisionBounds; private int lastRootWidth; private int lastRootHeight; - UnlockedChipPlacementController(SnapshotRenderer renderer, UnlockedVerticalOffsetScaler offsetScaler) { + UnlockedChipPlacementController(SnapshotRenderer renderer) { this.renderer = renderer; - this.offsetScaler = offsetScaler; } void clear() { @@ -33,31 +33,169 @@ final class UnlockedChipPlacementController { ArrayList metricSources, SbtSettings settings ) { - ArrayList rawPlacements = renderer.rawPlacements(root, visibleSources); + ArrayList rawPlacements = renderer.rawPlacements( + root, + withNaturalChipBounds(root, visibleSources)); + rawPlacements = withVisualChipCollision(rawPlacements); if (!rawPlacements.isEmpty()) { rememberMetrics(root, rawPlacements); - ArrayList placements = offsetPlacements( - rawPlacements, - offsetScaler.chip( - settings.layoutChipVerticalOffsetPx, - representativeChipHeight(rawPlacements))); heldPlacements = new ChipHoldState( - copyAsHeldPlacements(placements), + copyAsHeldPlacements(rawPlacements), root.getWidth(), root.getHeight(), 0); - return placements; + return rawPlacements; } ArrayList placements = isAnyChipHidingEnabled(settings) ? new ArrayList<>() : heldPlacements(root); if (placements.isEmpty()) { - placements = emptyPlacement(root, metricSources, settings); + placements = emptyPlacement(root, metricSources); } return placements; } + private ArrayList withVisualChipCollision(ArrayList placements) { + if (placements == null || placements.isEmpty()) { + return placements; + } + ArrayList result = null; + for (int i = 0; i < placements.size(); i++) { + SnapshotPlacement placement = placements.get(i); + if (placement == null + || placement.source == null + || placement.source.mode() != SnapshotMode.STATUS_CHIP + || placement.contentBounds == null + || placement.bounds == null) { + continue; + } + Bounds visualCollision = new Bounds( + placement.contentBounds.left, + placement.contentBounds.top, + placement.contentBounds.width, + placement.contentBounds.height); + if (visualCollision.equals(placement.contentBounds)) { + continue; + } + if (result == null) { + result = new ArrayList<>(placements); + } + result.set(i, new SnapshotPlacement( + placement.source, + placement.bounds, + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + visualCollision, + placement.sourceOffsetX)); + } + return result != null ? result : placements; + } + + private ArrayList withNaturalChipBounds( + View root, + ArrayList sources + ) { + if (root == null || sources == null || sources.isEmpty()) { + return sources; + } + ArrayList result = null; + for (int i = 0; i < sources.size(); i++) { + SnapshotSource source = sources.get(i); + SnapshotSource normalized = withNaturalChipBounds(root, source); + if (normalized != source && result == null) { + result = new ArrayList<>(sources); + } + if (result != null) { + result.set(i, normalized); + } + } + return result != null ? result : sources; + } + + private SnapshotSource withNaturalChipBounds(View root, SnapshotSource source) { + if (source == null || source.mode() != SnapshotMode.STATUS_CHIP) { + return source; + } + Bounds bounds = source.boundsOverride() != null + ? source.boundsOverride() + : ViewGeometry.boundsRelativeTo(root, source.view()); + if (bounds == null || bounds.width <= 0 || bounds.height <= 0) { + return source; + } + int naturalWidth = naturalChipWidth(source.view(), bounds.width, root.getWidth()); + if (naturalWidth <= bounds.width) { + return source; + } + return new SnapshotSource( + source.view(), + source.mode(), + source.keyHint(), + new Bounds(bounds.left, bounds.top, naturalWidth, bounds.height)); + } + + private int naturalChipWidth(View view, int currentWidth, int maxWidth) { + if (view == null) { + return currentWidth; + } + int width = naturalContentWidth(view); + if (maxWidth > 0) { + width = Math.min(width, maxWidth); + } + return Math.max(currentWidth, width); + } + + private int naturalContentWidth(View view) { + if (view == null) { + return 0; + } + int width = measuredWidth(view); + if (view instanceof TextView textView) { + width = Math.max(width, naturalTextWidth(textView)); + } + if (view instanceof ViewGroup group) { + int childRight = group.getPaddingLeft() + group.getPaddingRight(); + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child == null || child.getVisibility() == View.GONE) { + continue; + } + ViewGroup.LayoutParams lp = child.getLayoutParams(); + int margins = 0; + if (lp instanceof ViewGroup.MarginLayoutParams marginParams) { + margins = marginParams.leftMargin + marginParams.rightMargin; + } + childRight = Math.max( + childRight, + child.getLeft() + margins + naturalContentWidth(child)); + } + width = Math.max(width, childRight + group.getPaddingRight()); + } + return width; + } + + private int measuredWidth(View view) { + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + ViewGroup.LayoutParams lp = view.getLayoutParams(); + if (lp != null && lp.width > 0) { + width = Math.max(width, lp.width); + } + return Math.max(0, width); + } + + private int naturalTextWidth(TextView textView) { + CharSequence text = textView.getText(); + int textWidth = text != null && text.length() > 0 + ? (int) Math.ceil(textView.getPaint().measureText(text, 0, text.length())) + : 0; + return textWidth + + textView.getCompoundPaddingLeft() + + textView.getCompoundPaddingRight(); + } + private ArrayList heldPlacements(View root) { ChipHoldState state = heldPlacements; if (state == null || state.placements.isEmpty()) { @@ -81,20 +219,16 @@ final class UnlockedChipPlacementController { private ArrayList emptyPlacement( View root, - ArrayList sources, - SbtSettings settings + ArrayList sources ) { ArrayList result = new ArrayList<>(); Bounds bounds = emptyBounds(root, sources); if (bounds == null || bounds.height <= 0) { return result; } - int verticalOffset = offsetScaler.chip( - settings != null ? settings.layoutChipVerticalOffsetPx : 0, - bounds.height); result.add(new SnapshotPlacement( null, - new Bounds(0, bounds.top - verticalOffset, 1, bounds.height), + new Bounds(0, bounds.top, 1, bounds.height), SnapshotKeys.EMPTY_CHIP, false, false, @@ -119,7 +253,9 @@ final class UnlockedChipPlacementController { return null; } for (SnapshotSource source : sources) { - Bounds bounds = ViewGeometry.boundsRelativeTo(root, source.view()); + Bounds bounds = source.boundsOverride() != null + ? source.boundsOverride() + : ViewGeometry.boundsRelativeTo(root, source.view()); if (bounds == null || bounds.width <= 0 || bounds.height <= 0) { continue; } @@ -198,40 +334,6 @@ final class UnlockedChipPlacementController { return held; } - private ArrayList offsetPlacements(ArrayList placements, int verticalOffsetPx) { - if (placements == null || placements.isEmpty() || verticalOffsetPx == 0) { - return placements; - } - ArrayList result = new ArrayList<>(); - int dy = -verticalOffsetPx; - for (SnapshotPlacement placement : placements) { - Bounds bounds = placement.bounds; - result.add(new SnapshotPlacement( - placement.source, - new Bounds(bounds.left, bounds.top + dy, bounds.width, bounds.height), - placement.key, - placement.signal, - placement.overflowDot, - placement.overflowDotColor, - placement.heldSignal, - placement.contentBounds, - placement.sourceOffsetX)); - } - return result; - } - - private int representativeChipHeight(ArrayList placements) { - Bounds bounds = collisionUnion(placements); - return bounds != null ? Math.max(0, bounds.height) : representativeHeight(placements); - } - - private int representativeHeight(ArrayList placements) { - if (placements == null || placements.isEmpty()) { - return 0; - } - return Math.max(0, placements.get(0).bounds.height); - } - private record ChipHoldState( ArrayList placements, int rootWidth, 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 46443ae..384bf57 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 @@ -5,10 +5,14 @@ import android.view.ViewGroup; import android.widget.TextView; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.WeakHashMap; +import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; +import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; @@ -20,34 +24,44 @@ import se.ajpanton.statusbartweak.shell.settings.SbtSettings; * into owned unlocked rows instead of copying each stock X position. */ public final class UnlockedIconSnapshotRenderController { + private final RuntimeContext runtimeContext; private final SnapshotRenderer statusRenderer; private final SnapshotRenderer notificationRenderer; private final SnapshotRenderer chipRenderer; private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer(); private final ClockProxyRenderer carrierRenderer = new ClockProxyRenderer(); - private final UnlockedStockSourceCollector sourceCollector = new UnlockedStockSourceCollector(); + private final UnlockedStockSourceCollector sourceCollector; private final UnlockedVerticalOffsetScaler offsetScaler = new UnlockedVerticalOffsetScaler(); private final UnlockedChipPlacementController chipPlacementController; private final UnlockedLayoutPlanner layoutPlanner; private final WeakHashMap clockBaselineDeltaByRoot = new WeakHashMap<>(); + private final LinkedHashMap clockSourceBaselineByRootKey = new LinkedHashMap<>(); private final WeakHashMap lastLayoutSignatureByRoot = new WeakHashMap<>(); private final WeakHashMap lastCollectedIconsByRoot = new WeakHashMap<>(); private final WeakHashMap lastLayoutPlanByRoot = new WeakHashMap<>(); private final WeakHashMap lastClockGeometryByRoot = new WeakHashMap<>(); private final WeakHashMap lastClockSourceGeometryByRoot = new WeakHashMap<>(); private final WeakHashMap lastClockAppearanceByRoot = new WeakHashMap<>(); + private final WeakHashMap forcedChipRefreshByRoot = new WeakHashMap<>(); public UnlockedIconSnapshotRenderController() { + this(null); + } + + public UnlockedIconSnapshotRenderController(RuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext; + sourceCollector = new UnlockedStockSourceCollector(runtimeContext); statusRenderer = new SnapshotRenderer(true, true, false); notificationRenderer = new SnapshotRenderer(false, false, false); chipRenderer = new SnapshotRenderer(false, false, true); - chipPlacementController = new UnlockedChipPlacementController(chipRenderer, offsetScaler); + chipPlacementController = new UnlockedChipPlacementController(chipRenderer); layoutPlanner = new UnlockedLayoutPlanner( statusRenderer, notificationRenderer, chipRenderer, chipPlacementController, - offsetScaler); + offsetScaler, + runtimeContext); } public RenderResult renderUnlocked( @@ -60,7 +74,9 @@ public final class UnlockedIconSnapshotRenderController { SceneKey scene, TextView externalCarrierView ) { - if (scene == null || (!scene.isUnlocked() && !scene.isLockscreen()) || !root.isAttachedToWindow()) { + if (scene == null + || (!scene.isUnlocked() && !scene.isLockscreen() && !scene.isAod()) + || !root.isAttachedToWindow()) { clearAll(); return new RenderResult(true, true); } @@ -92,11 +108,14 @@ public final class UnlockedIconSnapshotRenderController { anchors, previousIcons); LayoutInputSignature previousSignature = lastLayoutSignatureByRoot.get(root); + boolean forceChipRefresh = Boolean.TRUE.equals(forcedChipRefreshByRoot.remove(root)); + boolean sceneChanged = previousSignature != null + && !java.util.Objects.equals(signature.scene, previousSignature.scene); boolean stockSourcesChanged = previousSignature == null || signature.stockSurfaceSignature() != previousSignature.stockSurfaceSignature(); boolean chipSourcesChanged = previousSignature == null || signature.chipSources != previousSignature.chipSources; - if (signature.equals(previousSignature)) { + if (signature.equals(previousSignature) && !forceChipRefresh) { return new RenderResult( false, false, @@ -109,6 +128,7 @@ public final class UnlockedIconSnapshotRenderController { ClockLayout currentClockLayout = null; Integer currentClockGeometry = null; if (isClockOnlySignatureChange(previousSignature, signature) + && !forceChipRefresh && scene.isUnlocked() && settings.clockEnabledUnlocked && previousIcons != null) { @@ -158,8 +178,11 @@ public final class UnlockedIconSnapshotRenderController { } } CollectedIcons collectedIcons = previousIcons; - if (stockSourcesChanged || collectedIcons == null) { - collectedIcons = sourceCollector.collect(root, anchors); + if (forceChipRefresh && collectedIcons != null) { + collectedIcons = sourceCollector.collectChips(root, anchors, collectedIcons); + lastCollectedIconsByRoot.put(root, collectedIcons); + } else if (sceneChanged || stockSourcesChanged || collectedIcons == null) { + collectedIcons = sourceCollector.collect(root, anchors, scene); lastCollectedIconsByRoot.put(root, collectedIcons); } else if (chipSourcesChanged) { collectedIcons = sourceCollector.collectChips(root, anchors, collectedIcons); @@ -190,7 +213,6 @@ public final class UnlockedIconSnapshotRenderController { settings, currentClockLayout, scene); - lastLayoutPlanByRoot.put(root, layoutPlan); lastClockGeometryByRoot.put(root, currentClockGeometry != null ? currentClockGeometry : 0); lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry); layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons); @@ -200,7 +222,9 @@ public final class UnlockedIconSnapshotRenderController { } else { clockRenderer.clear(clockHost); } - if (scene.isLockscreen() && settings.layoutCarrierEnabledLockscreen && layoutPlan.carrierPlacement != null) { + if (scene.isLockscreen() + && settings.layoutCarrierEnabledLockscreen + && layoutPlan.carrierPlacement != null) { carrierRenderer.render(carrierHost, layoutPlan.carrierPlacement); } else { carrierRenderer.clear(carrierHost); @@ -265,6 +289,32 @@ public final class UnlockedIconSnapshotRenderController { } } + public boolean refreshUnlockedChipContent( + View root, + ViewGroup chipHost, + SceneKey scene + ) { + if (root == null + || chipHost == null + || scene == null + || !scene.isUnlocked() + || !root.isAttachedToWindow()) { + return false; + } + LayoutPlan layoutPlan = lastLayoutPlanByRoot.get(root); + if (layoutPlan == null + || layoutPlan.chipPlacements == null + || layoutPlan.chipPlacements.isEmpty()) { + return false; + } + SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); + if (settings == null || !settings.layoutChipEnabledUnlocked) { + return false; + } + chipRenderer.renderPlacements(chipHost, layoutPlan.chipPlacements); + return true; + } + public boolean refreshUnlockedClockText( View root, ViewGroup clockHost, @@ -273,7 +323,7 @@ public final class UnlockedIconSnapshotRenderController { if (root == null || clockHost == null || scene == null - || !scene.isUnlocked() + || (!scene.isUnlocked() && !scene.isLockscreen()) || !root.isAttachedToWindow()) { return false; } @@ -282,7 +332,7 @@ public final class UnlockedIconSnapshotRenderController { return false; } SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); - if (settings == null || !settings.clockEnabledUnlocked) { + if (settings == null || !clockEnabledForScene(settings, scene)) { return false; } CollectedIcons collectedIcons = lastCollectedIconsByRoot.get(root); @@ -355,9 +405,13 @@ public final class UnlockedIconSnapshotRenderController { if (settings == null || scene == null) { return false; } - return scene.isLockscreen() - ? settings.layoutStatusEnabledLockscreen - : settings.layoutStatusEnabledUnlocked; + if (scene.isLockscreen()) { + return settings.layoutStatusEnabledLockscreen; + } + if (scene.isAod()) { + return settings.layoutStatusEnabledAod; + } + return settings.layoutStatusEnabledUnlocked; } private boolean notificationsEnabledForScene(SbtSettings settings, SceneKey scene) { @@ -366,8 +420,14 @@ public final class UnlockedIconSnapshotRenderController { } if (scene.isLockscreen()) { return settings.layoutNotifEnabledLockscreen - && scene.notificationDisplayStyle() != se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle.NONE - && scene.notificationDisplayStyle() != se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle.CARDS; + && scene.notificationDisplayStyle() != NotificationDisplayStyle.NONE + && scene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS; + } + if (scene.isAod()) { + NotificationDisplayStyle style = scene.notificationDisplayStyle(); + return settings.layoutNotifEnabledAod + && (style == NotificationDisplayStyle.ICONS + || style == NotificationDisplayStyle.DOT); } return settings.layoutNotifEnabledUnlocked; } @@ -556,6 +616,8 @@ public final class UnlockedIconSnapshotRenderController { chipRenderer.clearAll(); sourceCollector.clear(); clockBaselineDeltaByRoot.clear(); + clockSourceBaselineByRootKey.clear(); + forcedChipRefreshByRoot.clear(); lastLayoutSignatureByRoot.clear(); lastCollectedIconsByRoot.clear(); lastLayoutPlanByRoot.clear(); @@ -565,9 +627,31 @@ public final class UnlockedIconSnapshotRenderController { chipPlacementController.clear(); } + public void clearRoot(View root) { + if (root == null) { + return; + } + forcedChipRefreshByRoot.remove(root); + sourceCollector.clearRoot(root); + clockBaselineDeltaByRoot.remove(root); + lastLayoutSignatureByRoot.remove(root); + lastCollectedIconsByRoot.remove(root); + lastLayoutPlanByRoot.remove(root); + lastClockGeometryByRoot.remove(root); + lastClockSourceGeometryByRoot.remove(root); + lastClockAppearanceByRoot.remove(root); + } + + public void forceChipRefresh(View root) { + if (root != null) { + forcedChipRefreshByRoot.put(root, true); + } + } + public void beginRootTransition() { statusRenderer.beginTransitionHold(); sourceCollector.clear(); + forcedChipRefreshByRoot.clear(); lastLayoutSignatureByRoot.clear(); lastCollectedIconsByRoot.clear(); lastLayoutPlanByRoot.clear(); @@ -615,12 +699,12 @@ public final class UnlockedIconSnapshotRenderController { if (model == null || model.isEmpty()) { return null; } + int baselineY = stableClockBaselineY(root, source, clockContainer); return ClockLayout.measure( source, model, UnlockedLayoutPlanner.positionFor(settings.clockPosition), - stableClockBaselineY(root, source, clockContainer) - - offsetScaler.clock(settings.clockVerticalOffsetPx, source), + baselineY - offsetScaler.clock(settings.clockVerticalOffsetPx, source), root.getWidth(), cutoutBounds, settings.clockMiddleAutoNearestSide, @@ -872,6 +956,7 @@ public final class UnlockedIconSnapshotRenderController { int sourceBaseline = source.getBaseline() > 0 ? source.getBaseline() : (sourceBounds != null ? Math.max(0, (sourceBounds.height * 3) / 4) : 0); + sourceBaseline = stableClockSourceBaseline(root, source, sourceBaseline); Bounds containerBounds = ViewGeometry.boundsRelativeTo(root, clockContainer); if (containerBounds != null && containerBounds.width > 0 @@ -890,6 +975,28 @@ public final class UnlockedIconSnapshotRenderController { return sourceBounds != null ? sourceBounds.top + sourceBaseline : 0; } + private int stableClockSourceBaseline(View root, TextView source, int sourceBaseline) { + if (root == null || source == null || root.getWidth() <= 0 || sourceBaseline <= 0) { + return sourceBaseline; + } + int key = 31 * root.getWidth() + Math.round(source.getTextSize()); + Integer previous = clockSourceBaselineByRootKey.get(key); + int stable = previous == null ? sourceBaseline : Math.min(previous, sourceBaseline); + clockSourceBaselineByRootKey.put(key, stable); + while (clockSourceBaselineByRootKey.size() > 8) { + Integer eldest = null; + for (Map.Entry entry : clockSourceBaselineByRootKey.entrySet()) { + eldest = entry.getKey(); + break; + } + if (eldest == null) { + break; + } + clockSourceBaselineByRootKey.remove(eldest); + } + return stable; + } + public record RenderResult( boolean layoutChanged, boolean stockSourcesChanged, 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 42268c4..a2698e4 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 @@ -18,6 +18,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { final SnapshotRenderer renderer; final ArrayList rawPlacements; final boolean showDotIfTruncated; + final boolean forceOverflowDotOnly; final Bounds reservedBounds; final ClockLayout clockLayout; @@ -26,6 +27,8 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { Bounds placedBounds; int clipStartPx; int clipEndPx; + private int solverCollisionTop = Integer.MIN_VALUE; + private int solverCollisionHeight = Integer.MIN_VALUE; private UnlockedLayoutBand( LayoutItemKind kind, @@ -36,6 +39,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { boolean middleAutoNearestSide, AnchorSide middleSide, boolean showDotIfTruncated, + boolean forceOverflowDotOnly, Bounds reservedBounds, ClockLayout clockLayout, SplitRegion splitRegion @@ -45,6 +49,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { this.renderer = renderer; this.rawPlacements = rawPlacements; this.showDotIfTruncated = showDotIfTruncated; + this.forceOverflowDotOnly = forceOverflowDotOnly; this.reservedBounds = reservedBounds; this.clockLayout = clockLayout; } @@ -64,6 +69,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { settings.clockMiddleAutoNearestSide, sideFor(settings.clockMiddleCutoutSide), false, + false, null, clockLayout, SplitRegion.NONE); @@ -84,6 +90,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { settings.clockMiddleAutoNearestSide, sideFor(settings.clockMiddleCutoutSide), false, + false, null, clockLayout, splitRegion); @@ -105,6 +112,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { middleAutoNearestSide, middleSide, false, + false, null, carrierLayout, SplitRegion.NONE); @@ -129,6 +137,37 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { middleAutoNearestSide, middleSide, showDotIfTruncated, + false, + null, + null, + SplitRegion.NONE); + } + + UnlockedLayoutBand withSolverCollision(int top, int height) { + solverCollisionTop = top; + solverCollisionHeight = Math.max(0, height); + return this; + } + + static UnlockedLayoutBand dotIcons( + LayoutItemKind kind, + ViewGroup host, + SnapshotRenderer renderer, + ArrayList rawPlacements, + LayoutPosition position, + boolean middleAutoNearestSide, + AnchorSide middleSide + ) { + return new UnlockedLayoutBand( + kind, + host, + renderer, + rawPlacements, + position, + middleAutoNearestSide, + middleSide, + true, + true, null, null, SplitRegion.NONE); @@ -201,6 +240,16 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { if (kind != LayoutItemKind.NOTIFICATIONS && kind != LayoutItemKind.STATUS) { return; } + if (forceOverflowDotOnly) { + int height = dotHeight(rawPlacements); + SnapshotPlacement dotPlacement = dotPlacement( + Math.max(1, Math.round(Math.max(1, height) * 0.75f)), + height, + rowTop(rawPlacements)); + placements = new ArrayList<>(); + placements.add(dotPlacement); + return; + } int targetVisible = Math.max(0, visibleIcons); placements = iconSlice(rawPlacements, firstIconIndex, targetVisible); if (dot) { @@ -295,7 +344,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { withoutDot.add(placement); } } - SnapshotPlacement dotPlacement = dotPlacement(width, height); + SnapshotPlacement dotPlacement = dotPlacement(width, height, rowTop(withoutDot)); if (kind == LayoutItemKind.STATUS) { withoutDot.add(0, dotPlacement); } else { @@ -417,6 +466,25 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { return boxes; } + ArrayList solverCollisionBoxes() { + ArrayList boxes = collisionBoxes(); + if (isTextKind() + || solverCollisionTop == Integer.MIN_VALUE + || solverCollisionHeight <= 0 + || boxes.isEmpty()) { + return boxes; + } + ArrayList solverBoxes = new ArrayList<>(); + for (Bounds box : boxes) { + solverBoxes.add(new Bounds( + box.left, + solverCollisionTop, + box.width, + solverCollisionHeight)); + } + return solverBoxes; + } + @Override protected ArrayList placedCollisionBounds() { ArrayList boxes = new ArrayList<>(); @@ -454,8 +522,8 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { private int overflowDotVisualWidth(SnapshotPlacement placement) { Bounds bounds = placement.bounds; int base = Math.max(1, Math.min(bounds.width, bounds.height)); - int radius = Math.max(1, Math.round(base * 0.16f)); - return Math.min(bounds.width, radius * 2 + 2); + int radius = Math.max(1, Math.round(base * 0.125f)); + return Math.min(bounds.width, radius * 2); } private Bounds collisionBounds(SnapshotPlacement placement) { @@ -487,7 +555,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { boolean hadDot = hasOverflowDot(icons); icons.remove(removeIndex); if (showDotIfTruncated && !hadDot) { - SnapshotPlacement dot = dotPlacement(dotWidth(icons), dotHeight(icons)); + SnapshotPlacement dot = dotPlacement(dotWidth(icons), dotHeight(icons), rowTop(icons)); if (fromStart) { icons.add(0, dot); } else { @@ -604,17 +672,61 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { return 0; } - private SnapshotPlacement dotPlacement(int width, int height) { + private int rowTop(ArrayList preferred) { + int top = rowTop(preferred, true); + if (top != Integer.MAX_VALUE) { + return top; + } + top = rowTop(rawPlacements, true); + if (top != Integer.MAX_VALUE) { + return top; + } + top = rowTop(preferred, false); + if (top != Integer.MAX_VALUE) { + return top; + } + top = rowTop(rawPlacements, false); + return top != Integer.MAX_VALUE ? top : top(); + } + + private int rowTop(ArrayList placements, boolean shrinkableOnly) { + if (placements == null || placements.isEmpty()) { + return Integer.MAX_VALUE; + } + int top = Integer.MAX_VALUE; + for (SnapshotPlacement placement : placements) { + if (placement == null || (shrinkableOnly && !isShrinkableIcon(placement))) { + continue; + } + top = Math.min(top, collisionBounds(placement).top); + } + return top; + } + + private SnapshotPlacement dotPlacement(int width, int height, int rowTop) { + int safeWidth = Math.max(1, width); + int safeHeight = Math.max(1, height); return new SnapshotPlacement( null, - new Bounds(0, top(), Math.max(1, width), Math.max(1, height)), + new Bounds(0, rowTop, safeWidth, safeHeight), SnapshotKeys.OVERFLOW_DOT + ":" + kind.name().toLowerCase(java.util.Locale.ROOT), false, true, dotColor(), null, - new Bounds(0, 0, Math.max(1, width), Math.max(1, height)), - 0); + new Bounds(0, 0, safeWidth, safeHeight), + dotCenterOffsetX(safeWidth, safeHeight)); + } + + private int dotCenterOffsetX(int width, int height) { + int fullWidth = Math.max(1, Math.round(Math.max(1, height) * 0.75f)); + if (width >= fullWidth) { + return 0; + } + float radius = Math.max(1f, Math.min(width / 2f, height * 0.125f)); + float centered = width / 2f; + float target = kind == LayoutItemKind.STATUS ? radius : width - radius; + return Math.round(target - centered); } private int dotColor() { @@ -649,7 +761,7 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { int height = Math.max(1, dotHeight(activePlacements())); return new SnapshotPlacement( null, - new Bounds(0, top(), 1, height), + new Bounds(0, rowTop(activePlacements()), 1, height), SnapshotKeys.EMPTY_ICON_CONTAINER + ":" + kind.name().toLowerCase(java.util.Locale.ROOT), false, false, @@ -689,6 +801,14 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { collisionWidth, placement.contentBounds.height) : null; + Bounds sourceRenderBounds = placement.sourceRenderBounds; + int sourceOffsetX = placement.sourceOffsetX + removeLeft; + if (kind == LayoutItemKind.CHIP && sourceRenderBounds != null && clippedContent != null) { + sourceRenderBounds = chipSourceRenderBounds( + sourceRenderBounds, + clippedContent); + sourceOffsetX = 0; + } return new SnapshotPlacement( placement.source, clippedBounds, @@ -698,7 +818,20 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { placement.overflowDotColor, placement.heldSignal, clippedContent, - placement.sourceOffsetX + removeLeft); + sourceOffsetX, + sourceRenderBounds); + } + + private Bounds chipSourceRenderBounds(Bounds sourceRenderBounds, Bounds clippedContent) { + if (sourceRenderBounds == null || clippedContent == null) { + return sourceRenderBounds; + } + int width = Math.max(1, clippedContent.left + clippedContent.width); + return new Bounds( + sourceRenderBounds.left, + sourceRenderBounds.top, + width, + sourceRenderBounds.height); } private int sumWidths(ArrayList snapshotPlacements) { 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 3653d8e..3985cf1 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 @@ -7,8 +7,10 @@ import android.view.ViewGroup; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; +import java.util.WeakHashMap; import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; @@ -29,13 +31,17 @@ final class UnlockedLayoutPlanner { private final UnlockedVerticalOffsetScaler offsetScaler; private final NotificationSeenAppReporter notificationSeenAppReporter = new NotificationSeenAppReporter(); + private final WeakHashMap aodStatusRightInsetByRoot = new WeakHashMap<>(); + private final WeakHashMap aodInitialStatusbarHeightByRoot = new WeakHashMap<>(); + private final Map recentNonAodEdgesByRootWidth = new LinkedHashMap<>(); UnlockedLayoutPlanner( SnapshotRenderer statusRenderer, SnapshotRenderer notificationRenderer, SnapshotRenderer chipRenderer, UnlockedChipPlacementController chipPlacementController, - UnlockedVerticalOffsetScaler offsetScaler + UnlockedVerticalOffsetScaler offsetScaler, + RuntimeContext runtimeContext ) { this.statusRenderer = statusRenderer; this.notificationRenderer = notificationRenderer; @@ -57,10 +63,32 @@ final class UnlockedLayoutPlanner { ClockLayout clockLayout, SceneKey scene ) { + clearDebugContainerBounds(clockHost, carrierHost, chipHost, statusHost, notificationHost); + if (scene == null || !scene.isAod()) { + aodStatusRightInsetByRoot.remove(root); + aodInitialStatusbarHeightByRoot.remove(root); + } ArrayList bands = new ArrayList<>(); Rect cutout = ViewGeometry.middleCutout(root); - Bounds layoutCutoutBounds = cutoutBounds(cutout, settings.layoutPaddingCutoutPx); - Bounds clockCutoutBounds = cutoutBounds(cutout, settings.clockMiddleCutoutGapPx); + Bounds layoutCutoutBounds = scene != null && scene.isAod() + ? aodCutoutBounds(root, cutout, settings.layoutPaddingCutoutPx) + : cutoutBounds(cutout, settings.layoutPaddingCutoutPx); + Bounds clockCutoutBounds = scene != null && scene.isAod() + ? aodCutoutBounds(root, cutout, settings.clockMiddleCutoutGapPx) + : cutoutBounds(cutout, settings.clockMiddleCutoutGapPx); + Bounds aodStatusAnchorBounds = scene != null && scene.isAod() + ? aodStatusAnchorBounds(root, anchors, scene) + : null; + Bounds aodStatusContentBounds = scene != null && scene.isAod() + ? aodStatusContentBounds(root, aodStatusAnchorBounds) + : null; + Bounds aodNotificationBounds = scene != null && scene.isAod() + ? aodNotificationBounds(root, anchors, scene) + : null; + Bounds aodStatusbarBounds = scene != null && scene.isAod() + ? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds) + : null; + int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds); if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) { bands.add(UnlockedLayoutBand.clock( clockHost, @@ -77,43 +105,42 @@ final class UnlockedLayoutPlanner { settings.layoutCarrierMiddleAutoNearestSide, sideFor(settings.layoutCarrierMiddleSide))); } - if (scene != null && scene.isUnlocked() && settings.layoutChipEnabledUnlocked) { - ArrayList placements = chipPlacementController.placements( - root, - collectedIcons.statusChips, - collectedIcons.statusChipMetrics, - settings); - if (!placements.isEmpty()) { - bands.add(UnlockedLayoutBand.icons( - LayoutItemKind.CHIP, - chipHost, - chipRenderer, - placements, - positionFor(settings.layoutChipPosition), - settings.layoutChipMiddleAutoNearestSide, - sideFor(settings.layoutChipMiddleSide), - false)); - } - } int statusCount = statusCountForScene(settings, scene); + if (scene != null && scene.isAod() && !isUsableAodAnchor(aodStatusAnchorBounds)) { + statusCount = 0; + } if (statusCount > 0) { ArrayList rawPlacements = SnapshotIconFilter.statusPlacements( - statusRenderer.rawPlacements(root, collectedIcons.statusIcons), + statusRenderer.rawPlacements( + root, + collectedIcons.statusIcons, + scene != null && scene.isAod()), settings, scene); - int representativeHeight = representativeHeight(rawPlacements); + rawPlacements = resizePlacementsToHeight( + rawPlacements, + iconHeightPx(statusbarHeight, statusIconHeightSteps(settings, scene))); String[] positions = statusPositionsForScene(settings, scene); String[] middleSides = statusMiddleSidesForScene(settings, scene); boolean[] autoNearest = statusAutoNearestForScene(settings, scene); int[] verticalOffsets = statusVerticalOffsetsForScene(settings, scene); + int statusHeightSteps = statusIconHeightSteps(settings, scene); for (int i = 0; i < statusCount; i++) { + int verticalOffsetSteps = intAt( + verticalOffsets, + i, + settings.layoutStatusVerticalOffsetPx); ArrayList placements = - offsetPlacements( + bottomAlignAodPlacements( rawPlacements, - offsetScaler.status( - intAt(verticalOffsets, i, settings.layoutStatusVerticalOffsetPx), - representativeHeight)); + scene != null && scene.isAod() + ? aodStatusContentBounds + : aodStatusbarBounds); + placements = + offsetPlacements( + placements, + stepsToPx(verticalOffsetSteps, statusbarHeight)); if (!placements.isEmpty()) { bands.add(UnlockedLayoutBand.icons( LayoutItemKind.STATUS, @@ -126,62 +153,126 @@ final class UnlockedLayoutPlanner { i, settings.layoutStatusMiddleAutoNearestSide), sideFor(stringAt(middleSides, i, settings.layoutStatusMiddleSide)), - true)); + true) + .withSolverCollision( + logicalRowTop(statusHeightSteps, verticalOffsetSteps), + statusHeightSteps)); } } } int notificationCount = notificationCountForScene(settings, scene); + Bounds notificationRowBounds = null; if (notificationCount > 0) { ArrayList unfilteredPlacements = notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons); notificationSeenAppReporter.report( root != null ? root.getContext() : null, unfilteredPlacements); - ArrayList rawPlacements = - SnapshotIconFilter.notificationPlacements(unfilteredPlacements, settings, scene); - if (scene != null - && scene.isLockscreen() - && scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) { - rawPlacements = dotOnlyPlacements(rawPlacements); - notificationCount = rawPlacements.isEmpty() ? 0 : 1; - } - int representativeHeight = representativeHeight(rawPlacements); + ArrayList rawPlacements = resizePlacementsToHeight( + SnapshotIconFilter.notificationPlacements(unfilteredPlacements, settings, scene), + iconHeightPx(statusbarHeight, notificationIconHeightSteps(settings, scene))); + notificationRowBounds = placementBounds(rawPlacements); + boolean dotNotificationScene = isDotNotificationScene(scene); + notificationCount = dotNotificationScene && !rawPlacements.isEmpty() ? 1 : notificationCount; String[] positions = notificationPositionsForScene(settings, scene); String[] middleSides = notificationMiddleSidesForScene(settings, scene); boolean[] autoNearest = notificationAutoNearestForScene(settings, scene); int[] verticalOffsets = notificationVerticalOffsetsForScene(settings, scene); + int notificationHeightSteps = notificationIconHeightSteps(settings, scene); for (int i = 0; i < notificationCount; i++) { + int verticalOffsetSteps = intAt( + verticalOffsets, + i, + settings.layoutNotifVerticalOffsetPx); ArrayList placements = - offsetPlacements( + alignAodNotificationPlacements( rawPlacements, - offsetScaler.notification( - intAt(verticalOffsets, i, settings.layoutNotifVerticalOffsetPx), - representativeHeight)); + aodNotificationBounds, + aodStatusbarBounds); + placements = + offsetPlacements( + placements, + stepsToPx(verticalOffsetSteps, statusbarHeight)); if (!placements.isEmpty()) { - bands.add(UnlockedLayoutBand.icons( - LayoutItemKind.NOTIFICATIONS, - notificationHost, - notificationRenderer, - placements, - positionFor(stringAt(positions, i, settings.layoutNotifPosition)), - booleanAt( - autoNearest, - i, - settings.layoutNotifMiddleAutoNearestSide), - sideFor(stringAt(middleSides, i, settings.layoutNotifMiddleSide)), - true)); + LayoutPosition position = positionFor(stringAt(positions, i, settings.layoutNotifPosition)); + boolean middleAutoNearest = booleanAt( + autoNearest, + i, + settings.layoutNotifMiddleAutoNearestSide); + AnchorSide middleSide = sideFor(stringAt(middleSides, i, settings.layoutNotifMiddleSide)); + bands.add(dotNotificationScene + ? UnlockedLayoutBand.dotIcons( + LayoutItemKind.NOTIFICATIONS, + notificationHost, + notificationRenderer, + placements, + position, + middleAutoNearest, + middleSide) + .withSolverCollision( + logicalRowTop(notificationHeightSteps, verticalOffsetSteps), + notificationHeightSteps) + : UnlockedLayoutBand.icons( + LayoutItemKind.NOTIFICATIONS, + notificationHost, + notificationRenderer, + placements, + position, + middleAutoNearest, + middleSide, + true) + .withSolverCollision( + logicalRowTop(notificationHeightSteps, verticalOffsetSteps), + notificationHeightSteps)); } } } + if (scene != null && scene.isUnlocked() && settings.layoutChipEnabledUnlocked) { + Bounds chipBaselineBounds = chipBaselineBounds( + root, + anchors, + settings, + scene, + statusbarHeight, + notificationRowBounds); + ArrayList placements = chipPlacementController.placements( + root, + collectedIcons.statusChips, + collectedIcons.statusChipMetrics, + settings); + placements = scaleChipPlacementsToHeight( + placements, + iconHeightPx(statusbarHeight, settings.layoutChipHeightSteps)); + placements = bottomAlignPlacements(placements, chipBaselineBounds); + placements = offsetPlacements( + placements, + stepsToPx(settings.layoutChipVerticalOffsetPx, statusbarHeight)); + if (!placements.isEmpty()) { + bands.add(UnlockedLayoutBand.icons( + LayoutItemKind.CHIP, + chipHost, + chipRenderer, + placements, + positionFor(settings.layoutChipPosition), + settings.layoutChipMiddleAutoNearestSide, + sideFor(settings.layoutChipMiddleSide), + false) + .withSolverCollision( + logicalRowTop( + settings.layoutChipHeightSteps, + settings.layoutChipVerticalOffsetPx), + settings.layoutChipHeightSteps)); + } + } if (bands.isEmpty()) { return new LayoutPlan(null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } ArrayList itemOrder = orderedKinds(settings.layoutItemOrder); splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings); sortBandsForItemOrder(bands, itemOrder); - LayoutEdges edges = layoutEdges(root, settings, anchors); + LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds); int itemPadding = Math.max(0, settings.layoutItemPaddingPx); - solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding); + solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding, scene); ArrayList statusPlacements = new ArrayList<>(); ArrayList notificationPlacements = new ArrayList<>(); @@ -239,7 +330,8 @@ final class UnlockedLayoutPlanner { ArrayList bands, Bounds cutoutBounds, SbtSettings settings, - int itemPadding + int itemPadding, + SceneKey scene ) { ArrayList items = new ArrayList<>(); ArrayList itemBands = new ArrayList<>(); @@ -278,7 +370,9 @@ final class UnlockedLayoutPlanner { } else if (band.kind == LayoutItemKind.CHIP || band.kind == LayoutItemKind.CLOCK || band.kind == LayoutItemKind.CARRIER) { - band.applyPackedContinuousWidth(Math.round((float) item.width()), packedAnchorSide(band)); + band.applyPackedContinuousWidth( + Math.round((float) item.width()), + packedContinuousClipSide(band)); } band.place(Math.round((float) item.x)); } @@ -307,6 +401,17 @@ final class UnlockedLayoutPlanner { } } + private void clearDebugContainerBounds(ViewGroup... hosts) { + if (hosts == null) { + return; + } + for (ViewGroup host : hosts) { + if (host != null) { + host.setTag(R.id.sbt_debug_container_bounds, null); + } + } + } + private Bounds relativeCutout(Bounds cutoutBounds, LayoutEdges edges) { if (cutoutBounds == null || edges == null) { return null; @@ -321,7 +426,7 @@ final class UnlockedLayoutPlanner { private PackedStatusBarLayoutSolver.LayoutItem packedItemForBand(UnlockedLayoutBand band, int index) { ArrayList boxes = new ArrayList<>(); - for (Bounds box : band.collisionBoxes()) { + for (Bounds box : band.solverCollisionBoxes()) { boxes.add(new PackedStatusBarLayoutSolver.Box(box.left, box.top, box.width, box.height)); } if (boxes.isEmpty()) { @@ -336,12 +441,18 @@ final class UnlockedLayoutPlanner { if ((band.kind == LayoutItemKind.NOTIFICATIONS || band.kind == LayoutItemKind.STATUS) && !isFixedDotBand(band)) { item.iconGroup = packedKind(band.kind).toLowerCase(java.util.Locale.ROOT); - item.iconCount = band.rawPlacements != null ? band.rawPlacements.size() : 0; + item.iconCount = band.forceOverflowDotOnly + ? 1 + : band.rawPlacements != null ? band.rawPlacements.size() : 0; item.visibleIcons = item.iconCount; item.poolRemaining = item.iconCount; - item.iconWidth = representativeIconWidth(band); - item.iconWidths = iconWidths(band); - item.dotWidth = Math.max(1, Math.round(band.height() * 0.75f)); + item.iconWidth = band.forceOverflowDotOnly + ? overflowDotCollisionWidth(band) + : representativeIconWidth(band); + item.iconWidths = band.forceOverflowDotOnly + ? singleWidth(item.iconWidth) + : iconWidths(band); + item.dotWidth = overflowDotCollisionWidth(band); item.compactDotWidth = representativeCompactDotWidth(band); item.allowZero = false; item.allowDot = true; @@ -354,6 +465,16 @@ final class UnlockedLayoutPlanner { return item; } + private int overflowDotCollisionWidth(UnlockedLayoutBand band) { + return Math.max(1, Math.round(Math.max(1, band.height()) * 0.75f)); + } + + private ArrayList singleWidth(int width) { + ArrayList widths = new ArrayList<>(); + widths.add(Math.max(1, width)); + return widths; + } + private int representativeIconWidth(UnlockedLayoutBand band) { ArrayList placements = band.rawPlacements; if (placements == null || placements.isEmpty()) { @@ -501,6 +622,13 @@ final class UnlockedLayoutPlanner { return band.middleSide; } + private AnchorSide packedContinuousClipSide(UnlockedLayoutBand band) { + if (band.position == LayoutPosition.MIDDLE && band.splitRegion == SplitRegion.NONE) { + return band.middleSide == AnchorSide.RIGHT ? AnchorSide.LEFT : AnchorSide.RIGHT; + } + return packedAnchorSide(band); + } + private boolean clockEnabledForScene(SbtSettings settings, SceneKey scene) { if (settings == null || scene == null) { return false; @@ -541,7 +669,9 @@ final class UnlockedLayoutPlanner { : collectedIcons.carrierView.getMeasuredHeight(); int baseline = collectedIcons.carrierView.getBaseline() > 0 ? collectedIcons.carrierView.getBaseline() - : Math.max(0, ((sourceHeight > 0 ? sourceHeight : anchorBounds.height) * 3) / 4); + : ClockLayout.centeredTextBaseline( + collectedIcons.carrierView, + sourceHeight > 0 ? sourceHeight : anchorBounds.height); int verticalOffset = offsetScaler.clock( settings.layoutCarrierVerticalOffsetPx, collectedIcons.carrierView); @@ -555,6 +685,9 @@ final class UnlockedLayoutPlanner { if (scene != null && scene.isLockscreen()) { return settings.layoutStatusEnabledLockscreen ? settings.layoutStatusLockCount : 0; } + if (scene != null && scene.isAod()) { + return settings.layoutStatusEnabledAod ? settings.layoutStatusAodCount : 0; + } return settings.layoutStatusUnlockedCount; } @@ -569,79 +702,103 @@ final class UnlockedLayoutPlanner { } return settings.layoutNotifEnabledLockscreen ? settings.layoutNotifLockCount : 0; } + if (scene != null && scene.isAod()) { + NotificationDisplayStyle style = scene.notificationDisplayStyle(); + if (style == NotificationDisplayStyle.DOT) { + return settings.layoutNotifEnabledAod ? 1 : 0; + } + if (style != NotificationDisplayStyle.ICONS) { + return 0; + } + return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0; + } return settings.layoutNotifUnlockedCount; } + private boolean isDotNotificationScene(SceneKey scene) { + return scene != null + && (scene.isLockscreen() || scene.isAod()) + && scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT; + } + private String[] statusPositionsForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutStatusLockPositions - : settings.layoutStatusPositions; + if (scene != null && scene.isLockscreen()) { + return settings.layoutStatusLockPositions; + } + if (scene != null && scene.isAod()) { + return settings.layoutStatusAodPositions; + } + return settings.layoutStatusPositions; } private String[] statusMiddleSidesForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutStatusLockMiddleSides - : settings.layoutStatusMiddleSides; + if (scene != null && scene.isLockscreen()) { + return settings.layoutStatusLockMiddleSides; + } + if (scene != null && scene.isAod()) { + return settings.layoutStatusAodMiddleSides; + } + return settings.layoutStatusMiddleSides; } private boolean[] statusAutoNearestForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutStatusLockMiddleAutoNearestSides - : settings.layoutStatusMiddleAutoNearestSides; + if (scene != null && scene.isLockscreen()) { + return settings.layoutStatusLockMiddleAutoNearestSides; + } + if (scene != null && scene.isAod()) { + return settings.layoutStatusAodMiddleAutoNearestSides; + } + return settings.layoutStatusMiddleAutoNearestSides; } private int[] statusVerticalOffsetsForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutStatusLockVerticalOffsetsPx - : settings.layoutStatusVerticalOffsetsPx; + if (scene != null && scene.isLockscreen()) { + return settings.layoutStatusLockVerticalOffsetsPx; + } + if (scene != null && scene.isAod()) { + return settings.layoutStatusAodVerticalOffsetsPx; + } + return settings.layoutStatusVerticalOffsetsPx; } private String[] notificationPositionsForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutNotifLockPositions - : settings.layoutNotifPositions; + if (scene != null && scene.isLockscreen()) { + return settings.layoutNotifLockPositions; + } + if (scene != null && scene.isAod()) { + return settings.layoutNotifAodPositions; + } + return settings.layoutNotifPositions; } private String[] notificationMiddleSidesForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutNotifLockMiddleSides - : settings.layoutNotifMiddleSides; + if (scene != null && scene.isLockscreen()) { + return settings.layoutNotifLockMiddleSides; + } + if (scene != null && scene.isAod()) { + return settings.layoutNotifAodMiddleSides; + } + return settings.layoutNotifMiddleSides; } private boolean[] notificationAutoNearestForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutNotifLockMiddleAutoNearestSides - : settings.layoutNotifMiddleAutoNearestSides; + if (scene != null && scene.isLockscreen()) { + return settings.layoutNotifLockMiddleAutoNearestSides; + } + if (scene != null && scene.isAod()) { + return settings.layoutNotifAodMiddleAutoNearestSides; + } + return settings.layoutNotifMiddleAutoNearestSides; } private int[] notificationVerticalOffsetsForScene(SbtSettings settings, SceneKey scene) { - return scene != null && scene.isLockscreen() - ? settings.layoutNotifLockVerticalOffsetsPx - : settings.layoutNotifVerticalOffsetsPx; - } - - private ArrayList dotOnlyPlacements(ArrayList source) { - ArrayList result = new ArrayList<>(); - if (source == null || source.isEmpty()) { - return result; + if (scene != null && scene.isLockscreen()) { + return settings.layoutNotifLockVerticalOffsetsPx; } - SnapshotPlacement first = source.get(0); - int height = Math.max(1, first.bounds.height); - int width = Math.max(1, Math.round(height * 0.75f)); - int color = SnapshotAppearanceSignature.preferredColor( - first.source != null ? first.source.view() : null, - android.graphics.Color.WHITE); - result.add(new SnapshotPlacement( - null, - new Bounds(0, first.bounds.top, width, height), - SnapshotKeys.OVERFLOW_DOT + ":lockscreen_notifications", - false, - true, - color, - null, - new Bounds(0, 0, width, height), - 0)); - return result; + if (scene != null && scene.isAod()) { + return settings.layoutNotifAodVerticalOffsetsPx; + } + return settings.layoutNotifVerticalOffsetsPx; } private boolean isFixedDotBand(UnlockedLayoutBand band) { @@ -651,6 +808,47 @@ final class UnlockedLayoutPlanner { && band.rawPlacements.get(0).overflowDot; } + private Bounds chipBaselineBounds( + View root, + RootAnchors anchors, + SbtSettings settings, + SceneKey scene, + int statusbarHeight, + Bounds notificationRowBounds + ) { + if (scene == null || !scene.isUnlocked() || settings == null) { + return null; + } + if (notificationRowBounds != null && notificationRowBounds.height > 0) { + return new Bounds(0, notificationRowBounds.top, 0, notificationRowBounds.height); + } + Bounds statusBounds = statusIconContainerBounds(root, anchors); + if (statusBounds == null || statusBounds.top < 0) { + return null; + } + int rowHeight = iconHeightPx(statusbarHeight, notificationIconHeightSteps(settings, scene)); + return new Bounds(0, statusBounds.top, 0, rowHeight); + } + + private Bounds placementBounds(ArrayList placements) { + if (placements == null || placements.isEmpty()) { + return null; + } + Bounds result = null; + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement != null ? placement.bounds : null; + if (bounds == null || bounds.width <= 0 || bounds.height <= 0) { + continue; + } + result = result == null ? bounds : result.union(bounds); + } + return result; + } + + private int logicalRowTop(int heightSteps, int verticalOffsetSteps) { + return 100 - Math.max(0, heightSteps) - verticalOffsetSteps; + } + private ArrayList offsetPlacements(ArrayList placements, int verticalOffsetPx) { if (placements == null || placements.isEmpty() || verticalOffsetPx == 0) { return placements; @@ -668,7 +866,319 @@ final class UnlockedLayoutPlanner { placement.overflowDotColor, placement.heldSignal, placement.contentBounds, - placement.sourceOffsetX)); + placement.sourceOffsetX, + placement.sourceRenderBounds)); + } + return result; + } + + private ArrayList bottomAlignPlacements( + ArrayList placements, + Bounds baselineBounds + ) { + if (placements == null || placements.isEmpty() || baselineBounds == null) { + return placements; + } + int targetBottom = baselineBounds.top + baselineBounds.height; + ArrayList result = new ArrayList<>(); + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement != null ? placement.bounds : null; + Bounds collision = relativeCollisionBounds(placement); + if (bounds == null || collision == null || collision.height <= 0) { + continue; + } + int top = targetBottom - collision.top - collision.height; + result.add(new SnapshotPlacement( + placement.source, + new Bounds(bounds.left, top, bounds.width, bounds.height), + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + placement.contentBounds, + placement.sourceOffsetX, + placement.sourceRenderBounds)); + } + return result; + } + + private Bounds relativeCollisionBounds(SnapshotPlacement placement) { + if (placement == null || placement.bounds == null) { + return null; + } + Bounds content = placement.contentBounds; + if (content != null && content.width >= 0 && content.height > 0) { + return content; + } + return new Bounds(0, 0, placement.bounds.width, placement.bounds.height); + } + + private ArrayList scaleChipPlacementsToHeight( + ArrayList placements, + int targetContentHeight + ) { + if (placements == null || placements.isEmpty() || targetContentHeight <= 0) { + return placements; + } + ArrayList result = new ArrayList<>(); + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement != null ? placement.bounds : null; + if (bounds == null || bounds.height <= 0) { + continue; + } + Bounds content = usableContentBounds(placement); + float scale = (float) targetContentHeight / content.height; + int width = Math.max(1, Math.round(bounds.width * scale)); + int height = Math.max(1, Math.round(bounds.height * scale)); + Bounds scaledContent = scaleBounds(placement.contentBounds, scale); + int bottom = bounds.top + content.top + content.height; + int scaledContentBottom = scaledContent != null + ? scaledContent.top + scaledContent.height + : height; + Bounds resized = new Bounds(bounds.left, bottom - scaledContentBottom, width, height); + result.add(new SnapshotPlacement( + placement.source, + resized, + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + scaledContent, + Math.round(placement.sourceOffsetX * scale), + bounds)); + } + return result; + } + + private Bounds usableContentBounds(SnapshotPlacement placement) { + Bounds bounds = placement != null ? placement.bounds : null; + Bounds content = placement != null ? placement.contentBounds : null; + if (content != null && content.height > 0 && content.width >= 0) { + return content; + } + return new Bounds(0, 0, Math.max(1, bounds != null ? bounds.width : 1), + Math.max(1, bounds != null ? bounds.height : 1)); + } + + private ArrayList resizePlacementsToHeight( + ArrayList placements, + int targetHeight + ) { + if (placements == null || placements.isEmpty() || targetHeight <= 0) { + return placements; + } + ArrayList result = new ArrayList<>(); + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement != null ? placement.bounds : null; + if (bounds == null || bounds.height <= 0) { + continue; + } + int width = Math.max(1, Math.round((float) bounds.width * targetHeight / bounds.height)); + int bottom = bounds.top + bounds.height; + Bounds resized = new Bounds(bounds.left, bottom - targetHeight, width, targetHeight); + result.add(new SnapshotPlacement( + placement.source, + resized, + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + scaleBounds(placement.contentBounds, bounds, resized), + Math.round((float) placement.sourceOffsetX * targetHeight / bounds.height), + sourceRenderBoundsForResize(placement))); + } + return result; + } + + private Bounds sourceRenderBoundsForResize(SnapshotPlacement placement) { + if (placement == null || placement.source == null || placement.bounds == null) { + return placement != null ? placement.sourceRenderBounds : null; + } + if (placement.sourceRenderBounds != null) { + return placement.sourceRenderBounds; + } + return placement.source.mode() == SnapshotMode.VIEW ? placement.bounds : null; + } + + private Bounds scaleBounds(Bounds contentBounds, float scale) { + if (contentBounds == null || scale <= 0f) { + return null; + } + return new Bounds( + Math.round(contentBounds.left * scale), + Math.round(contentBounds.top * scale), + Math.max(0, Math.round(contentBounds.width * scale)), + Math.max(1, Math.round(contentBounds.height * scale))); + } + + private Bounds scaleBounds(Bounds contentBounds, Bounds oldBounds, Bounds newBounds) { + if (contentBounds == null || oldBounds == null || newBounds == null || oldBounds.height <= 0) { + return null; + } + float scale = (float) newBounds.height / oldBounds.height; + return new Bounds( + Math.round(contentBounds.left * scale), + Math.round(contentBounds.top * scale), + Math.max(1, Math.round(contentBounds.width * scale)), + Math.max(1, Math.round(contentBounds.height * scale))); + } + + private int stepsToPx(int steps, int statusbarHeight) { + if (steps == 0 || statusbarHeight <= 0) { + return 0; + } + return Math.round((float) steps * statusbarHeight / 100f); + } + + private int iconHeightPx(int statusbarHeight, int steps) { + int height = Math.round((float) Math.max(1, statusbarHeight) * Math.max(1, steps) / 100f); + return Math.max(1, height); + } + + private int statusIconHeightSteps(SbtSettings settings, SceneKey scene) { + if (scene != null && scene.isLockscreen()) { + return settings.layoutStatusLockIconHeightSteps; + } + if (scene != null && scene.isAod()) { + return settings.layoutStatusAodIconHeightSteps; + } + return settings.layoutStatusIconHeightSteps; + } + + private int notificationIconHeightSteps(SbtSettings settings, SceneKey scene) { + if (scene != null && scene.isLockscreen()) { + return settings.layoutNotifLockIconHeightSteps; + } + if (scene != null && scene.isAod()) { + return settings.layoutNotifAodIconHeightSteps; + } + return settings.layoutNotifIconHeightSteps; + } + + private int statusbarHeight( + View root, + RootAnchors anchors, + SceneKey scene, + Bounds aodStatusContentBounds + ) { + int sharedHeight = StatusbarHeightStore.get(root); + if (sharedHeight > 0) { + return sharedHeight; + } + Bounds statusBounds = StatusbarHeightSource.bounds(root, scene, anchors); + if (statusBounds != null && statusBounds.top >= 0 && statusBounds.height > 0) { + int height = Math.max(1, statusBounds.top + statusBounds.height); + if (scene != null && scene.isAod() && root != null) { + Integer stored = aodInitialStatusbarHeightByRoot.get(root); + if (stored != null && stored > 0) { + return stored; + } + aodInitialStatusbarHeightByRoot.put(root, height); + } else { + StatusbarHeightStore.put(root, height); + } + return height; + } + if (scene != null && scene.isAod() && root != null) { + Integer stored = aodInitialStatusbarHeightByRoot.get(root); + if (stored != null && stored > 0) { + return stored; + } + } + if (aodStatusContentBounds != null + && aodStatusContentBounds.top >= 0 + && aodStatusContentBounds.height > 0) { + int height = Math.max(1, aodStatusContentBounds.top + aodStatusContentBounds.height); + if (scene == null || !scene.isAod()) { + StatusbarHeightStore.put(root, height); + } + return height; + } + return Math.max(1, root != null ? root.getHeight() : 1); + } + + private Bounds statusIconContainerBounds(View root, RootAnchors anchors) { + if (root == null || anchors == null) { + return null; + } + Bounds statusIcons = ViewGeometry.boundsRelativeTo(root, anchors.statusIcons); + if (statusIcons != null && statusIcons.height > 0) { + return statusIcons; + } + return statusUnion(root, anchors); + } + + private ArrayList bottomAlignAodPlacements( + ArrayList placements, + Bounds aodStatusbarBounds + ) { + return bottomAlignAodPlacements(placements, aodStatusbarBounds, 0); + } + + private ArrayList bottomAlignAodPlacements( + ArrayList placements, + Bounds aodStatusbarBounds, + int bottomInsetPx + ) { + if (placements == null || placements.isEmpty() || aodStatusbarBounds == null) { + return placements; + } + int bottom = aodStatusbarBounds.top + + aodStatusbarBounds.height + - Math.max(0, bottomInsetPx); + ArrayList result = new ArrayList<>(); + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement.bounds; + if (bounds == null) { + continue; + } + result.add(new SnapshotPlacement( + placement.source, + new Bounds(bounds.left, bottom - bounds.height, bounds.width, bounds.height), + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + placement.contentBounds, + placement.sourceOffsetX, + placement.sourceRenderBounds)); + } + return result; + } + + private ArrayList alignAodNotificationPlacements( + ArrayList placements, + Bounds notificationBounds, + Bounds fallbackStatusbarBounds + ) { + if (placements == null || placements.isEmpty()) { + return placements; + } + if (notificationBounds == null || notificationBounds.height <= 0) { + return bottomAlignAodPlacements(placements, fallbackStatusbarBounds); + } + ArrayList result = new ArrayList<>(); + int bottom = notificationBounds.top + notificationBounds.height; + for (SnapshotPlacement placement : placements) { + Bounds bounds = placement.bounds; + if (bounds == null) { + continue; + } + result.add(new SnapshotPlacement( + placement.source, + new Bounds(bounds.left, bottom - bounds.height, bounds.width, bounds.height), + placement.key, + placement.signal, + placement.overflowDot, + placement.overflowDotColor, + placement.heldSignal, + placement.contentBounds, + placement.sourceOffsetX, + placement.sourceRenderBounds)); } return result; } @@ -723,9 +1233,25 @@ final class UnlockedLayoutPlanner { private LayoutEdges layoutEdges( View root, SbtSettings settings, - RootAnchors anchors + RootAnchors anchors, + SceneKey scene, + Bounds aodStatusAnchorBounds ) { int rootWidth = root != null ? root.getWidth() : 0; + if (scene != null && scene.isAod()) { + LayoutEdges aodEdges = aodBurnInEdges( + root, + settings, + anchors, + scene, + rootWidth, + aodStatusAnchorBounds); + LayoutEdges stableEdges = stableAodEdges(rootWidth, aodEdges); + if (stableEdges != null) { + return stableEdges; + } + return aodEdges != null ? aodEdges : new LayoutEdges(0, 0); + } StockInsets stockInsets = stockInsets(root, rootWidth, anchors); int left = settings.layoutPaddingLeftPx; int right = rootWidth - settings.layoutPaddingRightPx; @@ -735,7 +1261,192 @@ final class UnlockedLayoutPlanner { if (settings.layoutPaddingRightAddStock) { right -= stockInsets.right; } - return new LayoutEdges(left, right); + LayoutEdges edges = new LayoutEdges(left, right); + rememberNonAodEdges(rootWidth, scene, edges); + return edges; + } + + private LayoutEdges stableAodEdges(int rootWidth, LayoutEdges aodEdges) { + if (rootWidth <= 0) { + return aodEdges; + } + LayoutEdges stableEdges = recentNonAodEdgesByRootWidth.get(rootWidth); + if (stableEdges == null || stableEdges.left() < 0 || stableEdges.left() >= rootWidth) { + return aodEdges; + } + int right = aodEdges != null ? aodEdges.right() : stableEdges.right(); + right = Math.max(stableEdges.left() + 1, Math.min(rootWidth, right)); + return new LayoutEdges(stableEdges.left(), right); + } + + private void rememberNonAodEdges(int rootWidth, SceneKey scene, LayoutEdges edges) { + if (rootWidth <= 0 + || scene == null + || scene.isAod() + || edges == null + || edges.right() <= edges.left()) { + return; + } + recentNonAodEdgesByRootWidth.put(rootWidth, edges); + while (recentNonAodEdgesByRootWidth.size() > 4) { + Integer oldestWidth = recentNonAodEdgesByRootWidth.keySet().iterator().next(); + recentNonAodEdgesByRootWidth.remove(oldestWidth); + } + } + + private LayoutEdges aodBurnInEdges( + View root, + SbtSettings settings, + RootAnchors anchors, + SceneKey scene, + int rootWidth, + Bounds statusBounds + ) { + if (root == null || settings == null || rootWidth <= 0) { + return null; + } + Bounds notificationBounds = aodNotificationBounds(root, anchors, scene); + Bounds bounds = aodStatusbarBounds(root, settings, notificationBounds, statusBounds); + if (bounds == null) { + return null; + } + return new LayoutEdges(bounds.left, bounds.left + bounds.width); + } + + private Bounds aodStatusbarBounds( + View root, + SbtSettings settings, + Bounds notificationBounds, + Bounds statusBounds + ) { + if (root == null || settings == null || root.getWidth() <= 0) { + return null; + } + boolean hasNotification = isUsableAodAnchor(notificationBounds); + boolean hasStatus = isUsableAodAnchor(statusBounds); + if (!hasNotification && !hasStatus) { + return null; + } + int left = hasNotification ? notificationBounds.left : 0; + int right = hasStatus ? statusBounds.left + statusBounds.width : root.getWidth(); + left += settings.layoutPaddingLeftPx; + right -= settings.layoutPaddingRightPx; + int notificationBottom = hasNotification + ? notificationBounds.top + notificationBounds.height + : 0; + int statusBottom = hasStatus + ? statusBounds.top + statusBounds.height + : 0; + int bottom = Math.max(notificationBottom, statusBottom); + if (right <= left || bottom <= 0) { + return null; + } + return new Bounds(left, 0, right - left, bottom); + } + + private Bounds aodStatusAnchorBounds( + View root, + RootAnchors anchors, + SceneKey scene + ) { + if (root == null || root.getWidth() <= 0) { + return null; + } + Bounds notificationBounds = aodNotificationBounds(root, anchors, scene); + Bounds liveMovingStatusBounds = AodLiveStatusbarSource.movingBoundsForRoot(root); + Bounds storedStatusBounds = AodStatusbarSourceStore.statusBoundsForRoot( + root, + aodNotificationContainer(root, scene)); + if (isPlausibleAodStatusAnchor(root, liveMovingStatusBounds, notificationBounds)) { + return adjustedAodStatusAnchorBounds(root, liveMovingStatusBounds, storedStatusBounds); + } + if (isPlausibleAodStatusAnchor(root, storedStatusBounds, notificationBounds)) { + return storedStatusBounds; + } + return null; + } + + private Bounds aodStatusContentBounds(View root, Bounds fallbackBounds) { + Bounds liveContentBounds = AodLiveStatusbarSource.movingIconContentBoundsForRoot(root); + if (isUsableAodAnchor(liveContentBounds)) { + return liveContentBounds; + } + return fallbackBounds; + } + + private Bounds adjustedAodStatusAnchorBounds( + View root, + Bounds liveMovingStatusBounds, + Bounds storedStatusBounds + ) { + if (root == null || liveMovingStatusBounds == null) { + return liveMovingStatusBounds; + } + Integer inset = aodStatusRightInsetByRoot.get(root); + if (isUsableAodAnchor(storedStatusBounds)) { + int liveRight = liveMovingStatusBounds.left + liveMovingStatusBounds.width; + int storedRight = storedStatusBounds.left + storedStatusBounds.width; + int measuredInset = storedRight - liveRight; + if (Math.abs(measuredInset) <= 8) { + inset = measuredInset; + aodStatusRightInsetByRoot.put(root, inset); + } + } + if (inset == null || inset == 0) { + return liveMovingStatusBounds; + } + int adjustedWidth = liveMovingStatusBounds.width + inset; + if (adjustedWidth <= 0) { + return liveMovingStatusBounds; + } + return new Bounds( + liveMovingStatusBounds.left, + liveMovingStatusBounds.top, + adjustedWidth, + liveMovingStatusBounds.height); + } + + private Bounds aodNotificationBounds(View root, RootAnchors anchors, SceneKey scene) { + if (scene != null + && scene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) { + return null; + } + return LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForAodIconsRoot(root); + } + + private ViewGroup aodNotificationContainer(View root, SceneKey scene) { + if (scene != null + && scene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS) { + return LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); + } + return LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root); + } + + private boolean isPlausibleAodStatusAnchor( + View root, + Bounds bounds, + Bounds notificationBounds + ) { + if (root == null || !isUsableAodAnchor(bounds)) { + return false; + } + int right = bounds.left + bounds.width; + int bottom = bounds.top + bounds.height; + if (bounds.left < 0 + || right > root.getWidth() + || bounds.top < 0 + || bottom > root.getHeight()) { + return false; + } + if (isUsableAodAnchor(notificationBounds) + && bounds.left < notificationBounds.left + notificationBounds.width) { + return false; + } + return true; + } + + private boolean isUsableAodAnchor(Bounds bounds) { + return bounds != null && bounds.width > 0 && bounds.height > 0; } private StockInsets stockInsets( @@ -850,6 +1561,14 @@ final class UnlockedLayoutPlanner { return new Bounds(left, cutout.top, Math.max(0, right - left), cutout.height()); } + private static Bounds aodCutoutBounds(View root, Rect cutout, int cutoutGap) { + Bounds bounds = cutoutBounds(cutout, cutoutGap); + if (bounds == null || root == null) { + return bounds; + } + return new Bounds(bounds.left, 0, bounds.width, Math.max(root.getHeight(), bounds.height)); + } + static LayoutPosition positionFor(String value) { if ("right".equals(value)) { return LayoutPosition.RIGHT; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java index c840299..0eebace 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java @@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.runtime.render; import android.content.res.Resources; import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; @@ -9,20 +10,36 @@ import android.widget.TextView; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.WeakHashMap; +import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; +import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class UnlockedStockSourceCollector { private final WeakHashMap anchorsByRoot = new WeakHashMap<>(); + UnlockedStockSourceCollector() { + this(null); + } + + UnlockedStockSourceCollector(RuntimeContext runtimeContext) { + } + void clear() { anchorsByRoot.clear(); } + void clearRoot(View root) { + if (root != null) { + anchorsByRoot.remove(root); + } + } + RootAnchors anchorsFor(View root) { RootAnchors cached = anchorsByRoot.get(root); if (cached != null && cached.isUsable()) { @@ -37,21 +54,40 @@ final class UnlockedStockSourceCollector { return anchors; } - CollectedIcons collect(View root, RootAnchors anchors) { + CollectedIcons collect(View root, RootAnchors anchors, SceneKey scene) { ArrayList statusIcons = new ArrayList<>(); ArrayList notificationIcons = new ArrayList<>(); ArrayList statusChips = new ArrayList<>(); ArrayList statusChipMetrics = new ArrayList<>(); - if (anchors.statusRoot != null) { - collectStatusSnapshotViews(anchors.statusRoot, statusIcons); + if (scene != null && scene.isAod()) { + collectAodStatusSnapshotViews(root, statusIcons, scene); + } + if (statusIcons.isEmpty()) { + if (anchors.statusRoot != null) { + collectStatusSnapshotViews(anchors.statusRoot, statusIcons); + } else if (scene != null && scene.isAod()) { + collectStatusSnapshotViews(root, statusIcons); + } + } + if (scene == null || !scene.isAod()) { + mergeActiveHiddenStatusSources(root, anchors.statusIcons, statusIcons); } if (anchors.battery != null && !isDescendantOf(anchors.battery, anchors.statusRoot)) { collectStatusSnapshotViews(anchors.battery, statusIcons); } - if (anchors.notificationRoot != null) { + normalizeStatusIconSourceBounds(root, statusIcons); + if (scene != null && scene.isAod()) { + if (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS + || scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) { + notificationIcons.addAll( + LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root)); + } + } else if (anchors.notificationRoot != null) { collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons); } - collectStatusChipSnapshotViews(root, statusChips, statusChipMetrics); + if (scene == null || scene.isUnlocked()) { + collectStatusChipSnapshotViews(root, statusChips, statusChipMetrics); + } return new CollectedIcons( anchors.clockView, statusIcons, @@ -61,6 +97,53 @@ final class UnlockedStockSourceCollector { anchors.carrierView); } + private void collectAodStatusSnapshotViews( + View root, + ArrayList out, + SceneKey scene + ) { + if (root == null || out == null) { + return; + } + boolean iconMode = scene != null + && (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS + || scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT); + View excludedNotificationContainer = iconMode + ? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root) + : LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); + for (View source : AodStatusbarSourceStore.statusSourcesForRoot( + root, + excludedNotificationContainer)) { + collectStatusSnapshotViews(source, out, true); + } + } + + private String reflectString(View view, String methodName, String... fieldNames) { + Object methodValue = ReflectionSupport.invokeMethod(view, methodName, new Class[0]); + if (methodValue != null) { + return safeText(String.valueOf(methodValue)); + } + if (fieldNames != null) { + for (String fieldName : fieldNames) { + Object fieldValue = ReflectionSupport.getFieldValue(view, fieldName); + if (fieldValue != null) { + return safeText(String.valueOf(fieldValue)); + } + } + } + return ""; + } + + private String safeText(Object value) { + if (value == null) { + return ""; + } + return String.valueOf(value) + .replace('\n', ' ') + .replace('\r', ' ') + .replace(';', ','); + } + CollectedIcons collectChips( View root, RootAnchors anchors, @@ -89,9 +172,17 @@ final class UnlockedStockSourceCollector { RootAnchors anchors, CollectedIcons previousIcons ) { - int chipSourceSignature = settings.layoutChipEnabledUnlocked + int chipSourceSignature = scene != null + && scene.isUnlocked() + && settings.layoutChipEnabledUnlocked ? chipSourceSignature(root, previousIcons) : 0; + int statusSourceSignature = sourceLayoutListSignature( + previousIcons != null ? previousIcons.statusIcons : null); + if (scene != null && scene.isAod()) { + statusSourceSignature = 31 * statusSourceSignature + + aodLayoutAnchorSignature(root, anchors, scene); + } return new LayoutInputSignature( scene != null ? scene.toString() : "", root.getWidth(), @@ -102,7 +193,7 @@ final class UnlockedStockSourceCollector { cutout != null ? cutout.top : Integer.MIN_VALUE, cutout != null ? cutout.right : Integer.MIN_VALUE, cutout != null ? cutout.bottom : Integer.MIN_VALUE, - layoutSettingsSignature(settings), + loggingAwareSettingsSignature(root, settings), clockTimeSignature(settings), viewSignatureOrZero(anchors != null ? anchors.clockView : null), shallowTreeSignature(anchors != null ? anchors.clockContainer : null), @@ -120,11 +211,36 @@ final class UnlockedStockSourceCollector { anchors != null ? anchors.clockContainer : null), notificationRootSignature(anchors != null ? anchors.notificationRoot : null), viewSignatureOrZero(anchors != null ? anchors.carrierView : null), - sourceLayoutListSignature(previousIcons != null ? previousIcons.statusIcons : null), + statusSourceSignature, sourceLayoutListSignature(previousIcons != null ? previousIcons.notificationIcons : null), chipSourceSignature); } + private int aodLayoutAnchorSignature(View root, RootAnchors anchors, SceneKey scene) { + if (root == null) { + return 0; + } + boolean iconMode = scene != null + && (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS + || scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT); + View notificationContainer = iconMode + ? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root) + : LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); + Bounds notificationBounds = iconMode + ? LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForAodIconsRoot(root) + : LockscreenCardsNotificationIconSourceStore.sourceContainerBoundsForRoot(root); + if (!iconMode && notificationBounds == null) { + notificationBounds = ViewGeometry.boundsRelativeTo( + root, + anchors != null ? anchors.notificationRoot : null); + } + Bounds statusBounds = AodStatusbarSourceStore.statusBoundsForRoot(root, notificationContainer); + int result = 17; + result = 31 * result + boundsSignature(notificationBounds); + result = 31 * result + boundsSignature(statusBounds); + return result; + } + private void collectKnownStatusChipSnapshotViews( View root, CollectedIcons previousIcons, @@ -136,8 +252,7 @@ final class UnlockedStockSourceCollector { } if (previousIcons.statusChips != null) { for (SnapshotSource source : previousIcons.statusChips) { - View view = source.view(); - if (isStatusChipSnapshotCandidate(root, view)) { + if (isKnownStatusChipSnapshotSource(root, source)) { visibleOut.add(source); metricOut.add(source); return; @@ -146,7 +261,13 @@ final class UnlockedStockSourceCollector { } if (previousIcons.statusChipMetrics != null) { for (SnapshotSource source : previousIcons.statusChipMetrics) { - if (isStatusChipMetricCandidate(root, source.view())) { + if (isKnownStatusChipSnapshotSource(root, source)) { + visibleOut.add(statusChipSource(root, source.view())); + metricOut.add(statusChipSource(root, source.view())); + return; + } + if (isStatusChipMetricCandidate(root, source.view(), true) + && hasVisibleStatusChipContent(source.view())) { metricOut.add(source); return; } @@ -191,7 +312,7 @@ final class UnlockedStockSourceCollector { if (battery == null && idName.equals("battery")) { battery = view; } - if (notificationRoot == null && idName.equals("notificationIcons")) { + if (notificationRoot == null && isNotificationRootCandidate(view, idName)) { notificationRoot = view; } if (carrierView == null && view instanceof TextView textView && isCarrierTextView(textView)) { @@ -252,8 +373,35 @@ final class UnlockedStockSourceCollector { return Integer.MAX_VALUE; } + private boolean isNotificationRootCandidate(View view, String idName) { + if (idName.equals("notificationIcons") + || idName.equals("notification_icononly_container") + || idName.equals("notification_icononly_area") + || idName.equals("keyguard_icononly_container_view") + || idName.equals("common_notification_widget")) { + return true; + } + String className = view != null ? view.getClass().getName() : ""; + return className.contains("NotificationIconsOnlyContainer") + || className.contains("SecShelfNotificationIconContainer"); + } + private void collectStatusSnapshotViews(View source, ArrayList out) { + collectStatusSnapshotViews(source, out, false); + } + + private void collectStatusSnapshotViews( + View source, + ArrayList out, + boolean includeHidden + ) { + if (isOwnedRuntimeView(source)) { + return; + } boolean candidate = isStatusSnapshotCandidate(source); + if (includeHidden) { + candidate = isStatusSnapshotCandidate(source, true); + } if (isModernSignalContainer(source)) { if (candidate) { out.add(new SnapshotSource(source, SnapshotMode.VIEW)); @@ -268,11 +416,269 @@ final class UnlockedStockSourceCollector { } if (source instanceof ViewGroup group) { for (int i = 0; i < group.getChildCount(); i++) { - collectStatusSnapshotViews(group.getChildAt(i), out); + collectStatusSnapshotViews(group.getChildAt(i), out, includeHidden); } } } + private void mergeActiveHiddenStatusSources( + View root, + View statusIcons, + ArrayList out + ) { + if (root == null || statusIcons == null || out == null) { + return; + } + ArrayList activeSlots = StatusIconSlotVisibilityStore.activeSlotList(); + if (activeSlots.isEmpty()) { + return; + } + LinkedHashMap existingBySlot = new LinkedHashMap<>(); + for (SnapshotSource source : out) { + if (source != null && isDescendantOf(source.view(), statusIcons)) { + String slot = reflectString(source.view(), "getSlot", "mSlot", "slot"); + if (!slot.isEmpty()) { + existingBySlot.put(slot, source); + } + } + } + LinkedHashMap childBySlot = new LinkedHashMap<>(); + for (View child : directChildren(statusIcons)) { + String slot = reflectString(child, "getSlot", "mSlot", "slot"); + if (!slot.isEmpty() && isDrawableStatusIconView(child)) { + childBySlot.put(slot, child); + } + } + ArrayList mergedStatusIcons = new ArrayList<>(); + int syntheticIndex = 0; + Bounds metricBounds = representativeStatusIconBounds(root, statusIcons, out); + int size = Math.max(1, metricBounds.height); + for (String slot : activeSlots) { + SnapshotSource existing = existingBySlot.get(slot); + if (existing != null) { + SnapshotSource ordered = orderedStatusSource( + root, + existing, + metricBounds, + syntheticIndex, + size); + mergedStatusIcons.add(ordered); + syntheticIndex++; + continue; + } + View child = childBySlot.get(slot); + if (child == null) { + continue; + } + int width = syntheticStatusIconWidth(child, size); + int height = syntheticStatusIconHeight(child, size); + Bounds bounds = orderedStatusBounds(metricBounds, syntheticIndex, width, height); + mergedStatusIcons.add(new SnapshotSource( + child, + SnapshotMode.VIEW, + slot, + bounds)); + syntheticIndex++; + } + if (mergedStatusIcons.isEmpty()) { + return; + } + ArrayList merged = new ArrayList<>(); + boolean inserted = false; + for (SnapshotSource source : out) { + if (source != null && isDescendantOf(source.view(), statusIcons)) { + if (!inserted) { + merged.addAll(mergedStatusIcons); + inserted = true; + } + continue; + } + merged.add(source); + } + if (!inserted) { + merged.addAll(mergedStatusIcons); + } + out.clear(); + out.addAll(merged); + } + + private SnapshotSource orderedStatusSource( + View root, + SnapshotSource source, + Bounds metricBounds, + int index, + int fallbackSize + ) { + Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, source.view()); + int width = sourceBounds != null && sourceBounds.width > 0 + ? sourceBounds.width + : fallbackSize; + int height = sourceBounds != null && sourceBounds.height > 0 + ? sourceBounds.height + : fallbackSize; + return new SnapshotSource( + source.view(), + source.mode(), + source.keyHint(), + orderedStatusBounds(metricBounds, index, width, height)); + } + + private Bounds orderedStatusBounds( + Bounds metricBounds, + int index, + int width, + int height + ) { + int fallbackHeight = Math.max(1, metricBounds.height); + return new Bounds( + metricBounds.left + Math.max(0, index) * fallbackHeight, + metricBounds.top, + Math.max(1, width), + Math.max(1, height)); + } + + private int syntheticStatusIconWidth(View view, int fallbackSize) { + int width = measuredWidth(view); + if (width > 0) { + return width; + } + int drawableWidth = drawableWidth(view); + if (drawableWidth > 0) { + return normalizedStatusIconWidth(drawableWidth, fallbackSize); + } + return Math.max(1, fallbackSize); + } + + private void normalizeStatusIconSourceBounds(View root, ArrayList sources) { + if (root == null || sources == null || sources.isEmpty()) { + return; + } + for (int i = 0; i < sources.size(); i++) { + SnapshotSource source = sources.get(i); + if (source == null || source.mode() != SnapshotMode.VIEW || !isDrawableStatusIconView(source.view())) { + continue; + } + Bounds bounds = source.boundsOverride() != null + ? source.boundsOverride() + : ViewGeometry.boundsRelativeTo(root, source.view()); + if (bounds == null || bounds.height <= 0) { + continue; + } + int drawableWidth = drawableWidth(source.view()); + if (drawableWidth <= 0) { + continue; + } + int width = normalizedStatusIconWidth(drawableWidth, bounds.height); + if (width == bounds.width) { + continue; + } + sources.set(i, new SnapshotSource( + source.view(), + source.mode(), + source.keyHint(), + new Bounds(bounds.left, bounds.top, width, bounds.height))); + } + } + + private int normalizedStatusIconWidth(int drawableWidth, int iconHeight) { + return Math.max(1, drawableWidth + statusIconHorizontalPadding(iconHeight)); + } + + private int statusIconHorizontalPadding(int iconHeight) { + return Math.max(1, Math.round(Math.max(1, iconHeight) * 0.09f)); + } + + private int drawableWidth(View view) { + Drawable drawable = view instanceof ImageView imageView ? imageView.getDrawable() : null; + if (drawable == null) { + return 0; + } + int width = drawable.getBounds().width(); + if (width > 0) { + return width; + } + return Math.max(0, drawable.getIntrinsicWidth()); + } + + private int syntheticStatusIconHeight(View view, int fallbackSize) { + int height = measuredHeight(view); + return height > 0 ? height : Math.max(1, fallbackSize); + } + + private int measuredWidth(View view) { + if (view == null) { + return 0; + } + if (view.getWidth() > 0) { + return view.getWidth(); + } + if (view.getMeasuredWidth() > 0) { + return view.getMeasuredWidth(); + } + ViewGroup.LayoutParams lp = view.getLayoutParams(); + return lp != null && lp.width > 0 ? lp.width : 0; + } + + private int measuredHeight(View view) { + if (view == null) { + return 0; + } + if (view.getHeight() > 0) { + return view.getHeight(); + } + if (view.getMeasuredHeight() > 0) { + return view.getMeasuredHeight(); + } + ViewGroup.LayoutParams lp = view.getLayoutParams(); + return lp != null && lp.height > 0 ? lp.height : 0; + } + + private ArrayList directChildren(View view) { + ArrayList children = new ArrayList<>(); + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child != null) { + children.add(child); + } + } + } + return children; + } + + private Bounds representativeStatusIconBounds( + View root, + View statusIcons, + ArrayList sources + ) { + if (sources != null) { + for (SnapshotSource source : sources) { + if (source == null || !isDescendantOf(source.view(), statusIcons)) { + continue; + } + Bounds bounds = ViewGeometry.boundsRelativeTo(root, source.view()); + if (bounds != null && bounds.width > 0 && bounds.height > 0) { + int size = Math.max(1, bounds.height); + return new Bounds(bounds.left, bounds.top, size, size); + } + } + } + Bounds containerBounds = ViewGeometry.boundsRelativeTo(root, statusIcons); + if (containerBounds != null && containerBounds.height > 0) { + int size = Math.max(1, containerBounds.height); + return new Bounds(containerBounds.left, containerBounds.top, size, size); + } + return new Bounds(0, 0, 32, 32); + } + + private boolean isDrawableStatusIconView(View view) { + if (view == null || !view.getClass().getName().contains("StatusBarIconView")) { + return false; + } + Drawable drawable = view instanceof ImageView imageView ? imageView.getDrawable() : null; + return drawable != null; + } + private boolean isModernSignalContainer(View view) { String className = view.getClass().getName(); return className.contains("ModernStatusBarWifiView") @@ -304,12 +710,13 @@ final class UnlockedStockSourceCollector { while (!queue.isEmpty()) { View view = queue.removeFirst(); if (isStatusChipSnapshotCandidate(root, view)) { - visibleOut.add(new SnapshotSource(view, SnapshotMode.VIEW)); - metricOut.add(new SnapshotSource(view, SnapshotMode.VIEW)); + SnapshotSource source = statusChipSource(root, view); + visibleOut.add(source); + metricOut.add(source); return; } if (metricOut.isEmpty() && isStatusChipMetricCandidate(root, view)) { - metricOut.add(new SnapshotSource(view, SnapshotMode.VIEW)); + metricOut.add(statusChipSource(root, view)); } if (view instanceof ViewGroup group) { for (int i = 0; i < group.getChildCount(); i++) { @@ -320,9 +727,58 @@ final class UnlockedStockSourceCollector { } private boolean isStatusChipSnapshotCandidate(View root, View view) { + return isStatusChipSnapshotCandidate(root, view, false); + } + + private SnapshotSource statusChipSource(View root, View view) { + Bounds bounds = ViewGeometry.boundsRelativeTo(root, view); + if (bounds == null || bounds.width <= 0 || bounds.height <= 0) { + return new SnapshotSource(view, SnapshotMode.STATUS_CHIP); + } + int measuredWidth = view.getMeasuredWidth(); + int measuredHeight = view.getMeasuredHeight(); + if (measuredWidth > bounds.width || measuredHeight > bounds.height) { + bounds = new Bounds( + bounds.left, + bounds.top, + Math.max(bounds.width, measuredWidth), + Math.max(bounds.height, measuredHeight)); + } + return new SnapshotSource(view, SnapshotMode.STATUS_CHIP, null, bounds); + } + + private boolean isKnownStatusChipSnapshotSource(View root, SnapshotSource source) { + if (source == null || source.mode() != SnapshotMode.STATUS_CHIP || root == null) { + return false; + } + View view = source.view(); + if (view == null + || view == root + || !view.isAttachedToWindow() + || !isDescendantOf(view, root) + || ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false) + || !isStatusChipCandidate(view) + || !hasVisibleStatusChipContent(view)) { + return false; + } + Bounds bounds = source.boundsOverride() != null + ? source.boundsOverride() + : ViewGeometry.boundsRelativeTo(root, view); + return bounds != null + && bounds.width > 0 + && bounds.height > 0 + && bounds.top >= 0 + && bounds.top <= root.getHeight() + && bounds.width < root.getWidth() / 2; + } + + private boolean isStatusChipSnapshotCandidate(View root, View view, boolean allowHidden) { if (view == null || view == root || view.getVisibility() != View.VISIBLE) { return false; } + if (!allowHidden && view.getAlpha() <= 0f && !hasVisibleStatusChipContent(view)) { + return false; + } if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) { return false; } @@ -348,10 +804,46 @@ final class UnlockedStockSourceCollector { return true; } + private boolean hasVisibleStatusChipContent(View source) { + if (!(source instanceof ViewGroup group)) { + return false; + } + ArrayDeque queue = new ArrayDeque<>(); + for (int i = 0; i < group.getChildCount(); i++) { + queue.addLast(group.getChildAt(i)); + } + while (!queue.isEmpty()) { + View view = queue.removeFirst(); + if (view == null) { + continue; + } + if (view.getVisibility() == View.VISIBLE && view.getAlpha() > 0f) { + int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); + int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); + if (width > 0 && height > 0) { + return true; + } + } + if (view instanceof ViewGroup childGroup) { + for (int i = 0; i < childGroup.getChildCount(); i++) { + queue.addLast(childGroup.getChildAt(i)); + } + } + } + return false; + } + private boolean isStatusChipMetricCandidate(View root, View view) { + return isStatusChipMetricCandidate(root, view, false); + } + + private boolean isStatusChipMetricCandidate(View root, View view, boolean allowHidden) { if (view == null || view == root || root == null) { return false; } + if (!allowHidden && view.getAlpha() <= 0f && !hasVisibleStatusChipContent(view)) { + return false; + } int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) { @@ -386,7 +878,14 @@ final class UnlockedStockSourceCollector { } private boolean isStatusSnapshotCandidate(View view) { - if (view.getVisibility() != View.VISIBLE) { + return isStatusSnapshotCandidate(view, false); + } + + private boolean isStatusSnapshotCandidate(View view, boolean includeHidden) { + if (isOwnedRuntimeView(view)) { + return false; + } + if (!includeHidden && view.getVisibility() != View.VISIBLE) { return false; } int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); @@ -424,6 +923,9 @@ final class UnlockedStockSourceCollector { } private boolean isNotificationSnapshotCandidate(View view) { + if (isOwnedRuntimeView(view)) { + return false; + } if (view instanceof TextView) { return false; } @@ -466,6 +968,19 @@ final class UnlockedStockSourceCollector { return false; } + private boolean isOwnedRuntimeView(View view) { + View current = view; + while (current != null) { + Object tag = current.getTag(); + if (tag instanceof String value && value.startsWith("statusbartweak.")) { + return true; + } + Object parent = current.getParent(); + current = parent instanceof View ? (View) parent : null; + } + return false; + } + private static int viewSignatureOrZero(View view) { return view != null ? viewSignature(view) : 0; } @@ -571,9 +1086,6 @@ final class UnlockedStockSourceCollector { hasKnownChipSource = true; result = 31 * result + chipSourceListSignature(previousIcons.statusChipMetrics); } - if (!hasKnownChipSource) { - return 0; - } } if (!hasKnownChipSource) { result = 31 * result + statusChipCandidateSignature(root); @@ -590,7 +1102,7 @@ final class UnlockedStockSourceCollector { queue.add(root); while (!queue.isEmpty()) { View view = queue.removeFirst(); - if (view != root && isStatusChipCandidate(view)) { + if (view != root && isStatusChipCandidate(view) && hasVisibleStatusChipContent(view)) { result = 31 * result + chipViewLayoutSignature(view); } if (view instanceof ViewGroup group) { @@ -602,7 +1114,7 @@ final class UnlockedStockSourceCollector { return result; } - private static int chipSourceListSignature(ArrayList sources) { + private int chipSourceListSignature(ArrayList sources) { if (sources == null || sources.isEmpty()) { return 0; } @@ -610,6 +1122,7 @@ final class UnlockedStockSourceCollector { result = 31 * result + sources.size(); for (SnapshotSource source : sources) { result = 31 * result + source.mode().ordinal(); + result = 31 * result + (hasVisibleStatusChipContent(source.view()) ? 1 : 0); result = 31 * result + chipViewLayoutSignature(source.view()); } return result; @@ -679,6 +1192,18 @@ final class UnlockedStockSourceCollector { return result; } + private static int boundsSignature(Bounds bounds) { + if (bounds == null) { + return 0; + } + int result = 17; + result = 31 * result + bounds.left; + result = 31 * result + bounds.top; + result = 31 * result + bounds.width; + result = 31 * result + bounds.height; + return result; + } + private static int drawableLayoutSignature(android.graphics.drawable.Drawable drawable) { if (drawable == null) { return 0; @@ -761,6 +1286,7 @@ final class UnlockedStockSourceCollector { settings.layoutChipMiddleSide, settings.layoutChipMiddleAutoNearestSide, settings.layoutChipVerticalOffsetPx, + settings.layoutChipHeightSteps, settings.layoutNotifPosition, settings.layoutNotifSortOrder, settings.layoutNotifEnabledUnlocked, @@ -775,6 +1301,12 @@ final class UnlockedStockSourceCollector { java.util.Arrays.hashCode(settings.layoutNotifLockMiddleSides), java.util.Arrays.hashCode(settings.layoutNotifLockMiddleAutoNearestSides), java.util.Arrays.hashCode(settings.layoutNotifLockVerticalOffsetsPx), + settings.layoutNotifEnabledAod, + settings.layoutNotifAodCount, + java.util.Arrays.hashCode(settings.layoutNotifAodPositions), + java.util.Arrays.hashCode(settings.layoutNotifAodMiddleSides), + java.util.Arrays.hashCode(settings.layoutNotifAodMiddleAutoNearestSides), + java.util.Arrays.hashCode(settings.layoutNotifAodVerticalOffsetsPx), settings.layoutNotifMiddleSide, settings.layoutNotifMiddleAutoNearestSide, settings.layoutNotifVerticalOffsetPx, @@ -791,6 +1323,12 @@ final class UnlockedStockSourceCollector { java.util.Arrays.hashCode(settings.layoutStatusLockMiddleSides), java.util.Arrays.hashCode(settings.layoutStatusLockMiddleAutoNearestSides), java.util.Arrays.hashCode(settings.layoutStatusLockVerticalOffsetsPx), + settings.layoutStatusEnabledAod, + settings.layoutStatusAodCount, + java.util.Arrays.hashCode(settings.layoutStatusAodPositions), + java.util.Arrays.hashCode(settings.layoutStatusAodMiddleSides), + java.util.Arrays.hashCode(settings.layoutStatusAodMiddleAutoNearestSides), + java.util.Arrays.hashCode(settings.layoutStatusAodVerticalOffsetsPx), settings.layoutStatusMiddleSide, settings.layoutStatusMiddleAutoNearestSide, settings.layoutStatusVerticalOffsetPx, @@ -802,4 +1340,12 @@ final class UnlockedStockSourceCollector { settings.statusChipsHideNavUnlocked, settings.statusChipsHideCallUnlocked); } + + private int loggingAwareSettingsSignature(View root, SbtSettings settings) { + int result = layoutSettingsSignature(settings); + boolean logsEnabled = root != null + && root.getContext() != null + && SbtSettings.isDebugLogWritingEnabled(root.getContext()); + return 31 * result + (logsEnabled ? 1 : 0); + } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedVerticalOffsetScaler.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedVerticalOffsetScaler.java index 3c14180..cbfff03 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedVerticalOffsetScaler.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedVerticalOffsetScaler.java @@ -5,7 +5,6 @@ import android.widget.TextView; final class UnlockedVerticalOffsetScaler { private int baselineNotificationIconSizePx; private int baselineStatusIconHeightPx; - private int baselineChipHeightPx; private float baselineClockTextSizePx; int notification(int offsetPx, int currentIconHeightPx) { @@ -22,11 +21,6 @@ final class UnlockedVerticalOffsetScaler { return scaledOffset(offsetPx, currentIconHeightPx, baselineStatusIconHeightPx); } - int chip(int offsetPx, int currentChipHeightPx) { - baselineChipHeightPx = updateBaseline(baselineChipHeightPx, currentChipHeightPx); - return scaledOffset(offsetPx, currentChipHeightPx, baselineChipHeightPx); - } - int clock(int offsetPx, TextView source) { if (offsetPx == 0 || source == null || source.getTextSize() <= 0f) { return offsetPx; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/AppIconRules.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/AppIconRules.java index ddc0662..cbb036e 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/AppIconRules.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/AppIconRules.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; import java.util.regex.Pattern; public final class AppIconRules { @@ -24,6 +25,9 @@ public final class AppIconRules { public static final String EXTRA_PACKAGE = "package"; public static final String EXTRA_FIRST_SEEN_MS = "first_seen_ms"; + public static final String EXTRA_LAST_SEEN_MS = "last_seen_ms"; + public static final String EXTRA_LAST_SEEN_TIMEZONE_OFFSET_MINUTES = + "last_seen_timezone_offset_minutes"; public static final int MODE_AOD = 1; public static final int MODE_LOCK = 1 << 1; @@ -73,7 +77,7 @@ public final class AppIconRules { prefs.edit().putStringSet(PREF_KEY_BLOCKED_MODES, encoded).apply(); } - public static Map loadSeenSession(SharedPreferences prefs) { + public static Map loadSeenSession(SharedPreferences prefs) { if (prefs == null) { return new HashMap<>(); } @@ -97,7 +101,7 @@ public final class AppIconRules { prefs.edit().putStringSet(PREF_KEY_EXCEPTION_RULES, encoded).apply(); } - public static void saveSeenSession(SharedPreferences prefs, Map map) { + public static void saveSeenSession(SharedPreferences prefs, Map map) { if (prefs == null) { return; } @@ -112,18 +116,37 @@ public final class AppIconRules { prefs.edit().remove(PREF_KEY_SEEN_SESSION).apply(); } - public static void noteSeen(Map seen, String pkg, long firstSeenMs) { + public static boolean noteSeen(Map seen, + String pkg, + long seenMs, + int timezoneOffsetMinutes) { if (seen == null || !isValidPackageName(pkg)) { - return; + return false; } - Long existing = seen.get(pkg); + if (seenMs <= 0L) { + seenMs = System.currentTimeMillis(); + } + SeenApp existing = seen.get(pkg); if (existing == null) { - seen.put(pkg, firstSeenMs); - return; + seen.put(pkg, new SeenApp(pkg, seenMs, seenMs, timezoneOffsetMinutes)); + return true; } - if (firstSeenMs > 0 && firstSeenMs < existing) { - seen.put(pkg, firstSeenMs); + long firstSeenMs = existing.firstSeenMs > 0L + ? Math.min(existing.firstSeenMs, seenMs) + : seenMs; + long lastSeenMs = existing.lastSeenMs; + int lastSeenOffsetMinutes = existing.lastSeenTimezoneOffsetMinutes; + if (lastSeenMs <= 0L || seenMs >= lastSeenMs) { + lastSeenMs = seenMs; + lastSeenOffsetMinutes = timezoneOffsetMinutes; } + if (firstSeenMs == existing.firstSeenMs + && lastSeenMs == existing.lastSeenMs + && lastSeenOffsetMinutes == existing.lastSeenTimezoneOffsetMinutes) { + return false; + } + seen.put(pkg, new SeenApp(pkg, firstSeenMs, lastSeenMs, lastSeenOffsetMinutes)); + return true; } public static boolean isBlocked(Map masks, String pkg, String mode) { @@ -367,8 +390,8 @@ public final class AppIconRules { return MODE_NAME_UNKNOWN; } - public static Map parseSeenSession(Set rawSet) { - Map out = new HashMap<>(); + public static Map parseSeenSession(Set rawSet) { + Map out = new HashMap<>(); if (rawSet == null || rawSet.isEmpty()) { return out; } @@ -377,7 +400,7 @@ public final class AppIconRules { continue; } String[] parts = entry.split("\\|", -1); - if (parts.length != 2) { + if (parts.length != 2 && parts.length != 4) { continue; } String pkg = parts[0]; @@ -385,38 +408,61 @@ public final class AppIconRules { continue; } long firstSeen; + long lastSeen; + int lastSeenOffsetMinutes; try { firstSeen = Long.parseLong(parts[1]); + if (parts.length == 4) { + lastSeen = Long.parseLong(parts[2]); + lastSeenOffsetMinutes = Integer.parseInt(parts[3]); + } else { + lastSeen = firstSeen; + lastSeenOffsetMinutes = currentTimezoneOffsetMinutes(); + } } catch (NumberFormatException ignored) { continue; } - if (firstSeen <= 0L) { + if (firstSeen <= 0L || lastSeen <= 0L) { continue; } - Long existing = out.get(pkg); - if (existing == null || firstSeen < existing) { - out.put(pkg, firstSeen); + SeenApp existing = out.get(pkg); + if (existing == null) { + out.put(pkg, new SeenApp(pkg, firstSeen, lastSeen, lastSeenOffsetMinutes)); + } else { + long mergedFirstSeen = Math.min(existing.firstSeenMs, firstSeen); + long mergedLastSeen = existing.lastSeenMs; + int mergedOffset = existing.lastSeenTimezoneOffsetMinutes; + if (lastSeen >= mergedLastSeen) { + mergedLastSeen = lastSeen; + mergedOffset = lastSeenOffsetMinutes; + } + out.put(pkg, new SeenApp(pkg, mergedFirstSeen, mergedLastSeen, mergedOffset)); } } return out; } - public static Set encodeSeenSession(Map map) { + public static Set encodeSeenSession(Map map) { Set out = new HashSet<>(); if (map == null || map.isEmpty()) { return out; } - for (Map.Entry e : map.entrySet()) { + for (Map.Entry e : map.entrySet()) { String pkg = e.getKey(); - Long firstSeenObj = e.getValue(); - if (!isValidPackageName(pkg) || firstSeenObj == null) { + SeenApp app = e.getValue(); + if (!isValidPackageName(pkg) || app == null) { continue; } - long firstSeen = firstSeenObj; + long firstSeen = app.firstSeenMs; + long lastSeen = app.lastSeenMs; if (firstSeen <= 0L) { continue; } - out.add(pkg + "|" + firstSeen); + if (lastSeen <= 0L) { + lastSeen = firstSeen; + } + out.add(pkg + "|" + firstSeen + "|" + lastSeen + "|" + + app.lastSeenTimezoneOffsetMinutes); } return out; } @@ -424,10 +470,17 @@ public final class AppIconRules { public static final class SeenApp { public final String packageName; public final long firstSeenMs; + public final long lastSeenMs; + public final int lastSeenTimezoneOffsetMinutes; - public SeenApp(String packageName, long firstSeenMs) { + public SeenApp(String packageName, + long firstSeenMs, + long lastSeenMs, + int lastSeenTimezoneOffsetMinutes) { this.packageName = packageName; this.firstSeenMs = firstSeenMs; + this.lastSeenMs = lastSeenMs; + this.lastSeenTimezoneOffsetMinutes = lastSeenTimezoneOffsetMinutes; } } @@ -459,18 +512,23 @@ public final class AppIconRules { } } - public static List sortSeenOldestFirst(Map seen) { + public static int currentTimezoneOffsetMinutes() { + long now = System.currentTimeMillis(); + return TimeZone.getDefault().getOffset(now) / 60000; + } + + public static List sortSeenOldestFirst(Map seen) { List out = new ArrayList<>(); if (seen == null || seen.isEmpty()) { return out; } - for (Map.Entry e : seen.entrySet()) { + for (Map.Entry e : seen.entrySet()) { String pkg = e.getKey(); - Long firstSeen = e.getValue(); - if (!isValidPackageName(pkg) || firstSeen == null || firstSeen <= 0L) { + SeenApp app = e.getValue(); + if (!isValidPackageName(pkg) || app == null || app.firstSeenMs <= 0L) { continue; } - out.add(new SeenApp(pkg, firstSeen)); + out.add(app); } out.sort((a, b) -> Long.compare(a.firstSeenMs, b.firstSeenMs)); return out; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java index 210c1ec..fbaf5f8 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java @@ -92,6 +92,7 @@ public final class SbtDefaults { public static final boolean DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT = false; public static final boolean DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT = false; public static final boolean DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT = false; + public static final boolean DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT = false; public static final int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0; public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0; @@ -114,6 +115,7 @@ public final class SbtDefaults { public static final String LAYOUT_CHIP_MIDDLE_SIDE_DEFAULT = "left"; public static final boolean LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true; public static final int LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT = 0; + public static final int LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT = 52; public static final String LAYOUT_NOTIF_POSITION_DEFAULT = "left"; public static final String LAYOUT_NOTIF_SORT_ORDER_DEFAULT = "automatic"; public static final boolean LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT = true; @@ -124,6 +126,11 @@ public final class SbtDefaults { public static final String LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT = "left"; public static final boolean LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true; public static final int LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT = 0; + public static final int LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT = 46; + public static final int LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT = 46; + public static final int LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT = 46; + public static final int LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT = 59; + public static final int LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT = 65; public static final String LAYOUT_STATUS_POSITION_DEFAULT = "right"; public static final boolean LAYOUT_STATUS_ENABLED_AOD_DEFAULT = true; public static final boolean LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT = true; @@ -133,6 +140,9 @@ public final class SbtDefaults { public static final String LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT = "left"; public static final boolean LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true; public static final int LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT = 0; + public static final int LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT = 38; + public static final int LAYOUT_ICON_HEIGHT_STEPS_MIN = 10; + public static final int LAYOUT_ICON_HEIGHT_STEPS_MAX = 100; public static final int LAYOUT_ICON_CONTAINER_COUNT_MIN = 0; public static final int LAYOUT_ICON_CONTAINER_COUNT_MAX = 3; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index 10ab8c0..e99ee90 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -35,6 +35,8 @@ public final class SbtSettings { "debug_visualize_chip_container"; public static final String KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT = "debug_visualize_camera_cutout"; + public static final String KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS = + "debug_visualize_aod_stock_containers"; public static final String KEY_LOCKED_NOTIFICATION_MODE = "locked_notification_mode"; public static final String KEY_UNLOCKED_MAX_ICONS_PER_ROW = "unlocked_max_icons_per_row"; @@ -50,10 +52,12 @@ public final class SbtSettings { public static final String KEY_CARDS_AOD_MAX_ROWS = "cards_aod_max_rows"; public static final String KEY_CARDS_AOD_EVEN_DISTRIBUTION = "cards_aod_even_distribution"; public static final String KEY_CARDS_AOD_LIMIT_TO_WIDTH = "cards_aod_limit_to_width"; + public static final String KEY_CARDS_AOD_ICON_HEIGHT_STEPS = "cards_aod_icon_height_steps"; public static final String KEY_CARDS_LOCK_MAX_ICONS_PER_ROW = "cards_lock_max_icons_per_row"; public static final String KEY_CARDS_LOCK_MAX_ROWS = "cards_lock_max_rows"; public static final String KEY_CARDS_LOCK_EVEN_DISTRIBUTION = "cards_lock_even_distribution"; public static final String KEY_CARDS_LOCK_LIMIT_TO_WIDTH = "cards_lock_limit_to_width"; + public static final String KEY_CARDS_LOCK_ICON_HEIGHT_STEPS = "cards_lock_icon_height_steps"; public static final String KEY_STATUS_CHIPS_HIDE_ALL = "status_chips_hide_all"; public static final String KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED = "status_chips_hide_media_unlocked"; public static final String KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED = "status_chips_hide_nav_unlocked"; @@ -105,6 +109,7 @@ public final class SbtSettings { public static final String KEY_LAYOUT_CHIP_MIDDLE_SIDE = "layout_chip_middle_side"; public static final String KEY_LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE = "layout_chip_middle_auto_nearest_side"; public static final String KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX = "layout_chip_vertical_offset_px"; + public static final String KEY_LAYOUT_CHIP_HEIGHT_STEPS = "layout_chip_height_steps"; public static final String KEY_LAYOUT_NOTIF_POSITION = "layout_notif_position"; public static final String KEY_LAYOUT_NOTIF_SORT_ORDER = "layout_notif_sort_order"; public static final String KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED = "layout_notif_show_dot_if_truncated"; @@ -115,6 +120,7 @@ public final class SbtSettings { public static final String KEY_LAYOUT_NOTIF_MIDDLE_SIDE = "layout_notif_middle_side"; public static final String KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE = "layout_notif_middle_auto_nearest_side"; public static final String KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX = "layout_notif_vertical_offset_px"; + public static final String KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS = "layout_notif_icon_height_steps"; public static final String KEY_LAYOUT_STATUS_POSITION = "layout_status_position"; public static final String KEY_LAYOUT_STATUS_ENABLED_AOD = "layout_status_enabled_aod"; public static final String KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN = "layout_status_enabled_lockscreen"; @@ -124,6 +130,7 @@ public final class SbtSettings { public static final String KEY_LAYOUT_STATUS_MIDDLE_SIDE = "layout_status_middle_side"; public static final String KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE = "layout_status_middle_auto_nearest_side"; public static final String KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX = "layout_status_vertical_offset_px"; + public static final String KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS = "layout_status_icon_height_steps"; public static final String KEY_SYSTEM_ICON_BLOCKED_MODES = SystemIconRules.PREF_KEY_BLOCKED_MODES; public static final String KEY_SYSTEM_ICON_HIDE_SLOTS = "system_icon_hide_slots"; public static final String KEY_APP_ICON_BLOCKED_MODES = AppIconRules.PREF_KEY_BLOCKED_MODES; @@ -177,6 +184,30 @@ public final class SbtSettings { return suffixedKey("layout_lock_notifications_vertical_offset_px", index); } + public static String layoutLockNotifIconHeightKey() { + return "layout_lock_notifications_icon_height_steps"; + } + + public static String layoutAodNotifPositionKey(int index) { + return suffixedKey("layout_aod_notifications_position", index); + } + + public static String layoutAodNotifMiddleSideKey(int index) { + return suffixedKey("layout_aod_notifications_middle_side", index); + } + + public static String layoutAodNotifMiddleAutoNearestSideKey(int index) { + return suffixedKey("layout_aod_notifications_middle_auto_nearest_side", index); + } + + public static String layoutAodNotifVerticalOffsetKey(int index) { + return suffixedKey("layout_aod_notifications_vertical_offset_px", index); + } + + public static String layoutAodNotifIconHeightKey() { + return "layout_aod_notifications_icon_height_steps"; + } + public static String layoutLockStatusPositionKey(int index) { return suffixedKey("layout_lock_status_position", index); } @@ -193,6 +224,30 @@ public final class SbtSettings { return suffixedKey("layout_lock_status_vertical_offset_px", index); } + public static String layoutLockStatusIconHeightKey() { + return "layout_lock_status_icon_height_steps"; + } + + public static String layoutAodStatusPositionKey(int index) { + return suffixedKey("layout_aod_status_position", index); + } + + public static String layoutAodStatusMiddleSideKey(int index) { + return suffixedKey("layout_aod_status_middle_side", index); + } + + public static String layoutAodStatusMiddleAutoNearestSideKey(int index) { + return suffixedKey("layout_aod_status_middle_auto_nearest_side", index); + } + + public static String layoutAodStatusVerticalOffsetKey(int index) { + return suffixedKey("layout_aod_status_vertical_offset_px", index); + } + + public static String layoutAodStatusIconHeightKey() { + return "layout_aod_status_icon_height_steps"; + } + private static String suffixedKey(String base, int index) { return index <= 0 ? base : base + "_" + (index + 1); } @@ -209,10 +264,12 @@ public final class SbtSettings { public final int cardsAodMaxRows; public final boolean cardsAodEvenDistribution; public final boolean cardsAodLimitToWidth; + public final int cardsAodIconHeightSteps; public final int cardsLockMaxIconsPerRow; public final int cardsLockMaxRows; public final boolean cardsLockEvenDistribution; public final boolean cardsLockLimitToWidth; + public final int cardsLockIconHeightSteps; public final boolean keepAodVisibleDuringCall; public final boolean batteryLockscreenUnpluggedPercent; public final boolean batteryAodUnpluggedPercent; @@ -259,6 +316,7 @@ public final class SbtSettings { public final String layoutChipMiddleSide; public final boolean layoutChipMiddleAutoNearestSide; public final int layoutChipVerticalOffsetPx; + public final int layoutChipHeightSteps; public final String layoutNotifPosition; public final String layoutNotifSortOrder; public final boolean layoutNotifShowDotIfTruncated; @@ -275,9 +333,17 @@ public final class SbtSettings { public final String[] layoutNotifLockMiddleSides; public final boolean[] layoutNotifLockMiddleAutoNearestSides; public final int[] layoutNotifLockVerticalOffsetsPx; + public final int layoutNotifAodCount; + public final String[] layoutNotifAodPositions; + public final String[] layoutNotifAodMiddleSides; + public final boolean[] layoutNotifAodMiddleAutoNearestSides; + public final int[] layoutNotifAodVerticalOffsetsPx; public final String layoutNotifMiddleSide; public final boolean layoutNotifMiddleAutoNearestSide; public final int layoutNotifVerticalOffsetPx; + public final int layoutNotifIconHeightSteps; + public final int layoutNotifLockIconHeightSteps; + public final int layoutNotifAodIconHeightSteps; public final String layoutStatusPosition; public final boolean layoutStatusEnabledAod; public final boolean layoutStatusEnabledLockscreen; @@ -292,10 +358,18 @@ public final class SbtSettings { public final String[] layoutStatusLockMiddleSides; public final boolean[] layoutStatusLockMiddleAutoNearestSides; public final int[] layoutStatusLockVerticalOffsetsPx; + public final int layoutStatusAodCount; + public final String[] layoutStatusAodPositions; + public final String[] layoutStatusAodMiddleSides; + public final boolean[] layoutStatusAodMiddleAutoNearestSides; + public final int[] layoutStatusAodVerticalOffsetsPx; public final boolean layoutStatusShowDotIfTruncated; public final String layoutStatusMiddleSide; public final boolean layoutStatusMiddleAutoNearestSide; public final int layoutStatusVerticalOffsetPx; + public final int layoutStatusIconHeightSteps; + public final int layoutStatusLockIconHeightSteps; + public final int layoutStatusAodIconHeightSteps; public final Map clockCameraTypeOverrides; public final Map systemIconBlockedModes; public final boolean statusChipsHideAll; @@ -310,6 +384,7 @@ public final class SbtSettings { public final boolean debugVisualizeClockContainer; public final boolean debugVisualizeChipContainer; public final boolean debugVisualizeCameraCutout; + public final boolean debugVisualizeAodStockContainers; private SbtSettings(boolean enableAodCutoutLimit, boolean enableLockCutoutLimit, @@ -324,10 +399,12 @@ public final class SbtSettings { int cardsAodMaxRows, boolean cardsAodEvenDistribution, boolean cardsAodLimitToWidth, + int cardsAodIconHeightSteps, int cardsLockMaxIconsPerRow, int cardsLockMaxRows, boolean cardsLockEvenDistribution, boolean cardsLockLimitToWidth, + int cardsLockIconHeightSteps, boolean keepAodVisibleDuringCall, boolean batteryLockscreenUnpluggedPercent, boolean batteryAodUnpluggedPercent, @@ -374,6 +451,7 @@ public final class SbtSettings { String layoutChipMiddleSide, boolean layoutChipMiddleAutoNearestSide, int layoutChipVerticalOffsetPx, + int layoutChipHeightSteps, String layoutNotifPosition, String layoutNotifSortOrder, boolean layoutNotifShowDotIfTruncated, @@ -383,6 +461,9 @@ public final class SbtSettings { String layoutNotifMiddleSide, boolean layoutNotifMiddleAutoNearestSide, int layoutNotifVerticalOffsetPx, + int layoutNotifIconHeightSteps, + int layoutNotifLockIconHeightSteps, + int layoutNotifAodIconHeightSteps, int layoutNotifUnlockedCount, String[] layoutNotifPositions, String[] layoutNotifMiddleSides, @@ -393,6 +474,11 @@ public final class SbtSettings { String[] layoutNotifLockMiddleSides, boolean[] layoutNotifLockMiddleAutoNearestSides, int[] layoutNotifLockVerticalOffsetsPx, + int layoutNotifAodCount, + String[] layoutNotifAodPositions, + String[] layoutNotifAodMiddleSides, + boolean[] layoutNotifAodMiddleAutoNearestSides, + int[] layoutNotifAodVerticalOffsetsPx, String layoutStatusPosition, boolean layoutStatusEnabledAod, boolean layoutStatusEnabledLockscreen, @@ -401,6 +487,9 @@ public final class SbtSettings { String layoutStatusMiddleSide, boolean layoutStatusMiddleAutoNearestSide, int layoutStatusVerticalOffsetPx, + int layoutStatusIconHeightSteps, + int layoutStatusLockIconHeightSteps, + int layoutStatusAodIconHeightSteps, int layoutStatusUnlockedCount, String[] layoutStatusPositions, String[] layoutStatusMiddleSides, @@ -411,6 +500,11 @@ public final class SbtSettings { String[] layoutStatusLockMiddleSides, boolean[] layoutStatusLockMiddleAutoNearestSides, int[] layoutStatusLockVerticalOffsetsPx, + int layoutStatusAodCount, + String[] layoutStatusAodPositions, + String[] layoutStatusAodMiddleSides, + boolean[] layoutStatusAodMiddleAutoNearestSides, + int[] layoutStatusAodVerticalOffsetsPx, Map clockCameraTypeOverrides, Map systemIconBlockedModes, boolean statusChipsHideAll, @@ -424,7 +518,8 @@ public final class SbtSettings { boolean debugVisualizeStatusContainer, boolean debugVisualizeClockContainer, boolean debugVisualizeChipContainer, - boolean debugVisualizeCameraCutout) { + boolean debugVisualizeCameraCutout, + boolean debugVisualizeAodStockContainers) { this.enableAodCutoutLimit = enableAodCutoutLimit; this.enableLockCutoutLimit = enableLockCutoutLimit; this.unlockedMaxIconsPerRow = unlockedMaxIconsPerRow; @@ -438,10 +533,12 @@ public final class SbtSettings { this.cardsAodMaxRows = cardsAodMaxRows; this.cardsAodEvenDistribution = cardsAodEvenDistribution; this.cardsAodLimitToWidth = cardsAodLimitToWidth; + this.cardsAodIconHeightSteps = clampIconHeightSteps(cardsAodIconHeightSteps); this.cardsLockMaxIconsPerRow = cardsLockMaxIconsPerRow; this.cardsLockMaxRows = cardsLockMaxRows; this.cardsLockEvenDistribution = cardsLockEvenDistribution; this.cardsLockLimitToWidth = cardsLockLimitToWidth; + this.cardsLockIconHeightSteps = clampIconHeightSteps(cardsLockIconHeightSteps); this.keepAodVisibleDuringCall = keepAodVisibleDuringCall; this.batteryLockscreenUnpluggedPercent = batteryLockscreenUnpluggedPercent; this.batteryAodUnpluggedPercent = batteryAodUnpluggedPercent; @@ -550,6 +647,7 @@ public final class SbtSettings { layoutChipVerticalOffsetPx, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutChipHeightSteps = clampIconHeightSteps(layoutChipHeightSteps); this.layoutNotifPosition = layoutNotifPosition != null ? layoutNotifPosition : SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT; @@ -568,6 +666,9 @@ public final class SbtSettings { layoutNotifVerticalOffsetPx, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutNotifIconHeightSteps = clampIconHeightSteps(layoutNotifIconHeightSteps); + this.layoutNotifLockIconHeightSteps = clampIconHeightSteps(layoutNotifLockIconHeightSteps); + this.layoutNotifAodIconHeightSteps = clampIconHeightSteps(layoutNotifAodIconHeightSteps); this.layoutNotifUnlockedCount = Math.max( SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, layoutNotifUnlockedCount)); @@ -608,6 +709,25 @@ public final class SbtSettings { SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutNotifAodCount = clampIconContainerCount(layoutNotifAodCount); + this.layoutNotifAodPositions = sanitizeStringArray( + layoutNotifAodPositions, + this.layoutNotifPosition, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifAodMiddleSides = sanitizeStringArray( + layoutNotifAodMiddleSides, + this.layoutNotifMiddleSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifAodMiddleAutoNearestSides = sanitizeBooleanArray( + layoutNotifAodMiddleAutoNearestSides, + this.layoutNotifMiddleAutoNearestSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifAodVerticalOffsetsPx = sanitizeIntArray( + layoutNotifAodVerticalOffsetsPx, + this.layoutNotifVerticalOffsetPx, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); this.layoutStatusPosition = layoutStatusPosition != null ? layoutStatusPosition : SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT; @@ -623,6 +743,9 @@ public final class SbtSettings { layoutStatusVerticalOffsetPx, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutStatusIconHeightSteps = clampIconHeightSteps(layoutStatusIconHeightSteps); + this.layoutStatusLockIconHeightSteps = clampIconHeightSteps(layoutStatusLockIconHeightSteps); + this.layoutStatusAodIconHeightSteps = clampIconHeightSteps(layoutStatusAodIconHeightSteps); this.layoutStatusUnlockedCount = Math.max( SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, layoutStatusUnlockedCount)); @@ -663,6 +786,25 @@ public final class SbtSettings { SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutStatusAodCount = clampIconContainerCount(layoutStatusAodCount); + this.layoutStatusAodPositions = sanitizeStringArray( + layoutStatusAodPositions, + this.layoutStatusPosition, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusAodMiddleSides = sanitizeStringArray( + layoutStatusAodMiddleSides, + this.layoutStatusMiddleSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusAodMiddleAutoNearestSides = sanitizeBooleanArray( + layoutStatusAodMiddleAutoNearestSides, + this.layoutStatusMiddleAutoNearestSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusAodVerticalOffsetsPx = sanitizeIntArray( + layoutStatusAodVerticalOffsetsPx, + this.layoutStatusVerticalOffsetPx, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); this.clockCameraTypeOverrides = clockCameraTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(clockCameraTypeOverrides)) : Collections.emptyMap(); @@ -694,6 +836,7 @@ public final class SbtSettings { this.debugVisualizeClockContainer = debugVisualizeClockContainer; this.debugVisualizeChipContainer = debugVisualizeChipContainer; this.debugVisualizeCameraCutout = debugVisualizeCameraCutout; + this.debugVisualizeAodStockContainers = debugVisualizeAodStockContainers; } public static SbtSettings defaults() { @@ -711,10 +854,12 @@ public final class SbtSettings { SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT, SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT, + SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT, SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT, SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT, + SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT, SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT, SbtDefaults.BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT, SbtDefaults.BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT, @@ -761,6 +906,7 @@ public final class SbtSettings { SbtDefaults.LAYOUT_CHIP_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT, SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT, @@ -770,6 +916,14 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + null, + null, + null, + null, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, null, null, @@ -788,6 +942,14 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + null, + null, + null, + null, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, null, null, @@ -811,7 +973,8 @@ public final class SbtSettings { SbtDefaults.DEBUG_VISUALIZE_STATUS_CONTAINER_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT, - SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT + SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT, + SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT ); } @@ -872,20 +1035,25 @@ public final class SbtSettings { if (context == null || !AppIconRules.isValidPackageName(packageName)) { return; } + long seenMs = firstSeenMs > 0L ? firstSeenMs : System.currentTimeMillis(); + int offsetMinutes = AppIconRules.currentTimezoneOffsetMinutes(); try { if (!MODULE_PACKAGE.equals(context.getPackageName()) && hasSettingsProvider(context)) { Uri uri = Uri.parse("content://" + SbtSettingsProvider.AUTHORITY); Bundle extras = new Bundle(); extras.putString(AppIconRules.EXTRA_PACKAGE, packageName); - extras.putLong(AppIconRules.EXTRA_FIRST_SEEN_MS, firstSeenMs); + extras.putLong(AppIconRules.EXTRA_FIRST_SEEN_MS, seenMs); + extras.putLong(AppIconRules.EXTRA_LAST_SEEN_MS, seenMs); + extras.putInt(AppIconRules.EXTRA_LAST_SEEN_TIMEZONE_OFFSET_MINUTES, offsetMinutes); context.getContentResolver() .call(uri, SbtSettingsProvider.METHOD_NOTE_SEEN_ICON_APP, null, extras); return; } SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); - Map seen = AppIconRules.loadSeenSession(prefs); - AppIconRules.noteSeen(seen, packageName, firstSeenMs); - AppIconRules.saveSeenSession(prefs, seen); + Map seen = AppIconRules.loadSeenSession(prefs); + if (AppIconRules.noteSeen(seen, packageName, seenMs, offsetMinutes)) { + AppIconRules.saveSeenSession(prefs, seen); + } } catch (Throwable ignored) { // This list is advisory UI state; rendering should never fail because it cannot be updated. } @@ -932,6 +1100,10 @@ public final class SbtSettings { SbtDefaults.CARDS_AOD_MAX_ROWS_MAX), prefs.getBoolean(KEY_CARDS_AOD_EVEN_DISTRIBUTION, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT), prefs.getBoolean(KEY_CARDS_AOD_LIMIT_TO_WIDTH, SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT), + clampInt(prefs.getInt(KEY_CARDS_AOD_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), clampInt(prefs.getInt(KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT), SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MIN, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MAX), @@ -940,6 +1112,10 @@ public final class SbtSettings { SbtDefaults.CARDS_LOCK_MAX_ROWS_MAX), prefs.getBoolean(KEY_CARDS_LOCK_EVEN_DISTRIBUTION, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT), prefs.getBoolean(KEY_CARDS_LOCK_LIMIT_TO_WIDTH, SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT), + clampInt(prefs.getInt(KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), prefs.getBoolean(KEY_AOD_KEEP_VISIBLE_DURING_CALL, SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT), prefs.getBoolean(KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT), prefs.getBoolean(KEY_BATTERY_AOD_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT), @@ -1007,6 +1183,8 @@ public final class SbtSettings { SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampIconHeightSteps(prefs.getInt(KEY_LAYOUT_CHIP_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT)), prefs.getString(KEY_LAYOUT_NOTIF_POSITION, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), prefs.getString(KEY_LAYOUT_NOTIF_SORT_ORDER, SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT), prefs.getBoolean(KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED, SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT), @@ -1019,6 +1197,18 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampInt(prefs.getInt(KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(prefs.getInt(layoutLockNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(prefs.getInt(layoutAodNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), readIconContainerCount( prefs, KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, @@ -1063,6 +1253,28 @@ public final class SbtSettings { prefs, SbtSettings::layoutLockNotifVerticalOffsetKey, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), + readIconContainerCount( + prefs, + "layout_aod_notifications_count", + KEY_LAYOUT_NOTIF_ENABLED_AOD, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutAodNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutAodNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromPrefs( + prefs, + SbtSettings::layoutAodNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromPrefs( + prefs, + SbtSettings::layoutAodNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), prefs.getString(KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), prefs.getBoolean(KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), prefs.getBoolean(KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT), @@ -1074,6 +1286,18 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampInt(prefs.getInt(KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(prefs.getInt(layoutLockStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(prefs.getInt(layoutAodStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), readIconContainerCount( prefs, KEY_LAYOUT_STATUS_UNLOCKED_COUNT, @@ -1118,6 +1342,28 @@ public final class SbtSettings { prefs, SbtSettings::layoutLockStatusVerticalOffsetKey, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), + readIconContainerCount( + prefs, + "layout_aod_status_count", + KEY_LAYOUT_STATUS_ENABLED_AOD, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutAodStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutAodStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromPrefs( + prefs, + SbtSettings::layoutAodStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromPrefs( + prefs, + SbtSettings::layoutAodStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.parseOverrides(copyStringSet( prefs.getStringSet(KEY_CLOCK_CAMERA_TYPE_OVERRIDES, Collections.emptySet()))), systemIconBlockedModes, @@ -1144,7 +1390,10 @@ public final class SbtSettings { SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT), prefs.getBoolean( KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, - SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT) + SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT), + prefs.getBoolean( + KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, + SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT) ); } @@ -1181,6 +1430,10 @@ public final class SbtSettings { SbtDefaults.CARDS_AOD_MAX_ROWS_MAX), bundle.getBoolean(KEY_CARDS_AOD_EVEN_DISTRIBUTION, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT), bundle.getBoolean(KEY_CARDS_AOD_LIMIT_TO_WIDTH, SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT), + clampInt(bundle.getInt(KEY_CARDS_AOD_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), clampInt(bundle.getInt(KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT), SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MIN, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MAX), @@ -1189,6 +1442,10 @@ public final class SbtSettings { SbtDefaults.CARDS_LOCK_MAX_ROWS_MAX), bundle.getBoolean(KEY_CARDS_LOCK_EVEN_DISTRIBUTION, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT), bundle.getBoolean(KEY_CARDS_LOCK_LIMIT_TO_WIDTH, SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT), + clampInt(bundle.getInt(KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), bundle.getBoolean(KEY_AOD_KEEP_VISIBLE_DURING_CALL, SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT), bundle.getBoolean(KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT), bundle.getBoolean(KEY_BATTERY_AOD_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT), @@ -1254,6 +1511,8 @@ public final class SbtSettings { SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampIconHeightSteps(bundle.getInt(KEY_LAYOUT_CHIP_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT)), bundle.getString(KEY_LAYOUT_NOTIF_POSITION, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), bundle.getString(KEY_LAYOUT_NOTIF_SORT_ORDER, SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT), bundle.getBoolean(KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED, SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT), @@ -1266,6 +1525,18 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampInt(bundle.getInt(KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(bundle.getInt(layoutLockNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(bundle.getInt(layoutAodNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), readIconContainerCount( bundle, KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, @@ -1310,6 +1581,28 @@ public final class SbtSettings { bundle, SbtSettings::layoutLockNotifVerticalOffsetKey, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), + readIconContainerCount( + bundle, + "layout_aod_notifications_count", + KEY_LAYOUT_NOTIF_ENABLED_AOD, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutAodNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutAodNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromBundle( + bundle, + SbtSettings::layoutAodNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromBundle( + bundle, + SbtSettings::layoutAodNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), bundle.getString(KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), bundle.getBoolean(KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), bundle.getBoolean(KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT), @@ -1321,6 +1614,18 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + clampInt(bundle.getInt(KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(bundle.getInt(layoutLockStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + clampInt(bundle.getInt(layoutAodStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), readIconContainerCount( bundle, KEY_LAYOUT_STATUS_UNLOCKED_COUNT, @@ -1365,6 +1670,28 @@ public final class SbtSettings { bundle, SbtSettings::layoutLockStatusVerticalOffsetKey, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), + readIconContainerCount( + bundle, + "layout_aod_status_count", + KEY_LAYOUT_STATUS_ENABLED_AOD, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutAodStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutAodStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromBundle( + bundle, + SbtSettings::layoutAodStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromBundle( + bundle, + SbtSettings::layoutAodStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.parseOverrides(readStringSetFromBundle(bundle, KEY_CLOCK_CAMERA_TYPE_OVERRIDES)), systemIconBlockedModes, bundle.getBoolean(KEY_STATUS_CHIPS_HIDE_ALL, false), @@ -1388,7 +1715,10 @@ public final class SbtSettings { SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT), bundle.getBoolean( KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, - SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT) + SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT), + bundle.getBoolean( + KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, + SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT) ); } @@ -1406,6 +1736,12 @@ public final class SbtSettings { Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, value)); } + private static int clampIconHeightSteps(int value) { + return Math.max( + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX, value)); + } + private static int readIconContainerCount( SharedPreferences prefs, String countKey, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java index c1e03be..6f08000 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java @@ -71,6 +71,9 @@ public class SbtSettingsProvider extends ContentProvider { prefs.getBoolean(SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT)); out.putBoolean(SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH, prefs.getBoolean(SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH, SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT)); + out.putInt(SbtSettings.KEY_CARDS_AOD_ICON_HEIGHT_STEPS, + prefs.getInt(SbtSettings.KEY_CARDS_AOD_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT)); out.putInt(SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, prefs.getInt(SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT)); out.putInt(SbtSettings.KEY_CARDS_LOCK_MAX_ROWS, @@ -79,6 +82,9 @@ public class SbtSettingsProvider extends ContentProvider { prefs.getBoolean(SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT)); out.putBoolean(SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, prefs.getBoolean(SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT)); + out.putInt(SbtSettings.KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, + prefs.getInt(SbtSettings.KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT)); out.putBoolean(SbtSettings.KEY_AOD_KEEP_VISIBLE_DURING_CALL, prefs.getBoolean(SbtSettings.KEY_AOD_KEEP_VISIBLE_DURING_CALL, SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT)); out.putBoolean(SbtSettings.KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT, @@ -157,6 +163,9 @@ public class SbtSettingsProvider extends ContentProvider { out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT)); + out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, + prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, + SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT)); out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX, SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT)); @@ -216,6 +225,9 @@ public class SbtSettingsProvider extends ContentProvider { out.putInt(SbtSettings.KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX, SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT)); + out.putInt(SbtSettings.KEY_LAYOUT_CHIP_HEIGHT_STEPS, + prefs.getInt(SbtSettings.KEY_LAYOUT_CHIP_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT)); out.putString(SbtSettings.KEY_LAYOUT_NOTIF_POSITION, prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_POSITION, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT)); @@ -250,6 +262,9 @@ public class SbtSettingsProvider extends ContentProvider { out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT)); + out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, + prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT)); putIconCopySettings( out, prefs, @@ -268,6 +283,9 @@ public class SbtSettingsProvider extends ContentProvider { SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT) ? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT : 0)); + out.putInt(SbtSettings.layoutLockNotifIconHeightKey(), + prefs.getInt(SbtSettings.layoutLockNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT)); putIconCopySettings( out, prefs, @@ -279,6 +297,27 @@ public class SbtSettingsProvider extends ContentProvider { SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtSettings::layoutLockNotifVerticalOffsetKey, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); + out.putInt("layout_aod_notifications_count", + prefs.getInt("layout_aod_notifications_count", + prefs.getBoolean( + SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD, + SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT) + ? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT + : 0)); + out.putInt(SbtSettings.layoutAodNotifIconHeightKey(), + prefs.getInt(SbtSettings.layoutAodNotifIconHeightKey(), + SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT)); + putIconCopySettings( + out, + prefs, + SbtSettings::layoutAodNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, + SbtSettings::layoutAodNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, + SbtSettings::layoutAodNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, + SbtSettings::layoutAodNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); out.putString(SbtSettings.KEY_LAYOUT_STATUS_POSITION, prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT)); @@ -310,6 +349,9 @@ public class SbtSettingsProvider extends ContentProvider { out.putInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT)); + out.putInt(SbtSettings.KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, + prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT)); putIconCopySettings( out, prefs, @@ -328,6 +370,9 @@ public class SbtSettingsProvider extends ContentProvider { SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT) ? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT : 0)); + out.putInt(SbtSettings.layoutLockStatusIconHeightKey(), + prefs.getInt(SbtSettings.layoutLockStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT)); putIconCopySettings( out, prefs, @@ -339,6 +384,27 @@ public class SbtSettingsProvider extends ContentProvider { SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtSettings::layoutLockStatusVerticalOffsetKey, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); + out.putInt("layout_aod_status_count", + prefs.getInt("layout_aod_status_count", + prefs.getBoolean( + SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD, + SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT) + ? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT + : 0)); + out.putInt(SbtSettings.layoutAodStatusIconHeightKey(), + prefs.getInt(SbtSettings.layoutAodStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT)); + putIconCopySettings( + out, + prefs, + SbtSettings::layoutAodStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, + SbtSettings::layoutAodStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, + SbtSettings::layoutAodStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, + SbtSettings::layoutAodStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); out.putStringArrayList( SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, new ArrayList<>(prefs.getStringSet( @@ -372,16 +438,25 @@ public class SbtSettingsProvider extends ContentProvider { } if (METHOD_NOTE_SEEN_ICON_APP.equals(method)) { String pkg = extras != null ? extras.getString(AppIconRules.EXTRA_PACKAGE) : null; - long firstSeenMs = extras != null + long seenMs = extras != null ? extras.getLong(AppIconRules.EXTRA_FIRST_SEEN_MS, System.currentTimeMillis()) : System.currentTimeMillis(); + if (extras != null && extras.containsKey(AppIconRules.EXTRA_LAST_SEEN_MS)) { + seenMs = extras.getLong(AppIconRules.EXTRA_LAST_SEEN_MS, seenMs); + } + int offsetMinutes = extras != null + ? extras.getInt( + AppIconRules.EXTRA_LAST_SEEN_TIMEZONE_OFFSET_MINUTES, + AppIconRules.currentTimezoneOffsetMinutes()) + : AppIconRules.currentTimezoneOffsetMinutes(); if (!AppIconRules.isValidPackageName(pkg)) { return Bundle.EMPTY; } synchronized (LOCK) { - Map seen = AppIconRules.loadSeenSession(prefs); - AppIconRules.noteSeen(seen, pkg, firstSeenMs); - AppIconRules.saveSeenSession(prefs, seen); + Map seen = AppIconRules.loadSeenSession(prefs); + if (AppIconRules.noteSeen(seen, pkg, seenMs, offsetMinutes)) { + AppIconRules.saveSeenSession(prefs, seen); + } } return Bundle.EMPTY; } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/HiddenAppsUiSupport.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/HiddenAppsUiSupport.java index 243d482..70decbe 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/HiddenAppsUiSupport.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/HiddenAppsUiSupport.java @@ -32,13 +32,16 @@ import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.TimeZone; final class HiddenAppsUiSupport { interface RowChangedListener { @@ -81,7 +84,7 @@ final class HiddenAppsUiSupport { Map blocked = AppIconRules.loadBlockedModes(prefs); Map> exceptionRules = AppIconRules.loadExceptionRules(prefs); - Map seen = AppIconRules.loadSeenSession(prefs); + Map seen = AppIconRules.loadSeenSession(prefs); ArrayList blockedItems = new ArrayList<>(); for (Map.Entry entry : blocked.entrySet()) { @@ -96,6 +99,7 @@ final class HiddenAppsUiSupport { item.blockLock = (mask & AppIconRules.MODE_LOCK) != 0; item.blockUnlock = (mask & AppIconRules.MODE_UNLOCK) != 0; item.exceptionRules = toEditableExceptionRules(exceptionRules.get(pkg)); + applySeenInfo(item, seen.get(pkg)); blockedItems.add(item); } blockedItems.sort((a, b) -> { @@ -122,11 +126,20 @@ final class HiddenAppsUiSupport { } RowItem item = buildRowItem(context, packageManager, fallbackIcon, seenApp.packageName); item.exceptionRules = toEditableExceptionRules(exceptionRules.get(seenApp.packageName)); + applySeenInfo(item, seenApp); rows.add(item); alreadyAdded.add(seenApp.packageName); } } + private static void applySeenInfo(RowItem item, AppIconRules.SeenApp seenApp) { + if (item == null || seenApp == null) { + return; + } + item.lastSeenMs = seenApp.lastSeenMs; + item.lastSeenTimezoneOffsetMinutes = seenApp.lastSeenTimezoneOffsetMinutes; + } + private static RowItem buildRowItem(Context context, PackageManager packageManager, Drawable fallbackIcon, @@ -466,6 +479,41 @@ final class HiddenAppsUiSupport { return normalized.isEmpty() ? null : normalized; } + private static String formatLastSeen(Context context, RowItem row) { + if (context == null || row == null || row.lastSeenMs <= 0L) { + return null; + } + int offsetMinutes = row.lastSeenTimezoneOffsetMinutes; + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); + format.setTimeZone(timeZoneForOffset(offsetMinutes)); + String timestamp = format.format(new Date(row.lastSeenMs)); + int currentOffsetMinutes = AppIconRules.currentTimezoneOffsetMinutes(); + if (offsetMinutes != currentOffsetMinutes) { + timestamp += formatOffsetSuffix(offsetMinutes); + } + return context.getString(R.string.hidden_apps_last_seen, timestamp); + } + + private static TimeZone timeZoneForOffset(int offsetMinutes) { + int absMinutes = Math.abs(offsetMinutes); + int hours = absMinutes / 60; + int minutes = absMinutes % 60; + String id = String.format(Locale.ROOT, "GMT%s%02d:%02d", + offsetMinutes < 0 ? "-" : "+", hours, minutes); + return TimeZone.getTimeZone(id); + } + + private static String formatOffsetSuffix(int offsetMinutes) { + int absMinutes = Math.abs(offsetMinutes); + int hours = absMinutes / 60; + int minutes = absMinutes % 60; + if (minutes == 0) { + return String.format(Locale.ROOT, "%s%02d", offsetMinutes < 0 ? "-" : "+", hours); + } + return String.format(Locale.ROOT, "%s%02d:%02d", + offsetMinutes < 0 ? "-" : "+", hours, minutes); + } + static final class RowItem { final String packageName; final String label; @@ -473,6 +521,8 @@ final class HiddenAppsUiSupport { boolean blockAod; boolean blockLock; boolean blockUnlock; + long lastSeenMs; + int lastSeenTimezoneOffsetMinutes; ArrayList exceptionRules = new ArrayList<>(); RowItem(String packageName, String label, Drawable icon) { @@ -520,6 +570,7 @@ final class HiddenAppsUiSupport { TextView crossUnlock; TextView label; TextView pkg; + TextView lastSeen; } static final class HiddenAppsAdapter extends BaseAdapter { @@ -571,6 +622,7 @@ final class HiddenAppsUiSupport { holder.crossUnlock = convertView.findViewById(R.id.icon_mode_unlock_cross); holder.label = convertView.findViewById(R.id.hidden_item_label); holder.pkg = convertView.findViewById(R.id.hidden_item_package); + holder.lastSeen = convertView.findViewById(R.id.hidden_item_last_seen); convertView.setTag(holder); } else { holder = (RowHolder) convertView.getTag(); @@ -585,6 +637,14 @@ final class HiddenAppsUiSupport { } else { holder.pkg.setText(row.packageName); } + String lastSeenText = formatLastSeen(context, row); + if (lastSeenText == null) { + holder.lastSeen.setVisibility(View.GONE); + holder.lastSeen.setText(""); + } else { + holder.lastSeen.setVisibility(View.VISIBLE); + holder.lastSeen.setText(lastSeenText); + } bindModeIconTriple( holder.iconAod, holder.crossAod, holder.iconLock, holder.crossLock, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java index ba97d0e..17307d5 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java @@ -59,6 +59,8 @@ public class IconsDebugFragment extends Fragment { root.findViewById(R.id.debug_visualize_chip_container_switch); SwitchMaterial visualizeCameraCutoutSwitch = root.findViewById(R.id.debug_visualize_camera_cutout_switch); + SwitchMaterial visualizeAodStockContainersSwitch = + root.findViewById(R.id.debug_visualize_aod_stock_containers_switch); MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button); SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch); TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified); @@ -106,13 +108,19 @@ public class IconsDebugFragment extends Fragment { visualizeCameraCutoutSwitch, SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT); + bindBooleanSwitch( + prefs, + visualizeAodStockContainersSwitch, + SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, + SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT); bindRestartSystemUiButton(restartSystemUiButton); updateVisualizerSwitchAvailability( prefs, visualizeNotificationContainerSwitch, visualizeStatusContainerSwitch, visualizeClockContainerSwitch, - visualizeChipContainerSwitch); + visualizeChipContainerSwitch, + visualizeAodStockContainersSwitch); Runnable refreshCameraUi = () -> updateCameraUi( prefs, currentCameraIdentified, @@ -134,7 +142,8 @@ public class IconsDebugFragment extends Fragment { visualizeNotificationContainerSwitch, visualizeStatusContainerSwitch, visualizeClockContainerSwitch, - visualizeChipContainerSwitch); + visualizeChipContainerSwitch, + visualizeAodStockContainersSwitch); } }; prefs.registerOnSharedPreferenceChangeListener(listener); @@ -192,7 +201,8 @@ public class IconsDebugFragment extends Fragment { SwitchMaterial notificationSwitch, SwitchMaterial statusSwitch, SwitchMaterial clockSwitch, - SwitchMaterial chipSwitch) { + SwitchMaterial chipSwitch, + SwitchMaterial aodStockSwitch) { boolean layoutEnabled = prefs != null && prefs.getBoolean( SbtSettings.KEY_CLOCK_ENABLED, SbtDefaults.CLOCK_ENABLED_DEFAULT); @@ -208,6 +218,9 @@ public class IconsDebugFragment extends Fragment { if (chipSwitch != null) { chipSwitch.setEnabled(layoutEnabled); } + if (aodStockSwitch != null) { + aodStockSwitch.setEnabled(layoutEnabled); + } } private void restartSystemUi() { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java index 1bb480f..86c1e7e 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java @@ -153,6 +153,7 @@ public final class LayoutFragment extends Fragment { root.findViewById(R.id.chip_vertical_offset_plus_fast), root.findViewById(R.id.chip_vertical_offset_reset), SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT); + addChipHeightControl(root, prefs); bindStepper(prefs, SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, root.findViewById(R.id.status_vertical_offset_input), @@ -226,10 +227,12 @@ public final class LayoutFragment extends Fragment { SbtSettings::layoutNotifMiddleAutoNearestSideKey, SbtSettings::layoutNotifMiddleSideKey, SbtSettings::layoutNotifVerticalOffsetKey, + SbtSettings.KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT, LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS); setupUnlockedIconContainerCount( root, @@ -244,10 +247,12 @@ public final class LayoutFragment extends Fragment { SbtSettings::layoutStatusMiddleAutoNearestSideKey, SbtSettings::layoutStatusMiddleSideKey, SbtSettings::layoutStatusVerticalOffsetKey, + SbtSettings.KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT, LayoutSectionCopySupport.Section.STATUS); return root; } @@ -269,10 +274,12 @@ public final class LayoutFragment extends Fragment { IndexedKey autoNearestKey, IndexedKey middleSideKey, IndexedKey verticalOffsetKey, + String iconHeightKey, String positionDefault, boolean autoNearestDefault, String middleSideDefault, int verticalOffsetDefault, + int iconHeightDefault, LayoutSectionCopySupport.Section copySection ) { CheckBox checkbox = root.findViewById(checkboxId); @@ -319,6 +326,11 @@ public final class LayoutFragment extends Fragment { rebuild[0].run(); } })); + countContainer.addView(iconHeightControl( + prefs, + iconHeightKey, + iconHeightDefault, + R.string.layout_icon_height_steps)); int sourceCardIndex = cardsParent != null ? cardsParent.indexOfChild(sourceCard) : -1; if (cardsParent == null || sourceCardIndex < 0) { return; @@ -407,6 +419,101 @@ public final class LayoutFragment extends Fragment { return outer; } + private View iconHeightControl( + SharedPreferences prefs, + String key, + int defaultValue, + int labelRes + ) { + LinearLayout outer = new LinearLayout(requireContext()); + outer.setOrientation(LinearLayout.VERTICAL); + TextView label = new TextView(requireContext()); + label.setText(labelRes); + label.setPadding(0, dp(8), 0, 0); + outer.addView(label); + + LinearLayout row = new LinearLayout(requireContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + MaterialButton minus = smallButton("-"); + MaterialButton plus = smallButton("+"); + MaterialButton reset = smallButton("D"); + EditText input = new EditText(requireContext()); + input.setGravity(android.view.Gravity.CENTER); + input.setSingleLine(true); + input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(3)}); + int current = clampValue( + prefs.getInt(key, defaultValue), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX); + input.setText(String.valueOf(current)); + row.addView(minus, compactButtonParams()); + row.addView(input, compactInputParams()); + row.addView(plus, compactButtonParams()); + LinearLayout.LayoutParams resetParams = compactButtonParams(); + resetParams.leftMargin = dp(8); + row.addView(reset, resetParams); + outer.addView(row); + + View.OnClickListener listener = view -> { + int next = clampValue( + parseInt(input, current) + (view == minus ? -1 : 1), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX); + prefs.edit().putInt(key, next).apply(); + SbtSettings.ensureReadable(requireContext()); + input.setText(String.valueOf(next)); + }; + minus.setOnClickListener(listener); + plus.setOnClickListener(listener); + reset.setOnClickListener(v -> { + int next = clampValue( + defaultValue, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX); + prefs.edit().putInt(key, next).apply(); + SbtSettings.ensureReadable(requireContext()); + input.setText(String.valueOf(next)); + }); + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clampValue( + parseInt(input, current), + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX); + prefs.edit().putInt(key, next).apply(); + SbtSettings.ensureReadable(requireContext()); + input.setText(String.valueOf(next)); + } + }); + return outer; + } + + private void addChipHeightControl(View root, SharedPreferences prefs) { + View offsetInput = root.findViewById(R.id.chip_vertical_offset_input); + if (offsetInput == null) { + return; + } + View row = offsetInput; + while (row != null + && !(row instanceof LinearLayout && row.getParent() instanceof LinearLayout)) { + if (!(row.getParent() instanceof View)) { + return; + } + row = (View) row.getParent(); + } + if (row == null || !(row.getParent() instanceof LinearLayout parent)) { + return; + } + int index = parent.indexOfChild(row); + parent.addView(iconHeightControl( + prefs, + SbtSettings.KEY_LAYOUT_CHIP_HEIGHT_STEPS, + SbtDefaults.LAYOUT_CHIP_HEIGHT_STEPS_DEFAULT, + R.string.layout_chip_height_steps), Math.min(parent.getChildCount(), index + 1)); + } + private View iconCopyCardFromLayout( int sourceCheckboxId, int titleId, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutSectionCopySupport.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutSectionCopySupport.java index 242820e..397481b 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutSectionCopySupport.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutSectionCopySupport.java @@ -187,12 +187,14 @@ final class LayoutSectionCopySupport { copyInt(editor, prefs, src.maxIcons, dst.maxIcons, src.maxIconsDefault); copyBoolean(editor, prefs, src.evenDistribution, dst.evenDistribution, src.evenDistributionDefault); copyBoolean(editor, prefs, src.limitWidth, dst.limitWidth, src.limitWidthDefault); + copyInt(editor, prefs, src.iconHeight, dst.iconHeight, src.iconHeightDefault); } private static void copyIcons(SharedPreferences.Editor editor, SharedPreferences prefs, KeySet src, KeySet dst) { copyBoolean(editor, prefs, src.enabled, dst.enabled, src.enabledDefault); int count = prefs.getInt(src.count, src.countDefault); editor.putInt(dst.count, count); + copyInt(editor, prefs, src.iconHeight, dst.iconHeight, src.iconHeightDefault); for (int i = 0; i < MAX_COPIES; i++) { copyString(editor, prefs, src.positionAt(i), dst.positionAt(i), src.positionDefault); copyBoolean(editor, prefs, src.autoNearestAt(i), dst.autoNearestAt(i), src.autoNearestDefault); @@ -277,12 +279,14 @@ final class LayoutSectionCopySupport { "layout_lock_notifications_middle_auto_nearest_side", "layout_lock_notifications_middle_side", "layout_lock_notifications_vertical_offset_px", + SbtSettings.layoutLockNotifIconHeightKey(), SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT); } if (mode == Mode.AOD) { return iconKeys( @@ -292,12 +296,14 @@ final class LayoutSectionCopySupport { "layout_aod_notifications_middle_auto_nearest_side", "layout_aod_notifications_middle_side", "layout_aod_notifications_vertical_offset_px", + SbtSettings.layoutAodNotifIconHeightKey(), SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT); } return iconKeys( SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, @@ -306,12 +312,14 @@ final class LayoutSectionCopySupport { SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE, SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE, SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, + SbtSettings.KEY_LAYOUT_NOTIF_ICON_HEIGHT_STEPS, SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ICON_HEIGHT_STEPS_DEFAULT); } private static KeySet statusKeys(Mode mode) { @@ -323,12 +331,14 @@ final class LayoutSectionCopySupport { "layout_lock_status_middle_auto_nearest_side", "layout_lock_status_middle_side", "layout_lock_status_vertical_offset_px", + SbtSettings.layoutLockStatusIconHeightKey(), SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT); } if (mode == Mode.AOD) { return iconKeys( @@ -338,12 +348,14 @@ final class LayoutSectionCopySupport { "layout_aod_status_middle_auto_nearest_side", "layout_aod_status_middle_side", "layout_aod_status_vertical_offset_px", + SbtSettings.layoutAodStatusIconHeightKey(), SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT); } return iconKeys( SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, @@ -352,12 +364,14 @@ final class LayoutSectionCopySupport { SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE, SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE, SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, + SbtSettings.KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS, SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, - SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT); } private static KeySet cardKeys(Mode mode) { @@ -367,20 +381,24 @@ final class LayoutSectionCopySupport { SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW, SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION, SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH, + SbtSettings.KEY_CARDS_AOD_ICON_HEIGHT_STEPS, SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT, SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT, - SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT); + SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT, + SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT); } return KeySet.cards( SbtSettings.KEY_CARDS_LOCK_MAX_ROWS, SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION, SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, + SbtSettings.KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT, - SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT); + SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT, + SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT); } private static KeySet iconKeys( @@ -390,12 +408,14 @@ final class LayoutSectionCopySupport { String autoNearest, String middleSide, String verticalOffset, + String iconHeight, boolean enabledDefault, int countDefault, String positionDefault, boolean autoNearestDefault, String middleSideDefault, - int verticalOffsetDefault + int verticalOffsetDefault, + int iconHeightDefault ) { return new KeySet( enabled, @@ -403,12 +423,14 @@ final class LayoutSectionCopySupport { autoNearest, middleSide, verticalOffset, + iconHeight, count, enabledDefault, positionDefault, autoNearestDefault, middleSideDefault, verticalOffsetDefault, + iconHeightDefault, countDefault); } @@ -537,12 +559,14 @@ final class LayoutSectionCopySupport { final String autoNearest; final String middleSide; final String verticalOffset; + final String iconHeight; final String count; final boolean enabledDefault; final String positionDefault; final boolean autoNearestDefault; final String middleSideDefault; final int verticalOffsetDefault; + final int iconHeightDefault; final int countDefault; final String maxRows; final String maxIcons; @@ -566,18 +590,53 @@ final class LayoutSectionCopySupport { String middleSideDefault, int verticalOffsetDefault, int countDefault + ) { + this( + enabled, + position, + autoNearest, + middleSide, + verticalOffset, + null, + count, + enabledDefault, + positionDefault, + autoNearestDefault, + middleSideDefault, + verticalOffsetDefault, + 0, + countDefault); + } + + KeySet( + String enabled, + String position, + String autoNearest, + String middleSide, + String verticalOffset, + String iconHeight, + String count, + boolean enabledDefault, + String positionDefault, + boolean autoNearestDefault, + String middleSideDefault, + int verticalOffsetDefault, + int iconHeightDefault, + int countDefault ) { this.enabled = enabled; this.position = position; this.autoNearest = autoNearest; this.middleSide = middleSide; this.verticalOffset = verticalOffset; + this.iconHeight = iconHeight; this.count = count; this.enabledDefault = enabledDefault; this.positionDefault = positionDefault; this.autoNearestDefault = autoNearestDefault; this.middleSideDefault = middleSideDefault; this.verticalOffsetDefault = verticalOffsetDefault; + this.iconHeightDefault = iconHeightDefault; this.countDefault = countDefault; this.maxRows = null; this.maxIcons = null; @@ -594,13 +653,16 @@ final class LayoutSectionCopySupport { String maxIcons, String evenDistribution, String limitWidth, + String iconHeight, int maxRowsDefault, int maxIconsDefault, boolean evenDistributionDefault, - boolean limitWidthDefault + boolean limitWidthDefault, + int iconHeightDefault ) { return new KeySet(maxRows, maxIcons, evenDistribution, limitWidth, - maxRowsDefault, maxIconsDefault, evenDistributionDefault, limitWidthDefault); + iconHeight, maxRowsDefault, maxIconsDefault, + evenDistributionDefault, limitWidthDefault, iconHeightDefault); } private KeySet( @@ -608,22 +670,26 @@ final class LayoutSectionCopySupport { String maxIcons, String evenDistribution, String limitWidth, + String iconHeight, int maxRowsDefault, int maxIconsDefault, boolean evenDistributionDefault, - boolean limitWidthDefault + boolean limitWidthDefault, + int iconHeightDefault ) { this.enabled = null; this.position = null; this.autoNearest = null; this.middleSide = null; this.verticalOffset = null; + this.iconHeight = iconHeight; this.count = null; this.enabledDefault = false; this.positionDefault = null; this.autoNearestDefault = false; this.middleSideDefault = null; this.verticalOffsetDefault = 0; + this.iconHeightDefault = iconHeightDefault; this.countDefault = 0; this.maxRows = maxRows; this.maxIcons = maxIcons; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java index 64ae8fe..c4968f4 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java @@ -174,6 +174,8 @@ abstract class LockedSceneLayoutFragment extends Fragment { SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + aod ? SbtSettings.layoutAodStatusIconHeightKey() : SbtSettings.layoutLockStatusIconHeightKey(), + SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT, LayoutSectionCopySupport.Section.STATUS, false, refresh)); @@ -205,6 +207,10 @@ abstract class LockedSceneLayoutFragment extends Fragment { SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + aod ? SbtSettings.layoutAodNotifIconHeightKey() : SbtSettings.layoutLockNotifIconHeightKey(), + aod + ? SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT + : SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT, LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS, false, refresh)); @@ -309,6 +315,8 @@ abstract class LockedSceneLayoutFragment extends Fragment { boolean autoNearestDefault, String middleSideDefault, int verticalOffsetDefault, + String iconHeightKey, + int iconHeightDefault, LayoutSectionCopySupport.Section copySection, boolean cardsMode, @Nullable Runnable refresh @@ -346,6 +354,8 @@ abstract class LockedSceneLayoutFragment extends Fragment { middleSideDefault, indexedSceneKey(base + "_vertical_offset_px", 0), verticalOffsetDefault, + iconHeightKey, + iconHeightDefault, legacyEnabledKey, countKey, count, @@ -381,6 +391,8 @@ abstract class LockedSceneLayoutFragment extends Fragment { indexedSceneKey(base + "_vertical_offset_px", i), verticalOffsetDefault, null, + 0, + null, null, count, null)); @@ -440,6 +452,8 @@ abstract class LockedSceneLayoutFragment extends Fragment { String middleSideDefault, String verticalOffsetKey, int verticalOffsetDefault, + @Nullable String iconHeightKey, + int iconHeightDefault, @Nullable String legacyEnabledKey, @Nullable String countKey, int count, @@ -453,6 +467,18 @@ abstract class LockedSceneLayoutFragment extends Fragment { sectionContent.addView( iconContainerCountControl(context, prefs, countKey, legacyEnabledKey, count, onCountChanged), insertIndex); + if (iconHeightKey != null) { + sectionContent.addView( + sizeStepper( + context, + prefs, + R.string.layout_icon_height_steps, + iconHeightKey, + iconHeightDefault, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX), + Math.min(insertIndex + 1, sectionContent.getChildCount())); + } } updateIconCardTitle(card, titleId, index, count); bindXmlPositionSection( @@ -623,6 +649,16 @@ abstract class LockedSceneLayoutFragment extends Fragment { R.string.label_limit_screen_width, aod ? SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH : SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, aod ? SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT : SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT)); + content.addView(sizeStepper( + context, + prefs, + R.string.layout_icon_height_steps, + aod ? SbtSettings.KEY_CARDS_AOD_ICON_HEIGHT_STEPS : SbtSettings.KEY_CARDS_LOCK_ICON_HEIGHT_STEPS, + aod + ? SbtDefaults.LAYOUT_CARDS_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT + : SbtDefaults.LAYOUT_CARDS_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MIN, + SbtDefaults.LAYOUT_ICON_HEIGHT_STEPS_MAX)); return card(context, R.string.layout_notification_position_title, content); } @@ -855,6 +891,23 @@ abstract class LockedSceneLayoutFragment extends Fragment { } private View stepper(Context context, SharedPreferences prefs, int label, String key, int def, int min, int max) { + return stepper(context, prefs, label, key, def, min, max, false); + } + + private View sizeStepper(Context context, SharedPreferences prefs, int label, String key, int def, int min, int max) { + return stepper(context, prefs, label, key, def, min, max, true); + } + + private View stepper( + Context context, + SharedPreferences prefs, + int label, + String key, + int def, + int min, + int max, + boolean showDefaultButton + ) { LinearLayout outer = vertical(context); outer.addView(body(context, label)); LinearLayout row = new LinearLayout(context); @@ -862,11 +915,17 @@ abstract class LockedSceneLayoutFragment extends Fragment { row.setGravity(Gravity.CENTER_VERTICAL); MaterialButton minus = button(context, "-"); MaterialButton plus = button(context, "+"); + MaterialButton reset = showDefaultButton ? button(context, "D") : null; EditText input = numericInput(context, true); input.setText(String.valueOf(clamp(prefs.getInt(key, def), min, max))); row.addView(minus, buttonParams()); row.addView(input, inputParams()); row.addView(plus, buttonParams()); + if (reset != null) { + LinearLayout.LayoutParams resetParams = buttonParams(); + resetParams.leftMargin = dp(8); + row.addView(reset, resetParams); + } outer.addView(row); View.OnClickListener listener = view -> { int delta = view == minus ? -1 : 1; @@ -876,6 +935,13 @@ abstract class LockedSceneLayoutFragment extends Fragment { }; minus.setOnClickListener(listener); plus.setOnClickListener(listener); + if (reset != null) { + reset.setOnClickListener(v -> { + int next = clamp(def, min, max); + input.setText(String.valueOf(next)); + persistIntPref(prefs, key, next); + }); + } input.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) { int next = clamp(parseInt(input, def), min, max); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java index 0a93a51..6783e69 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java @@ -64,7 +64,7 @@ public class SystemIconsFragment extends Fragment { "hotspot"), new SystemIconEntry(R.string.system_icon_nfc, new String[]{"stat_sys_nfc"}, - android.R.drawable.ic_menu_share, + R.drawable.sbt_ic_nfc, "nfc_on", "nfc"), new SystemIconEntry(R.string.system_icon_vpn, new String[]{"stat_sys_branded_vpn"}, diff --git a/app/src/main/res/drawable/sbt_ic_nfc.xml b/app/src/main/res/drawable/sbt_ic_nfc.xml new file mode 100644 index 0000000..a5802c2 --- /dev/null +++ b/app/src/main/res/drawable/sbt_ic_nfc.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_icons_debug.xml b/app/src/main/res/layout/fragment_icons_debug.xml index de05698..01a0608 100644 --- a/app/src/main/res/layout/fragment_icons_debug.xml +++ b/app/src/main/res/layout/fragment_icons_debug.xml @@ -70,6 +70,12 @@ android:layout_height="wrap_content" android:text="@string/debug_visualize_camera_cutout" /> + + diff --git a/app/src/main/res/layout/item_hidden_app.xml b/app/src/main/res/layout/item_hidden_app.xml index 825c5f9..5f6fa77 100644 --- a/app/src/main/res/layout/item_hidden_app.xml +++ b/app/src/main/res/layout/item_hidden_app.xml @@ -109,6 +109,15 @@ android:ellipsize="end" android:maxLines="1" android:textAppearance="?attr/textAppearanceCaption" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e8d231d..c5f42e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -26,6 +26,7 @@ Clock container Status chip container Camera cutout + AOD stock containers and derived statusbar SystemUI Restart SystemUI SystemUI restart requested. @@ -100,6 +101,7 @@ Close Regex %1$s \u2022 %2$d exceptions + Last seen: %1$s Default settings Add exception Delete @@ -171,6 +173,8 @@ Enabled Copy from Number of containers + Icon height (% of statusbar) + Chip height (% of statusbar) Locked notifications mode Mirrors the Samsung notification display style for AOD and lockscreen. Misc diff --git a/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java b/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java index 6c03cbf..65dcee0 100644 --- a/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java +++ b/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java @@ -155,6 +155,89 @@ public final class PackedStatusBarLayoutSolverTest { assertTrue(notifications.width() == 65.0); } + @Test + public void singleIconGroupsKeepDotWhenChipConsumesSpace() { + ArrayList items = new ArrayList<>(); + LayoutItem notifications = iconItem("Notifs", "Notifs", "notifications", 4, 30, 120, 0, 30); + LayoutItem status = iconItem("Status", "Status", "status", 4, 30, 120, 0, 30); + LayoutItem chip = LayoutItem.fixed("Chip", "Chip", Position.LEFT, Fallback.LEFT, 200, 30, 0); + items.add(notifications); + items.add(status); + items.add(chip); + + PackedStatusBarLayoutSolver.solve( + new Spec(70, 0, 0, 0, new ArrayList<>(Arrays.asList("Notifs", "Status", "Chip"))), + items); + + assertTrue(notifications.visibleIcons == 0); + assertTrue(notifications.dot); + assertTrue(notifications.width() > 0); + assertTrue(status.visibleIcons == 0); + assertTrue(status.dot); + assertTrue(status.width() > 0); + } + + @Test + public void groupEdgeDotIsNotCompactedAgainstNonIconNeighbor() { + ArrayList items = new ArrayList<>(); + items.add(new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(102, 14, 33, 38), + new Box(0, 32, 134, 47)))); + LayoutItem status = iconItem("Status", "Status", "status", 4, 27, 108, 39, 36); + status.direction = PackedStatusBarLayoutSolver.Direction.LEFT; + LayoutItem chip = LayoutItem.fixed("Chip", "Chip", Position.LEFT, Fallback.LEFT, 200, 44, 33); + LayoutItem notifications = iconItem("Notifs", "Notifs", "notifications", 4, 27, 108, 36, 36); + notifications.direction = PackedStatusBarLayoutSolver.Direction.RIGHT; + items.add(status); + items.add(chip); + items.add(notifications); + + PackedStatusBarLayoutSolver.solve( + productionSpec(834, 387, 59, 0), + items); + + assertTrue(status.visibleIcons == 0); + assertTrue(status.dot); + assertTrue(status.width() == 27.0); + } + + @Test + public void finalIconGroupKeepsDotWhenChipConsumesAllRemainingSpaceBeforeCutout() { + ArrayList items = new ArrayList<>(); + items.add(new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(102, 14, 33, 38), + new Box(0, 32, 134, 47)))); + LayoutItem status = iconItem("Status", "Status", "status", 4, 24, 96, 39, 32); + status.direction = PackedStatusBarLayoutSolver.Direction.LEFT; + LayoutItem chip = LayoutItem.fixed("Chip", "Chip", Position.LEFT, Fallback.LEFT, 397, 44, 33); + LayoutItem notifications = iconItem("Notifs", "Notifs", "notifications", 4, 29, 116, 36, 39); + notifications.direction = PackedStatusBarLayoutSolver.Direction.RIGHT; + notifications.compactDotWidth = 27; + items.add(status); + items.add(chip); + items.add(notifications); + + PackedStatusBarLayoutSolver.solve( + productionSpec(834, 387, 59, 0), + items); + + assertTrue(notifications.visibleIcons == 0); + assertTrue(notifications.dot); + assertTrue(notifications.width() > 0); + assertTrue(notifications.right() <= 387); + assertTrue(chip.right() <= notifications.left()); + } + private static Spec cutoutSpec() { return new Spec( 1200, diff --git a/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java b/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java index a68d339..ff2cafd 100644 --- a/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java +++ b/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java @@ -66,6 +66,42 @@ public final class UnlockedLayoutBandTest { assertEquals("icon4", band.placements.get(2).key); } + @Test + public void clippedStatusChipRendersAtClippedSourceWidthInsteadOfCroppingScaledBitmap() { + ArrayList placements = new ArrayList<>(); + placements.add(new SnapshotPlacement( + null, + new Bounds(181, 26, 417, 59), + "chip", + false, + false, + 0xffffffff, + null, + new Bounds(0, 7, 397, 44), + 0, + new Bounds(181, 26, 417, 59))); + UnlockedLayoutBand band = UnlockedLayoutBand.icons( + LayoutItemKind.CHIP, + null, + null, + placements, + LayoutPosition.LEFT, + false, + AnchorSide.LEFT, + false); + + band.prepareForSide(AnchorSide.RIGHT, "normal"); + band.applyPackedContinuousWidth(211, AnchorSide.RIGHT); + band.place(182); + + assertEquals(1, band.placements.size()); + SnapshotPlacement chip = band.placements.get(0); + assertEquals(231, chip.bounds.width); + assertEquals(211, chip.contentBounds.width); + assertEquals(211, chip.sourceRenderBounds.width); + assertEquals(0, chip.sourceOffsetX); + } + private static ArrayList placements(int count, int width, int height) { ArrayList result = new ArrayList<>(); for (int i = 0; i < count; i++) {