Finish lockscreen layout support

This commit is contained in:
ajp_anton
2026-05-30 17:51:11 +00:00
parent bd7b07def0
commit 86888774cc
19 changed files with 2905 additions and 110 deletions
@@ -10,11 +10,14 @@ import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
@@ -22,7 +25,10 @@ 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.mode.NotificationDisplayStyle;
import se.ajpanton.statusbartweak.runtime.mode.SystemNotificationDisplayStyleDetector;
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.UnlockedIconSnapshotRenderController;
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult;
@@ -37,6 +43,8 @@ final class StockLayoutCanvasController {
private static final String CLASS_AOD_ICONS_CONTROLLER = "aod.T4";
private static final String CLASS_AOD_ICONS_ONLY_CONTAINER =
"com.samsung.android.uniform.widget.notification.NotificationIconsOnlyContainer";
private static final String CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER =
"com.android.systemui.statusbar.phone.LegacyNotificationIconAreaControllerImpl";
private static final Set<String> TARGET_ID_NAMES = Set.of(
"left_clock_container",
@@ -60,6 +68,8 @@ final class StockLayoutCanvasController {
private final Map<View, HiddenViewState> hiddenViewStates = new WeakHashMap<>();
private final Map<View, RootGeometry> rootGeometries = new WeakHashMap<>();
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
private final Map<View, Integer> settingsIdentityByRoot = new WeakHashMap<>();
private final Map<View, Long> clockTimeBucketByRoot = new WeakHashMap<>();
private final Set<View> confirmedAodPluginViews =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> layoutOffCleanedRoots =
@@ -70,12 +80,18 @@ final class StockLayoutCanvasController {
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> trackedLockedStatusBarIconSurfaces =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingUnlockedAppearanceRefreshRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> dirtyUnlockedLayoutRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<ClassLoader> hookedPluginClassLoaders =
Collections.newSetFromMap(new IdentityHashMap<>());
private final Map<View, UnlockedHosts> unlockedHostsByRoot = new WeakHashMap<>();
private final OwnedIconHostManager ownedIconHostManager = new OwnedIconHostManager();
private final DebugBoundsOverlayController debugBoundsOverlayController =
new DebugBoundsOverlayController();
private final LockscreenCardsNotificationStripRenderer lockscreenCardsNotificationStripRenderer =
new LockscreenCardsNotificationStripRenderer();
private final UnlockedIconSnapshotRenderController unlockedIconRenderController;
// The keyguard carrier text lives outside the statusbar root on this device.
// Keep a weak source reference so the lockscreen statusbar can render it when
@@ -94,10 +110,212 @@ final class StockLayoutCanvasController {
return;
}
installPluginManagerHooks(classLoader);
installCardsNotificationIconSourceHook(classLoader);
installUnlockedAppearanceInvalidationHooks(classLoader);
installViewRootHook(classLoader);
hooksInstalled = true;
}
private void installUnlockedAppearanceInvalidationHooks(ClassLoader classLoader) {
Class<?> darkIconDispatcherClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.DarkIconDispatcherImpl",
classLoader);
if (darkIconDispatcherClass != null) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
darkIconDispatcherClass,
"applyIconTint",
chain -> {
Object result = chain.proceed();
scheduleUnlockedAppearanceRefreshForAllRoots();
return result;
});
}
Class<?> statusBarIconViewClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.StatusBarIconView",
classLoader);
if (statusBarIconViewClass != null) {
hookViewAppearanceInvalidation(statusBarIconViewClass, "onDarkChanged");
hookViewAppearanceInvalidation(statusBarIconViewClass, "setStaticDrawableColor");
hookViewAppearanceInvalidation(statusBarIconViewClass, "setDecorColor");
hookViewAppearanceInvalidation(statusBarIconViewClass, "setIconColor");
hookViewAppearanceInvalidation(statusBarIconViewClass, "updateIconColor");
hookViewLayoutInvalidation(statusBarIconViewClass, "onLayout");
hookViewLayoutInvalidation(statusBarIconViewClass, "setVisibleState");
}
Class<?> notificationIconAreaClass = ReflectionSupport.findClassIfExists(
CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER,
classLoader);
if (notificationIconAreaClass != null) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
notificationIconAreaClass,
"onDarkChanged",
chain -> {
Object result = chain.proceed();
scheduleUnlockedAppearanceRefreshForAllRoots();
return result;
});
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
notificationIconAreaClass,
"updateTintForIcon",
chain -> {
Object result = chain.proceed();
Object icon = chain.getArgs().isEmpty() ? null : chain.getArg(0);
if (icon instanceof View view) {
scheduleUnlockedAppearanceRefresh(view);
} else {
scheduleUnlockedAppearanceRefreshForAllRoots();
}
return result;
});
}
Class<?> clockClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.policy.QSClockIndicatorView",
classLoader);
if (clockClass != null) {
hookViewAppearanceInvalidation(clockClass, "onDarkChanged");
hookViewAppearanceInvalidation(clockClass, "setTextColor");
hookClockTextInvalidation(clockClass, "notifyTimeChanged");
hookClockTextInvalidation(clockClass, "setText");
hookViewLayoutInvalidation(clockClass, "onLayout");
}
Class<?> notificationContainerClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.NotificationIconContainer",
classLoader);
if (notificationContainerClass != null) {
hookViewLayoutInvalidation(notificationContainerClass, "onLayout");
hookViewLayoutInvalidation(notificationContainerClass, "calculateIconXTranslations");
}
Class<?> statusIconContainerClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.StatusIconContainer",
classLoader);
if (statusIconContainerClass != null) {
hookViewLayoutInvalidation(statusIconContainerClass, "onLayout");
}
Class<?> batteryClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.battery.BatteryMeterView",
classLoader);
if (batteryClass != null) {
hookViewLayoutInvalidation(batteryClass, "onLayout");
}
}
private void hookViewAppearanceInvalidation(Class<?> type, String methodName) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
type,
methodName,
chain -> {
Object result = chain.proceed();
if (chain.getThisObject() instanceof View view) {
scheduleUnlockedAppearanceRefresh(view);
} else {
scheduleUnlockedAppearanceRefreshForAllRoots();
}
return result;
});
}
private void hookViewLayoutInvalidation(Class<?> type, String methodName) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
type,
methodName,
chain -> {
Object result = chain.proceed();
if (chain.getThisObject() instanceof View view) {
markUnlockedLayoutDirty(view);
}
return result;
});
}
private void hookClockTextInvalidation(Class<?> type, String methodName) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
type,
methodName,
chain -> {
Object result = chain.proceed();
if (chain.getThisObject() instanceof View view) {
scheduleUnlockedClockTextRefresh(view);
}
return result;
});
}
private void installCardsNotificationIconSourceHook(ClassLoader classLoader) {
Class<?> controllerClass = ReflectionSupport.findClassIfExists(
CLASS_LEGACY_NOTIFICATION_ICON_AREA_CONTROLLER,
classLoader);
if (controllerClass == null) {
return;
}
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
controllerClass,
"updateIconsForLayout",
chain -> {
List<Object> args = chain.getArgs();
int functionIndex = -1;
int containerIndex = -1;
for (int i = 0; i < args.size(); i++) {
Object arg = args.get(i);
if (functionIndex < 0 && arg instanceof Function<?, ?>) {
functionIndex = i;
}
if (containerIndex < 0
&& arg instanceof View view
&& view.getClass().getName().contains("NotificationIconContainer")) {
containerIndex = i;
}
}
if (functionIndex < 0 || containerIndex < 0) {
return chain.proceed();
}
Object containerObj = args.get(containerIndex);
if (!(containerObj instanceof ViewGroup container)) {
return chain.proceed();
}
if (!isCardsNotificationIconSourceContainer(container)) {
Object result = chain.proceed();
markUnlockedLayoutDirty(container);
return result;
}
@SuppressWarnings("unchecked")
Function<Object, Object> original = (Function<Object, Object>) args.get(functionIndex);
ArrayList<LockscreenCardsNotificationIconSourceStore.SourceEntry> captured =
new ArrayList<>();
Function<Object, Object> wrapped = entryObj -> {
String key = LockscreenCardsNotificationIconSourceStore.keyForEntry(entryObj);
Object result = original.apply(entryObj);
if (result == null) {
result = fallbackStatusBarIcon(entryObj);
}
if (result instanceof View view && !isStockOverflowDotView(view)) {
if (key == null) {
key = LockscreenCardsNotificationIconSourceStore.keyForIconView(view);
}
captured.add(LockscreenCardsNotificationIconSourceStore.source(view, key));
}
return result;
};
Object[] newArgs = args.toArray(new Object[0]);
newArgs[functionIndex] = wrapped;
Object result = chain.proceed(newArgs);
LockscreenCardsNotificationIconSourceStore.put(container, captured);
return result;
});
}
private void installPluginManagerHooks(ClassLoader classLoader) {
Class<?> managerClass = ReflectionSupport.findClassIfExists(CLASS_PLUGIN_AOD_MANAGER, classLoader);
if (managerClass == null) {
@@ -176,6 +394,10 @@ final class StockLayoutCanvasController {
}
private void applyBeforeDraw(View root) {
applyBeforeDrawInternal(root);
}
private void applyBeforeDrawInternal(View root) {
if (root == null) {
return;
}
@@ -190,17 +412,130 @@ final class StockLayoutCanvasController {
if (activeScene != null && !activeScene.isUnlocked()) {
hideTrackedLockedStatusBarIconSurfaces(null);
}
if (activeScene != null
&& activeScene.isLockscreen()
&& activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& canContainLockedStatusBarIconSurfaces(root)) {
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
}
if (!isUnlockedStatusBarRoot(root)) {
return;
}
if (activeScene != null && (activeScene.isUnlocked() || activeScene.isLockscreen())) {
if (activeScene != null && activeScene.isUnlocked()) {
if (isKnownRootGeometryChanged(root)) {
ownedIconHostManager.setUnlockedHostsVisible(root, false);
removeDebugOverlay(root);
}
hideTrackedUnlockedStockSurfaces(root);
}
}
private void scheduleUnlockedAppearanceRefresh(View sourceView) {
View root = unlockedStatusBarRootFor(sourceView);
if (root == null) {
return;
}
scheduleUnlockedAppearanceRefreshForRoot(root);
}
private void scheduleUnlockedClockTextRefresh(View sourceView) {
View root = unlockedStatusBarRootFor(sourceView);
if (root == null) {
return;
}
scheduleUnlockedClockTextRefreshForRoot(root);
}
private void markUnlockedLayoutDirty(View sourceView) {
View root = unlockedStatusBarRootFor(sourceView);
if (root != null) {
dirtyUnlockedLayoutRoots.add(root);
}
}
private void markDirtyIfSettingsChanged(View root, SbtSettings settings, boolean statusBarRoot) {
if (!statusBarRoot || root == null || settings == null) {
return;
}
int identity = System.identityHashCode(settings);
Integer previous = settingsIdentityByRoot.put(root, identity);
if (previous != null && previous != identity) {
dirtyUnlockedLayoutRoots.add(root);
}
}
private void markDirtyIfClockTimeChanged(View root, SbtSettings settings, boolean statusBarRoot) {
if (!statusBarRoot || root == null || settings == null || !settings.clockEnabledUnlocked) {
return;
}
long bucket = clockTimeBucket(settings);
Long previous = clockTimeBucketByRoot.put(root, bucket);
if (previous != null && previous != bucket) {
scheduleUnlockedClockTextRefreshForRoot(root);
}
}
private long clockTimeBucket(SbtSettings settings) {
if (settings == null
|| (!settings.clockShowSeconds
&& !settings.clockShowDatePrefix
&& !settings.clockCustomFormatEnabled)) {
return 0L;
}
String pattern = settings.clockCustomFormat != null ? settings.clockCustomFormat : "";
boolean seconds = settings.clockShowSeconds
|| (settings.clockCustomFormatEnabled && pattern.contains("s"));
long divisor = seconds ? 1_000L : 60_000L;
return System.currentTimeMillis() / divisor;
}
private void scheduleUnlockedAppearanceRefreshForAllRoots() {
for (View root : new ArrayList<>(unlockedHostsByRoot.keySet())) {
scheduleUnlockedAppearanceRefreshForRoot(root);
}
}
private void scheduleUnlockedAppearanceRefreshForRoot(View root) {
if (root == null
|| !root.isAttachedToWindow()
|| !isUnlockedStatusBarRoot(root)
|| !pendingUnlockedAppearanceRefreshRoots.add(root)) {
return;
}
root.post(() -> {
pendingUnlockedAppearanceRefreshRoots.remove(root);
refreshUnlockedAppearanceNow(root);
});
}
private void scheduleUnlockedClockTextRefreshForRoot(View root) {
if (root == null
|| !root.isAttachedToWindow()
|| !isUnlockedStatusBarRoot(root)
|| !pendingUnlockedAppearanceRefreshRoots.add(root)) {
return;
}
root.post(() -> {
pendingUnlockedAppearanceRefreshRoots.remove(root);
refreshUnlockedClockTextNow(root);
});
}
private void refreshUnlockedAppearanceNow(View root) {
if (root == null || !root.isAttachedToWindow()) {
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.clockEnabled) {
return;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene == null || !activeScene.isUnlocked()) {
return;
}
UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root);
if (unlockedHosts != null) {
if (unlockedHosts == null || !areUnlockedHostsAttached(root, unlockedHosts)) {
return;
}
unlockedIconRenderController.refreshUnlockedAppearance(
root,
unlockedHosts.clockHost,
@@ -211,10 +546,48 @@ final class StockLayoutCanvasController {
activeScene);
ownedIconHostManager.bringUnlockedHostsToFront(root);
}
private View unlockedStatusBarRootFor(View view) {
View current = view;
while (current != null) {
if (isUnlockedStatusBarRoot(current) && unlockedHostsByRoot.containsKey(current)) {
return current;
}
Object parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
return null;
}
private void applyToRoot(View root) {
applyToRootInternal(root);
}
private void refreshUnlockedClockTextNow(View root) {
if (root == null || !root.isAttachedToWindow()) {
return;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.clockEnabled || !settings.clockEnabledUnlocked) {
return;
}
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
if (activeScene == null || !activeScene.isUnlocked()) {
return;
}
UnlockedHosts unlockedHosts = unlockedHostsByRoot.get(root);
if (unlockedHosts == null || !areUnlockedHostsAttached(root, unlockedHosts)) {
return;
}
if (unlockedIconRenderController.refreshUnlockedClockText(
root,
unlockedHosts.clockHost,
activeScene)) {
ownedIconHostManager.bringUnlockedHostsToFront(root);
}
}
private void applyToRootInternal(View root) {
if (root == null) {
return;
}
@@ -222,14 +595,12 @@ final class StockLayoutCanvasController {
SbtSettings settings = RuntimeSettingsCache.get(context);
boolean layoutEnabled = settings.clockEnabled;
boolean statusBarRoot = isUnlockedStatusBarRoot(root);
markDirtyIfSettingsChanged(root, settings, statusBarRoot);
markDirtyIfClockTimeChanged(root, settings, statusBarRoot);
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
if (!layoutEnabled) {
if (statusBarRoot) {
applyDebugOverlayIfNeeded(root, false, settings, false);
} else {
removeDebugOverlay(root);
}
cleanupLayoutOffRoot(root);
updateActiveScene(root, context);
applyLayoutOffRoot(root, settings, statusBarRoot, runtimeContext.getModeStateRepository().getActiveScene());
return;
}
layoutOffCleanedRoots.remove(root);
@@ -238,7 +609,10 @@ final class StockLayoutCanvasController {
boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
if (!statusBarRoot) {
if (!unlockedScene && canContainLockedStatusBarIconSurfaces(root)) {
renderLockscreenCardsNotificationStripIfNeeded(root, settings, activeScene);
hideOrDiscoverLockedStatusBarIconSurfaces(root);
} else {
removeLockscreenCardsNotificationHost(root);
}
removeDebugOverlay(root);
return;
@@ -254,6 +628,10 @@ final class StockLayoutCanvasController {
if (!unlockedScene) {
restoreTrackedLockedStatusBarIconSurfaces(root);
}
boolean shouldRender = !unlockedIconRenderController.hasLayoutPlan(root)
|| dirtyUnlockedLayoutRoots.remove(root)
|| !unlockedScene;
if (shouldRender) {
markRootGeometryBeforeDraw(root);
renderResult = unlockedIconRenderController.renderUnlocked(
root,
@@ -270,6 +648,7 @@ final class StockLayoutCanvasController {
ownedIconHostManager.bringUnlockedHostsToFront(root);
}
}
}
if (!unlockedScene) {
trackedUnlockedStockSurfaces.clear();
restoreTransientUnlockedSources();
@@ -306,6 +685,40 @@ final class StockLayoutCanvasController {
});
}
private void applyLayoutOffRoot(
View root,
SbtSettings settings,
boolean statusBarRoot,
SceneKey activeScene
) {
if (statusBarRoot) {
applyDebugOverlayIfNeeded(root, false, settings, false);
} 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,
@@ -442,7 +855,7 @@ final class StockLayoutCanvasController {
if (isLockedStatusBarIconSurface(view)) {
rememberLockedCarrierTextSource(view);
trackedLockedStatusBarIconSurfaces.add(view);
hideView(view, isCarrierView(view));
hideView(view, keepLockedSurfaceLaidOut(view));
}
});
}
@@ -467,7 +880,7 @@ final class StockLayoutCanvasController {
}
if (root == null || isDescendantOf(view, root)) {
foundInRoot = true;
hideView(view, isCarrierView(view));
hideView(view, keepLockedSurfaceLaidOut(view));
}
}
while (!staleViews.isEmpty()) {
@@ -531,7 +944,7 @@ final class StockLayoutCanvasController {
String viewClassName = view.getClass().getName();
boolean carrierSurface = idName.toLowerCase(java.util.Locale.ROOT).contains("carrier")
|| viewClassName.toLowerCase(java.util.Locale.ROOT).contains("carrier");
boolean shelfSurface = viewClassName.contains("NotificationShelf");
boolean shelfSurface = isCardsNotificationIconSurface(view);
boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName)
|| "notification_icononly_area".equals(idName)
|| "keyguard_icononly_container_view".equals(idName)
@@ -573,6 +986,54 @@ final class StockLayoutCanvasController {
return false;
}
private boolean keepLockedSurfaceLaidOut(View view) {
return isCarrierView(view) || isCardsNotificationIconSurface(view);
}
private boolean isCardsNotificationIconSurface(View view) {
if (view == null) {
return false;
}
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);
}
private boolean isCardsNotificationShelfSurface(View view) {
if (view == null) {
return false;
}
String className = view.getClass().getName();
return className.contains("SecShelfNotificationIconContainer")
|| className.contains("NotificationShelfBackground");
}
private boolean isCardsNotificationIconSourceContainer(View view) {
return view != null && view.getClass().getName().contains("SecShelfNotificationIconContainer");
}
private Object fallbackStatusBarIcon(Object entryObj) {
Object icons = ReflectionSupport.getFieldValue(entryObj, "mIcons");
return icons != null ? ReflectionSupport.getFieldValue(icons, "mStatusBarIcon") : null;
}
private boolean isStockOverflowDotView(View view) {
if (view == null) {
return false;
}
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
return idName.contains("overflow")
|| idName.contains("dot")
|| className.contains("overflow")
|| className.contains("dot");
}
private boolean isCarrierView(View view) {
if (view == null) {
return false;
@@ -703,6 +1164,10 @@ final class StockLayoutCanvasController {
unlockedIconRenderController.clearAll();
ownedIconHostManager.removeUnlockedHosts(root);
unlockedHostsByRoot.remove(root);
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
removeDebugOverlay(root);
root.requestLayout();
root.invalidate();
@@ -807,28 +1272,74 @@ final class StockLayoutCanvasController {
return true;
}
private void releaseHiddenViews() {
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;
ArrayDeque<View> views = new ArrayDeque<>(hiddenViewStates.keySet());
while (!views.isEmpty()) {
View view = views.removeFirst();
hiddenViewStates.remove(view);
view.setAlpha(1f);
view.setVisibility(View.VISIBLE);
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);
continue;
}
restoreView(view);
if ((unlockedScene && wasUnlockedStockSurface)
|| (steadyLockscreenScene && isCardsNotificationShelfSurface(view))) {
forceViewVisible(view);
}
}
hiddenViewStates.clear();
trackedStatusChipSources.clear();
if (!deferredLockedShadeSurfaces) {
trackedLockedStatusBarIconSurfaces.clear();
}
trackedUnlockedStockSurfaces.clear();
return deferredLockedShadeSurfaces;
}
private void cleanupLayoutOffRoot(View root) {
private void cleanupLayoutOffRoot(View root, SceneKey activeScene) {
boolean deferredLockedShadeSurfaces = false;
if (!hiddenViewStates.isEmpty()) {
releaseHiddenViews();
deferredLockedShadeSurfaces = releaseHiddenViews(activeScene);
}
trackedStatusChipSources.clear();
if (!deferredLockedShadeSurfaces) {
trackedLockedStatusBarIconSurfaces.clear();
}
trackedUnlockedStockSurfaces.clear();
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
removeLockscreenCardsNotificationHost(root);
return;
}
rootGeometries.remove(root);
@@ -836,6 +1347,40 @@ final class StockLayoutCanvasController {
ownedIconHostManager.removeUnlockedHosts(root);
}
private void forceViewVisible(View view) {
if (view == null) {
return;
}
view.setAlpha(1f);
view.setVisibility(View.VISIBLE);
requestViewAndParentLayout(view);
}
private void requestViewAndParentLayout(View view) {
if (view == null) {
return;
}
view.requestLayout();
view.invalidate();
Object parent = view.getParent();
if (parent instanceof View parentView) {
parentView.requestLayout();
parentView.invalidate();
}
}
private boolean isDescendantOfNotificationShadeWindow(View view) {
View current = view;
while (current != null) {
if (current.getClass().getName().contains("NotificationShadeWindowView")) {
return true;
}
Object parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
return false;
}
private boolean isDescendantOf(View child, View ancestor) {
if (child == null || ancestor == null) {
return false;
@@ -871,6 +1416,97 @@ final class StockLayoutCanvasController {
}
}
private void renderLockscreenCardsNotificationStripIfNeeded(
View root,
SbtSettings settings,
SceneKey activeScene
) {
View visibleBouncerSurface = findVisibleBouncerSurface(root);
boolean suppressForTransition = runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
|| visibleBouncerSurface != null;
if (root == null
|| settings == null
|| activeScene == null
|| !activeScene.isLockscreen()
|| activeScene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS
|| suppressForTransition) {
removeLockscreenCardsNotificationHost(root);
return;
}
View host = ownedIconHostManager.ensureLockscreenCardsNotificationHost(root);
if (host instanceof ViewGroup hostGroup) {
lockscreenCardsNotificationStripRenderer.render(root, hostGroup, settings, activeScene);
host.bringToFront();
}
}
private View findVisibleBouncerSurface(View root) {
if (root == null) {
return null;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(root);
while (!queue.isEmpty()) {
View view = queue.removeFirst();
if (view != root && isVisibleBouncerSurface(view)) {
return view;
}
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 boolean isVisibleBouncerSurface(View view) {
if (view == null || view.getVisibility() != View.VISIBLE || view.getAlpha() <= 0f) {
return false;
}
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
return false;
}
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
return idName.contains("bouncer")
|| idName.contains("keyguard_security")
|| idName.contains("keyguard_pin")
|| idName.contains("keyguard_password")
|| idName.contains("keyguard_pattern")
|| className.contains("bouncer")
|| className.contains("keyguardsecurity")
|| className.contains("keyguardpin")
|| className.contains("keyguardpassword")
|| className.contains("keyguardpattern");
}
private void removeLockscreenCardsNotificationHost(View root) {
View host = root instanceof ViewGroup group
? findDirectOwnedHost(group, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST)
: null;
if (host instanceof ViewGroup hostGroup) {
lockscreenCardsNotificationStripRenderer.clear(hostGroup);
}
ownedIconHostManager.removeHost(root, OwnedIconHostManager.TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
}
private View findDirectOwnedHost(ViewGroup root, String tag) {
if (root == null || tag == null) {
return null;
}
for (int i = 0; i < root.getChildCount(); i++) {
View child = root.getChildAt(i);
if (tag.equals(child.getTag())) {
return child;
}
}
return null;
}
private UnlockedHosts updateUnlockedOwnedHosts(
View root,
SceneKey activeScene
@@ -903,6 +1539,10 @@ final class StockLayoutCanvasController {
unlockedIconRenderController.clearAll();
ownedIconHostManager.removeUnlockedHosts(root);
unlockedHostsByRoot.remove(root);
pendingUnlockedAppearanceRefreshRoots.remove(root);
dirtyUnlockedLayoutRoots.remove(root);
settingsIdentityByRoot.remove(root);
clockTimeBucketByRoot.remove(root);
return null;
}
@@ -932,12 +1572,14 @@ final class StockLayoutCanvasController {
if (runtimeContext.getModeStateRepository().hasSystemUiStatusBarState()) {
return;
}
NotificationDisplayStyle notificationStyle =
SystemNotificationDisplayStyleDetector.read(context);
if (isAodRoot(root) || (isKeyguardLocked(context) && !isInteractive(context))) {
runtimeContext.getModeStateRepository().setAod(NotificationDisplayStyle.UNKNOWN);
runtimeContext.getModeStateRepository().setAod(notificationStyle);
return;
}
if (isKeyguardLocked(context)) {
runtimeContext.getModeStateRepository().setLockscreen(NotificationDisplayStyle.UNKNOWN);
runtimeContext.getModeStateRepository().setLockscreen(notificationStyle);
return;
}
runtimeContext.getModeStateRepository().setUnlocked();
@@ -16,6 +16,7 @@ public final class ModeStateRepository {
private SceneKey activeScene = SceneKey.unlocked();
private boolean systemUiStatusBarStateKnown;
private int systemUiStatusBarState;
private boolean layoutEnabled;
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
@@ -27,6 +28,10 @@ public final class ModeStateRepository {
return systemUiStatusBarStateKnown;
}
public synchronized boolean isSystemUiShadeLocked() {
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
}
public synchronized boolean isLayoutEnabled() {
return layoutEnabled;
}
@@ -67,14 +72,23 @@ public final class ModeStateRepository {
activeScene = Objects.requireNonNull(sceneKey, "sceneKey");
}
public synchronized void setSystemUiStatusBarState(int state, boolean dozing) {
public synchronized 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(NotificationDisplayStyle.UNKNOWN);
activeScene = SceneKey.aod(style);
} else if (state == 0) {
activeScene = SceneKey.unlocked();
} else {
activeScene = SceneKey.lockscreen(NotificationDisplayStyle.UNKNOWN);
activeScene = SceneKey.lockscreen(style);
}
}
@@ -0,0 +1,39 @@
package se.ajpanton.statusbartweak.runtime.mode;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
/**
* Reads Samsung's lockscreen/AOD notification display style from the system
* setting SystemUI itself uses for Cards / Icons / Dot.
*/
public final class SystemNotificationDisplayStyleDetector {
private static final String KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION =
"lockscreen_minimizing_notification";
private SystemNotificationDisplayStyleDetector() {
}
public static NotificationDisplayStyle read(Context context) {
ContentResolver resolver = context != null ? context.getContentResolver() : null;
if (resolver == null) {
return NotificationDisplayStyle.UNKNOWN;
}
try {
return styleForMinimizingValue(
Settings.System.getInt(resolver, KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION));
} catch (Throwable ignored) {
return NotificationDisplayStyle.UNKNOWN;
}
}
static NotificationDisplayStyle styleForMinimizingValue(int value) {
return switch (value) {
case 0 -> NotificationDisplayStyle.CARDS;
case 1 -> NotificationDisplayStyle.ICONS;
case 2 -> NotificationDisplayStyle.DOT;
default -> NotificationDisplayStyle.UNKNOWN;
};
}
}
@@ -1,5 +1,7 @@
package se.ajpanton.statusbartweak.runtime.mode;
import android.content.Context;
import io.github.libxposed.api.XposedModuleInterface;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
@@ -65,6 +67,26 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
int state = rawState instanceof Integer integer ? integer : 0;
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
context.getModeStateRepository().setSystemUiStatusBarState(state, dozing);
context.getModeStateRepository().setSystemUiStatusBarState(
state,
dozing,
SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
}
private Context systemContext(Object controller) {
Object context = ReflectionSupport.getFieldValue(controller, "mContext");
if (context instanceof Context androidContext) {
return androidContext;
}
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Object app = ReflectionSupport.invokeStaticMethod(
activityThreadClass,
"currentApplication",
new Class<?>[0]);
return app instanceof Context androidContext ? androidContext : null;
} catch (Throwable ignored) {
return null;
}
}
}
@@ -0,0 +1,287 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;
import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
public final class LockscreenCardsNotificationIconSourceStore {
private static final WeakHashMap<ViewGroup, ArrayList<SourceEntry>> SOURCES_BY_CONTAINER =
new WeakHashMap<>();
private LockscreenCardsNotificationIconSourceStore() {
}
public static void put(ViewGroup container, ArrayList<SourceEntry> sources) {
if (container == null || sources == null || sources.isEmpty()) {
if (container != null) {
SOURCES_BY_CONTAINER.remove(container);
}
return;
}
ArrayList<SourceEntry> attached = new ArrayList<>();
Set<View> seen = Collections.newSetFromMap(new IdentityHashMap<>());
for (SourceEntry source : sources) {
if (source == null || source.view == null || !seen.add(source.view)) {
continue;
}
attached.add(source);
}
if (attached.isEmpty()) {
SOURCES_BY_CONTAINER.remove(container);
} else {
SOURCES_BY_CONTAINER.put(container, attached);
}
}
static ArrayList<SnapshotSource> snapshotSources(ViewGroup container, View root) {
ArrayList<SourceEntry> views = SOURCES_BY_CONTAINER.get(container);
ArrayList<SnapshotSource> result = new ArrayList<>();
if (views == null || views.isEmpty()) {
return result;
}
int skipCards = visibleShelfStartIndex(container, views);
for (int i = 0; i < views.size(); i++) {
if (i < skipCards) {
continue;
}
SourceEntry entry = views.get(i);
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));
}
return result;
}
private static int visibleShelfStartIndex(ViewGroup container, ArrayList<SourceEntry> entries) {
if (container == null || entries == null || entries.isEmpty()) {
return 0;
}
ArrayList<View> visibleShelfIcons = visibleShelfIcons(container);
for (View icon : visibleShelfIcons) {
String key = keyForIconView(icon);
for (int i = 0; i < entries.size(); i++) {
SourceEntry entry = entries.get(i);
if (entry == null) {
continue;
}
if (entry.view == icon) {
return i;
}
if (key != null && entry.key != null && key.equals(entry.key)) {
return i;
}
}
}
return 0;
}
private static ArrayList<View> visibleShelfIcons(ViewGroup container) {
ArrayList<View> result = new ArrayList<>();
ArrayList<View> stack = new ArrayList<>();
stack.add(container);
for (int i = 0; i < stack.size(); i++) {
View view = stack.get(i);
if (view != container && isVisibleShelfIcon(view)) {
result.add(view);
continue;
}
if (view instanceof ViewGroup group) {
for (int child = 0; child < group.getChildCount(); child++) {
View childView = group.getChildAt(child);
if (childView != null) {
stack.add(childView);
}
}
}
}
return result;
}
private static boolean isVisibleShelfIcon(View view) {
if (view == null || view.getVisibility() != View.VISIBLE) {
return false;
}
if (isOverflowDotCandidate(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) {
return false;
}
String className = view.getClass().getName();
return className.contains("StatusBarIconView")
|| className.contains("IconView")
|| view instanceof android.widget.ImageView;
}
private static boolean isOverflowDotCandidate(View view) {
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
return idName.contains("overflow")
|| idName.contains("dot")
|| className.contains("overflow")
|| className.contains("dot");
}
public static SourceEntry source(View view, String key) {
return new SourceEntry(view, key);
}
public static String keyForEntry(Object entry) {
if (entry == null) {
return null;
}
Object methodValue = ReflectionSupport.invokeMethod(entry, "getKey", new Class<?>[0]);
if (methodValue instanceof String string && !string.isEmpty()) {
return string;
}
for (String fieldName : new String[]{"mKey", "key", "notificationKey", "mNotificationKey"}) {
Object value = ReflectionSupport.getFieldValue(entry, fieldName);
if (value instanceof String string && !string.isEmpty()) {
return string;
}
}
return null;
}
public static String keyForIconView(View view) {
if (view == null) {
return null;
}
Object methodValue = ReflectionSupport.invokeMethod(view, "getNotificationKey", new Class<?>[0]);
if (methodValue instanceof String string && !string.isEmpty()) {
return string;
}
for (String fieldName : new String[]{"mNotificationKey", "notificationKey", "mKey", "key"}) {
Object value = ReflectionSupport.getFieldValue(view, fieldName);
if (value instanceof String string && !string.isEmpty()) {
return string;
}
}
return null;
}
private static int visibleCardCountBeforeShelf(View root, View shelf) {
if (!(root instanceof ViewGroup group)) {
return 0;
}
int shelfTop = Integer.MAX_VALUE;
if (shelf != null) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds shelfBounds =
ViewGeometry.boundsRelativeTo(root, shelf);
if (shelfBounds != null) {
shelfTop = shelfBounds.top;
}
}
int count = 0;
ArrayList<View> stack = new ArrayList<>();
stack.add(group);
for (int i = 0; i < stack.size(); i++) {
View view = stack.get(i);
if (isNotificationRow(view) && isVisibleCardRow(view)) {
se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds rowBounds =
ViewGeometry.boundsRelativeTo(root, view);
if (rowBounds != null && rowBounds.top < shelfTop) {
count++;
}
}
if (view instanceof ViewGroup viewGroup) {
for (int child = 0; child < viewGroup.getChildCount(); child++) {
View childView = viewGroup.getChildAt(child);
if (childView != null) {
stack.add(childView);
}
}
}
}
return count;
}
private static boolean isNotificationRow(View view) {
if (view == null) {
return false;
}
String className = view.getClass().getName();
return className.contains("ExpandableNotificationRow");
}
private static String resolveIdName(View view) {
if (view == null || view.getId() == View.NO_ID) {
return "";
}
try {
return view.getResources().getResourceEntryName(view.getId());
} catch (Throwable ignored) {
return "";
}
}
private static boolean isVisibleCardRow(View view) {
if (view == null || view.getVisibility() == View.GONE) {
return false;
}
if (!view.isShown() || effectiveAlpha(view) <= 0.05f) {
return false;
}
if (ReflectionSupport.getBooleanField(view, "mInShelf", false)
|| ReflectionSupport.getBooleanField(view, "mTransformingInShelf", false)) {
return false;
}
Object viewState = ReflectionSupport.getFieldValue(view, "mViewState");
if (viewState != null
&& (ReflectionSupport.getBooleanField(viewState, "hidden", false)
|| ReflectionSupport.getBooleanField(viewState, "gone", false))) {
return false;
}
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
return width > 0 && height > 0 && actualHeight(view, height) > 0;
}
private static int actualHeight(View view, int fallback) {
Object methodValue = ReflectionSupport.invokeMethod(view, "getActualHeight", new Class<?>[0]);
if (methodValue instanceof Integer integer && integer > 0) {
return integer;
}
Object fieldValue = ReflectionSupport.getFieldValue(view, "mActualHeight");
if (fieldValue instanceof Integer integer && integer > 0) {
return integer;
}
return fallback;
}
private static float effectiveAlpha(View view) {
float alpha = 1f;
Object current = view;
while (current instanceof View currentView) {
alpha *= currentView.getAlpha();
current = currentView.getParent();
}
return alpha;
}
public static final class SourceEntry {
final View view;
final String key;
private SourceEntry(View view, String key) {
this.view = view;
this.key = key;
}
}
}
@@ -0,0 +1,536 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.WeakHashMap;
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;
/**
* Mirrors Samsung's cards-mode shelf/icon-only notification strip into an owned
* host. The stock shelf already represents notifications not currently shown as
* cards, so this renderer treats it as the source list and only owns placement.
*/
public final class LockscreenCardsNotificationStripRenderer {
private static final int DEFAULT_PILL_COLOR = 0xAA202124;
private static final String PILL_BACKGROUND_TAG = "statusbartweak.cards.notification.pill";
private final SnapshotRenderer renderer = new SnapshotRenderer(false, false, false);
private final WeakHashMap<ViewGroup, ArrayList<View>> pillViewsByHost = new WeakHashMap<>();
public RenderResult render(
View root,
ViewGroup host,
SbtSettings settings,
SceneKey scene
) {
if (root == null
|| host == null
|| settings == null
|| scene == null
|| !scene.isLockscreen()
|| scene.notificationDisplayStyle() != NotificationDisplayStyle.CARDS
|| settings.cardsLockMaxRows <= 0) {
clear(host);
return RenderResult.empty();
}
ViewGroup sourceRoot = findSourceRoot(root);
if (sourceRoot == null) {
clear(host);
return RenderResult.empty();
}
if (hasHiddenShelfAncestor(sourceRoot, root)) {
clear(host);
return RenderResult.empty();
}
syncHostAnimation(host, sourceRoot);
ArrayList<SnapshotSource> sources = LockscreenCardsNotificationIconSourceStore.snapshotSources(sourceRoot, root);
if (sources.isEmpty()) {
sources = collectSources(sourceRoot);
}
if (sources.isEmpty()) {
clear(host);
return RenderResult.empty();
}
ArrayList<SnapshotPlacement> placements = SnapshotIconFilter.notificationPlacements(
renderer.rawPlacements(root, sources),
settings,
scene);
StripLayout layout = layoutPlacements(root, sourceRoot, placements, settings);
renderPills(host, layout.rowBounds, resolvePillColor(sourceRoot));
Bounds renderedBounds = renderer.renderPlacements(host, layout.placements);
return new RenderResult(sourceRoot, renderedBounds);
}
public void clear(ViewGroup host) {
renderer.clear(host);
clearPills(host);
}
public void clearAll() {
renderer.clearAll();
}
private ViewGroup findSourceRoot(View root) {
if (!(root instanceof ViewGroup group)) {
return null;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(group);
ViewGroup shelf = null;
ViewGroup iconOnly = null;
while (!queue.isEmpty()) {
View view = queue.removeFirst();
if (view instanceof ViewGroup candidate) {
if (isShelfIconContainer(candidate)) {
shelf = candidate;
} else if (isIconOnlyContainer(candidate) && iconOnly == null) {
iconOnly = candidate;
}
for (int i = 0; i < candidate.getChildCount(); i++) {
View child = candidate.getChildAt(i);
if (child != null) {
queue.addLast(child);
}
}
}
}
return shelf != null ? shelf : iconOnly;
}
private boolean hasHiddenShelfAncestor(View sourceRoot, View root) {
Object current = sourceRoot != null ? sourceRoot.getParent() : null;
while (current instanceof View currentView) {
if (currentView == root) {
return false;
}
if (currentView.getClass().getName().contains("NotificationShelf")) {
return currentView.getVisibility() != View.VISIBLE || currentView.getAlpha() <= 0f;
}
current = currentView.getParent();
}
return false;
}
private void syncHostAnimation(ViewGroup host, View sourceRoot) {
if (host == null || sourceRoot == null) {
return;
}
float alpha = effectiveAncestorAlpha(sourceRoot);
if (host.getAlpha() != alpha) {
host.setAlpha(alpha);
}
}
private float effectiveAncestorAlpha(View view) {
float alpha = 1f;
Object current = view.getParent();
while (current instanceof View currentView) {
alpha *= currentView.getAlpha();
current = currentView.getParent();
}
return Math.max(0f, Math.min(1f, alpha));
}
private ArrayList<SnapshotSource> collectSources(ViewGroup sourceRoot) {
ArrayList<SnapshotSource> result = new ArrayList<>();
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(sourceRoot);
while (!queue.isEmpty()) {
View view = queue.removeFirst();
if (view != sourceRoot && isNotificationIconCandidate(view)) {
result.add(new SnapshotSource(view, SnapshotMode.NOTIFICATION_DRAWABLE));
continue;
}
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 result;
}
private StripLayout layoutPlacements(
View root,
View sourceRoot,
ArrayList<SnapshotPlacement> placements,
SbtSettings settings
) {
ArrayList<SnapshotPlacement> result = new ArrayList<>();
ArrayList<Bounds> 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 capacity = maxPerRow * maxRows;
if (capacity <= 0) {
return new StripLayout(result, rowBounds);
}
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 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
? Math.max(0, root.getWidth())
: Integer.MAX_VALUE;
int index = 0;
int[] rowCounts = rowCounts(visible, maxPerRow, maxRows, settings.cardsLockEvenDistribution);
int dotColor = dotColor(placements);
int rowGap = dp(root, 8);
for (int row = 0; row < rowCounts.length && index < visible; row++) {
int count = rowCounts[row];
if (count <= 0) {
continue;
}
int rowWidth = count * iconWidth;
int left = centerX - rowWidth / 2;
if (availableWidth != Integer.MAX_VALUE) {
left = Math.max(0, Math.min(left, Math.max(0, availableWidth - rowWidth)));
}
int y = top + row * (iconHeight + rowGap);
rowBounds.add(new Bounds(left, y, rowWidth, iconHeight));
for (int col = 0; col < count && index < visible; col++, index++) {
boolean dotSlot = showDot && index == visible - 1;
SnapshotPlacement placement = dotSlot
? dotPlacement(iconWidth, iconHeight, dotColor)
: placements.get(index);
result.add(new SnapshotPlacement(
placement.source,
new Bounds(left + col * iconWidth, y, iconWidth, iconHeight),
placement.key,
placement.signal,
placement.overflowDot,
placement.overflowDotColor,
placement.heldSignal,
new Bounds(0, 0, iconWidth, iconHeight),
0));
}
}
return new StripLayout(result, rowBounds);
}
private int[] rowCounts(
int slots,
int maxPerRow,
int maxRows,
boolean evenDistribution
) {
if (slots <= 0 || maxPerRow <= 0 || maxRows <= 0) {
return new int[0];
}
int rowCount = Math.min(maxRows, (slots + maxPerRow - 1) / maxPerRow);
int[] result = new int[rowCount];
if (evenDistribution && rowCount > 1) {
int base = slots / rowCount;
int remainder = slots % rowCount;
for (int row = 0; row < rowCount; row++) {
result[row] = base + (row < remainder ? 1 : 0);
}
return result;
}
int remaining = slots;
for (int row = 0; row < rowCount; row++) {
result[row] = Math.min(maxPerRow, remaining);
remaining -= result[row];
}
return result;
}
private SnapshotPlacement dotPlacement(int width, int height, int color) {
return new SnapshotPlacement(
null,
new Bounds(0, 0, Math.max(1, width), Math.max(1, height)),
SnapshotKeys.OVERFLOW_DOT + ":cards_notifications",
false,
true,
color,
null,
new Bounds(0, 0, Math.max(1, width), Math.max(1, height)),
0);
}
private int dotColor(ArrayList<SnapshotPlacement> placements) {
if (placements != null) {
for (SnapshotPlacement placement : placements) {
if (placement == null || placement.source == null) {
continue;
}
int color = SnapshotAppearanceSignature.preferredColor(placement.source.view(), Color.TRANSPARENT);
if (Color.alpha(color) != 0) {
return color;
}
}
}
return Color.WHITE;
}
private void renderPills(ViewGroup host, ArrayList<Bounds> rowBounds, int color) {
if (host == null || rowBounds == null || rowBounds.isEmpty()) {
clearPills(host);
return;
}
ArrayList<View> pills = pillViewsByHost.get(host);
if (pills == null) {
pills = new ArrayList<>();
pillViewsByHost.put(host, pills);
}
while (pills.size() < rowBounds.size()) {
View pill = new View(host.getContext());
pill.setTag(PILL_BACKGROUND_TAG);
pill.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
pills.add(pill);
}
int horizontalPadding = dp(host, 8);
int verticalPadding = dp(host, 3);
for (int i = 0; i < pills.size(); i++) {
View pill = pills.get(i);
if (i >= rowBounds.size()) {
if (pill.getParent() == host) {
host.removeView(pill);
}
continue;
}
Bounds row = rowBounds.get(i);
int left = row.left - horizontalPadding;
int top = row.top - verticalPadding;
int width = Math.max(1, row.width + horizontalPadding * 2);
int height = Math.max(1, row.height + verticalPadding * 2);
GradientDrawable background = new GradientDrawable();
background.setColor(color);
background.setCornerRadius(height / 2f);
pill.setBackground(background);
if (pill.getParent() != host) {
host.addView(pill, Math.min(i, host.getChildCount()));
} else {
host.removeView(pill);
host.addView(pill, Math.min(i, host.getChildCount()));
}
if (applyPillLayoutParamsIfChanged(host, pill, left, top, width, height)) {
host.requestLayout();
}
if (pill.getVisibility() != View.VISIBLE) {
pill.setVisibility(View.VISIBLE);
}
if (pill.getAlpha() != 1f) {
pill.setAlpha(1f);
}
if (pill.getLeft() != left
|| pill.getTop() != top
|| pill.getRight() != left + width
|| pill.getBottom() != top + height) {
pill.layout(left, top, left + width, top + height);
}
}
host.invalidate();
}
private void clearPills(ViewGroup host) {
if (host == null) {
return;
}
ArrayList<View> pills = pillViewsByHost.remove(host);
if (pills == null) {
return;
}
for (View pill : pills) {
if (pill != null && pill.getParent() == host) {
host.removeView(pill);
}
}
host.invalidate();
}
private int resolvePillColor(View sourceRoot) {
View backgroundSource = findShelfBackground(sourceRoot);
int color = backgroundColor(backgroundSource);
if (Color.alpha(color) != 0) {
return color;
}
color = backgroundColor(sourceRoot);
return Color.alpha(color) != 0 ? color : DEFAULT_PILL_COLOR;
}
private View findShelfBackground(View root) {
if (!(root instanceof ViewGroup group)) {
return null;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(group);
while (!queue.isEmpty()) {
View view = queue.removeFirst();
String className = view.getClass().getName();
String idName = resolveIdName(view);
if (className.contains("NotificationShelfBackground")
|| idName.toLowerCase(java.util.Locale.ROOT).contains("background")) {
return view;
}
if (view instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
View child = childGroup.getChildAt(i);
if (child != null) {
queue.addLast(child);
}
}
}
}
return null;
}
private int backgroundColor(View view) {
if (view == null) {
return Color.TRANSPARENT;
}
Drawable background = view.getBackground();
if (background instanceof ColorDrawable colorDrawable) {
return colorDrawable.getColor();
}
return Color.TRANSPARENT;
}
private int dp(View view, int value) {
float density = view.getResources().getDisplayMetrics().density;
return Math.max(0, Math.round(value * density));
}
private boolean applyPillLayoutParamsIfChanged(
ViewGroup host,
View pill,
int left,
int top,
int width,
int height
) {
ViewGroup.LayoutParams current = pill.getLayoutParams();
if (current != null
&& current.width == width
&& current.height == height
&& current instanceof ViewGroup.MarginLayoutParams marginParams
&& marginParams.leftMargin == left
&& marginParams.topMargin == top) {
return false;
}
pill.setLayoutParams(layoutParamsFor(host, left, top, width, height));
return true;
}
private ViewGroup.LayoutParams layoutParamsFor(
ViewGroup host,
int left,
int top,
int width,
int height
) {
if (host instanceof FrameLayout) {
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(width, height);
params.leftMargin = left;
params.topMargin = top;
return params;
}
ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(width, height);
params.leftMargin = left;
params.topMargin = top;
return params;
}
private int representativeWidth(ArrayList<SnapshotPlacement> 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<SnapshotPlacement> placements) {
for (SnapshotPlacement placement : placements) {
if (placement != null && placement.bounds != null && placement.bounds.height > 0) {
return placement.bounds.height;
}
}
return 1;
}
private boolean isShelfIconContainer(View view) {
String className = view.getClass().getName();
return className.contains("SecShelfNotificationIconContainer");
}
private boolean isIconOnlyContainer(View view) {
String idName = resolveIdName(view);
return "keyguard_icononly_container_view".equals(idName)
|| "notification_icononly_container".equals(idName)
|| "notification_icononly_area".equals(idName)
|| "common_notification_widget".equals(idName);
}
private boolean isNotificationIconCandidate(View view) {
if (view == null || view instanceof TextView || view.getVisibility() != View.VISIBLE) {
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 || isOverflowDotCandidate(view)) {
return false;
}
String className = view.getClass().getName();
return className.contains("StatusBarIconView")
|| className.contains("IconView")
|| view instanceof ImageView;
}
private boolean isOverflowDotCandidate(View view) {
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
return idName.contains("overflow")
|| idName.contains("dot")
|| className.contains("overflow")
|| className.contains("dot");
}
private String resolveIdName(View view) {
if (view == null || view.getId() == View.NO_ID) {
return "";
}
try {
return view.getResources().getResourceEntryName(view.getId());
} catch (Throwable ignored) {
return "";
}
}
public record RenderResult(View sourceRoot, Bounds renderedBounds) {
static RenderResult empty() {
return new RenderResult(null, null);
}
}
private record StripLayout(
ArrayList<SnapshotPlacement> placements,
ArrayList<Bounds> rowBounds
) {
}
}
@@ -24,6 +24,8 @@ public final class OwnedIconHostManager {
"statusbartweak.unlocked.carrier.host";
public static final String TAG_UNLOCKED_CHIP_HOST =
"statusbartweak.unlocked.chip.host";
public static final String TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST =
"statusbartweak.lockscreen.cards.notification.host";
public FrameLayout ensureUnlockedStatusHost(View root) {
return ensureHost(root, TAG_UNLOCKED_STATUS_HOST);
@@ -45,6 +47,10 @@ public final class OwnedIconHostManager {
return ensureHost(root, TAG_UNLOCKED_CARRIER_HOST);
}
public FrameLayout ensureLockscreenCardsNotificationHost(View root) {
return ensureHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
}
public FrameLayout ensureHost(View root, String tag) {
Objects.requireNonNull(tag, "tag");
if (!(root instanceof ViewGroup parent)) {
@@ -64,7 +70,6 @@ public final class OwnedIconHostManager {
parent.addView(host, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
disableAncestorClipping(parent);
host.bringToFront();
return host;
}
@@ -85,6 +90,7 @@ public final class OwnedIconHostManager {
removeHost(root, TAG_UNLOCKED_CLOCK_HOST);
removeHost(root, TAG_UNLOCKED_CARRIER_HOST);
removeHost(root, TAG_UNLOCKED_CHIP_HOST);
removeHost(root, TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST);
}
public void setUnlockedHostsVisible(View root, boolean visible) {
@@ -112,7 +118,8 @@ public final class OwnedIconHostManager {
|| TAG_UNLOCKED_NOTIFICATION_HOST.equals(tag)
|| TAG_UNLOCKED_CLOCK_HOST.equals(tag)
|| TAG_UNLOCKED_CARRIER_HOST.equals(tag)
|| TAG_UNLOCKED_CHIP_HOST.equals(tag);
|| TAG_UNLOCKED_CHIP_HOST.equals(tag)
|| TAG_LOCKSCREEN_CARDS_NOTIFICATION_HOST.equals(tag);
}
private FrameLayout findDirectHost(ViewGroup parent, String tag) {
@@ -146,15 +153,4 @@ public final class OwnedIconHostManager {
}
}
private void disableAncestorClipping(View view) {
Object current = view;
int depth = 0;
while (current instanceof ViewGroup group && depth < 8) {
group.setClipChildren(false);
group.setClipToPadding(false);
group.setClipToOutline(false);
current = group.getParent();
depth++;
}
}
}
@@ -0,0 +1,278 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.content.res.Resources;
import android.view.View;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
import se.ajpanton.statusbartweak.shell.settings.SystemIconRules;
final class SnapshotIconFilter {
private SnapshotIconFilter() {
}
static ArrayList<SnapshotPlacement> statusPlacements(
ArrayList<SnapshotPlacement> placements,
SbtSettings settings,
SceneKey scene
) {
String mode = modeName(scene);
if (placements == null
|| placements.isEmpty()
|| settings == null
|| settings.systemIconBlockedModes.isEmpty()
|| mode == null) {
return placements != null ? placements : new ArrayList<>();
}
ArrayList<SnapshotPlacement> filtered = new ArrayList<>(placements.size());
for (SnapshotPlacement placement : placements) {
if (!isStatusBlocked(placement, settings.systemIconBlockedModes, mode)) {
filtered.add(placement);
}
}
return filtered;
}
static ArrayList<SnapshotPlacement> notificationPlacements(
ArrayList<SnapshotPlacement> placements,
SbtSettings settings,
SceneKey scene
) {
String mode = modeName(scene);
if (placements == null
|| placements.isEmpty()
|| settings == null
|| mode == null) {
return placements != null ? placements : new ArrayList<>();
}
ArrayList<SnapshotPlacement> filtered = new ArrayList<>(placements.size());
for (SnapshotPlacement placement : placements) {
String pkg = notificationPackage(placement != null ? placement.key : null);
if (pkg == null
|| !isNotificationBlocked(
placement,
pkg,
settings.appIconBlockedModes,
settings.appIconExceptionRules,
mode)) {
filtered.add(placement);
}
}
return filtered;
}
static String notificationPackage(String key) {
if (key == null) {
return null;
}
String normalized = key.trim();
int suffix = normalized.indexOf('@');
if (suffix >= 0) {
normalized = normalized.substring(0, suffix);
}
int slash = normalized.indexOf('/');
if (slash >= 0) {
normalized = normalized.substring(0, slash);
}
return AppIconRules.isValidPackageName(normalized) ? normalized : null;
}
static boolean isNotificationBlocked(
SnapshotPlacement placement,
String pkg,
Map<String, Integer> blockedModes,
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptionRules,
String mode
) {
if (!AppIconRules.isValidPackageName(pkg) || mode == null) {
return false;
}
AppIconRules.ExceptionRule matchingRule = matchingExceptionRule(
placement,
exceptionRules != null ? exceptionRules.get(pkg) : null);
if (matchingRule != null) {
return matchingRule.blocksMode(mode);
}
return AppIconRules.isBlocked(blockedModes, pkg, mode);
}
static boolean isStatusBlocked(
SnapshotPlacement placement,
Map<String, Integer> blockedModes,
String mode
) {
if (placement == null || blockedModes == null || blockedModes.isEmpty() || mode == null) {
return false;
}
String text = statusMatchText(placement);
for (String slot : statusSlots(text)) {
if (SystemIconRules.isBlocked(blockedModes, slot, mode)) {
return true;
}
}
return false;
}
private static AppIconRules.ExceptionRule matchingExceptionRule(
SnapshotPlacement placement,
ArrayList<AppIconRules.ExceptionRule> rules
) {
if (rules == null || rules.isEmpty()) {
return null;
}
String text = notificationMatchText(placement);
for (AppIconRules.ExceptionRule rule : rules) {
if (rule == null || rule.regex == null || rule.regex.trim().isEmpty()) {
continue;
}
try {
if (Pattern.compile(rule.regex).matcher(text).find()) {
return rule;
}
} catch (PatternSyntaxException ignored) {
// Invalid user regexes are ignored instead of blocking all icons.
}
}
return null;
}
private static String notificationMatchText(SnapshotPlacement placement) {
StringBuilder sb = new StringBuilder();
append(sb, placement != null ? placement.key : null);
View view = placement != null && placement.source != null ? placement.source.view() : null;
appendViewText(sb, view);
return sb.toString();
}
private static String statusMatchText(SnapshotPlacement placement) {
StringBuilder sb = new StringBuilder();
append(sb, placement.key);
View view = placement.source != null ? placement.source.view() : null;
appendViewText(sb, view);
return sb.toString().toLowerCase(Locale.ROOT);
}
private static void appendViewText(StringBuilder sb, View view) {
if (view == null) {
return;
}
append(sb, view.getClass().getName());
append(sb, resolveIdName(view));
CharSequence contentDescription = view.getContentDescription();
append(sb, contentDescription != null ? contentDescription.toString() : null);
}
private static ArrayList<String> statusSlots(String matchText) {
ArrayList<String> slots = new ArrayList<>();
add(slots, normalizedSlot(matchText));
if (matchText.contains("signal:wifi") || matchText.contains("wifi")) {
add(slots, "wifi");
}
if (matchText.contains("signal:mobile") || matchText.contains("mobile")) {
add(slots, "mobile");
}
if (matchText.contains("battery")) {
add(slots, "battery");
add(slots, "battery_icon");
}
if (matchText.contains("zen") || matchText.contains("dnd") || matchText.contains("disturb")) {
add(slots, "zen");
add(slots, "dnd");
add(slots, "donotdisturb");
}
if (matchText.contains("volume") || matchText.contains("mute")) {
add(slots, "volume");
add(slots, "mute");
}
if (matchText.contains("bluetooth")) {
add(slots, "bluetooth");
add(slots, "bluetooth_connected");
}
if (matchText.contains("hotspot")) {
add(slots, "hotspot");
}
if (matchText.contains("alarm")) {
add(slots, "alarm_clock");
}
if (matchText.contains("airplane")) {
add(slots, "airplane");
add(slots, "stat_sys_airplane_mode");
}
if (matchText.contains("vpn")) {
add(slots, "vpn");
}
if (matchText.contains("nfc")) {
add(slots, "nfc");
add(slots, "nfc_on");
}
if (matchText.contains("data_saver")) {
add(slots, "data_saver");
}
if (matchText.contains("location")) {
add(slots, "location");
}
return slots;
}
private static String normalizedSlot(String value) {
if (value == null) {
return "";
}
String normalized = value.trim().toLowerCase(Locale.ROOT);
if (normalized.startsWith("signal:")) {
normalized = normalized.substring("signal:".length());
}
return normalized;
}
private static String modeName(SceneKey scene) {
if (scene == null) {
return null;
}
if (scene.isAod()) {
return "AOD";
}
if (scene.isLockscreen()) {
return "LOCK";
}
if (scene.isUnlocked()) {
return "UNLOCK";
}
return null;
}
private static void add(ArrayList<String> values, String value) {
if (value == null || value.isEmpty() || values.contains(value)) {
return;
}
values.add(value);
}
private static void append(StringBuilder sb, String value) {
if (value == null || value.isEmpty()) {
return;
}
if (sb.length() > 0) {
sb.append('\n');
}
sb.append(value);
}
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 "";
}
}
}
@@ -265,6 +265,47 @@ public final class UnlockedIconSnapshotRenderController {
}
}
public boolean refreshUnlockedClockText(
View root,
ViewGroup clockHost,
SceneKey scene
) {
if (root == null
|| clockHost == null
|| scene == null
|| !scene.isUnlocked()
|| !root.isAttachedToWindow()) {
return false;
}
LayoutPlan layoutPlan = lastLayoutPlanByRoot.get(root);
if (layoutPlan == null || layoutPlan.clockPlacement == null) {
return false;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (settings == null || !settings.clockEnabledUnlocked) {
return false;
}
CollectedIcons collectedIcons = lastCollectedIconsByRoot.get(root);
int referenceColor = clockReferenceColor(layoutPlan.clockPlacement, collectedIcons);
ClockPlacement updatedClock = clockPlacementWithTextFromModel(
layoutPlan.clockPlacement,
layoutPlan.clockPlacement.source,
settings,
referenceColor);
if (updatedClock == null) {
return false;
}
LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock);
lastLayoutPlanByRoot.put(root, updatedPlan);
lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings));
clockRenderer.render(clockHost, updatedClock);
return true;
}
public boolean hasLayoutPlan(View root) {
return root != null && lastLayoutPlanByRoot.containsKey(root);
}
private void clearLockscreenHosts(
ViewGroup clockHost,
ViewGroup carrierHost,
@@ -96,7 +96,10 @@ final class UnlockedLayoutPlanner {
int statusCount = statusCountForScene(settings, scene);
if (statusCount > 0) {
ArrayList<SnapshotPlacement> rawPlacements =
statusRenderer.rawPlacements(root, collectedIcons.statusIcons);
SnapshotIconFilter.statusPlacements(
statusRenderer.rawPlacements(root, collectedIcons.statusIcons),
settings,
scene);
int representativeHeight = representativeHeight(rawPlacements);
String[] positions = statusPositionsForScene(settings, scene);
String[] middleSides = statusMiddleSidesForScene(settings, scene);
@@ -128,7 +131,10 @@ final class UnlockedLayoutPlanner {
int notificationCount = notificationCountForScene(settings, scene);
if (notificationCount > 0) {
ArrayList<SnapshotPlacement> rawPlacements =
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons);
SnapshotIconFilter.notificationPlacements(
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons),
settings,
scene);
if (scene != null
&& scene.isLockscreen()
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
@@ -120,8 +120,8 @@ final class UnlockedStockSourceCollector {
anchors != null ? anchors.clockContainer : null),
notificationRootSignature(anchors != null ? anchors.notificationRoot : null),
viewSignatureOrZero(anchors != null ? anchors.carrierView : null),
sourceListSignature(previousIcons != null ? previousIcons.statusIcons : null),
sourceListSignature(previousIcons != null ? previousIcons.notificationIcons : null),
sourceLayoutListSignature(previousIcons != null ? previousIcons.statusIcons : null),
sourceLayoutListSignature(previousIcons != null ? previousIcons.notificationIcons : null),
chipSourceSignature);
}
@@ -466,20 +466,6 @@ final class UnlockedStockSourceCollector {
return false;
}
private static int viewTreeSignature(View view) {
if (view == null) {
return 0;
}
int result = viewSignature(view);
if (view instanceof ViewGroup group) {
result = 31 * result + group.getChildCount();
for (int i = 0; i < group.getChildCount(); i++) {
result = 31 * result + viewTreeSignature(group.getChildAt(i));
}
}
return result;
}
private static int viewSignatureOrZero(View view) {
return view != null ? viewSignature(view) : 0;
}
@@ -520,7 +506,6 @@ final class UnlockedStockSourceCollector {
result = 31 * result + view.getClass().getName().hashCode();
result = 31 * result + view.getId();
result = 31 * result + view.getVisibility();
result = 31 * result + Float.floatToIntBits(view.getAlpha());
if (view instanceof ViewGroup group) {
result = 31 * result + group.getChildCount();
for (int i = 0; i < group.getChildCount(); i++) {
@@ -529,13 +514,12 @@ final class UnlockedStockSourceCollector {
result = 31 * result + child.getClass().getName().hashCode();
result = 31 * result + child.getId();
result = 31 * result + child.getVisibility();
result = 31 * result + Float.floatToIntBits(child.getAlpha());
}
}
return result;
}
private static int sourceListSignature(ArrayList<SnapshotSource> sources) {
private static int sourceLayoutListSignature(ArrayList<SnapshotSource> sources) {
if (sources == null || sources.isEmpty()) {
return 0;
}
@@ -543,7 +527,34 @@ final class UnlockedStockSourceCollector {
result = 31 * result + sources.size();
for (SnapshotSource source : sources) {
result = 31 * result + source.mode().ordinal();
result = 31 * result + viewTreeSignature(source.view());
result = 31 * result + sourceLayoutSignature(source.view());
}
return result;
}
private static int sourceLayoutSignature(View view) {
if (view == null) {
return 0;
}
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
int result = 17;
result = 31 * result + System.identityHashCode(view);
result = 31 * result + view.getClass().getName().hashCode();
result = 31 * result + view.getId();
result = 31 * result + view.getVisibility();
result = 31 * result + width;
result = 31 * result + height;
CharSequence contentDescription = view.getContentDescription();
result = 31 * result + (contentDescription != null ? contentDescription.toString().hashCode() : 0);
if (view instanceof ImageView imageView) {
result = 31 * result + drawableLayoutSignature(imageView.getDrawable());
}
if (view instanceof ViewGroup group) {
result = 31 * result + group.getChildCount();
for (int i = 0; i < group.getChildCount(); i++) {
result = 31 * result + sourceLayoutSignature(group.getChildAt(i));
}
}
return result;
}
@@ -560,6 +571,9 @@ final class UnlockedStockSourceCollector {
hasKnownChipSource = true;
result = 31 * result + chipSourceListSignature(previousIcons.statusChipMetrics);
}
if (!hasKnownChipSource) {
return 0;
}
}
if (!hasKnownChipSource) {
result = 31 * result + statusChipCandidateSignature(root);
@@ -780,6 +794,9 @@ final class UnlockedStockSourceCollector {
settings.layoutStatusMiddleSide,
settings.layoutStatusMiddleAutoNearestSide,
settings.layoutStatusVerticalOffsetPx,
settings.systemIconBlockedModes,
settings.appIconBlockedModes,
settings.appIconExceptionRules,
settings.statusChipsHideAll,
settings.statusChipsHideMediaUnlocked,
settings.statusChipsHideNavUnlocked,
@@ -1417,8 +1417,10 @@ public final class SbtSettings {
String fallback
) {
String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
String previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = prefs.getString(key.key(i), fallback);
previous = prefs.contains(key.key(i)) ? prefs.getString(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
@@ -1429,40 +1431,50 @@ public final class SbtSettings {
boolean fallback
) {
boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
boolean previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = prefs.getBoolean(key.key(i), fallback);
previous = prefs.contains(key.key(i)) ? prefs.getBoolean(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
private static int[] readIntArrayFromPrefs(SharedPreferences prefs, IndexedKey key, int fallback) {
int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
int previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = prefs.getInt(key.key(i), fallback);
previous = prefs.contains(key.key(i)) ? prefs.getInt(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
private static String[] readStringArrayFromBundle(Bundle bundle, IndexedKey key, String fallback) {
String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
String previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = bundle.getString(key.key(i), fallback);
previous = bundle.containsKey(key.key(i)) ? bundle.getString(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
private static boolean[] readBooleanArrayFromBundle(Bundle bundle, IndexedKey key, boolean fallback) {
boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
boolean previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = bundle.getBoolean(key.key(i), fallback);
previous = bundle.containsKey(key.key(i)) ? bundle.getBoolean(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
private static int[] readIntArrayFromBundle(Bundle bundle, IndexedKey key, int fallback) {
int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX];
int previous = fallback;
for (int i = 0; i < result.length; i++) {
result[i] = bundle.getInt(key.key(i), fallback);
previous = bundle.containsKey(key.key(i)) ? bundle.getInt(key.key(i), previous) : previous;
result[i] = previous;
}
return result;
}
@@ -411,11 +411,27 @@ public class SbtSettingsProvider extends ContentProvider {
IndexedKey verticalOffsetKey,
int verticalOffsetDefault
) {
String position = positionDefault;
String middleSide = middleSideDefault;
boolean autoNearest = autoNearestDefault;
int verticalOffset = verticalOffsetDefault;
for (int i = 0; i < SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX; i++) {
out.putString(positionKey.key(i), prefs.getString(positionKey.key(i), positionDefault));
out.putString(middleSideKey.key(i), prefs.getString(middleSideKey.key(i), middleSideDefault));
out.putBoolean(autoNearestKey.key(i), prefs.getBoolean(autoNearestKey.key(i), autoNearestDefault));
out.putInt(verticalOffsetKey.key(i), prefs.getInt(verticalOffsetKey.key(i), verticalOffsetDefault));
position = prefs.contains(positionKey.key(i))
? prefs.getString(positionKey.key(i), position)
: position;
middleSide = prefs.contains(middleSideKey.key(i))
? prefs.getString(middleSideKey.key(i), middleSide)
: middleSide;
autoNearest = prefs.contains(autoNearestKey.key(i))
? prefs.getBoolean(autoNearestKey.key(i), autoNearest)
: autoNearest;
verticalOffset = prefs.contains(verticalOffsetKey.key(i))
? prefs.getInt(verticalOffsetKey.key(i), verticalOffset)
: verticalOffset;
out.putString(positionKey.key(i), position);
out.putString(middleSideKey.key(i), middleSide);
out.putBoolean(autoNearestKey.key(i), autoNearest);
out.putInt(verticalOffsetKey.key(i), verticalOffset);
}
}
@@ -178,6 +178,14 @@ public final class LayoutFragment extends Fragment {
bindCheckBoxPref(prefs, root.findViewById(R.id.clock_position_mode_unlocked),
SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT);
LayoutSectionCopySupport.addDropdown(
requireContext(),
prefs,
root.findViewById(R.id.clock_position_mode_unlocked),
LayoutSectionCopySupport.Mode.UNLOCKED,
LayoutSectionCopySupport.Section.CLOCK,
false,
this::refreshSelf);
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_position_mode_aod),
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT);
@@ -221,7 +229,8 @@ public final class LayoutFragment extends Fragment {
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,
LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS);
setupUnlockedIconContainerCount(
root,
prefs,
@@ -238,7 +247,8 @@ public final class LayoutFragment extends Fragment {
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,
LayoutSectionCopySupport.Section.STATUS);
return root;
}
@@ -262,7 +272,8 @@ public final class LayoutFragment extends Fragment {
String positionDefault,
boolean autoNearestDefault,
String middleSideDefault,
int verticalOffsetDefault
int verticalOffsetDefault,
LayoutSectionCopySupport.Section copySection
) {
CheckBox checkbox = root.findViewById(checkboxId);
if (checkbox == null || !(checkbox.getParent() instanceof ViewGroup modeRow)
@@ -287,6 +298,17 @@ public final class LayoutFragment extends Fragment {
removeGeneratedCopyCards(cardsParent, copyTag);
int count = iconContainerCount(prefs, countKey, countDefault, legacyEnabledKey, legacyEnabledDefault);
updateIconCardTitle(sourceCard, sectionTitleId, 0, count);
boolean cardsMode = LockedNotificationModeUi.MODE_CARDS.equals(
LockedNotificationModeUi.currentMode(requireContext()));
LayoutSectionCopySupport.addDropdownRow(
requireContext(),
prefs,
countContainer,
LayoutSectionCopySupport.Mode.UNLOCKED,
copySection,
cardsMode,
0,
this::refreshSelf);
countContainer.addView(iconContainerCountControl(
prefs,
countKey,
@@ -322,6 +344,22 @@ public final class LayoutFragment extends Fragment {
rebuild[0].run();
}
private void refreshSelf() {
View view = getView();
if (view == null || !isAdded()) {
return;
}
view.post(() -> {
if (isAdded()) {
getParentFragmentManager()
.beginTransaction()
.detach(this)
.attach(this)
.commitAllowingStateLoss();
}
});
}
private View iconContainerCountControl(
SharedPreferences prefs,
String countKey,
@@ -0,0 +1,669 @@
package se.ajpanton.statusbartweak.shell.ui;
import android.content.Context;
import android.content.SharedPreferences;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TextView;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import se.ajpanton.statusbartweak.R;
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
final class LayoutSectionCopySupport {
private static final String COPY_ROW_TAG = "layout_section_copy_row";
enum Mode {
UNLOCKED,
LOCKSCREEN,
AOD
}
enum Section {
CLOCK,
STATUS,
NOTIFICATIONS_ICONS,
NOTIFICATIONS_CARDS
}
private static final int MAX_COPIES = SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX;
private LayoutSectionCopySupport() {
}
static void addDropdown(
Context context,
SharedPreferences prefs,
CheckBox enabled,
Mode destinationMode,
Section section,
boolean cardsMode,
@Nullable Runnable refresh
) {
if (context == null || prefs == null || enabled == null) {
return;
}
ArrayList<Option> options = optionsFor(context, destinationMode, section, cardsMode);
if (options.isEmpty()) {
return;
}
Spinner spinner = new Spinner(context);
CopyAdapter adapter = new CopyAdapter(context, options);
spinner.setAdapter(adapter);
spinner.setSelection(0, false);
spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
if (position <= 0 || position >= options.size()) {
return;
}
Option option = options.get(position);
spinner.setSelection(0, false);
if (!option.enabled) {
return;
}
copy(prefs, option.mode, destinationMode, section);
SbtSettings.ensureReadable(context);
if (refresh != null) {
refresh.run();
}
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {
}
});
insertBesideEnabled(context, enabled, spinner);
}
static void addCardsDropdown(
Context context,
SharedPreferences prefs,
LinearLayout content,
Mode destinationMode,
@Nullable Runnable refresh
) {
addDropdownRow(context, prefs, content, destinationMode, Section.NOTIFICATIONS_CARDS, true, 0, refresh);
}
static void addDropdownRow(
Context context,
SharedPreferences prefs,
LinearLayout content,
Mode destinationMode,
Section section,
boolean cardsMode,
int insertIndex,
@Nullable Runnable refresh
) {
if (context == null || prefs == null || content == null) {
return;
}
ArrayList<Option> options = optionsFor(context, destinationMode, section, cardsMode);
if (options.isEmpty()) {
return;
}
removeExistingRows(content);
LinearLayout row = buildDropdownRow(context, prefs, options, destinationMode, section, refresh);
int index = Math.max(0, Math.min(insertIndex, content.getChildCount()));
content.addView(row, index);
}
private static ArrayList<Option> optionsFor(
Context context,
Mode destinationMode,
Section section,
boolean cardsMode
) {
ArrayList<Option> result = new ArrayList<>();
result.add(new Option(null, context.getString(R.string.layout_copy_from), true));
for (Mode mode : Mode.values()) {
if (mode == destinationMode || !hasSection(mode, section)) {
continue;
}
result.add(new Option(mode, label(context, mode), canCopy(mode, destinationMode, section, cardsMode)));
}
if (result.size() == 1) {
result.clear();
}
return result;
}
private static boolean hasSection(Mode mode, Section section) {
if (section == Section.CLOCK) {
return mode != Mode.AOD;
}
if (section == Section.NOTIFICATIONS_CARDS) {
return true;
}
return section == Section.STATUS || section == Section.NOTIFICATIONS_ICONS;
}
private static boolean canCopy(Mode source, Mode destination, Section section, boolean cardsMode) {
if (section == Section.NOTIFICATIONS_ICONS) {
boolean sourceCards = cardsMode && source != Mode.UNLOCKED;
boolean destinationCards = cardsMode && destination != Mode.UNLOCKED;
return sourceCards == destinationCards;
}
if (section == Section.NOTIFICATIONS_CARDS) {
return cardsMode && source != Mode.UNLOCKED && destination != Mode.UNLOCKED;
}
return true;
}
private static void copy(SharedPreferences prefs, Mode source, Mode destination, Section section) {
if (source == null || source == destination) {
return;
}
SharedPreferences.Editor editor = prefs.edit();
if (section == Section.CLOCK) {
copySingle(editor, prefs, clockKeys(source), clockKeys(destination));
} else if (section == Section.STATUS) {
copyIcons(editor, prefs, statusKeys(source), statusKeys(destination));
} else if (section == Section.NOTIFICATIONS_ICONS) {
copyIcons(editor, prefs, notificationKeys(source), notificationKeys(destination));
} else if (section == Section.NOTIFICATIONS_CARDS) {
copySingle(editor, prefs, cardKeys(source), cardKeys(destination));
}
editor.apply();
}
private static void copySingle(SharedPreferences.Editor editor, SharedPreferences prefs, KeySet src, KeySet dst) {
copyBoolean(editor, prefs, src.enabled, dst.enabled, src.enabledDefault);
copyString(editor, prefs, src.position, dst.position, src.positionDefault);
copyBoolean(editor, prefs, src.autoNearest, dst.autoNearest, src.autoNearestDefault);
copyString(editor, prefs, src.middleSide, dst.middleSide, src.middleSideDefault);
copyInt(editor, prefs, src.verticalOffset, dst.verticalOffset, src.verticalOffsetDefault);
copyInt(editor, prefs, src.maxRows, dst.maxRows, src.maxRowsDefault);
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);
}
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);
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);
copyString(editor, prefs, src.middleSideAt(i), dst.middleSideAt(i), src.middleSideDefault);
copyInt(editor, prefs, src.verticalOffsetAt(i), dst.verticalOffsetAt(i), src.verticalOffsetDefault);
}
}
private static void copyString(
SharedPreferences.Editor editor,
SharedPreferences prefs,
@Nullable String src,
@Nullable String dst,
String fallback
) {
if (src != null && dst != null) {
editor.putString(dst, prefs.getString(src, fallback));
}
}
private static void copyBoolean(
SharedPreferences.Editor editor,
SharedPreferences prefs,
@Nullable String src,
@Nullable String dst,
boolean fallback
) {
if (src != null && dst != null) {
editor.putBoolean(dst, prefs.getBoolean(src, fallback));
}
}
private static void copyInt(
SharedPreferences.Editor editor,
SharedPreferences prefs,
@Nullable String src,
@Nullable String dst,
int fallback
) {
if (src != null && dst != null) {
editor.putInt(dst, prefs.getInt(src, fallback));
}
}
private static KeySet clockKeys(Mode mode) {
if (mode == Mode.LOCKSCREEN) {
return new KeySet(
SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN,
"layout_lock_clock_position",
"layout_lock_clock_middle_auto_nearest_side",
"layout_lock_clock_middle_side",
"layout_lock_clock_vertical_offset_px",
null,
SbtDefaults.CLOCK_ENABLED_LOCKSCREEN_DEFAULT,
SbtDefaults.CLOCK_POSITION_DEFAULT,
SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT,
1);
}
return new KeySet(
SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
SbtSettings.KEY_CLOCK_POSITION,
SbtSettings.KEY_CLOCK_MIDDLE_AUTO_NEAREST_SIDE,
SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_SIDE,
SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX,
null,
SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT,
SbtDefaults.CLOCK_POSITION_DEFAULT,
SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT,
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT,
1);
}
private static KeySet notificationKeys(Mode mode) {
if (mode == Mode.LOCKSCREEN) {
return iconKeys(
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
"layout_lock_notifications_count",
"layout_lock_notifications_position",
"layout_lock_notifications_middle_auto_nearest_side",
"layout_lock_notifications_middle_side",
"layout_lock_notifications_vertical_offset_px",
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);
}
if (mode == Mode.AOD) {
return iconKeys(
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
"layout_aod_notifications_count",
"layout_aod_notifications_position",
"layout_aod_notifications_middle_auto_nearest_side",
"layout_aod_notifications_middle_side",
"layout_aod_notifications_vertical_offset_px",
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);
}
return iconKeys(
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
SbtSettings.KEY_LAYOUT_NOTIF_POSITION,
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE,
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX,
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);
}
private static KeySet statusKeys(Mode mode) {
if (mode == Mode.LOCKSCREEN) {
return iconKeys(
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
"layout_lock_status_count",
"layout_lock_status_position",
"layout_lock_status_middle_auto_nearest_side",
"layout_lock_status_middle_side",
"layout_lock_status_vertical_offset_px",
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);
}
if (mode == Mode.AOD) {
return iconKeys(
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
"layout_aod_status_count",
"layout_aod_status_position",
"layout_aod_status_middle_auto_nearest_side",
"layout_aod_status_middle_side",
"layout_aod_status_vertical_offset_px",
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);
}
return iconKeys(
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
SbtSettings.KEY_LAYOUT_STATUS_POSITION,
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE,
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX,
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);
}
private static KeySet cardKeys(Mode mode) {
if (mode == Mode.AOD) {
return KeySet.cards(
SbtSettings.KEY_CARDS_AOD_MAX_ROWS,
SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW,
SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION,
SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH,
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);
}
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,
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);
}
private static KeySet iconKeys(
String enabled,
String count,
String position,
String autoNearest,
String middleSide,
String verticalOffset,
boolean enabledDefault,
int countDefault,
String positionDefault,
boolean autoNearestDefault,
String middleSideDefault,
int verticalOffsetDefault
) {
return new KeySet(
enabled,
position,
autoNearest,
middleSide,
verticalOffset,
count,
enabledDefault,
positionDefault,
autoNearestDefault,
middleSideDefault,
verticalOffsetDefault,
countDefault);
}
private static String label(Context context, Mode mode) {
return switch (mode) {
case UNLOCKED -> context.getString(R.string.section_unlocked);
case LOCKSCREEN -> context.getString(R.string.section_lockscreen);
case AOD -> context.getString(R.string.section_aod);
};
}
private static void insertBesideEnabled(Context context, CheckBox enabled, Spinner spinner) {
if (!(enabled.getParent() instanceof LinearLayout parent)) {
return;
}
removeExistingRows(parent);
LinearLayout row = new LinearLayout(context);
row.setTag(COPY_ROW_TAG);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
row.setOrientation(LinearLayout.HORIZONTAL);
TextView label = new TextView(context);
label.setText(R.string.layout_copy_from);
row.addView(label);
LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
spinnerParams.leftMargin = dp(context, 8);
row.addView(spinner, spinnerParams);
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
rowParams.leftMargin = dp(context, 12);
if (parent.getOrientation() == LinearLayout.HORIZONTAL) {
parent.addView(row, rowParams);
} else {
parent.addView(row);
}
}
private static LinearLayout buildDropdownRow(
Context context,
SharedPreferences prefs,
ArrayList<Option> options,
Mode destinationMode,
Section section,
@Nullable Runnable refresh
) {
LinearLayout row = new LinearLayout(context);
row.setTag(COPY_ROW_TAG);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
TextView label = new TextView(context);
label.setText(R.string.layout_copy_from);
row.addView(label);
Spinner spinner = new Spinner(context);
spinner.setAdapter(new CopyAdapter(context, options));
spinner.setSelection(0, false);
spinner.setOnItemSelectedListener(new android.widget.AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(android.widget.AdapterView<?> parent, View view, int position, long id) {
if (position <= 0 || position >= options.size()) {
return;
}
Option option = options.get(position);
spinner.setSelection(0, false);
if (!option.enabled) {
return;
}
copy(prefs, option.mode, destinationMode, section);
SbtSettings.ensureReadable(context);
if (refresh != null) {
refresh.run();
}
}
@Override
public void onNothingSelected(android.widget.AdapterView<?> parent) {
}
});
LinearLayout.LayoutParams spinnerParams = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
spinnerParams.leftMargin = dp(context, 8);
row.addView(spinner, spinnerParams);
return row;
}
private static void removeExistingRows(LinearLayout parent) {
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
if (COPY_ROW_TAG.equals(parent.getChildAt(i).getTag())) {
parent.removeViewAt(i);
}
}
}
private static int dp(Context context, int value) {
return Math.round(value * context.getResources().getDisplayMetrics().density);
}
private record Option(@Nullable Mode mode, String label, boolean enabled) {
@Override
public String toString() {
return label;
}
}
private static final class CopyAdapter extends ArrayAdapter<Option> {
CopyAdapter(Context context, List<Option> options) {
super(context, android.R.layout.simple_spinner_item, options);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
@Override
public boolean isEnabled(int position) {
Option option = getItem(position);
return option != null && option.enabled;
}
@Override
public View getDropDownView(int position, @Nullable View convertView, ViewGroup parent) {
View view = super.getDropDownView(position, convertView, parent);
Option option = getItem(position);
view.setEnabled(option != null && option.enabled);
if (view instanceof TextView textView) {
textView.setAlpha(option != null && option.enabled ? 1f : 0.45f);
}
return view;
}
}
private static final class KeySet {
final String enabled;
final String position;
final String autoNearest;
final String middleSide;
final String verticalOffset;
final String count;
final boolean enabledDefault;
final String positionDefault;
final boolean autoNearestDefault;
final String middleSideDefault;
final int verticalOffsetDefault;
final int countDefault;
final String maxRows;
final String maxIcons;
final String evenDistribution;
final String limitWidth;
final int maxRowsDefault;
final int maxIconsDefault;
final boolean evenDistributionDefault;
final boolean limitWidthDefault;
KeySet(
String enabled,
String position,
String autoNearest,
String middleSide,
String verticalOffset,
String count,
boolean enabledDefault,
String positionDefault,
boolean autoNearestDefault,
String middleSideDefault,
int verticalOffsetDefault,
int countDefault
) {
this.enabled = enabled;
this.position = position;
this.autoNearest = autoNearest;
this.middleSide = middleSide;
this.verticalOffset = verticalOffset;
this.count = count;
this.enabledDefault = enabledDefault;
this.positionDefault = positionDefault;
this.autoNearestDefault = autoNearestDefault;
this.middleSideDefault = middleSideDefault;
this.verticalOffsetDefault = verticalOffsetDefault;
this.countDefault = countDefault;
this.maxRows = null;
this.maxIcons = null;
this.evenDistribution = null;
this.limitWidth = null;
this.maxRowsDefault = 0;
this.maxIconsDefault = 0;
this.evenDistributionDefault = false;
this.limitWidthDefault = false;
}
static KeySet cards(
String maxRows,
String maxIcons,
String evenDistribution,
String limitWidth,
int maxRowsDefault,
int maxIconsDefault,
boolean evenDistributionDefault,
boolean limitWidthDefault
) {
return new KeySet(maxRows, maxIcons, evenDistribution, limitWidth,
maxRowsDefault, maxIconsDefault, evenDistributionDefault, limitWidthDefault);
}
private KeySet(
String maxRows,
String maxIcons,
String evenDistribution,
String limitWidth,
int maxRowsDefault,
int maxIconsDefault,
boolean evenDistributionDefault,
boolean limitWidthDefault
) {
this.enabled = null;
this.position = null;
this.autoNearest = null;
this.middleSide = null;
this.verticalOffset = null;
this.count = null;
this.enabledDefault = false;
this.positionDefault = null;
this.autoNearestDefault = false;
this.middleSideDefault = null;
this.verticalOffsetDefault = 0;
this.countDefault = 0;
this.maxRows = maxRows;
this.maxIcons = maxIcons;
this.evenDistribution = evenDistribution;
this.limitWidth = limitWidth;
this.maxRowsDefault = maxRowsDefault;
this.maxIconsDefault = maxIconsDefault;
this.evenDistributionDefault = evenDistributionDefault;
this.limitWidthDefault = limitWidthDefault;
}
String positionAt(int index) {
return suffixed(position, index);
}
String autoNearestAt(int index) {
return suffixed(autoNearest, index);
}
String middleSideAt(int index) {
return suffixed(middleSide, index);
}
String verticalOffsetAt(int index) {
return suffixed(verticalOffset, index);
}
private String suffixed(String base, int index) {
if (base == null || index <= 0) {
return base;
}
return base + "_" + (index + 1);
}
}
}
@@ -80,9 +80,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
private void rebuildDynamic(Context context, SharedPreferences prefs, String mode) {
dynamicContent.removeAllViews();
Runnable refresh = () -> rebuildDynamic(context, prefs, LockedNotificationModeUi.currentMode(context));
LinkedHashMap<String, View> cardsByOrderKey = new LinkedHashMap<>();
if (!aod) {
cardsByOrderKey.put("clock", configuredPositionCardFromLayout(
View clockCard = configuredPositionCardFromLayout(
context,
prefs,
R.id.layout_clock_card,
@@ -108,7 +109,16 @@ abstract class LockedSceneLayoutFragment extends Fragment {
sceneKey("clock_middle_side"),
SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT,
sceneKey("clock_vertical_offset_px"),
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT));
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT);
LayoutSectionCopySupport.addDropdown(
context,
prefs,
clockCard.findViewById(R.id.clock_position_mode_lockscreen),
copyMode(),
LayoutSectionCopySupport.Section.CLOCK,
false,
refresh);
cardsByOrderKey.put("clock", clockCard);
View carrierCard = configuredPositionCardFromLayout(
context,
prefs,
@@ -163,9 +173,12 @@ abstract class LockedSceneLayoutFragment extends Fragment {
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,
LayoutSectionCopySupport.Section.STATUS,
false,
refresh));
if (LockedNotificationModeUi.MODE_CARDS.equals(mode)) {
cardsByOrderKey.put("notifications", cardsNotificationCard(context, prefs));
cardsByOrderKey.put("notifications", cardsNotificationCard(context, prefs, refresh));
} else {
cardsByOrderKey.put("notifications", configuredIconContainersCardFromLayout(
context,
@@ -191,7 +204,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
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,
LayoutSectionCopySupport.Section.NOTIFICATIONS_ICONS,
false,
refresh));
}
for (String key : LayoutOrderingUi.orderedKeys(prefs)) {
View card = cardsByOrderKey.remove(key);
@@ -292,7 +308,10 @@ abstract class LockedSceneLayoutFragment extends Fragment {
String positionDefault,
boolean autoNearestDefault,
String middleSideDefault,
int verticalOffsetDefault
int verticalOffsetDefault,
LayoutSectionCopySupport.Section copySection,
boolean cardsMode,
@Nullable Runnable refresh
) {
LinearLayout wrapper = vertical(context);
String countKey = sceneKey(base + "_count");
@@ -331,6 +350,7 @@ abstract class LockedSceneLayoutFragment extends Fragment {
countKey,
count,
rebuild[0]);
addCopyDropdownToCard(context, prefs, primary, copySection, cardsMode, refresh);
wrapper.addView(primary);
for (int i = 1; i < count; i++) {
wrapper.addView(configuredIconContainerCard(
@@ -370,6 +390,29 @@ abstract class LockedSceneLayoutFragment extends Fragment {
return wrapper;
}
private void addCopyDropdownToCard(
Context context,
SharedPreferences prefs,
View card,
LayoutSectionCopySupport.Section section,
boolean cardsMode,
@Nullable Runnable refresh
) {
LinearLayout content = cardContent(card);
if (content == null) {
return;
}
LayoutSectionCopySupport.addDropdownRow(
context,
prefs,
content,
copyMode(),
section,
cardsMode,
1,
refresh);
}
private View configuredIconContainerCard(
Context context,
SharedPreferences prefs,
@@ -549,8 +592,9 @@ abstract class LockedSceneLayoutFragment extends Fragment {
return null;
}
private View cardsNotificationCard(Context context, SharedPreferences prefs) {
private View cardsNotificationCard(Context context, SharedPreferences prefs, @Nullable Runnable refresh) {
LinearLayout content = vertical(context);
LayoutSectionCopySupport.addCardsDropdown(context, prefs, content, copyMode(), refresh);
content.addView(stepper(
context,
prefs,
@@ -908,6 +952,23 @@ abstract class LockedSceneLayoutFragment extends Fragment {
return index <= 0 ? sceneKey(suffix) : sceneKey(suffix) + "_" + (index + 1);
}
private LayoutSectionCopySupport.Mode copyMode() {
return aod ? LayoutSectionCopySupport.Mode.AOD : LayoutSectionCopySupport.Mode.LOCKSCREEN;
}
@Nullable
private LinearLayout cardContent(View card) {
if (!(card instanceof ViewGroup cardGroup) || cardGroup.getChildCount() <= 0) {
return null;
}
View inner = cardGroup.getChildAt(0);
if (!(inner instanceof ViewGroup innerGroup) || innerGroup.getChildCount() <= 1) {
return null;
}
View content = innerGroup.getChildAt(1);
return content instanceof LinearLayout layout ? layout : null;
}
private MaterialButton button(Context context, String text) {
MaterialButton button = new MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle);
button.setText(text);
+4 -3
View File
@@ -50,8 +50,8 @@
<string name="debug_camera_unknown">unknown</string>
<string name="notification_icons_title">Notification icons</string>
<string name="system_icons_title">Hide status icons</string>
<string name="system_icons_hint">Saved status-icon rules for the upcoming per-icon layout rewrite.</string>
<string name="system_icons_hide_hint">Currently unwired: changes are stored, but do not affect SystemUI yet. Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
<string name="system_icons_hint">Status-icon rules for the custom layout engine.</string>
<string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
<string name="system_icons_group_connectivity">Connectivity</string>
<string name="system_icons_group_notifications">Notifications</string>
<string name="system_icons_group_system">System</string>
@@ -94,7 +94,7 @@
<string name="hidden_apps_dialog_title">Notification icon app blocks</string>
<string name="hidden_apps_page_title">Hide notification icons</string>
<string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string>
<string name="hidden_apps_page_hint">Currently unwired: changes are stored for the upcoming per-icon rewrite and do not affect icon visibility yet. Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
<string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
<string name="hidden_apps_clear_session">Clear session list</string>
<string name="hidden_apps_empty">No apps yet. Generate a notification first.</string>
<string name="hidden_apps_close">Close</string>
@@ -169,6 +169,7 @@
<string name="layout_notification_position_title">Notification icons</string>
<string name="layout_status_position_title">Status icons</string>
<string name="layout_item_enabled">Enabled</string>
<string name="layout_copy_from">Copy from</string>
<string name="layout_icon_container_count">Number of containers</string>
<string name="layout_locked_notifications_title">Locked notifications mode</string>
<string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string>
@@ -0,0 +1,30 @@
package se.ajpanton.statusbartweak.runtime.mode;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public final class SystemNotificationDisplayStyleDetectorTest {
@Test
public void styleForMinimizingValueMapsSamsungModes() {
assertEquals(
NotificationDisplayStyle.CARDS,
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(0));
assertEquals(
NotificationDisplayStyle.ICONS,
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(1));
assertEquals(
NotificationDisplayStyle.DOT,
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(2));
}
@Test
public void styleForMinimizingValueRejectsUnknownValues() {
assertEquals(
NotificationDisplayStyle.UNKNOWN,
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(-1));
assertEquals(
NotificationDisplayStyle.UNKNOWN,
SystemNotificationDisplayStyleDetector.styleForMinimizingValue(99));
}
}
@@ -0,0 +1,90 @@
package se.ajpanton.statusbartweak.runtime.render;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
import se.ajpanton.statusbartweak.shell.settings.SystemIconRules;
public final class SnapshotIconFilterTest {
@Test
public void parsesPackageFromNormalizedNotificationKey() {
assertTrue("com.discord".equals(
SnapshotIconFilter.notificationPackage("com.discord/0x0@0x1234")));
}
@Test
public void blockedAppHidesNotificationWhenNoExceptionMatches() {
Map<String, Integer> blocked = new HashMap<>();
AppIconRules.setModeBlocked(blocked, "com.discord", false, true, false);
assertTrue(SnapshotIconFilter.isNotificationBlocked(
placement("com.discord/0x0@0x1234"),
"com.discord",
blocked,
new HashMap<>(),
"LOCK"));
}
@Test
public void matchingExceptionOverridesBlockedAppDefault() {
Map<String, Integer> blocked = new HashMap<>();
AppIconRules.setModeBlocked(blocked, "com.discord", false, true, false);
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptions = new HashMap<>();
ArrayList<AppIconRules.ExceptionRule> rules = new ArrayList<>();
rules.add(new AppIconRules.ExceptionRule("0x0", 0));
exceptions.put("com.discord", rules);
assertFalse(SnapshotIconFilter.isNotificationBlocked(
placement("com.discord/0x0@0x1234"),
"com.discord",
blocked,
exceptions,
"LOCK"));
}
@Test
public void signalStatusKeyMatchesStoredSystemIconRule() {
Map<String, Integer> blocked = new HashMap<>();
SystemIconRules.setModeBlocked(blocked, "wifi", SystemIconRules.MODE_UNLOCK, true);
assertTrue(SnapshotIconFilter.isStatusBlocked(
placement("signal:wifi"),
blocked,
"UNLOCK"));
}
@Test
public void invalidExceptionRegexDoesNotBlockByItself() {
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptions = new HashMap<>();
ArrayList<AppIconRules.ExceptionRule> rules = new ArrayList<>();
rules.add(new AppIconRules.ExceptionRule("[", AppIconRules.MODE_LOCK));
exceptions.put("com.discord", rules);
assertFalse(SnapshotIconFilter.isNotificationBlocked(
placement("com.discord/0x0@0x1234"),
"com.discord",
new HashMap<>(),
exceptions,
"LOCK"));
}
private static SnapshotPlacement placement(String key) {
return new SnapshotPlacement(
null,
new Bounds(0, 0, 10, 10),
key,
false,
false,
null,
new Bounds(0, 0, 10, 10),
0);
}
}