From 06cc2e21d99ab9e9eff54c2806446a95767a86b0 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Wed, 13 May 2026 03:06:17 +0000 Subject: [PATCH] Port packed layout solver and overhaul layout settings --- .../layout/StockLayoutCanvasController.java | 3 +- .../PackedStatusBarLayoutSolver.java | 1523 +++++++++++++++++ .../render/DebugBoundsOverlayController.java | 63 +- .../runtime/render/SnapshotRenderer.java | 5 +- .../runtime/render/UnlockedLayoutBand.java | 97 ++ .../runtime/render/UnlockedLayoutPlanner.java | 401 ++++- .../render/UnlockedStockSourceCollector.java | 12 +- .../NotificationDisplayStyleSettings.java | 87 + .../shell/settings/SbtDefaults.java | 20 +- .../shell/settings/SbtSettings.java | 314 ++++ .../shell/settings/SbtSettingsProvider.java | 60 + .../shell/ui/AodLayoutFragment.java | 7 + .../shell/ui/IconsDebugFragment.java | 80 + .../shell/ui/LayoutFragment.java | 398 ++++- .../shell/ui/LayoutHomeFragment.java | 351 ++++ .../shell/ui/LayoutOrderingUi.java | 324 ++++ .../shell/ui/LockedNotificationModeUi.java | 157 ++ .../shell/ui/LockedSceneLayoutFragment.java | 949 ++++++++++ .../shell/ui/LockscreenLayoutFragment.java | 7 + .../statusbartweak/shell/ui/MainActivity.java | 16 +- .../shell/ui/NotificationIconsFragment.java | 489 ------ .../main/res/layout/fragment_icons_debug.xml | 75 + app/src/main/res/layout/fragment_layout.xml | 23 +- .../layout/fragment_notification_icons.xml | 650 ------- app/src/main/res/menu/drawer_menu.xml | 12 +- app/src/main/res/values/ids.xml | 3 + app/src/main/res/values/strings.xml | 36 +- .../PackedStatusBarLayoutSolverTest.java | 296 ++++ .../render/UnlockedLayoutBandTest.java | 85 + 29 files changed, 5241 insertions(+), 1302 deletions(-) create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/debug/NotificationDisplayStyleSettings.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/AodLayoutFragment.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutHomeFragment.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutOrderingUi.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedNotificationModeUi.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockscreenLayoutFragment.java delete mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/ui/NotificationIconsFragment.java delete mode 100644 app/src/main/res/layout/fragment_notification_icons.xml create mode 100644 app/src/main/res/values/ids.xml create mode 100644 app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java create mode 100644 app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java index a7d3a67..526585d 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layout/StockLayoutCanvasController.java @@ -423,7 +423,8 @@ final class StockLayoutCanvasController { } private void hideOrDiscoverLockedStatusBarIconSurfaces(View root) { - if (!hideTrackedLockedStatusBarIconSurfaces(root)) { + hideTrackedLockedStatusBarIconSurfaces(root); + if (root != null) { trackAndHideLockedStatusBarIconSurfaces(root); } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java new file mode 100644 index 0000000..bc56a11 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolver.java @@ -0,0 +1,1523 @@ +package se.ajpanton.statusbartweak.runtime.layoutsolver; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; + +public final class PackedStatusBarLayoutSolver { + private PackedStatusBarLayoutSolver() { + } + + public static ArrayList solve(Spec spec, ArrayList items) { + if (spec == null || items == null || items.isEmpty()) { + return items != null ? items : new ArrayList<>(); + } + + configureIconGroups(items); + ArrayList regions = makeRegions(spec); + placeIntoRegions(spec, regions, items); + for (Region region : regions) { + collisionTree(region.items); + } + + ArrayList shrinkPriority = shrinkPriority(items, spec.shrinkOrder); + ArrayList constraints = new ArrayList<>(); + for (Region region : regions) { + ArrayList regionShrinkPriority = new ArrayList<>(); + for (LayoutItem item : shrinkPriority) { + if (region.items.contains(item)) { + regionShrinkPriority.add(item); + } + } + constraints.addAll(collectConstraints(region, regionShrinkPriority, spec.itemPadding)); + } + + if (!constraints.isEmpty()) { + truncate(constraints, shrinkPriority, items); + } + normalizeUnassignedIconGroups(items); + + for (Region region : regions) { + finalStack(spec, region); + } + return items; + } + + private static ArrayList makeRegions(Spec spec) { + ArrayList regions = new ArrayList<>(); + if (!spec.hasCutout()) { + regions.add(new Region(0, spec.screenWidth)); + return regions; + } + int cameraLeft = spec.cutoutLeft; + int cameraRight = spec.cutoutLeft + spec.cutoutWidth; + regions.add(new Region(0, cameraLeft)); + regions.add(new Region(cameraRight, spec.screenWidth)); + return regions; + } + + private static void placeIntoRegions(Spec spec, ArrayList regions, ArrayList items) { + int leftRegion = 0; + int rightRegion = regions.size() - 1; + boolean forceLeft = false; + boolean forceRight = false; + double middle = spec.screenWidth / 2.0; + int midRegion; + if (middle < regions.get(leftRegion).rightWall) { + midRegion = leftRegion; + } else if (regions.get(rightRegion).leftWall < middle) { + midRegion = rightRegion; + } else { + midRegion = -1; + forceLeft = true; + forceRight = true; + } + + for (LayoutItem item : items) { + if (item.position == Position.MIDDLE && item.fallback == Fallback.LEFT) { + if (forceLeft || overlapsCutout(item, middle, regions)) { + addToRegion(regions.get(leftRegion), item, 0); + forceLeft = true; + } else if (midRegion >= 0) { + addToRegion(regions.get(midRegion), item, 0); + } + } else if (item.position == Position.MIDDLE && item.fallback == Fallback.RIGHT) { + if (forceRight || overlapsCutout(item, middle, regions)) { + addToRegion(regions.get(rightRegion), item); + forceRight = true; + } else if (midRegion >= 0) { + addToRegion(regions.get(midRegion), item); + } + } + } + + for (int i = items.size() - 1; i >= 0; i--) { + LayoutItem item = items.get(i); + if (item.position == Position.LEFT) { + addToRegion(regions.get(leftRegion), item, 0); + } else if (item.position == Position.RIGHT) { + addToRegion(regions.get(rightRegion), item); + } + } + } + + private static void addToRegion(Region region, LayoutItem item) { + item.regionLeftWall = region.leftWall; + item.regionRightWall = region.rightWall; + region.items.add(item); + } + + private static void addToRegion(Region region, LayoutItem item, int index) { + item.regionLeftWall = region.leftWall; + item.regionRightWall = region.rightWall; + region.items.add(index, item); + } + + private static boolean overlapsCutout(LayoutItem item, double middle, ArrayList regions) { + if (regions.size() < 2) { + return false; + } + double itemLeft = middle - item.width() / 2.0; + double itemRight = middle + item.width() / 2.0; + return Math.max(itemLeft, regions.get(0).rightWall) < Math.min(itemRight, regions.get(1).leftWall); + } + + private static ArrayList shrinkPriority(ArrayList items, ArrayList shrinkOrder) { + ArrayList priority = new ArrayList<>(); + if (shrinkOrder == null) { + return priority; + } + for (String kind : shrinkOrder) { + for (LayoutItem item : items) { + if (kind != null && kind.equals(item.kind)) { + priority.add(item); + } + } + } + return priority; + } + + private static ArrayList collectConstraints( + Region region, + ArrayList shrinkPriority, + int itemPadding + ) { + stackLeft(region.items, region.leftWall, itemPadding); + ArrayList paths = paths(region.items, region.rightWall, itemPadding); + if (paths.isEmpty()) { + return new ArrayList<>(); + } + + ArrayList pathConstraints = uniquePathConstraints(paths, shrinkPriority); + ArrayList finalConstraints = new ArrayList<>(); + HashMap> constraintsByEndBox = new HashMap<>(); + HashMap relaxedWallByEndBox = new HashMap<>(); + + for (PathConstraint pathConstraint : pathConstraints) { + double totalCapacity = 0; + for (LayoutItem item : pathConstraint.shrinkables) { + totalCapacity += truncationPhaseShrinkCapacity(item); + } + double minimumPossibleRight = pathConstraint.effectiveRight - totalCapacity; + if (minimumPossibleRight > region.rightWall) { + Double current = relaxedWallByEndBox.get(pathConstraint.endBox); + if (current == null || minimumPossibleRight > current) { + relaxedWallByEndBox.put(pathConstraint.endBox, minimumPossibleRight); + } + } + + double required = pathConstraint.effectiveRight - region.rightWall; + if (required > 0) { + FinalConstraint constraint = new FinalConstraint( + pathConstraint.shrinkables, + required, + pathConstraint.effectiveRight, + pathConstraint.itemsOnPath); + finalConstraints.add(constraint); + constraintsByEndBox + .computeIfAbsent(pathConstraint.endBox, ignored -> new ArrayList<>()) + .add(constraint); + } + } + + for (java.util.Map.Entry entry : relaxedWallByEndBox.entrySet()) { + ArrayList constraints = constraintsByEndBox.get(entry.getKey()); + if (constraints == null) { + continue; + } + for (FinalConstraint constraint : constraints) { + constraint.required = constraint.effectiveRight - entry.getValue(); + } + } + + ArrayList positive = new ArrayList<>(); + for (FinalConstraint constraint : finalConstraints) { + if (constraint.required > 0) { + positive.add(constraint); + } + } + return positive; + } + + private static ArrayList uniquePathConstraints( + ArrayList paths, + ArrayList shrinkPriority + ) { + ArrayList result = new ArrayList<>(); + HashMap bestIndexByKey = new HashMap<>(); + for (PathState path : paths) { + ArrayList shrinkables = uniqueItemsOnPath(path, shrinkPriority); + ArrayList itemsOnPath = uniqueItems(path.items()); + double effectiveRight = path.right() - path.gaps; + PathConstraintKey key = new PathConstraintKey(path.last(), shrinkables, itemsOnPath); + Integer existingIndex = bestIndexByKey.get(key); + if (existingIndex == null) { + bestIndexByKey.put(key, result.size()); + result.add(new PathConstraint(path.last(), shrinkables, effectiveRight, itemsOnPath)); + continue; + } + PathConstraint existing = result.get(existingIndex); + if (effectiveRight > existing.effectiveRight) { + existing.effectiveRight = effectiveRight; + } + } + return result; + } + + private static void truncate( + ArrayList constraints, + ArrayList shrinkPriority, + ArrayList allItems + ) { + HashSet processed = new HashSet<>(); + ArrayList executionOrder = truncationExecutionOrder(shrinkPriority); + ArrayList absorptionOrder = truncationAbsorptionOrder(shrinkPriority); + HashMap absorptionIndex = new HashMap<>(); + for (int i = 0; i < absorptionOrder.size(); i++) { + absorptionIndex.put(absorptionOrder.get(i), i); + } + HashMap kindAbsorptionIndex = kindOrderIndex(shrinkPriority); + LinkedHashMap> iconGroups = groupedIconItems(allItems); + HashMap> remainingIconWidths = new HashMap<>(); + HashMap remainingIconStarts = new HashMap<>(); + for (java.util.Map.Entry> entry : iconGroups.entrySet()) { + remainingIconWidths.put(entry.getKey(), new ArrayList<>(entry.getValue().get(0).iconWidths())); + remainingIconStarts.put(entry.getKey(), 0); + } + + for (int i = 0; i < executionOrder.size(); i++) { + LayoutItem item = executionOrder.get(i); + double neededForItem = 0; + for (FinalConstraint constraint : constraints) { + if (constraint.required <= 0 || !constraint.shrinkables.contains(item)) { + continue; + } + double assumedAbsorberCapacity = 0; + for (LayoutItem other : constraint.shrinkables) { + if (processed.contains(other) || other == item) { + continue; + } + if (canAbsorbFor(item, other, kindAbsorptionIndex, absorptionIndex)) { + assumedAbsorberCapacity += contextualShrinkCapacity(item, other); + } + } + neededForItem = Math.max(neededForItem, constraint.required - assumedAbsorberCapacity); + } + + double amount = Math.min(Math.max(0, neededForItem), contextualShrinkCapacity(item, item)); + double directActual = item.isIconItem() + ? truncateIconItemWithoutDot(item, amount) + : item.truncateAtLeast(amount); + if (directActual != 0) { + adjustConstraintsForItemWidthDelta(constraints, item, directActual); + } + if (item.isIconItem()) { + ArrayList groupItems = iconGroups.get(item.iconGroup); + ArrayList remaining = remainingIconWidths.get(item.iconGroup); + int remainingStart = remainingIconStarts.getOrDefault(item.iconGroup, 0); + ArrayList remainingAfterItem = allocateCurrentIconItem( + item, + groupItems, + remaining, + remainingStart, + constraints); + remainingIconWidths.put( + item.iconGroup, + remainingAfterItem); + remainingIconStarts.put(item.iconGroup, remainingStart + item.visibleIcons); + } + processed.add(item); + + LayoutItem nextItem = i + 1 < executionOrder.size() ? executionOrder.get(i + 1) : null; + if (item.isIconItem() && (nextItem == null || !nextItem.kind.equals(item.kind))) { + finishIconGroup( + item.iconGroup, + iconGroups.get(item.iconGroup), + remainingIconWidths.get(item.iconGroup), + constraints); + } + } + } + + private static boolean canAbsorbFor( + LayoutItem current, + LayoutItem other, + HashMap kindAbsorptionIndex, + HashMap absorptionIndex + ) { + Integer currentKind = kindAbsorptionIndex.get(current.kind); + Integer otherKind = kindAbsorptionIndex.get(other.kind); + if (currentKind == null || otherKind == null) { + return false; + } + if (otherKind < currentKind) { + return true; + } + if (otherKind > currentKind) { + return false; + } + Integer currentIndex = absorptionIndex.get(current); + Integer otherIndex = absorptionIndex.get(other); + return currentIndex != null && otherIndex != null && otherIndex < currentIndex; + } + + private static double contextualShrinkCapacity(LayoutItem current, LayoutItem other) { + if (current.iconGroup != null && current.iconGroup.equals(other.iconGroup)) { + return other.width(); + } + return other.canShrinkBy(); + } + + private static double truncationPhaseShrinkCapacity(LayoutItem item) { + if (item.isIconItem()) { + return item.width(); + } + return item.canShrinkBy(); + } + + private static double truncateIconItemWithoutDot(LayoutItem item, double amount) { + boolean oldAllowZero = item.allowZero; + boolean oldAllowDot = item.allowDot; + item.allowZero = true; + item.allowDot = false; + try { + return item.truncateAtLeast(amount); + } finally { + item.allowZero = oldAllowZero; + item.allowDot = oldAllowDot; + } + } + + private static ArrayList truncationExecutionOrder(ArrayList shrinkPriority) { + LinkedHashMap> byKind = itemsByKind(shrinkPriority); + ArrayList kinds = new ArrayList<>(byKind.keySet()); + ArrayList result = new ArrayList<>(); + for (int i = kinds.size() - 1; i >= 0; i--) { + result.addAll(byKind.get(kinds.get(i))); + } + return result; + } + + private static ArrayList truncationAbsorptionOrder(ArrayList shrinkPriority) { + LinkedHashMap> byKind = itemsByKind(shrinkPriority); + ArrayList result = new ArrayList<>(); + for (ArrayList items : byKind.values()) { + for (int i = items.size() - 1; i >= 0; i--) { + result.add(items.get(i)); + } + } + return result; + } + + private static LinkedHashMap> itemsByKind(ArrayList items) { + LinkedHashMap> byKind = new LinkedHashMap<>(); + for (LayoutItem item : items) { + byKind.computeIfAbsent(item.kind, ignored -> new ArrayList<>()).add(item); + } + return byKind; + } + + private static LinkedHashMap> groupedIconItems(ArrayList items) { + LinkedHashMap> grouped = new LinkedHashMap<>(); + for (LayoutItem item : items) { + if (item.iconGroup != null) { + grouped.computeIfAbsent(item.iconGroup, ignored -> new ArrayList<>()).add(item); + } + } + return grouped; + } + + private static void normalizeUnassignedIconGroups(ArrayList items) { + LinkedHashMap> iconGroups = groupedIconItems(items); + for (ArrayList groupItems : iconGroups.values()) { + boolean alreadyAssigned = false; + for (LayoutItem item : groupItems) { + if (item.iconPoolAssigned) { + alreadyAssigned = true; + break; + } + } + if (alreadyAssigned || groupItems.isEmpty()) { + continue; + } + ArrayList remaining = new ArrayList<>(groupItems.get(0).iconWidths()); + int start = 0; + for (LayoutItem item : groupItems) { + setIconItemPool(item, remaining, start); + int used = item.visibleIcons; + remaining = sliceFrom(remaining, used); + start += used; + } + } + } + + private static ArrayList allocateCurrentIconItem( + LayoutItem item, + ArrayList groupItems, + ArrayList remainingWidths, + int remainingStart, + ArrayList constraints + ) { + HashMap oldWidths = widthsByItem(groupItems); + setIconItemPool(item, remainingWidths, remainingStart); + ArrayList remainingAfterItem = sliceFrom(remainingWidths, item.visibleIcons); + int remainingAfterStart = remainingStart + item.visibleIcons; + int itemIndex = groupItems.indexOf(item); + for (int i = itemIndex + 1; i < groupItems.size(); i++) { + setIconItemPool(groupItems.get(i), remainingAfterItem, remainingAfterStart); + } + adjustGroupConstraintDeltas(groupItems, oldWidths, constraints); + return remainingAfterItem; + } + + private static void finishIconGroup( + String group, + ArrayList groupItems, + ArrayList remainingWidths, + ArrayList constraints + ) { + if (group == null || groupItems == null || groupItems.isEmpty()) { + return; + } + HashMap oldWidths = widthsByItem(groupItems); + if (remainingWidths != null && !remainingWidths.isEmpty()) { + HashMap growthRooms = new HashMap<>(); + for (LayoutItem item : groupItems) { + growthRooms.put(item, expansionRoomForConstraints(item, constraints)); + } + placeFinalDot(groupItems, growthRooms); + } + adjustGroupConstraintDeltas(groupItems, oldWidths, constraints); + } + + private static void setIconItemPool(LayoutItem item, ArrayList widths, int startIndex) { + boolean oldAllowZero = item.allowZero; + boolean oldAllowDot = item.allowDot; + item.allowZero = true; + item.allowDot = false; + try { + item.widthLimit = item.width(); + item.setPoolIcons(widths, startIndex); + } finally { + item.allowZero = oldAllowZero; + item.allowDot = oldAllowDot; + } + } + + private static void placeFinalDot( + ArrayList groupItems, + HashMap growthRooms + ) { + HashMap capacities = new HashMap<>(); + for (LayoutItem item : groupItems) { + Double growth = growthRooms.get(item); + capacities.put(item, item.width() + (growth != null ? growth : 0)); + } + + while (true) { + int lastIcons = lastItemWithIcons(groupItems); + if (lastIcons < 0) { + lastIcons = 0; + } + for (int i = lastIcons; i < groupItems.size(); i++) { + LayoutItem item = groupItems.get(i); + double dotWidth = dotWidthForFinalPlacement(groupItems, item); + double iconWidth = sum(item.visibleIconWidths); + Double capacity = capacities.get(item); + if (capacity != null && iconWidth + dotWidth <= capacity) { + item.dot = true; + item.setPrimaryWidth(iconWidth + dotWidth); + item.widthLimit = item.width(); + return; + } + } + LayoutItem item = groupItems.get(lastIcons); + if (item.visibleIconWidths.isEmpty()) { + return; + } + item.visibleIconWidths.remove(item.visibleIconWidths.size() - 1); + item.visibleIcons = item.visibleIconWidths.size(); + item.setPrimaryWidth(sum(item.visibleIconWidths)); + item.widthLimit = item.width(); + } + } + + private static double dotWidthForFinalPlacement(ArrayList groupItems, LayoutItem item) { + double dotWidth = item.dotWidth(); + Direction dotSide = item.dotSide(); + int index = groupItems.indexOf(item); + if (dotSide == Direction.RIGHT) { + boolean allLaterZero = true; + for (int i = index + 1; i < groupItems.size(); i++) { + if (groupItems.get(i).width() > 0) { + allLaterZero = false; + break; + } + } + if (allLaterZero) { + return Math.min(dotWidth, item.dotWidth - item.dotWidth / 3.0); + } + } else { + boolean allEarlierZero = true; + for (int i = 0; i < index; i++) { + if (groupItems.get(i).width() > 0) { + allEarlierZero = false; + break; + } + } + if (allEarlierZero) { + return Math.min(dotWidth, item.dotWidth - item.dotWidth / 3.0); + } + } + return dotWidth; + } + + private static int lastItemWithIcons(ArrayList groupItems) { + for (int i = groupItems.size() - 1; i >= 0; i--) { + if (groupItems.get(i).visibleIcons > 0) { + return i; + } + } + return -1; + } + + private static double expansionRoomForConstraints(LayoutItem item, ArrayList constraints) { + boolean found = false; + double room = Double.MAX_VALUE; + for (FinalConstraint constraint : constraints) { + if (constraint.itemsOnPath.contains(item)) { + found = true; + room = Math.min(room, -constraint.required); + } + } + return found ? Math.max(0, room) : Double.POSITIVE_INFINITY; + } + + private static HashMap widthsByItem(ArrayList items) { + HashMap widths = new HashMap<>(); + for (LayoutItem item : items) { + widths.put(item, item.width()); + } + return widths; + } + + private static void adjustGroupConstraintDeltas( + ArrayList items, + HashMap oldWidths, + ArrayList constraints + ) { + for (LayoutItem item : items) { + Double oldWidth = oldWidths.get(item); + if (oldWidth == null) { + continue; + } + double delta = oldWidth - item.width(); + if (delta != 0) { + adjustConstraintsForItemWidthDelta(constraints, item, delta); + } + } + } + + private static void adjustConstraintsForItemWidthDelta( + ArrayList constraints, + LayoutItem item, + double delta + ) { + for (FinalConstraint constraint : constraints) { + if (constraint.itemsOnPath.contains(item)) { + constraint.required -= delta; + } + } + } + + private static ArrayList sliceFrom(ArrayList values, int start) { + ArrayList result = new ArrayList<>(); + if (values == null) { + return result; + } + for (int i = Math.max(0, start); i < values.size(); i++) { + result.add(values.get(i)); + } + return result; + } + + private static double sum(ArrayList values) { + double result = 0; + if (values != null) { + for (Integer value : values) { + result += Math.max(0, value != null ? value : 0); + } + } + return result; + } + + private static ArrayList paths(ArrayList items, int wall, int itemPadding) { + ArrayList paths = new ArrayList<>(); + ArrayList boxes = new ArrayList<>(); + for (LayoutItem item : items) { + boxes.addAll(item.boxes); + } + for (Box box : boxes) { + if (canStart(box)) { + dfs(new PathState(box), wall, itemPadding, paths); + } + } + return paths; + } + + private static void dfs(PathState path, int wall, int itemPadding, ArrayList paths) { + if (canEnd(path.last(), wall)) { + paths.add(path); + } + HashSet seen = new HashSet<>(path.boxes); + for (Box nextBox : nextBoxes(path)) { + if (!seen.contains(nextBox)) { + dfs(path.added(nextBox, itemPadding), wall, itemPadding, paths); + } + } + } + + private static ArrayList nextBoxes(PathState path) { + ArrayList result = new ArrayList<>(path.last().collisionRight); + if (canItemHop(path, path.last())) { + for (Box other : path.last().item.boxes) { + if (other != path.last()) { + result.add(other); + } + } + } + return result; + } + + private static boolean canStart(Box box) { + return box.collisionLeft.isEmpty() + && (box.item.boxes.size() <= 1 || box.left() == box.item.left()); + } + + private static boolean canEnd(Box box, int wall) { + return box.collisionRight.isEmpty() + && box.right() > wall + && (box.item.boxes.size() <= 1 || box.right() == box.item.right()); + } + + private static boolean canItemHop(PathState path, Box box) { + if (box.item.boxes.size() <= 1) { + return false; + } + int count = 0; + for (Box pathBox : path.boxes) { + if (pathBox.item == box.item) { + count++; + } + } + return count < 2; + } + + private static void finalStack(Spec spec, Region region) { + stackLeft(itemsWithPosition(region.items, Position.LEFT), region.leftWall, spec.itemPadding); + stackRight(itemsWithPosition(region.items, Position.RIGHT), region.rightWall, spec.itemPadding); + + ArrayList middleItems = itemsWithPosition(region.items, Position.MIDDLE); + double middle = spec.screenWidth / 2.0; + for (LayoutItem item : middleItems) { + item.setLeft(middle - item.width() / 2.0); + double shiftAmount = item.collisionAmountLeft(middleItems, spec.itemPadding); + if (shiftAmount != 0) { + item.shiftX(shiftAmount); + ArrayList tree = filterItems(item.collisionLeftItems(), middleItems); + if (!tree.isEmpty()) { + double left = minLeft(tree); + double right = item.right(); + double currentMid = (left + right) / 2.0; + for (LayoutItem collision : tree) { + collision.shiftX(middle - currentMid); + } + } + } + } + + for (LayoutItem item : middleItems) { + double amountLeft = Math.max(item.collisionAmountLeft(null, spec.itemPadding), region.leftWall - item.left()); + if (amountLeft > 0) { + ArrayList tree = filterItems(item.collisionRightItems(), middleItems); + if (tree.isEmpty()) { + item.shiftX(amountLeft); + continue; + } + for (LayoutItem collision : tree) { + collision.shiftX(amountLeft); + } + } + } + + for (int i = middleItems.size() - 1; i >= 0; i--) { + LayoutItem item = middleItems.get(i); + double amountRight = Math.max(item.collisionAmountRight(null, spec.itemPadding), item.right() - region.rightWall); + if (amountRight > 0) { + ArrayList tree = filterItems(item.collisionLeftItems(), middleItems); + if (tree.isEmpty()) { + item.shiftX(-amountRight); + continue; + } + for (LayoutItem collision : tree) { + collision.shiftX(-amountRight); + } + } + } + } + + private static ArrayList itemsWithPosition(ArrayList items, Position position) { + ArrayList result = new ArrayList<>(); + for (LayoutItem item : items) { + if (item.position == position) { + result.add(item); + } + } + return result; + } + + private static ArrayList filterItems(ArrayList source, ArrayList allowed) { + ArrayList result = new ArrayList<>(); + for (LayoutItem item : source) { + if (allowed.contains(item)) { + result.add(item); + } + } + return result; + } + + private static double minLeft(ArrayList items) { + double left = Double.MAX_VALUE; + for (LayoutItem item : items) { + left = Math.min(left, item.left()); + } + return left == Double.MAX_VALUE ? 0 : left; + } + + private static void stackLeft(ArrayList items, double againstWall, int itemPadding) { + for (LayoutItem item : items) { + item.setLeft(againstWall); + item.shiftX(item.collisionAmountLeft(null, itemPadding)); + } + } + + private static void stackRight(ArrayList items, double againstWall, int itemPadding) { + for (int i = items.size() - 1; i >= 0; i--) { + LayoutItem item = items.get(i); + item.setRight(againstWall); + item.shiftX(-item.collisionAmountRight(null, itemPadding)); + } + } + + public static void collisionTree(ArrayList items) { + for (LayoutItem item : items) { + for (Box box : item.boxes) { + box.collisionLeft.clear(); + box.collisionRight.clear(); + } + } + + ArrayList left = new ArrayList<>(); + for (LayoutItem item : items) { + for (int i = left.size() - 1; i >= 0; i--) { + LayoutItem candidate = left.get(i); + for (Box box : item.boxes) { + for (Box candidateBox : candidate.boxes) { + if (!verticalOverlap(box, candidateBox)) { + continue; + } + if (box.isInCollisionLeftTree(candidateBox)) { + continue; + } + box.collisionLeft.add(candidateBox); + candidateBox.collisionRight.add(box); + } + } + } + left.add(item); + } + } + + private static boolean verticalOverlap(Box a, Box b) { + return Math.max(a.top(), b.top()) < Math.min(a.bottom(), b.bottom()); + } + + private static ArrayList uniqueItemsOnPath(PathState path, ArrayList allowed) { + ArrayList result = new ArrayList<>(); + for (Box box : path.boxes) { + if (allowed.contains(box.item) && !result.contains(box.item)) { + result.add(box.item); + } + } + return result; + } + + private static ArrayList uniqueItems(ArrayList items) { + ArrayList result = new ArrayList<>(); + for (LayoutItem item : items) { + if (!result.contains(item)) { + result.add(item); + } + } + return result; + } + + private static HashMap kindOrderIndex(ArrayList items) { + HashMap result = new HashMap<>(); + for (LayoutItem item : items) { + if (!result.containsKey(item.kind)) { + result.put(item.kind, result.size()); + } + } + return result; + } + + private static void configureIconGroups(ArrayList items) { + LinkedHashMap> groups = groupedIconItems(items); + for (ArrayList groupItems : groups.values()) { + if (groupItems.isEmpty()) { + continue; + } + for (LayoutItem item : groupItems) { + item.poolIconWidths = item.iconWidths(); + item.visibleIconWidths = new ArrayList<>(item.poolIconWidths); + item.poolRemaining = item.poolIconWidths.size(); + item.visibleIcons = item.poolIconWidths.size(); + item.dot = false; + item.firstVisibleIconIndex = 0; + item.iconPoolAssigned = false; + item.widthLimit = item.width(); + } + groupItems.get(0).allowZero = false; + groupItems.get(0).allowDot = true; + for (int i = 1; i < groupItems.size(); i++) { + groupItems.get(i).allowZero = true; + groupItems.get(i).allowDot = false; + } + } + } + + public static final class Spec { + public final int screenWidth; + public final int cutoutLeft; + public final int cutoutWidth; + public final int itemPadding; + public final ArrayList shrinkOrder; + + public Spec(int screenWidth, int cutoutLeft, int cutoutWidth, ArrayList shrinkOrder) { + this(screenWidth, cutoutLeft, cutoutWidth, 0, shrinkOrder); + } + + public Spec(int screenWidth, int cutoutLeft, int cutoutWidth, int itemPadding, ArrayList shrinkOrder) { + this.screenWidth = Math.max(0, screenWidth); + this.cutoutLeft = cutoutLeft; + this.cutoutWidth = Math.max(0, cutoutWidth); + this.itemPadding = Math.max(0, itemPadding); + this.shrinkOrder = shrinkOrder != null ? shrinkOrder : new ArrayList<>(); + } + + boolean hasCutout() { + return cutoutWidth > 0; + } + } + + public enum Position { + LEFT, + MIDDLE, + RIGHT + } + + public enum Fallback { + LEFT, + RIGHT + } + + public enum Direction { + LEFT, + RIGHT + } + + public static final class LayoutItem { + public final String name; + public final String kind; + public final Position position; + public final Fallback fallback; + public final ArrayList boxes; + public String iconGroup; + public int iconCount; + public int iconWidth = 40; + public ArrayList iconWidths; + public int dotWidth = 40; + public int compactDotWidth = 30; + public boolean allowZero; + public boolean allowDot; + public Direction direction; + public int visibleIcons; + public int firstVisibleIconIndex; + public boolean dot; + private boolean iconPoolAssigned; + public int poolRemaining; + public ArrayList visibleIconWidths = new ArrayList<>(); + public ArrayList poolIconWidths = new ArrayList<>(); + public Double widthLimit; + public double x; + public double y; + private double regionLeftWall = Double.NaN; + private double regionRightWall = Double.NaN; + + public LayoutItem( + String name, + String kind, + Position position, + Fallback fallback, + ArrayList boxes + ) { + this.name = name; + this.kind = kind; + this.position = position; + this.fallback = fallback; + this.boxes = boxes != null ? boxes : new ArrayList<>(); + for (Box box : this.boxes) { + box.item = this; + } + } + + public static LayoutItem fixed( + String name, + String kind, + Position position, + Fallback fallback, + double width, + double height, + double y + ) { + LayoutItem item = new LayoutItem( + name, + kind, + position, + fallback, + boxes(new Box(0, 0, width, height))); + item.y = y; + return item; + } + + public static LayoutItem iconStrip( + String name, + String kind, + Position position, + Fallback fallback, + String iconGroup, + int iconCount, + double height, + double y + ) { + LayoutItem item = fixed(name, kind, position, fallback, 0, height, y); + item.iconGroup = iconGroup; + item.iconCount = Math.max(0, iconCount); + item.visibleIcons = item.iconCount; + item.poolRemaining = item.iconCount; + item.iconWidths = item.iconWidths(); + item.visibleIconWidths = new ArrayList<>(item.iconWidths); + item.poolIconWidths = new ArrayList<>(item.iconWidths); + item.widthLimit = sum(item.iconWidths); + item.setPrimaryWidth(item.widthLimit); + return item; + } + + public static ArrayList boxes(Box... boxes) { + ArrayList result = new ArrayList<>(); + if (boxes != null) { + for (Box box : boxes) { + result.add(box); + } + } + return result; + } + + public boolean isIconItem() { + return iconGroup != null; + } + + public double width() { + return right() - left(); + } + + public double left() { + double left = Double.MAX_VALUE; + for (Box box : boxes) { + left = Math.min(left, box.left()); + } + return left == Double.MAX_VALUE ? x : left; + } + + public double right() { + double right = -Double.MAX_VALUE; + for (Box box : boxes) { + right = Math.max(right, box.right()); + } + return right == -Double.MAX_VALUE ? x : right; + } + + public double top() { + double top = Double.MAX_VALUE; + for (Box box : boxes) { + top = Math.min(top, box.top()); + } + return top == Double.MAX_VALUE ? y : top; + } + + public double bottom() { + double bottom = -Double.MAX_VALUE; + for (Box box : boxes) { + bottom = Math.max(bottom, box.bottom()); + } + return bottom == -Double.MAX_VALUE ? y : bottom; + } + + public void setLeft(double left) { + x += left - left(); + } + + public void setRight(double right) { + x += right - right(); + } + + public void shiftX(double offset) { + x += offset; + } + + public double collisionAmountLeft(ArrayList items, int itemPadding) { + HashSet filter = boxesForItems(items); + double amount = 0; + for (Box box : boxes) { + for (Box collision : box.collisionLeft) { + if (filter != null && !filter.contains(collision)) { + continue; + } + for (VisibleBlocker blocker : visibleBlockers(collision, true, filter)) { + amount = Math.max( + amount, + blocker.edge + blocker.item.spacingTo(this, itemPadding) - box.left()); + } + } + } + return amount; + } + + public double collisionAmountRight(ArrayList items, int itemPadding) { + HashSet filter = boxesForItems(items); + double amount = 0; + for (Box box : boxes) { + for (Box collision : box.collisionRight) { + if (filter != null && !filter.contains(collision)) { + continue; + } + for (VisibleBlocker blocker : visibleBlockers(collision, false, filter)) { + amount = Math.max( + amount, + box.right() + spacingTo(blocker.item, itemPadding) - blocker.edge); + } + } + } + return amount; + } + + private double spacingTo(LayoutItem other, int itemPadding) { + if (width() <= 0 || other.width() <= 0) { + return 0; + } + if (iconGroup != null && iconGroup.equals(other.iconGroup)) { + return 0; + } + return Math.max(0, itemPadding); + } + + public ArrayList collisionLeftItems() { + return collisionItems(true); + } + + public ArrayList collisionRightItems() { + return collisionItems(false); + } + + public double minWidth() { + if (!isIconItem()) { + if ("Chip".equals(kind)) { + return 0; + } + return width(); + } + if (iconCount <= 0 || poolRemaining <= 0) { + return 0; + } + if (allowZero) { + return 0; + } + return dotWidth(0); + } + + public double canShrinkBy() { + return Math.max(0, width() - minWidth()); + } + + public double truncateAtLeast(double amount) { + amount = Math.max(0, Math.min(amount, canShrinkBy())); + if (amount <= 0) { + return 0; + } + double oldWidth = width(); + if (isIconItem()) { + widthLimit = Math.max(0, currentWidthLimit() - amount); + recomputeIconState(); + widthLimit = width(); + } else { + setPrimaryWidth(oldWidth - amount); + } + return oldWidth - width(); + } + + public void setPoolRemaining(int remaining) { + poolRemaining = Math.max(0, remaining); + recomputeIconState(); + } + + public void setPoolIcons(ArrayList widths, int startIndex) { + widthLimit = Math.min(currentWidthLimit(), width()); + poolIconWidths = widths != null ? new ArrayList<>(widths) : new ArrayList<>(); + firstVisibleIconIndex = Math.max(0, startIndex); + poolRemaining = poolIconWidths.size(); + iconPoolAssigned = true; + recomputeIconState(); + widthLimit = width(); + } + + public void recomputeIconState() { + if (!isIconItem()) { + return; + } + if (poolIconWidths.isEmpty() && iconCount > 0) { + poolIconWidths = iconWidths(); + poolRemaining = poolIconWidths.size(); + } + double maxWidth = Math.min(currentWidthLimit(), sum(poolIconWidths)); + IconState state = bestIconState(maxWidth); + visibleIcons = state.visibleIcons; + visibleIconWidths = firstN(poolIconWidths, visibleIcons); + dot = state.dot; + setPrimaryWidth(state.width); + } + + private double currentWidthLimit() { + return widthLimit != null ? widthLimit : width(); + } + + private IconState bestIconState(double maxWidth) { + if (poolRemaining <= 0) { + return new IconState(0, 0, false); + } + ArrayList prefixes = new ArrayList<>(); + prefixes.add(0.0); + for (Integer width : poolIconWidths) { + prefixes.add(prefixes.get(prefixes.size() - 1) + Math.max(0, width != null ? width : 0)); + } + + IconState best = null; + int maxIcons = poolIconWidths.size(); + for (int visible = maxIcons; visible >= 0; visible--) { + int hidden = poolRemaining - visible; + boolean stateDot = allowDot && hidden > 0; + double width; + if (visible == 0 && !stateDot && !allowZero) { + continue; + } else if (visible == 0 && stateDot && allowZero) { + width = dotWidth(); + } else if (visible > 0 || stateDot) { + width = prefixes.get(visible) + (stateDot ? dotWidth() : 0); + } else { + width = 0; + } + if (width <= maxWidth && (best == null || width > best.width)) { + best = new IconState(width, visible, stateDot); + } + } + return best != null ? best : new IconState(0, 0, false); + } + + private double dotWidth() { + return dotWidth(visibleIcons); + } + + private double dotWidth(int visible) { + double crop = 0; + Direction side = dotSide(); + if (side == Direction.LEFT && noCollisionsOnSide(true)) { + crop += dotWidth / 3.0; + } + if (side == Direction.RIGHT && noCollisionsOnSide(false)) { + crop += dotWidth / 3.0; + } + if (visible == 0) { + if (side != Direction.LEFT && noCollisionsOnSide(true)) { + crop += dotWidth / 3.0; + } + if (side != Direction.RIGHT && noCollisionsOnSide(false)) { + crop += dotWidth / 3.0; + } + } + return Math.max(0, dotWidth - crop); + } + + private boolean noCollisionsOnSide(boolean left) { + for (Box box : boxes) { + if (left ? !box.collisionLeft.isEmpty() : !box.collisionRight.isEmpty()) { + return false; + } + } + return true; + } + + private Direction dotSide() { + if (direction != null) { + return direction; + } + return position == Position.RIGHT ? Direction.LEFT : Direction.RIGHT; + } + + private void setPrimaryWidth(double width) { + if (!boxes.isEmpty()) { + boxes.get(0).width = Math.max(0, width); + } + } + + private ArrayList iconWidths() { + ArrayList widths = new ArrayList<>(); + for (int i = 0; i < iconCount; i++) { + if (iconWidths != null && i < iconWidths.size()) { + widths.add(Math.max(0, iconWidths.get(i))); + } else { + widths.add(Math.max(0, iconWidth)); + } + } + return widths; + } + + private HashSet boxesForItems(ArrayList items) { + if (items == null) { + return null; + } + HashSet result = new HashSet<>(); + for (LayoutItem item : items) { + result.addAll(item.boxes); + } + return result; + } + + private ArrayList visibleBlockers(Box box, boolean left, HashSet allowedBoxes) { + ArrayList result = new ArrayList<>(); + visibleBlockers(box, left, allowedBoxes, new HashSet<>(), result); + return result; + } + + private void visibleBlockers( + Box box, + boolean left, + HashSet allowedBoxes, + HashSet seen, + ArrayList result + ) { + if (!seen.add(box)) { + return; + } + if (allowedBoxes != null && !allowedBoxes.contains(box)) { + return; + } + if (box.item.width() > 0) { + result.add(new VisibleBlocker(left ? box.right() : box.left(), box.item)); + return; + } + ArrayList collisions = left ? box.collisionLeft : box.collisionRight; + if (collisions.isEmpty()) { + result.add(new VisibleBlocker(left ? box.right() : box.left(), box.item)); + return; + } + for (Box collision : collisions) { + visibleBlockers(collision, left, allowedBoxes, seen, result); + } + } + + private ArrayList collisionItems(boolean leftSide) { + ArrayList result = new ArrayList<>(); + HashSet seen = new HashSet<>(); + for (Box box : boxes) { + ArrayList collisions = leftSide ? box.collisionLeft : box.collisionRight; + for (Box collision : collisions) { + collectCollisionItems(collision, leftSide, seen, result); + } + } + return result; + } + + private void collectCollisionItems( + Box box, + boolean leftSide, + HashSet seen, + ArrayList result + ) { + if (seen.add(box.item)) { + result.add(box.item); + } + for (Box collision : leftSide ? box.collisionLeft : box.collisionRight) { + collectCollisionItems(collision, leftSide, seen, result); + } + } + + private ArrayList firstN(ArrayList values, int count) { + ArrayList result = new ArrayList<>(); + if (values == null) { + return result; + } + for (int i = 0; i < Math.min(Math.max(0, count), values.size()); i++) { + result.add(values.get(i)); + } + return result; + } + } + + public static final class Box { + public double relX; + public double relY; + public double width; + public double height; + public LayoutItem item; + public final ArrayList collisionLeft = new ArrayList<>(); + public final ArrayList collisionRight = new ArrayList<>(); + + public Box(double relX, double relY, double width, double height) { + this.relX = relX; + this.relY = relY; + this.width = Math.max(0, width); + this.height = Math.max(0, height); + } + + public double left() { + return item.x + relX; + } + + public double right() { + return left() + width; + } + + public double top() { + return item.y + relY; + } + + public double bottom() { + return top() + height; + } + + public boolean isInCollisionLeftTree(Box box) { + if (collisionLeft.contains(box)) { + return true; + } + for (Box collision : collisionLeft) { + if (collision.isInCollisionLeftTree(box)) { + return true; + } + } + return false; + } + } + + private static final class Region { + final int leftWall; + final int rightWall; + final ArrayList items = new ArrayList<>(); + + Region(int leftWall, int rightWall) { + this.leftWall = leftWall; + this.rightWall = Math.max(leftWall, rightWall); + } + } + + private static final class PathState { + final ArrayList boxes = new ArrayList<>(); + final double gaps; + + PathState(Box box) { + boxes.add(box); + gaps = 0; + } + + private PathState(ArrayList boxes, double gaps) { + this.boxes.addAll(boxes); + this.gaps = gaps; + } + + PathState added(Box box, int itemPadding) { + double nextGaps = gaps; + Box previous = last(); + if (previous.item != box.item) { + double actualGap = box.left() - previous.right(); + double requiredGap = previous.item.spacingTo(box.item, itemPadding); + nextGaps += Math.max(0, actualGap - requiredGap); + } + ArrayList nextBoxes = new ArrayList<>(boxes); + nextBoxes.add(box); + return new PathState(nextBoxes, nextGaps); + } + + Box last() { + return boxes.get(boxes.size() - 1); + } + + double right() { + return last().right(); + } + + ArrayList items() { + ArrayList result = new ArrayList<>(); + for (Box box : boxes) { + result.add(box.item); + } + return result; + } + } + + private static final class PathConstraint { + final Box endBox; + final ArrayList shrinkables; + double effectiveRight; + final ArrayList itemsOnPath; + + PathConstraint( + Box endBox, + ArrayList shrinkables, + double effectiveRight, + ArrayList itemsOnPath + ) { + this.endBox = endBox; + this.shrinkables = shrinkables; + this.effectiveRight = effectiveRight; + this.itemsOnPath = itemsOnPath; + } + } + + private static final class PathConstraintKey { + final Box endBox; + final ArrayList shrinkables; + final ArrayList itemsOnPath; + + PathConstraintKey(Box endBox, ArrayList shrinkables, ArrayList itemsOnPath) { + this.endBox = endBox; + this.shrinkables = new ArrayList<>(shrinkables); + this.itemsOnPath = new ArrayList<>(itemsOnPath); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PathConstraintKey that)) { + return false; + } + return endBox == that.endBox + && shrinkables.equals(that.shrinkables) + && itemsOnPath.equals(that.itemsOnPath); + } + + @Override + public int hashCode() { + int result = System.identityHashCode(endBox); + result = 31 * result + shrinkables.hashCode(); + result = 31 * result + itemsOnPath.hashCode(); + return result; + } + } + + private static final class FinalConstraint { + final ArrayList shrinkables; + double required; + final double effectiveRight; + final ArrayList itemsOnPath; + + FinalConstraint( + ArrayList shrinkables, + double required, + double effectiveRight, + ArrayList itemsOnPath + ) { + this.shrinkables = shrinkables; + this.required = required; + this.effectiveRight = effectiveRight; + this.itemsOnPath = itemsOnPath; + } + } + + private record IconState(double width, int visibleIcons, boolean dot) { + } + + private record VisibleBlocker(double edge, LayoutItem item) { + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java index 1d54498..1bc45e5 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/DebugBoundsOverlayController.java @@ -17,6 +17,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; public final class DebugBoundsOverlayController { @@ -55,37 +57,31 @@ public final class DebugBoundsOverlayController { addCutoutSpecs(root, specs); } if (layoutEnabled && settings.debugVisualizeNotificationContainer) { - Bounds bounds = unionHostChildren( + addHostContainerSpecs( root, parent, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST, - true); - if (bounds != null) { - specs.add(new OverlaySpec(bounds, COLOR_NOTIFICATION)); - } + COLOR_NOTIFICATION, + specs); } if (layoutEnabled && settings.debugVisualizeStatusContainer) { - Bounds bounds = unionHostChildren( + addHostContainerSpecs( root, parent, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST, - false); - if (bounds != null) { - specs.add(new OverlaySpec(bounds, COLOR_STATUS)); - } + COLOR_STATUS, + specs); } if (layoutEnabled && settings.debugVisualizeClockContainer) { addHostChildSpecs(root, parent, TAG_CLOCK_HOST, COLOR_CLOCK, specs); } if (layoutEnabled && settings.debugVisualizeChipContainer) { - Bounds bounds = unionHostChildren( + addHostContainerSpecs( root, parent, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST, - false); - if (bounds != null) { - specs.add(new OverlaySpec(bounds, COLOR_CHIP)); - } + COLOR_CHIP, + specs); } if (specs.isEmpty()) { removeOverlay(parent); @@ -144,15 +140,25 @@ public final class DebugBoundsOverlayController { return new Bounds(left, top, right - left, bottom - top); } - private Bounds unionHostChildren( + private void addHostContainerSpecs( View root, ViewGroup parent, String hostTag, - boolean stableRegularIconSlots + int color, + ArrayList specs ) { View host = findDirectTaggedChild(parent, hostTag); if (!(host instanceof ViewGroup group)) { - return null; + return; + } + ArrayList containerBounds = taggedContainerBounds(host); + if (!containerBounds.isEmpty()) { + for (Bounds bounds : containerBounds) { + if (bounds.width > 0 && bounds.height > 0) { + specs.add(new OverlaySpec(bounds, color)); + } + } + return; } Bounds union = null; for (int i = 0; i < group.getChildCount(); i++) { @@ -160,13 +166,30 @@ public final class DebugBoundsOverlayController { if (child == null || child.getVisibility() == View.GONE) { continue; } - Bounds bounds = overlayBoundsRelativeTo(root, child, stableRegularIconSlots); + Bounds bounds = overlayBoundsRelativeTo(root, child, false); if (bounds == null || bounds.width <= 0 || bounds.height <= 0) { continue; } union = union == null ? bounds : union.union(bounds); } - return union; + if (union != null) { + specs.add(new OverlaySpec(union, color)); + } + } + + @SuppressWarnings("unchecked") + private ArrayList taggedContainerBounds(View host) { + Object tag = host != null ? host.getTag(R.id.sbt_debug_container_bounds) : null; + if (!(tag instanceof ArrayList values)) { + return new ArrayList<>(); + } + ArrayList out = new ArrayList<>(); + for (Object value : values) { + if (value instanceof StatusBarLayoutSolver.Bounds bounds) { + out.add(new Bounds(bounds.left, bounds.top, bounds.width, bounds.height)); + } + } + return out; } private boolean isOverflowDot(View view) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java index b08719b..f3c7573 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java @@ -577,11 +577,10 @@ final class SnapshotRenderer { private void drawOverflowDot(Bitmap bitmap, int color) { bitmap.eraseColor(Color.TRANSPARENT); - int base = Math.min(bitmap.getWidth(), bitmap.getHeight()); - if (base <= 0) { + if (bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) { return; } - float radius = Math.max(1f, base * 0.16f); + float radius = Math.max(1f, Math.min(bitmap.getWidth() / 2f, bitmap.getHeight() * 0.16f)); Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(color); Canvas canvas = new Canvas(bitmap); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java index 4d89feb..1b6b3c5 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBand.java @@ -164,6 +164,32 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { return true; } + void applyPackedContinuousWidth(int solvedWidth, AnchorSide wallSide) { + if (!canClipContinuously()) { + return; + } + clipStartPx = 0; + clipEndPx = 0; + int amount = Math.max(0, naturalWidth() - Math.max(0, solvedWidth)); + if (amount > 0) { + clipContinuous(wallSide, amount); + } + } + + void applyPackedIconState(int firstIconIndex, int visibleIcons, boolean dot, int solvedWidth) { + if (kind != LayoutItemKind.NOTIFICATIONS && kind != LayoutItemKind.STATUS) { + return; + } + int targetVisible = Math.max(0, visibleIcons); + placements = iconSlice(rawPlacements, firstIconIndex, targetVisible); + if (dot) { + replaceOverflowDot(dotWidthForSolvedState(solvedWidth), dotHeight(placements)); + } + if (targetVisible == 0 && !dot && placements.isEmpty()) { + placements.add(emptyIconContainerPlacement()); + } + } + @Override protected int wallDotSlack(AnchorSide side) { SnapshotPlacement placement = wallPlacement(side); @@ -238,6 +264,25 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { return true; } + private void replaceOverflowDot(int width, int height) { + if (placements == null) { + placements = new ArrayList<>(); + } + ArrayList withoutDot = new ArrayList<>(); + for (SnapshotPlacement placement : placements) { + if (!placement.overflowDot) { + withoutDot.add(placement); + } + } + SnapshotPlacement dotPlacement = dotPlacement(width, height); + if (kind == LayoutItemKind.STATUS) { + withoutDot.add(0, dotPlacement); + } else { + withoutDot.add(dotPlacement); + } + placements = withoutDot; + } + @Override protected Object captureState() { return new LayoutBandState(placements == null, new ArrayList<>(activePlacements())); @@ -450,6 +495,46 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { && (placement.key == null || !placement.key.startsWith(SnapshotKeys.EMPTY_ICON_CONTAINER)); } + private ArrayList iconSlice( + ArrayList source, + int firstIconIndex, + int count + ) { + ArrayList result = new ArrayList<>(); + if (source == null || count <= 0) { + return result; + } + int skipped = 0; + int start = Math.max(0, firstIconIndex); + for (SnapshotPlacement placement : source) { + if (!isShrinkableIcon(placement)) { + continue; + } + if (skipped < start) { + skipped++; + continue; + } + result.add(placement); + if (result.size() >= count) { + break; + } + } + return result; + } + + private int shrinkableIconCount(ArrayList icons) { + int count = 0; + if (icons == null) { + return 0; + } + for (SnapshotPlacement icon : icons) { + if (isShrinkableIcon(icon)) { + count++; + } + } + return count; + } + private boolean hasOverflowDot(ArrayList icons) { for (SnapshotPlacement icon : icons) { if (icon.overflowDot) { @@ -476,6 +561,18 @@ final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand { return 0; } + private int dotWidthForSolvedState(int solvedWidth) { + int visibleWidth = 0; + for (SnapshotPlacement placement : activePlacements()) { + if (!placement.overflowDot) { + visibleWidth += collisionBounds(placement).width; + } + } + int solvedDotWidth = Math.max(1, solvedWidth - visibleWidth); + int fallbackWidth = Math.max(1, dotWidth(activePlacements())); + return Math.min(fallbackWidth, solvedDotWidth); + } + private int dotHeight(ArrayList icons) { if (!icons.isEmpty()) { return icons.get(0).bounds.height; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java index e71a211..3f49832 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutPlanner.java @@ -5,19 +5,18 @@ import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; +import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver; -import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Anchor; -import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorKind; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutEdges; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutItemKind; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition; -import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.PlacedBand; -import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Region; -import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.RegionPlan; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.SplitRegion; +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class UnlockedLayoutPlanner { @@ -81,106 +80,68 @@ final class UnlockedLayoutPlanner { false)); } } - if (settings.layoutStatusEnabledUnlocked) { + if (settings.layoutStatusUnlockedCount > 0) { ArrayList rawPlacements = statusRenderer.rawPlacements(root, collectedIcons.statusIcons); - ArrayList placements = - offsetPlacements( - rawPlacements, - offsetScaler.status( - settings.layoutStatusVerticalOffsetPx, - representativeHeight(rawPlacements))); - if (!placements.isEmpty()) { - bands.add(UnlockedLayoutBand.icons( - LayoutItemKind.STATUS, - statusHost, - statusRenderer, - placements, - positionFor(settings.layoutStatusPosition), - settings.layoutStatusMiddleAutoNearestSide, - sideFor(settings.layoutStatusMiddleSide), - settings.layoutStatusShowDotIfTruncated)); + int representativeHeight = representativeHeight(rawPlacements); + for (int i = 0; i < settings.layoutStatusUnlockedCount; i++) { + ArrayList placements = + offsetPlacements( + rawPlacements, + offsetScaler.status( + intAt(settings.layoutStatusVerticalOffsetsPx, i, settings.layoutStatusVerticalOffsetPx), + representativeHeight)); + if (!placements.isEmpty()) { + bands.add(UnlockedLayoutBand.icons( + LayoutItemKind.STATUS, + statusHost, + statusRenderer, + placements, + positionFor(stringAt(settings.layoutStatusPositions, i, settings.layoutStatusPosition)), + booleanAt( + settings.layoutStatusMiddleAutoNearestSides, + i, + settings.layoutStatusMiddleAutoNearestSide), + sideFor(stringAt(settings.layoutStatusMiddleSides, i, settings.layoutStatusMiddleSide)), + true)); + } } } - if (settings.layoutNotifEnabledUnlocked) { + if (settings.layoutNotifUnlockedCount > 0) { ArrayList rawPlacements = notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons); - ArrayList placements = - offsetPlacements( - rawPlacements, - offsetScaler.notification( - settings.layoutNotifVerticalOffsetPx, - representativeHeight(rawPlacements))); - if (!placements.isEmpty()) { - bands.add(UnlockedLayoutBand.icons( - LayoutItemKind.NOTIFICATIONS, - notificationHost, - notificationRenderer, - placements, - positionFor(settings.layoutNotifPosition), - settings.layoutNotifMiddleAutoNearestSide, - sideFor(settings.layoutNotifMiddleSide), - settings.layoutNotifShowDotIfTruncated)); + int representativeHeight = representativeHeight(rawPlacements); + for (int i = 0; i < settings.layoutNotifUnlockedCount; i++) { + ArrayList placements = + offsetPlacements( + rawPlacements, + offsetScaler.notification( + intAt(settings.layoutNotifVerticalOffsetsPx, i, settings.layoutNotifVerticalOffsetPx), + representativeHeight)); + if (!placements.isEmpty()) { + bands.add(UnlockedLayoutBand.icons( + LayoutItemKind.NOTIFICATIONS, + notificationHost, + notificationRenderer, + placements, + positionFor(stringAt(settings.layoutNotifPositions, i, settings.layoutNotifPosition)), + booleanAt( + settings.layoutNotifMiddleAutoNearestSides, + i, + settings.layoutNotifMiddleAutoNearestSide), + sideFor(stringAt(settings.layoutNotifMiddleSides, i, settings.layoutNotifMiddleSide)), + true)); + } } } if (bands.isEmpty()) { return new LayoutPlan(null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>()); } - ArrayList placedBands = new ArrayList<>(); ArrayList itemOrder = orderedKinds(settings.layoutItemOrder); splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings); LayoutEdges edges = layoutEdges(root, settings, anchors); int itemPadding = Math.max(0, settings.layoutItemPaddingPx); - RegionPlan regionPlan = StatusBarLayoutSolver.planRegions( - root.getWidth(), - edges, - bands, - itemOrder, - layoutCutoutBounds, - itemPadding); - for (Region region : regionPlan.regions()) { - StatusBarLayoutSolver.truncateToFit( - region.bands, - region.width(), - regionPlan.middleSide(), - settings.layoutNotifSortOrder, - itemPadding); - } - - StatusBarLayoutSolver.placeStackedBands( - StatusBarLayoutSolver.bandsForPosition(bands, LayoutPosition.LEFT, itemOrder), - new Anchor(edges.left(), AnchorSide.RIGHT, AnchorKind.WALL), - placedBands, - settings.layoutNotifSortOrder, - false, - itemPadding); - StatusBarLayoutSolver.placeStackedBands( - StatusBarLayoutSolver.bandsForPosition(bands, LayoutPosition.RIGHT, itemOrder), - new Anchor(edges.right(), AnchorSide.LEFT, AnchorKind.WALL), - placedBands, - settings.layoutNotifSortOrder, - false, - itemPadding); - ArrayList middleBands = StatusBarLayoutSolver.middleBandsForPlacement( - bands, - itemOrder, - regionPlan.middleSide()); - if (!middleBands.isEmpty()) { - Anchor anchor = StatusBarLayoutSolver.anchorForMiddle( - root.getWidth(), - middleBands, - placedBands, - layoutCutoutBounds, - regionPlan.middleSide(), - itemPadding); - StatusBarLayoutSolver.placeStackedBands( - middleBands, - anchor, - placedBands, - settings.layoutNotifSortOrder, - true, - itemPadding); - } + solveWithPackedLayout(root, edges, bands, layoutCutoutBounds, settings, itemPadding); ArrayList statusPlacements = new ArrayList<>(); ArrayList notificationPlacements = new ArrayList<>(); @@ -204,6 +165,266 @@ final class UnlockedLayoutPlanner { notificationPlacements); } + private void solveWithPackedLayout( + View root, + LayoutEdges edges, + ArrayList bands, + Bounds cutoutBounds, + SbtSettings settings, + int itemPadding + ) { + ArrayList items = new ArrayList<>(); + ArrayList itemBands = new ArrayList<>(); + for (int i = 0; i < bands.size(); i++) { + UnlockedLayoutBand band = bands.get(i); + AnchorSide side = packedAnchorSide(band); + band.prepareForSide(side, settings.layoutNotifSortOrder); + PackedStatusBarLayoutSolver.LayoutItem item = packedItemForBand(band, i); + items.add(item); + itemBands.add(band); + } + configurePackedIconGroups(items); + int dx = edges.left(); + int availableWidth = Math.max(0, edges.right() - edges.left()); + Bounds relativeCutout = relativeCutout(cutoutBounds, edges); + PackedStatusBarLayoutSolver.Spec spec = new PackedStatusBarLayoutSolver.Spec( + availableWidth, + relativeCutout != null ? relativeCutout.left : 0, + relativeCutout != null ? relativeCutout.width : 0, + itemPadding, + packedShrinkOrder()); + PackedStatusBarLayoutSolver.solve(spec, items); + for (PackedStatusBarLayoutSolver.LayoutItem item : items) { + item.x = Math.max(0, Math.min(availableWidth, item.x)) + dx; + } + for (int i = 0; i < items.size(); i++) { + UnlockedLayoutBand band = itemBands.get(i); + PackedStatusBarLayoutSolver.LayoutItem item = items.get(i); + if (band.kind == LayoutItemKind.NOTIFICATIONS || band.kind == LayoutItemKind.STATUS) { + band.applyPackedIconState( + item.firstVisibleIconIndex, + item.visibleIcons, + item.dot, + Math.round((float) item.width())); + } else if (band.kind == LayoutItemKind.CHIP || band.kind == LayoutItemKind.CLOCK) { + band.applyPackedContinuousWidth(Math.round((float) item.width()), packedAnchorSide(band)); + } + band.place(Math.round((float) item.x)); + } + updateDebugContainerBounds(itemBands); + } + + private void updateDebugContainerBounds(ArrayList bands) { + LinkedHashMap> boundsByHost = new LinkedHashMap<>(); + for (UnlockedLayoutBand band : bands) { + if (band.host == null + || band.kind == LayoutItemKind.CLOCK) { + continue; + } + ArrayList hostBounds = boundsByHost.computeIfAbsent( + band.host, + ignored -> new ArrayList<>()); + if (band.placedBounds == null + || band.placedBounds.width <= 0 + || band.placedBounds.height <= 0) { + continue; + } + hostBounds.add(band.placedBounds); + } + for (Map.Entry> entry : boundsByHost.entrySet()) { + entry.getKey().setTag(R.id.sbt_debug_container_bounds, entry.getValue()); + } + } + + private Bounds relativeCutout(Bounds cutoutBounds, LayoutEdges edges) { + if (cutoutBounds == null || edges == null) { + return null; + } + int left = Math.max(edges.left(), cutoutBounds.left); + int right = Math.min(edges.right(), cutoutBounds.left + cutoutBounds.width); + if (right <= left) { + return null; + } + return new Bounds(left - edges.left(), cutoutBounds.top, right - left, cutoutBounds.height); + } + + private PackedStatusBarLayoutSolver.LayoutItem packedItemForBand(UnlockedLayoutBand band, int index) { + ArrayList boxes = new ArrayList<>(); + for (Bounds box : band.collisionBoxes()) { + boxes.add(new PackedStatusBarLayoutSolver.Box(box.left, box.top, box.width, box.height)); + } + if (boxes.isEmpty()) { + boxes.add(new PackedStatusBarLayoutSolver.Box(0, band.top(), band.width(), band.height())); + } + PackedStatusBarLayoutSolver.LayoutItem item = new PackedStatusBarLayoutSolver.LayoutItem( + packedName(band, index), + packedKind(band.kind), + packedPosition(band.position), + packedFallback(band), + boxes); + if (band.kind == LayoutItemKind.NOTIFICATIONS || band.kind == LayoutItemKind.STATUS) { + item.iconGroup = packedKind(band.kind).toLowerCase(java.util.Locale.ROOT); + item.iconCount = band.rawPlacements != null ? band.rawPlacements.size() : 0; + item.visibleIcons = item.iconCount; + item.poolRemaining = item.iconCount; + item.iconWidth = representativeIconWidth(band); + item.iconWidths = iconWidths(band); + item.dotWidth = Math.max(1, Math.round(band.height() * 0.75f)); + item.compactDotWidth = representativeCompactDotWidth(band); + item.allowZero = false; + item.allowDot = true; + item.direction = band.kind == LayoutItemKind.STATUS + ? PackedStatusBarLayoutSolver.Direction.LEFT + : PackedStatusBarLayoutSolver.Direction.RIGHT; + item.widthLimit = sumIconWidths(item.iconWidths, item.iconCount, item.iconWidth); + } + item.y = 0; + return item; + } + + private int representativeIconWidth(UnlockedLayoutBand band) { + ArrayList placements = band.rawPlacements; + if (placements == null || placements.isEmpty()) { + return Math.max(1, band.width()); + } + return Math.max(1, placements.get(0).bounds.width); + } + + private ArrayList iconWidths(UnlockedLayoutBand band) { + ArrayList widths = new ArrayList<>(); + ArrayList placements = band.rawPlacements; + if (placements == null) { + return widths; + } + for (SnapshotPlacement placement : placements) { + Bounds bounds; + if (band.kind == LayoutItemKind.NOTIFICATIONS) { + bounds = placement != null ? placement.bounds : null; + } else { + bounds = placement != null ? placement.contentBounds : null; + if (bounds == null || bounds.width < 0) { + bounds = placement != null ? placement.bounds : null; + } + } + widths.add(Math.max(0, bounds != null ? bounds.width : 0)); + } + return widths; + } + + private double sumIconWidths(ArrayList widths, int count, int fallbackWidth) { + double total = 0; + for (int i = 0; i < Math.max(0, count); i++) { + if (widths != null && i < widths.size()) { + total += Math.max(0, widths.get(i)); + } else { + total += Math.max(0, fallbackWidth); + } + } + return total; + } + + private int representativeCompactDotWidth(UnlockedLayoutBand band) { + ArrayList placements = band.rawPlacements; + if (placements == null || placements.isEmpty()) { + return Math.max(1, band.width()); + } + Bounds bounds = placements.get(0).bounds; + int slotWidth = Math.max(1, bounds.width); + int base = Math.max(1, Math.min(slotWidth, bounds.height)); + int radius = Math.max(1, Math.round(base * 0.16f)); + int visualWidth = Math.min(slotWidth, radius * 2 + 2); + return Math.max(visualWidth, slotWidth - Math.max(0, (slotWidth - visualWidth) / 2)); + } + + private String stringAt(String[] values, int index, String fallback) { + return values != null && index >= 0 && index < values.length && values[index] != null + ? values[index] + : fallback; + } + + private boolean booleanAt(boolean[] values, int index, boolean fallback) { + return values != null && index >= 0 && index < values.length ? values[index] : fallback; + } + + private int intAt(int[] values, int index, int fallback) { + return values != null && index >= 0 && index < values.length ? values[index] : fallback; + } + + private void configurePackedIconGroups(ArrayList items) { + java.util.LinkedHashMap> groups = + new java.util.LinkedHashMap<>(); + for (PackedStatusBarLayoutSolver.LayoutItem item : items) { + if (item.iconGroup != null) { + groups.computeIfAbsent(item.iconGroup, ignored -> new ArrayList<>()).add(item); + } + } + for (ArrayList groupItems : groups.values()) { + for (int i = 0; i < groupItems.size(); i++) { + PackedStatusBarLayoutSolver.LayoutItem item = groupItems.get(i); + item.allowZero = i > 0; + item.allowDot = i == 0; + } + } + } + + private ArrayList packedShrinkOrder() { + ArrayList order = new ArrayList<>(); + order.add("Notifs"); + order.add("Status"); + order.add("Chip"); + order.add("Clock"); + return order; + } + + private String packedName(UnlockedLayoutBand band, int index) { + return packedKind(band.kind) + index; + } + + private String packedKind(LayoutItemKind kind) { + if (kind == LayoutItemKind.NOTIFICATIONS) { + return "Notifs"; + } + if (kind == LayoutItemKind.STATUS) { + return "Status"; + } + if (kind == LayoutItemKind.CHIP) { + return "Chip"; + } + return "Clock"; + } + + private PackedStatusBarLayoutSolver.Position packedPosition(LayoutPosition position) { + if (position == LayoutPosition.RIGHT) { + return PackedStatusBarLayoutSolver.Position.RIGHT; + } + if (position == LayoutPosition.MIDDLE) { + return PackedStatusBarLayoutSolver.Position.MIDDLE; + } + return PackedStatusBarLayoutSolver.Position.LEFT; + } + + private PackedStatusBarLayoutSolver.Fallback packedFallback(UnlockedLayoutBand band) { + return band.middleSide == AnchorSide.RIGHT + ? PackedStatusBarLayoutSolver.Fallback.RIGHT + : PackedStatusBarLayoutSolver.Fallback.LEFT; + } + + private AnchorSide packedAnchorSide(UnlockedLayoutBand band) { + if (band.splitRegion == SplitRegion.LEFT) { + return AnchorSide.LEFT; + } + if (band.splitRegion == SplitRegion.RIGHT) { + return AnchorSide.RIGHT; + } + if (band.position == LayoutPosition.LEFT) { + return AnchorSide.RIGHT; + } + if (band.position == LayoutPosition.RIGHT) { + return AnchorSide.LEFT; + } + return band.middleSide; + } + private ArrayList offsetPlacements(ArrayList placements, int verticalOffsetPx) { if (placements == null || placements.isEmpty() || verticalOffsetPx == 0) { return placements; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java index da4bb0d..749e5ea 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java @@ -718,14 +718,22 @@ final class UnlockedStockSourceCollector { settings.layoutChipVerticalOffsetPx, settings.layoutNotifPosition, settings.layoutNotifSortOrder, - settings.layoutNotifShowDotIfTruncated, settings.layoutNotifEnabledUnlocked, + settings.layoutNotifUnlockedCount, + java.util.Arrays.hashCode(settings.layoutNotifPositions), + java.util.Arrays.hashCode(settings.layoutNotifMiddleSides), + java.util.Arrays.hashCode(settings.layoutNotifMiddleAutoNearestSides), + java.util.Arrays.hashCode(settings.layoutNotifVerticalOffsetsPx), settings.layoutNotifMiddleSide, settings.layoutNotifMiddleAutoNearestSide, settings.layoutNotifVerticalOffsetPx, settings.layoutStatusPosition, settings.layoutStatusEnabledUnlocked, - settings.layoutStatusShowDotIfTruncated, + settings.layoutStatusUnlockedCount, + java.util.Arrays.hashCode(settings.layoutStatusPositions), + java.util.Arrays.hashCode(settings.layoutStatusMiddleSides), + java.util.Arrays.hashCode(settings.layoutStatusMiddleAutoNearestSides), + java.util.Arrays.hashCode(settings.layoutStatusVerticalOffsetsPx), settings.layoutStatusMiddleSide, settings.layoutStatusMiddleAutoNearestSide, settings.layoutStatusVerticalOffsetPx, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/debug/NotificationDisplayStyleSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/debug/NotificationDisplayStyleSettings.java new file mode 100644 index 0000000..4936426 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/debug/NotificationDisplayStyleSettings.java @@ -0,0 +1,87 @@ +package se.ajpanton.statusbartweak.shell.debug; + +import android.content.ContentResolver; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +public final class NotificationDisplayStyleSettings { + private static final String KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION = + "lockscreen_minimizing_notification"; + + private NotificationDisplayStyleSettings() { + } + + public static State read(Context context) { + ContentResolver resolver = context != null ? context.getContentResolver() : null; + Integer minimizing = getSystemInt(resolver, KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION); + return new State(minimizing, styleFor(minimizing)); + } + + public static boolean write(Context context, Style style) { + if (context == null || style == null || style == Style.UNKNOWN) { + return false; + } + int minimizing = switch (style) { + case CARDS -> 0; + case ICONS -> 1; + case DOT -> 2; + case UNKNOWN -> -1; + }; + if (minimizing < 0) { + return false; + } + return runRootCommand( + "settings put system " + KEY_LOCKSCREEN_MINIMIZING_NOTIFICATION + " " + minimizing); + } + + private static Style styleFor(Integer minimizing) { + if (minimizing == null) { + return Style.UNKNOWN; + } + return switch (minimizing) { + case 0 -> Style.CARDS; + case 1 -> Style.ICONS; + case 2 -> Style.DOT; + default -> Style.UNKNOWN; + }; + } + + private static Integer getSystemInt(ContentResolver resolver, String key) { + if (resolver == null || TextUtils.isEmpty(key)) { + return null; + } + try { + return Settings.System.getInt(resolver, key); + } catch (Throwable ignored) { + return null; + } + } + + private static boolean runRootCommand(String command) { + if (TextUtils.isEmpty(command)) { + return false; + } + try { + Process process = new ProcessBuilder("su", "-c", command) + .redirectErrorStream(true) + .start(); + return process.waitFor() == 0; + } catch (Throwable ignored) { + return false; + } + } + + public enum Style { + UNKNOWN, + CARDS, + ICONS, + DOT + } + + public record State( + Integer minimizingNotification, + Style style + ) { + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java index 881478b..d4ccf40 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java @@ -30,24 +30,24 @@ public final class SbtDefaults { public static final boolean ICONS_LOCK_EVEN_DISTRIBUTION_DEFAULT = false; // Cards mode (AOD). - public static final int CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT = 8; + public static final int CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT = 5; public static final int CARDS_AOD_MAX_ICONS_PER_ROW_MIN = 1; public static final int CARDS_AOD_MAX_ICONS_PER_ROW_MAX = 15; - public static final int CARDS_AOD_MAX_ROWS_DEFAULT = 3; - public static final int CARDS_AOD_MAX_ROWS_MIN = 1; + public static final int CARDS_AOD_MAX_ROWS_DEFAULT = 1; + public static final int CARDS_AOD_MAX_ROWS_MIN = 0; public static final int CARDS_AOD_MAX_ROWS_MAX = 5; public static final boolean CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT = true; - public static final boolean CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT = false; + public static final boolean CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT = true; // Cards mode (Lockscreen). - public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT = 8; + public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT = 5; public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_MIN = 1; public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_MAX = 15; - public static final int CARDS_LOCK_MAX_ROWS_DEFAULT = 3; - public static final int CARDS_LOCK_MAX_ROWS_MIN = 1; + public static final int CARDS_LOCK_MAX_ROWS_DEFAULT = 1; + public static final int CARDS_LOCK_MAX_ROWS_MIN = 0; public static final int CARDS_LOCK_MAX_ROWS_MAX = 5; public static final boolean CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT = true; - public static final boolean CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT = false; + public static final boolean CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT = true; public static final boolean AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT = false; public static final boolean BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT = true; public static final boolean BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT = true; @@ -115,6 +115,7 @@ public final class SbtDefaults { public static final boolean LAYOUT_NOTIF_ENABLED_AOD_DEFAULT = true; public static final boolean LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT = true; public static final boolean LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT = true; + public static final int LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT = 1; public static final String LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT = "left"; public static final boolean LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true; public static final int LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT = 0; @@ -122,10 +123,13 @@ public final class SbtDefaults { public static final boolean LAYOUT_STATUS_ENABLED_AOD_DEFAULT = true; public static final boolean LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT = true; public static final boolean LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT = true; + public static final int LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT = 1; public static final boolean LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT = true; public static final String LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT = "left"; public static final boolean LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true; public static final int LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT = 0; + public static final int LAYOUT_ICON_CONTAINER_COUNT_MIN = 0; + public static final int LAYOUT_ICON_CONTAINER_COUNT_MAX = 3; private SbtDefaults() { } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index 8c0b811..346fbbe 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -13,6 +13,10 @@ import java.util.Map; import java.util.Set; public final class SbtSettings { + private interface IndexedKey { + String key(int index); + } + public static final String MODULE_PACKAGE = "se.ajpanton.statusbartweak"; public static final String PREFS_NAME = "sbt_settings"; public static final String ACTION_SETTINGS_CHANGED = @@ -32,6 +36,8 @@ public final class SbtSettings { "debug_visualize_chip_container"; public static final String KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT = "debug_visualize_camera_cutout"; + public static final String KEY_LOCKED_NOTIFICATION_MODE = + "locked_notification_mode"; public static final String KEY_UNLOCKED_MAX_ICONS_PER_ROW = "unlocked_max_icons_per_row"; public static final String KEY_ICONS_AOD_MAX_ICONS_PER_ROW = "icons_aod_max_icons_per_row"; public static final String KEY_ICONS_AOD_MAX_ROWS = "icons_aod_max_rows"; @@ -101,6 +107,7 @@ public final class SbtSettings { public static final String KEY_LAYOUT_NOTIF_ENABLED_AOD = "layout_notif_enabled_aod"; public static final String KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN = "layout_notif_enabled_lockscreen"; public static final String KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED = "layout_notif_enabled_unlocked"; + public static final String KEY_LAYOUT_NOTIF_UNLOCKED_COUNT = "layout_notif_unlocked_count"; public static final String KEY_LAYOUT_NOTIF_MIDDLE_SIDE = "layout_notif_middle_side"; public static final String KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE = "layout_notif_middle_auto_nearest_side"; public static final String KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX = "layout_notif_vertical_offset_px"; @@ -108,6 +115,7 @@ public final class SbtSettings { public static final String KEY_LAYOUT_STATUS_ENABLED_AOD = "layout_status_enabled_aod"; public static final String KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN = "layout_status_enabled_lockscreen"; public static final String KEY_LAYOUT_STATUS_ENABLED_UNLOCKED = "layout_status_enabled_unlocked"; + public static final String KEY_LAYOUT_STATUS_UNLOCKED_COUNT = "layout_status_unlocked_count"; public static final String KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED = "layout_status_show_dot_if_truncated"; public static final String KEY_LAYOUT_STATUS_MIDDLE_SIDE = "layout_status_middle_side"; public static final String KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE = "layout_status_middle_auto_nearest_side"; @@ -116,6 +124,42 @@ public final class SbtSettings { public static final String KEY_SYSTEM_ICON_HIDE_SLOTS = "system_icon_hide_slots"; public static final String KEY_APP_ICON_BLOCKED_MODES = AppIconRules.PREF_KEY_BLOCKED_MODES; public static final String KEY_APP_ICON_EXCEPTION_RULES = AppIconRules.PREF_KEY_EXCEPTION_RULES; + + public static String layoutNotifPositionKey(int index) { + return suffixedKey(KEY_LAYOUT_NOTIF_POSITION, index); + } + + public static String layoutNotifMiddleSideKey(int index) { + return suffixedKey(KEY_LAYOUT_NOTIF_MIDDLE_SIDE, index); + } + + public static String layoutNotifMiddleAutoNearestSideKey(int index) { + return suffixedKey(KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE, index); + } + + public static String layoutNotifVerticalOffsetKey(int index) { + return suffixedKey(KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, index); + } + + public static String layoutStatusPositionKey(int index) { + return suffixedKey(KEY_LAYOUT_STATUS_POSITION, index); + } + + public static String layoutStatusMiddleSideKey(int index) { + return suffixedKey(KEY_LAYOUT_STATUS_MIDDLE_SIDE, index); + } + + public static String layoutStatusMiddleAutoNearestSideKey(int index) { + return suffixedKey(KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE, index); + } + + public static String layoutStatusVerticalOffsetKey(int index) { + return suffixedKey(KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, index); + } + + private static String suffixedKey(String base, int index) { + return index <= 0 ? base : base + "_" + (index + 1); + } public final boolean enableAodCutoutLimit; public final boolean enableLockCutoutLimit; public final int unlockedMaxIconsPerRow; @@ -180,6 +224,11 @@ public final class SbtSettings { public final boolean layoutNotifEnabledAod; public final boolean layoutNotifEnabledLockscreen; public final boolean layoutNotifEnabledUnlocked; + public final int layoutNotifUnlockedCount; + public final String[] layoutNotifPositions; + public final String[] layoutNotifMiddleSides; + public final boolean[] layoutNotifMiddleAutoNearestSides; + public final int[] layoutNotifVerticalOffsetsPx; public final String layoutNotifMiddleSide; public final boolean layoutNotifMiddleAutoNearestSide; public final int layoutNotifVerticalOffsetPx; @@ -187,6 +236,11 @@ public final class SbtSettings { public final boolean layoutStatusEnabledAod; public final boolean layoutStatusEnabledLockscreen; public final boolean layoutStatusEnabledUnlocked; + public final int layoutStatusUnlockedCount; + public final String[] layoutStatusPositions; + public final String[] layoutStatusMiddleSides; + public final boolean[] layoutStatusMiddleAutoNearestSides; + public final int[] layoutStatusVerticalOffsetsPx; public final boolean layoutStatusShowDotIfTruncated; public final String layoutStatusMiddleSide; public final boolean layoutStatusMiddleAutoNearestSide; @@ -272,6 +326,11 @@ public final class SbtSettings { String layoutNotifMiddleSide, boolean layoutNotifMiddleAutoNearestSide, int layoutNotifVerticalOffsetPx, + int layoutNotifUnlockedCount, + String[] layoutNotifPositions, + String[] layoutNotifMiddleSides, + boolean[] layoutNotifMiddleAutoNearestSides, + int[] layoutNotifVerticalOffsetsPx, String layoutStatusPosition, boolean layoutStatusEnabledAod, boolean layoutStatusEnabledLockscreen, @@ -280,6 +339,11 @@ public final class SbtSettings { String layoutStatusMiddleSide, boolean layoutStatusMiddleAutoNearestSide, int layoutStatusVerticalOffsetPx, + int layoutStatusUnlockedCount, + String[] layoutStatusPositions, + String[] layoutStatusMiddleSides, + boolean[] layoutStatusMiddleAutoNearestSides, + int[] layoutStatusVerticalOffsetsPx, Map clockCameraTypeOverrides, Map systemIconBlockedModes, boolean statusChipsHideAll, @@ -424,6 +488,27 @@ public final class SbtSettings { layoutNotifVerticalOffsetPx, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutNotifUnlockedCount = Math.max( + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, layoutNotifUnlockedCount)); + this.layoutNotifPositions = sanitizeStringArray( + layoutNotifPositions, + this.layoutNotifPosition, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifMiddleSides = sanitizeStringArray( + layoutNotifMiddleSides, + this.layoutNotifMiddleSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifMiddleAutoNearestSides = sanitizeBooleanArray( + layoutNotifMiddleAutoNearestSides, + this.layoutNotifMiddleAutoNearestSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutNotifVerticalOffsetsPx = sanitizeIntArray( + layoutNotifVerticalOffsetsPx, + this.layoutNotifVerticalOffsetPx, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); this.layoutStatusPosition = layoutStatusPosition != null ? layoutStatusPosition : SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT; @@ -439,6 +524,27 @@ public final class SbtSettings { layoutStatusVerticalOffsetPx, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + this.layoutStatusUnlockedCount = Math.max( + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, layoutStatusUnlockedCount)); + this.layoutStatusPositions = sanitizeStringArray( + layoutStatusPositions, + this.layoutStatusPosition, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusMiddleSides = sanitizeStringArray( + layoutStatusMiddleSides, + this.layoutStatusMiddleSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusMiddleAutoNearestSides = sanitizeBooleanArray( + layoutStatusMiddleAutoNearestSides, + this.layoutStatusMiddleAutoNearestSide, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + this.layoutStatusVerticalOffsetsPx = sanitizeIntArray( + layoutStatusVerticalOffsetsPx, + this.layoutStatusVerticalOffsetPx, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); this.clockCameraTypeOverrides = clockCameraTypeOverrides != null ? Collections.unmodifiableMap(new HashMap<>(clockCameraTypeOverrides)) : Collections.emptyMap(); @@ -540,6 +646,11 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + null, + null, + null, + null, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT, SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT, @@ -548,6 +659,11 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + null, + null, + null, + null, Collections.emptyMap(), Collections.emptyMap(), false, @@ -787,6 +903,28 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + readIconContainerCount( + prefs, + KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, + KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromPrefs( + prefs, + SbtSettings::layoutNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromPrefs( + prefs, + SbtSettings::layoutNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), prefs.getString(KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), prefs.getBoolean(KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), prefs.getBoolean(KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT), @@ -798,6 +936,28 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + readIconContainerCount( + prefs, + KEY_LAYOUT_STATUS_UNLOCKED_COUNT, + KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), + readStringArrayFromPrefs( + prefs, + SbtSettings::layoutStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromPrefs( + prefs, + SbtSettings::layoutStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromPrefs( + prefs, + SbtSettings::layoutStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.parseOverrides(copyStringSet( prefs.getStringSet(KEY_CLOCK_CAMERA_TYPE_OVERRIDES, Collections.emptySet()))), systemIconBlockedModes, @@ -940,6 +1100,28 @@ public final class SbtSettings { SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + readIconContainerCount( + bundle, + KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, + KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromBundle( + bundle, + SbtSettings::layoutNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromBundle( + bundle, + SbtSettings::layoutNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT), bundle.getString(KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), bundle.getBoolean(KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT), bundle.getBoolean(KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT), @@ -951,6 +1133,28 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX), + readIconContainerCount( + bundle, + KEY_LAYOUT_STATUS_UNLOCKED_COUNT, + KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT), + readStringArrayFromBundle( + bundle, + SbtSettings::layoutStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT), + readBooleanArrayFromBundle( + bundle, + SbtSettings::layoutStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT), + readIntArrayFromBundle( + bundle, + SbtSettings::layoutStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.parseOverrides(readStringSetFromBundle(bundle, KEY_CLOCK_CAMERA_TYPE_OVERRIDES)), systemIconBlockedModes, bundle.getBoolean(KEY_STATUS_CHIPS_HIDE_ALL, false), @@ -985,6 +1189,116 @@ public final class SbtSettings { return value; } + private static int readIconContainerCount( + SharedPreferences prefs, + String countKey, + String legacyEnabledKey, + int defaultCount, + boolean legacyEnabledDefault + ) { + int fallback = prefs.getBoolean(legacyEnabledKey, legacyEnabledDefault) ? defaultCount : 0; + int value = prefs.getInt(countKey, fallback); + return Math.max( + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, value)); + } + + private static int readIconContainerCount( + Bundle bundle, + String countKey, + String legacyEnabledKey, + int defaultCount, + boolean legacyEnabledDefault + ) { + int fallback = bundle.getBoolean(legacyEnabledKey, legacyEnabledDefault) ? defaultCount : 0; + int value = bundle.getInt(countKey, fallback); + return Math.max( + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, value)); + } + + private static String[] readStringArrayFromPrefs( + SharedPreferences prefs, + IndexedKey key, + String fallback + ) { + String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = prefs.getString(key.key(i), fallback); + } + return result; + } + + private static boolean[] readBooleanArrayFromPrefs( + SharedPreferences prefs, + IndexedKey key, + boolean fallback + ) { + boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = prefs.getBoolean(key.key(i), fallback); + } + return result; + } + + private static int[] readIntArrayFromPrefs(SharedPreferences prefs, IndexedKey key, int fallback) { + int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = prefs.getInt(key.key(i), fallback); + } + return result; + } + + private static String[] readStringArrayFromBundle(Bundle bundle, IndexedKey key, String fallback) { + String[] result = new String[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = bundle.getString(key.key(i), fallback); + } + return result; + } + + private static boolean[] readBooleanArrayFromBundle(Bundle bundle, IndexedKey key, boolean fallback) { + boolean[] result = new boolean[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = bundle.getBoolean(key.key(i), fallback); + } + return result; + } + + private static int[] readIntArrayFromBundle(Bundle bundle, IndexedKey key, int fallback) { + int[] result = new int[SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX]; + for (int i = 0; i < result.length; i++) { + result[i] = bundle.getInt(key.key(i), fallback); + } + return result; + } + + private static String[] sanitizeStringArray(String[] source, String fallback, int length) { + String[] result = new String[Math.max(0, length)]; + for (int i = 0; i < result.length; i++) { + String value = source != null && i < source.length ? source[i] : null; + result[i] = value != null ? value : fallback; + } + return result; + } + + private static boolean[] sanitizeBooleanArray(boolean[] source, boolean fallback, int length) { + boolean[] result = new boolean[Math.max(0, length)]; + for (int i = 0; i < result.length; i++) { + result[i] = source != null && i < source.length ? source[i] : fallback; + } + return result; + } + + private static int[] sanitizeIntArray(int[] source, int fallback, int length, int min, int max) { + int[] result = new int[Math.max(0, length)]; + for (int i = 0; i < result.length; i++) { + int value = source != null && i < source.length ? source[i] : fallback; + result[i] = clampInt(value, min, max); + } + return result; + } + private static Set copyStringSet(Set source) { if (source == null || source.isEmpty()) { return Collections.emptySet(); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java index 8811557..61b191a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java @@ -12,6 +12,10 @@ import java.util.ArrayList; import java.util.Map; public class SbtSettingsProvider extends ContentProvider { + private interface IndexedKey { + String key(int index); + } + public static final String AUTHORITY = "se.ajpanton.statusbartweak.settings"; public static final String METHOD_GET_UNLOCKED = "get_unlocked"; public static final String METHOD_GET_ALL = "get_all"; @@ -214,6 +218,13 @@ public class SbtSettingsProvider extends ContentProvider { out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT)); + out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, + prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, + prefs.getBoolean( + SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT) + ? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT + : 0)); out.putString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE, prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT)); @@ -223,6 +234,17 @@ public class SbtSettingsProvider extends ContentProvider { out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT)); + putIconCopySettings( + out, + prefs, + SbtSettings::layoutNotifPositionKey, + SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, + SbtSettings::layoutNotifMiddleSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, + SbtSettings::layoutNotifMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, + SbtSettings::layoutNotifVerticalOffsetKey, + SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); out.putString(SbtSettings.KEY_LAYOUT_STATUS_POSITION, prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_POSITION, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT)); @@ -235,6 +257,13 @@ public class SbtSettingsProvider extends ContentProvider { out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT)); + out.putInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, + prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, + prefs.getBoolean( + SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT) + ? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT + : 0)); out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED, prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED, SbtDefaults.LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT)); @@ -247,6 +276,17 @@ public class SbtSettingsProvider extends ContentProvider { out.putInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT)); + putIconCopySettings( + out, + prefs, + SbtSettings::layoutStatusPositionKey, + SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, + SbtSettings::layoutStatusMiddleSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, + SbtSettings::layoutStatusMiddleAutoNearestSideKey, + SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, + SbtSettings::layoutStatusVerticalOffsetKey, + SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); out.putStringArrayList( SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, new ArrayList<>(prefs.getStringSet( @@ -307,6 +347,26 @@ public class SbtSettingsProvider extends ContentProvider { return super.call(method, arg, extras); } + private static void putIconCopySettings( + Bundle out, + SharedPreferences prefs, + IndexedKey positionKey, + String positionDefault, + IndexedKey middleSideKey, + String middleSideDefault, + IndexedKey autoNearestKey, + boolean autoNearestDefault, + IndexedKey verticalOffsetKey, + int 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)); + } + } + @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { return null; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/AodLayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/AodLayoutFragment.java new file mode 100644 index 0000000..b571209 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/AodLayoutFragment.java @@ -0,0 +1,7 @@ +package se.ajpanton.statusbartweak.shell.ui; + +public final class AodLayoutFragment extends LockedSceneLayoutFragment { + public AodLayoutFragment() { + super(true); + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java index ba97d0e..0e64c82 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/IconsDebugFragment.java @@ -26,6 +26,7 @@ import java.util.Map; import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.shell.debug.DebugNotifications; +import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings; import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport; import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; @@ -61,6 +62,10 @@ public class IconsDebugFragment extends Fragment { root.findViewById(R.id.debug_visualize_camera_cutout_switch); MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button); SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch); + TextView notificationStyleCurrent = root.findViewById(R.id.debug_notification_style_current); + MaterialButton notificationStyleCards = root.findViewById(R.id.debug_notification_style_cards); + MaterialButton notificationStyleIcons = root.findViewById(R.id.debug_notification_style_icons); + MaterialButton notificationStyleDot = root.findViewById(R.id.debug_notification_style_dot); TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified); LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row); TextView currentCameraOverride = root.findViewById(R.id.debug_camera_current_override); @@ -107,6 +112,11 @@ public class IconsDebugFragment extends Fragment { SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT, SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT); bindRestartSystemUiButton(restartSystemUiButton); + bindNotificationStyleControls( + notificationStyleCurrent, + notificationStyleCards, + notificationStyleIcons, + notificationStyleDot); updateVisualizerSwitchAvailability( prefs, visualizeNotificationContainerSwitch, @@ -188,6 +198,76 @@ public class IconsDebugFragment extends Fragment { restartButton.setOnClickListener(v -> restartSystemUi()); } + private void bindNotificationStyleControls( + TextView currentView, + MaterialButton cardsButton, + MaterialButton iconsButton, + MaterialButton dotButton + ) { + updateNotificationStyleUi(currentView); + if (cardsButton != null) { + cardsButton.setOnClickListener(v -> writeNotificationStyle( + NotificationDisplayStyleSettings.Style.CARDS, + currentView)); + } + if (iconsButton != null) { + iconsButton.setOnClickListener(v -> writeNotificationStyle( + NotificationDisplayStyleSettings.Style.ICONS, + currentView)); + } + if (dotButton != null) { + dotButton.setOnClickListener(v -> writeNotificationStyle( + NotificationDisplayStyleSettings.Style.DOT, + currentView)); + } + } + + private void updateNotificationStyleUi(TextView currentView) { + Context context = requireContext(); + NotificationDisplayStyleSettings.State state = + NotificationDisplayStyleSettings.read(context); + if (currentView != null) { + currentView.setText(getString( + R.string.debug_notification_style_current, + readableNotificationStyle(state.style()))); + } + } + + private void writeNotificationStyle( + NotificationDisplayStyleSettings.Style style, + TextView currentView + ) { + Context appContext = requireContext().getApplicationContext(); + new Thread(() -> { + boolean success = NotificationDisplayStyleSettings.write(appContext, style); + if (!isAdded()) { + return; + } + requireActivity().runOnUiThread(() -> { + updateNotificationStyleUi(currentView); + Toast.makeText( + requireContext(), + success + ? R.string.debug_notification_style_write_done + : R.string.debug_notification_style_write_failed, + success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show(); + }); + }, "StatusBarTweak:NotificationStyleWrite").start(); + } + + private String readableNotificationStyle(NotificationDisplayStyleSettings.Style style) { + if (style == NotificationDisplayStyleSettings.Style.CARDS) { + return getString(R.string.debug_notification_style_cards); + } + if (style == NotificationDisplayStyleSettings.Style.ICONS) { + return getString(R.string.debug_notification_style_icons); + } + if (style == NotificationDisplayStyleSettings.Style.DOT) { + return getString(R.string.debug_notification_style_dot); + } + return getString(R.string.debug_notification_style_unknown); + } + private void updateVisualizerSwitchAvailability(SharedPreferences prefs, SwitchMaterial notificationSwitch, SwitchMaterial statusSwitch, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java index e4e5645..62ae352 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutFragment.java @@ -17,6 +17,7 @@ import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.CheckBox; import android.widget.EditText; +import android.widget.LinearLayout; import android.widget.RadioGroup; import android.widget.TextView; @@ -28,6 +29,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.switchmaterial.SwitchMaterial; import java.util.ArrayList; import java.util.Collections; @@ -45,6 +47,7 @@ public final class LayoutFragment extends Fragment { View root = inflater.inflate(R.layout.fragment_layout, container, false); SharedPreferences prefs = requireContext() .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); + hideUnlockedSubpageObsoleteControls(root); SwitchMaterial enabledSwitch = root.findViewById(R.id.clock_enabled_switch); enabledSwitch.setChecked(prefs.getBoolean( @@ -198,19 +201,400 @@ public final class LayoutFragment extends Fragment { bindCheckBoxPref(prefs, root.findViewById(R.id.status_position_mode_unlocked), SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT); - bindCheckBoxPref(prefs, root.findViewById(R.id.notif_show_dot_if_truncated), - SbtSettings.KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED, - SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT); - bindCheckBoxPref(prefs, root.findViewById(R.id.status_show_dot_if_truncated), - SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED, - SbtDefaults.LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT); - bindSortOrderGroup(prefs, root.findViewById(R.id.layout_sort_icons_group)); setupOrderingList(root, prefs); + setupUnlockedIconContainerCount( + root, + prefs, + R.id.notif_position_mode_unlocked, + R.string.layout_notification_position_title, + SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, + SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, + SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT, + SbtSettings::layoutNotifPositionKey, + SbtSettings::layoutNotifMiddleAutoNearestSideKey, + SbtSettings::layoutNotifMiddleSideKey, + SbtSettings::layoutNotifVerticalOffsetKey, + 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); + setupUnlockedIconContainerCount( + root, + prefs, + R.id.status_position_mode_unlocked, + R.string.layout_status_position_title, + SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, + SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, + SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, + SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT, + SbtSettings::layoutStatusPositionKey, + SbtSettings::layoutStatusMiddleAutoNearestSideKey, + SbtSettings::layoutStatusMiddleSideKey, + SbtSettings::layoutStatusVerticalOffsetKey, + 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 root; } + private interface IndexedKey { + String key(int index); + } + + private void setupUnlockedIconContainerCount( + View root, + SharedPreferences prefs, + int checkboxId, + int sectionTitleId, + String countKey, + int countDefault, + String legacyEnabledKey, + boolean legacyEnabledDefault, + IndexedKey positionKey, + IndexedKey autoNearestKey, + IndexedKey middleSideKey, + IndexedKey verticalOffsetKey, + String positionDefault, + boolean autoNearestDefault, + String middleSideDefault, + int verticalOffsetDefault + ) { + CheckBox checkbox = root.findViewById(checkboxId); + if (checkbox == null || !(checkbox.getParent() instanceof ViewGroup modeRow) + || !(modeRow.getParent() instanceof LinearLayout sectionContent)) { + return; + } + int rowIndex = sectionContent.indexOfChild(modeRow); + sectionContent.removeView(modeRow); + MaterialCardView sourceCard = findAncestorCard(sectionContent); + LinearLayout cardsParent = sourceCard != null && sourceCard.getParent() instanceof LinearLayout parent + ? parent + : null; + int sourceCardIndex = cardsParent != null ? cardsParent.indexOfChild(sourceCard) : -1; + String copyTag = "layout-copy-" + countKey; + + LinearLayout countContainer = new LinearLayout(requireContext()); + countContainer.setOrientation(LinearLayout.VERTICAL); + sectionContent.addView(countContainer, Math.max(0, rowIndex)); + + Runnable[] rebuild = new Runnable[1]; + rebuild[0] = () -> { + countContainer.removeAllViews(); + removeGeneratedCopyCards(cardsParent, copyTag); + int count = iconContainerCount(prefs, countKey, countDefault, legacyEnabledKey, legacyEnabledDefault); + countContainer.addView(iconContainerCountControl( + prefs, + countKey, + count, + legacyEnabledKey, + () -> { + if (rebuild[0] != null) { + rebuild[0].run(); + } + })); + if (cardsParent == null || sourceCardIndex < 0) { + return; + } + int insertIndex = sourceCardIndex + 1; + for (int i = 1; i < count; i++, insertIndex++) { + View copy = iconCopyCardFromLayout( + checkboxId, + sectionTitleId, + i, + positionKey, + autoNearestKey, + middleSideKey, + verticalOffsetKey, + positionDefault, + autoNearestDefault, + middleSideDefault, + verticalOffsetDefault, + copyTag); + cardsParent.addView(copy, Math.min(insertIndex, cardsParent.getChildCount())); + } + }; + rebuild[0].run(); + } + + private View iconContainerCountControl( + SharedPreferences prefs, + String countKey, + int current, + String legacyEnabledKey, + Runnable onChanged + ) { + LinearLayout outer = new LinearLayout(requireContext()); + outer.setOrientation(LinearLayout.VERTICAL); + TextView label = new TextView(requireContext()); + label.setText(R.string.layout_icon_container_count); + label.setPadding(0, dp(8), 0, 0); + outer.addView(label); + + LinearLayout row = new LinearLayout(requireContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + MaterialButton minus = smallButton("-"); + MaterialButton plus = smallButton("+"); + EditText input = new EditText(requireContext()); + input.setGravity(android.view.Gravity.CENTER); + input.setSingleLine(true); + input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)}); + input.setText(String.valueOf(current)); + row.addView(minus, compactButtonParams()); + row.addView(input, compactInputParams()); + row.addView(plus, compactButtonParams()); + outer.addView(row); + + View.OnClickListener listener = view -> { + int next = clampIconContainerCount(parseInt(input, current) + (view == minus ? -1 : 1)); + persistIconContainerCount(prefs, countKey, legacyEnabledKey, next); + onChanged.run(); + }; + minus.setOnClickListener(listener); + plus.setOnClickListener(listener); + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clampIconContainerCount(parseInt(input, current)); + persistIconContainerCount(prefs, countKey, legacyEnabledKey, next); + onChanged.run(); + } + }); + return outer; + } + + private View iconCopyCardFromLayout( + int sourceCheckboxId, + int titleId, + int index, + IndexedKey positionKey, + IndexedKey autoNearestKey, + IndexedKey middleSideKey, + IndexedKey verticalOffsetKey, + String positionDefault, + boolean autoNearestDefault, + String middleSideDefault, + int verticalOffsetDefault, + String copyTag + ) { + int cardId = sourceCheckboxId == R.id.notif_position_mode_unlocked + ? R.id.layout_notif_card + : R.id.layout_status_card; + View card = inflateLayoutCard(cardId); + card.setTag(copyTag); + TextView title = findCardTitle(card); + if (title != null) { + title.setText(getString(titleId) + " " + (index + 1)); + } + removeModeContainerForCheckbox(card, sourceCheckboxId); + SharedPreferences prefs = requireContext() + .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); + if (sourceCheckboxId == R.id.notif_position_mode_unlocked) { + setupPositionSection(card, + prefs, + R.id.notif_position_group, + R.id.notif_position_middle_options_container, + R.id.notif_middle_auto_nearest, + R.id.notif_middle_side_prompt, + positionKey.key(index), + positionDefault, + autoNearestKey.key(index), + middleSideKey.key(index)); + bindStepper(prefs, + verticalOffsetKey.key(index), + card.findViewById(R.id.notif_vertical_offset_input), + card.findViewById(R.id.notif_vertical_offset_minus_fast), + card.findViewById(R.id.notif_vertical_offset_minus), + card.findViewById(R.id.notif_vertical_offset_plus), + card.findViewById(R.id.notif_vertical_offset_plus_fast), + card.findViewById(R.id.notif_vertical_offset_reset), + verticalOffsetDefault); + } else { + setupPositionSection(card, + prefs, + R.id.status_position_group, + R.id.status_position_middle_options_container, + R.id.status_middle_auto_nearest, + R.id.status_middle_side_prompt, + positionKey.key(index), + positionDefault, + autoNearestKey.key(index), + middleSideKey.key(index)); + bindStepper(prefs, + verticalOffsetKey.key(index), + card.findViewById(R.id.status_vertical_offset_input), + card.findViewById(R.id.status_vertical_offset_minus_fast), + card.findViewById(R.id.status_vertical_offset_minus), + card.findViewById(R.id.status_vertical_offset_plus), + card.findViewById(R.id.status_vertical_offset_plus_fast), + card.findViewById(R.id.status_vertical_offset_reset), + verticalOffsetDefault); + } + return card; + } + + private MaterialButton smallButton(String text) { + MaterialButton button = new MaterialButton(requireContext(), null, com.google.android.material.R.attr.materialButtonOutlinedStyle); + button.setText(text); + button.setMinWidth(dp(40)); + button.setMinHeight(dp(40)); + button.setPadding(0, button.getPaddingTop(), 0, button.getPaddingBottom()); + return button; + } + + private LinearLayout.LayoutParams compactButtonParams() { + return new LinearLayout.LayoutParams(dp(40), ViewGroup.LayoutParams.WRAP_CONTENT); + } + + private LinearLayout.LayoutParams compactInputParams() { + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + dp(48), + ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = dp(8); + params.rightMargin = dp(8); + return params; + } + + @Nullable + private MaterialCardView findAncestorCard(View view) { + View current = view; + while (current != null) { + if (current instanceof MaterialCardView card) { + return card; + } + current = current.getParent() instanceof View parent ? parent : null; + } + return null; + } + + private void removeGeneratedCopyCards(@Nullable LinearLayout parent, String tag) { + if (parent == null) { + return; + } + for (int i = parent.getChildCount() - 1; i >= 0; i--) { + View child = parent.getChildAt(i); + if (tag.equals(child.getTag())) { + parent.removeViewAt(i); + } + } + } + + private View inflateLayoutCard(int cardId) { + View fullLayout = LayoutInflater.from(requireContext()).inflate(R.layout.fragment_layout, null, false); + View card = fullLayout.findViewById(cardId); + if (card == null) { + return new View(requireContext()); + } + if (card.getParent() instanceof ViewGroup parent) { + parent.removeView(card); + } + return card; + } + + @Nullable + private TextView findCardTitle(View card) { + if (!(card instanceof ViewGroup cardGroup) || cardGroup.getChildCount() <= 0) { + return null; + } + View inner = cardGroup.getChildAt(0); + if (!(inner instanceof ViewGroup innerGroup) || innerGroup.getChildCount() <= 0) { + return null; + } + View title = innerGroup.getChildAt(0); + return title instanceof TextView textView ? textView : null; + } + + @Nullable + private LinearLayout removeModeContainerForCheckbox(View root, int checkboxId) { + CheckBox checkbox = root.findViewById(checkboxId); + View modeContainer = directSectionChild(checkbox); + if (modeContainer == null || !(modeContainer.getParent() instanceof LinearLayout sectionContent)) { + return null; + } + sectionContent.removeView(modeContainer); + return sectionContent; + } + + @Nullable + private View directSectionChild(@Nullable View child) { + View current = child; + while (current != null && current.getParent() instanceof View parent) { + if (parent.getParent() instanceof MaterialCardView) { + return current; + } + current = parent; + } + return null; + } + + private int iconContainerCount( + SharedPreferences prefs, + String countKey, + int countDefault, + String legacyEnabledKey, + boolean legacyEnabledDefault + ) { + int fallback = prefs.getBoolean(legacyEnabledKey, legacyEnabledDefault) ? countDefault : 0; + return clampIconContainerCount(prefs.getInt(countKey, fallback)); + } + + private int clampIconContainerCount(int value) { + return Math.max( + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + Math.min(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, value)); + } + + private void persistIconContainerCount( + SharedPreferences prefs, + String countKey, + String legacyEnabledKey, + int count + ) { + prefs.edit() + .putInt(countKey, clampIconContainerCount(count)) + .putBoolean(legacyEnabledKey, count > 0) + .apply(); + SbtSettings.ensureReadable(requireContext()); + } + + private int dp(int value) { + return Math.round(value * getResources().getDisplayMetrics().density); + } + + private void hideUnlockedSubpageObsoleteControls(View root) { + hide(root, R.id.layout_master_card); + hide(root, R.id.layout_padding_card); + hide(root, R.id.layout_ordering_card); + hide(root, R.id.clock_position_mode_lockscreen); + hide(root, R.id.notif_position_mode_aod); + hide(root, R.id.notif_position_mode_lockscreen); + hide(root, R.id.chip_position_mode_lockscreen); + hide(root, R.id.status_position_mode_aod); + hide(root, R.id.status_position_mode_lockscreen); + setText(root, R.id.clock_position_mode_unlocked, R.string.layout_item_enabled); + setText(root, R.id.notif_position_mode_unlocked, R.string.layout_item_enabled); + setText(root, R.id.chip_position_mode_unlocked, R.string.layout_item_enabled); + setText(root, R.id.status_position_mode_unlocked, R.string.layout_item_enabled); + } + + private void hide(View root, int id) { + View view = root.findViewById(id); + if (view != null) { + view.setVisibility(View.GONE); + } + } + + private void setText(View root, int id, int stringId) { + TextView view = root.findViewById(id); + if (view != null) { + view.setText(stringId); + } + } + private void setupPositionSection(View root, SharedPreferences prefs, int groupId, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutHomeFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutHomeFragment.java new file mode 100644 index 0000000..c0bb1ad --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutHomeFragment.java @@ -0,0 +1,351 @@ +package se.ajpanton.statusbartweak.shell.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.InputFilter; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioGroup; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; +import com.google.android.material.switchmaterial.SwitchMaterial; + +import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; +import se.ajpanton.statusbartweak.shell.settings.SbtSettings; + +public final class LayoutHomeFragment extends Fragment { + private LinearLayout layoutOnContainer; + private LinearLayout miscLayoutOnContainer; + private LinearLayout miscUnlockedOffOnlyContainer; + private RadioGroup lockedModeGroup; + private TextView lockedModeHint; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + Context context = requireContext(); + SharedPreferences prefs = context.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); + ScrollView scroll = new ScrollView(context); + LinearLayout root = new LinearLayout(context); + root.setOrientation(LinearLayout.VERTICAL); + root.setPadding(dp(20), dp(20), dp(20), dp(20)); + scroll.addView(root); + + TextView title = title(context, R.string.layout_home_title); + root.addView(title); + + SwitchMaterial master = new SwitchMaterial(context); + master.setText(R.string.layout_enabled); + master.setChecked(prefs.getBoolean(SbtSettings.KEY_CLOCK_ENABLED, SbtDefaults.CLOCK_ENABLED_DEFAULT)); + root.addView(card(context, R.string.layout_master_toggle_title, master)); + + lockedModeGroup = new RadioGroup(context); + lockedModeGroup.setOrientation(RadioGroup.VERTICAL); + lockedModeHint = body(context, R.string.layout_locked_notifications_hint); + LinearLayout lockedContent = vertical(context); + lockedContent.addView(lockedModeHint); + lockedContent.addView(lockedModeGroup); + root.addView(card(context, R.string.layout_locked_notifications_title, lockedContent)); + + layoutOnContainer = vertical(context); + layoutOnContainer.addView(paddingCard(context, prefs)); + layoutOnContainer.addView(LayoutOrderingUi.createCard(context, prefs)); + root.addView(layoutOnContainer); + + root.addView(miscCard(context, prefs)); + + Runnable refreshAvailability = () -> refreshAvailability(prefs, master.isChecked()); + master.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_ENABLED, isChecked).apply(); + SbtSettings.ensureReadable(context); + refreshAvailability.run(); + }); + refreshAvailability.run(); + return scroll; + } + + private void refreshAvailability(SharedPreferences prefs, boolean layoutEnabled) { + setViewTreeEnabled(layoutOnContainer, layoutEnabled); + layoutOnContainer.setAlpha(layoutEnabled ? 1f : 0.45f); + setViewTreeEnabled(miscLayoutOnContainer, layoutEnabled); + miscLayoutOnContainer.setAlpha(layoutEnabled ? 1f : 0.45f); + setViewTreeEnabled(miscUnlockedOffOnlyContainer, !layoutEnabled); + miscUnlockedOffOnlyContainer.setAlpha(layoutEnabled ? 0.45f : 1f); + LockedNotificationModeUi.bind( + requireContext(), + prefs, + lockedModeGroup, + lockedModeHint, + layoutEnabled); + } + + private View paddingCard(Context context, SharedPreferences prefs) { + LinearLayout content = vertical(context); + content.addView(stepper( + context, + prefs, + R.string.layout_cutout_gap_label, + SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX, + SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT, + -999, + 999)); + content.addView(stepper( + context, + prefs, + R.string.layout_left_padding_label, + SbtSettings.KEY_LAYOUT_PADDING_LEFT_PX, + SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT, + -999, + 999, + checkBox( + context, + prefs, + R.string.layout_padding_add_stock, + SbtSettings.KEY_LAYOUT_PADDING_LEFT_ADD_STOCK, + SbtDefaults.LAYOUT_PADDING_LEFT_ADD_STOCK_DEFAULT))); + content.addView(stepper( + context, + prefs, + R.string.layout_right_padding_label, + SbtSettings.KEY_LAYOUT_PADDING_RIGHT_PX, + SbtDefaults.LAYOUT_PADDING_RIGHT_PX_DEFAULT, + -999, + 999, + checkBox( + context, + prefs, + R.string.layout_padding_add_stock, + SbtSettings.KEY_LAYOUT_PADDING_RIGHT_ADD_STOCK, + SbtDefaults.LAYOUT_PADDING_RIGHT_ADD_STOCK_DEFAULT))); + content.addView(stepper( + context, + prefs, + R.string.layout_item_padding_label, + SbtSettings.KEY_LAYOUT_ITEM_PADDING_PX, + SbtDefaults.LAYOUT_ITEM_PADDING_PX_DEFAULT, + SbtDefaults.LAYOUT_ITEM_PADDING_PX_MIN, + SbtDefaults.LAYOUT_ITEM_PADDING_PX_MAX)); + return card(context, R.string.layout_padding_title, content); + } + + private View miscCard(Context context, SharedPreferences prefs) { + LinearLayout content = vertical(context); + miscLayoutOnContainer = vertical(context); + miscLayoutOnContainer.addView(checkBox( + context, + prefs, + R.string.layout_ignore_under_display_cameras, + SbtSettings.KEY_LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS, + SbtDefaults.LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT)); + content.addView(miscLayoutOnContainer); + miscUnlockedOffOnlyContainer = vertical(context); + miscUnlockedOffOnlyContainer.addView(stepper( + context, + prefs, + R.string.label_max_unlocked_icons_per_row, + SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW, + SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT, + SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN, + SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MAX)); + content.addView(miscUnlockedOffOnlyContainer); + return card(context, R.string.layout_misc_title, content); + } + + private View stepper( + Context context, + SharedPreferences prefs, + int labelId, + String key, + int defaultValue, + int min, + int max + ) { + return stepper(context, prefs, labelId, key, defaultValue, min, max, null); + } + + private View stepper( + Context context, + SharedPreferences prefs, + int labelId, + String key, + int defaultValue, + int min, + int max, + @Nullable View trailingView + ) { + LinearLayout outer = vertical(context); + TextView label = body(context, labelId); + outer.addView(label); + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + row.setPadding(0, dp(6), 0, dp(8)); + MaterialButton minus = button(context, "-"); + MaterialButton plus = button(context, "+"); + EditText input = new EditText(context); + input.setGravity(android.view.Gravity.CENTER); + input.setSingleLine(true); + input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(5)}); + input.setText(String.valueOf(clamp(prefs.getInt(key, defaultValue), min, max))); + row.addView(minus, buttonParams()); + row.addView(input, inputParams()); + row.addView(plus, buttonParams()); + if (trailingView != null) { + LinearLayout.LayoutParams trailingParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + trailingParams.leftMargin = dp(8); + row.addView(trailingView, trailingParams); + } + outer.addView(row); + View.OnClickListener listener = v -> { + int delta = v == minus ? -1 : 1; + int next = clamp(parseInt(input, defaultValue) + delta, min, max); + input.setText(String.valueOf(next)); + prefs.edit().putInt(key, next).apply(); + SbtSettings.ensureReadable(context); + }; + minus.setOnClickListener(listener); + plus.setOnClickListener(listener); + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clamp(parseInt(input, defaultValue), min, max); + input.setText(String.valueOf(next)); + prefs.edit().putInt(key, next).apply(); + SbtSettings.ensureReadable(context); + } + }); + return outer; + } + + private CheckBox checkBox( + Context context, + SharedPreferences prefs, + int label, + String key, + boolean defaultValue + ) { + CheckBox checkBox = new CheckBox(context); + checkBox.setText(label); + checkBox.setChecked(prefs.getBoolean(key, defaultValue)); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(key, isChecked).apply(); + SbtSettings.ensureReadable(context); + }); + return checkBox; + } + + private MaterialButton button(Context context, String text) { + MaterialButton button = new MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle); + button.setText(text); + button.setMinWidth(dp(40)); + button.setMinHeight(dp(40)); + button.setPadding(0, button.getPaddingTop(), 0, button.getPaddingBottom()); + return button; + } + + private LinearLayout.LayoutParams buttonParams() { + return new LinearLayout.LayoutParams(dp(40), ViewGroup.LayoutParams.WRAP_CONTENT); + } + + private LinearLayout.LayoutParams inputParams() { + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + dp(48), + ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = dp(8); + params.rightMargin = dp(8); + return params; + } + + private MaterialCardView card(Context context, int titleId, View content) { + MaterialCardView card = new MaterialCardView(context); + LinearLayout.LayoutParams cardParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + cardParams.topMargin = dp(16); + card.setLayoutParams(cardParams); + card.setUseCompatPadding(true); + card.setStrokeColor(context.getColor(R.color.sbt_card_outline)); + card.setStrokeWidth(dp(1)); + LinearLayout inner = vertical(context); + inner.setPadding(dp(16), dp(16), dp(16), dp(16)); + TextView title = sectionTitle(context, titleId); + inner.addView(title); + inner.addView(content); + card.addView(inner); + return card; + } + + private LinearLayout vertical(Context context) { + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + return layout; + } + + private TextView title(Context context, int stringId) { + TextView view = new TextView(context); + view.setText(stringId); + view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Headline5); + return view; + } + + private TextView sectionTitle(Context context, int stringId) { + TextView view = new TextView(context); + view.setText(stringId); + view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Subtitle1); + view.setAllCaps(true); + view.setTypeface(view.getTypeface(), android.graphics.Typeface.BOLD); + return view; + } + + private TextView body(Context context, int stringId) { + TextView view = new TextView(context); + view.setText(stringId); + view.setPadding(0, dp(8), 0, 0); + return view; + } + + private void setViewTreeEnabled(View view, boolean enabled) { + if (view == null) { + return; + } + view.setEnabled(enabled); + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + setViewTreeEnabled(group.getChildAt(i), enabled); + } + } + } + + private int parseInt(EditText input, int fallback) { + try { + return Integer.parseInt(input.getText().toString().trim()); + } catch (Throwable ignored) { + return fallback; + } + } + + private int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private int dp(int value) { + return Math.round(value * getResources().getDisplayMetrics().density); + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutOrderingUi.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutOrderingUi.java new file mode 100644 index 0000000..e501675 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LayoutOrderingUi.java @@ -0,0 +1,324 @@ +package se.ajpanton.statusbartweak.shell.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.card.MaterialCardView; + +import java.util.ArrayList; +import java.util.Collections; +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 LayoutOrderingUi { + private LayoutOrderingUi() { + } + + static View createCard(Context context, SharedPreferences prefs) { + LinearLayout content = vertical(context); + TextView hint = body(context, R.string.layout_ordering_hint); + content.addView(hint); + + RecyclerView list = new RecyclerView(context); + list.setNestedScrollingEnabled(false); + LinearLayout.LayoutParams listParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + listParams.topMargin = dp(context, 12); + content.addView(list, listParams); + bindOrderingList(context, prefs, list); + + TextView sortTitle = body(context, R.string.layout_sort_icons_title); + sortTitle.setTypeface(sortTitle.getTypeface(), android.graphics.Typeface.BOLD); + LinearLayout.LayoutParams sortTitleParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + sortTitleParams.topMargin = dp(context, 16); + content.addView(sortTitle, sortTitleParams); + + RadioGroup sortGroup = new RadioGroup(context); + sortGroup.setOrientation(RadioGroup.HORIZONTAL); + addSortButton(context, sortGroup, R.string.layout_sort_icons_stock, "stock"); + addSortButton(context, sortGroup, R.string.layout_sort_icons_reversed, "reversed"); + addSortButton(context, sortGroup, R.string.layout_sort_icons_automatic, "automatic"); + sortGroup.check(idForTag(sortGroup, prefs.getString( + SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER, + SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT))); + sortGroup.setOnCheckedChangeListener((group, checkedId) -> { + prefs.edit() + .putString(SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER, tagForId(group, checkedId)) + .apply(); + SbtSettings.ensureReadable(context); + }); + LinearLayout.LayoutParams sortParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + sortParams.topMargin = dp(context, 8); + content.addView(sortGroup, sortParams); + + return card(context, R.string.layout_ordering_title, content); + } + + private static void bindOrderingList(Context context, SharedPreferences prefs, RecyclerView list) { + ArrayList items = parseOrderingItems( + context, + prefs.getString(SbtSettings.KEY_LAYOUT_ITEM_ORDER, SbtDefaults.LAYOUT_ITEM_ORDER_DEFAULT)); + + list.setLayoutManager(new LinearLayoutManager(context)); + OrderingAdapter adapter = new OrderingAdapter(items, order -> { + prefs.edit().putString(SbtSettings.KEY_LAYOUT_ITEM_ORDER, order).apply(); + SbtSettings.ensureReadable(context); + }); + list.setAdapter(adapter); + + final ItemTouchHelper[] helperHolder = new ItemTouchHelper[1]; + ItemTouchHelper helper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( + ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) { + @Override + public boolean onMove( + @NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target + ) { + int from = viewHolder.getAdapterPosition(); + int to = target.getAdapterPosition(); + if (from == RecyclerView.NO_POSITION || to == RecyclerView.NO_POSITION) { + return false; + } + adapter.moveItem(from, to); + return true; + } + + @Override + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + } + + @Override + public boolean isLongPressDragEnabled() { + return false; + } + }); + helper.attachToRecyclerView(list); + helperHolder[0] = helper; + adapter.setDragStarter(holder -> { + ItemTouchHelper current = helperHolder[0]; + if (current != null) { + current.startDrag(holder); + } + }); + } + + private static ArrayList parseOrderingItems(Context context, @Nullable String encoded) { + if (encoded == null || encoded.isEmpty() || "clock,notifications,status".equals(encoded)) { + encoded = SbtDefaults.LAYOUT_ITEM_ORDER_DEFAULT; + } + ArrayList items = new ArrayList<>(); + for (String token : encoded.split(",")) { + addOrderingItemIfValid(context, items, token != null ? token.trim() : ""); + } + addOrderingItemIfValid(context, items, "clock"); + addOrderingItemIfValid(context, items, "chip"); + addOrderingItemIfValid(context, items, "status"); + addOrderingItemIfValid(context, items, "notifications"); + return items; + } + + private static void addOrderingItemIfValid(Context context, ArrayList items, String key) { + if (key == null || key.isEmpty()) { + return; + } + for (OrderingItem item : items) { + if (item.key.equals(key)) { + return; + } + } + int labelRes; + if ("clock".equals(key)) { + labelRes = R.string.layout_ordering_clock; + } else if ("chip".equals(key)) { + labelRes = R.string.layout_ordering_chip; + } else if ("notifications".equals(key)) { + labelRes = R.string.layout_ordering_notif_icons; + } else if ("status".equals(key)) { + labelRes = R.string.layout_ordering_status_icons; + } else { + return; + } + items.add(new OrderingItem(key, context.getString(labelRes))); + } + + private static void addSortButton(Context context, RadioGroup group, int labelId, String tag) { + RadioButton button = new RadioButton(context); + button.setId(View.generateViewId()); + button.setTag(tag); + button.setText(labelId); + button.setSingleLine(true); + button.setMaxLines(1); + group.addView(button, new RadioGroup.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f)); + } + + private static int idForTag(RadioGroup group, @Nullable String tag) { + String normalized = tag != null ? tag : "automatic"; + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (normalized.equals(child.getTag())) { + return child.getId(); + } + } + return group.getChildCount() > 0 ? group.getChildAt(group.getChildCount() - 1).getId() : View.NO_ID; + } + + private static String tagForId(RadioGroup group, int id) { + View child = group.findViewById(id); + Object tag = child != null ? child.getTag() : null; + return tag instanceof String value ? value : "automatic"; + } + + private static MaterialCardView card(Context context, int titleId, View content) { + MaterialCardView card = new MaterialCardView(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = dp(context, 16); + card.setLayoutParams(params); + card.setUseCompatPadding(true); + card.setStrokeColor(context.getColor(R.color.sbt_card_outline)); + card.setStrokeWidth(dp(context, 1)); + + LinearLayout inner = vertical(context); + inner.setPadding(dp(context, 16), dp(context, 16), dp(context, 16), dp(context, 16)); + inner.addView(sectionTitle(context, titleId)); + inner.addView(content); + card.addView(inner); + return card; + } + + private static LinearLayout vertical(Context context) { + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + return layout; + } + + private static TextView sectionTitle(Context context, int stringId) { + TextView view = new TextView(context); + view.setText(stringId); + view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Subtitle1); + view.setAllCaps(true); + view.setTypeface(view.getTypeface(), android.graphics.Typeface.BOLD); + return view; + } + + private static TextView body(Context context, int stringId) { + TextView view = new TextView(context); + view.setText(stringId); + view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body2); + view.setPadding(0, dp(context, 8), 0, 0); + return view; + } + + private static int dp(Context context, int value) { + return Math.round(value * context.getResources().getDisplayMetrics().density); + } + + private record OrderingItem(String key, String label) { + } + + private static final class OrderingAdapter extends RecyclerView.Adapter { + interface DragStarter { + void startDrag(OrderingViewHolder holder); + } + + interface OrderChangedListener { + void onOrderChanged(String encodedOrder); + } + + private final List items; + private final OrderChangedListener orderChangedListener; + private DragStarter dragStarter; + + OrderingAdapter(List items, OrderChangedListener orderChangedListener) { + this.items = items; + this.orderChangedListener = orderChangedListener; + } + + void setDragStarter(DragStarter dragStarter) { + this.dragStarter = dragStarter; + } + + void moveItem(int from, int to) { + if (from == to || from < 0 || to < 0 || from >= items.size() || to >= items.size()) { + return; + } + Collections.swap(items, from, to); + notifyItemMoved(from, to); + orderChangedListener.onOrderChanged(encodeOrder()); + } + + private String encodeOrder() { + StringBuilder builder = new StringBuilder(); + for (OrderingItem item : items) { + if (builder.length() > 0) { + builder.append(','); + } + builder.append(item.key); + } + return builder.toString(); + } + + @NonNull + @Override + public OrderingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_layout_ordering, parent, false); + return new OrderingViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull OrderingViewHolder holder, int position) { + holder.label.setText(items.get(position).label); + holder.handle.setOnTouchListener((v, event) -> { + if (event.getActionMasked() == MotionEvent.ACTION_DOWN && dragStarter != null) { + dragStarter.startDrag(holder); + return true; + } + return false; + }); + } + + @Override + public int getItemCount() { + return items.size(); + } + } + + private static final class OrderingViewHolder extends RecyclerView.ViewHolder { + final TextView label; + final TextView handle; + + OrderingViewHolder(@NonNull View itemView) { + super(itemView); + label = itemView.findViewById(R.id.layout_ordering_item_label); + handle = itemView.findViewById(R.id.layout_ordering_item_handle); + } + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedNotificationModeUi.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedNotificationModeUi.java new file mode 100644 index 0000000..ed334c7 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedNotificationModeUi.java @@ -0,0 +1,157 @@ +package se.ajpanton.statusbartweak.shell.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.view.View; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.TextView; +import android.widget.Toast; + +import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings; +import se.ajpanton.statusbartweak.shell.settings.SbtSettings; + +final class LockedNotificationModeUi { + static final String MODE_CARDS = "cards"; + static final String MODE_ICONS = "icons"; + static final String MODE_DOT = "dot"; + static final String MODE_NONE = "none"; + + private LockedNotificationModeUi() { + } + + static void bind( + Context context, + SharedPreferences prefs, + RadioGroup group, + TextView hint, + boolean layoutEnabled + ) { + bind(context, prefs, group, hint, layoutEnabled, null); + } + + static void bind( + Context context, + SharedPreferences prefs, + RadioGroup group, + TextView hint, + boolean layoutEnabled, + Runnable onChanged + ) { + if (context == null || prefs == null || group == null) { + return; + } + group.setOnCheckedChangeListener(null); + RadioButton cards = button(context, R.string.debug_notification_style_cards, MODE_CARDS); + RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS); + RadioButton dot = button(context, R.string.debug_notification_style_dot, MODE_DOT); + RadioButton none = button(context, R.string.layout_locked_notifications_none, MODE_NONE); + group.removeAllViews(); + group.addView(cards); + group.addView(icons); + group.addView(dot); + group.addView(none); + + String stored = prefs.getString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_DOT); + NotificationDisplayStyleSettings.Style androidStyle = + NotificationDisplayStyleSettings.read(context).style(); + String current = currentMode(androidStyle, stored, layoutEnabled); + if (layoutEnabled && MODE_NONE.equals(stored) + && androidStyle != NotificationDisplayStyleSettings.Style.DOT) { + writeMode(context, MODE_NONE); + } + group.check(idForMode(group, current)); + + none.setEnabled(layoutEnabled); + none.setAlpha(layoutEnabled ? 1f : 0.45f); + if (hint != null) { + boolean rememberedNone = MODE_NONE.equals(stored) && !layoutEnabled; + hint.setText(rememberedNone + ? R.string.layout_locked_notifications_none_disabled + : R.string.layout_locked_notifications_hint); + } + + group.setOnCheckedChangeListener((radioGroup, checkedId) -> { + String mode = modeForId(radioGroup, checkedId); + if (MODE_NONE.equals(mode) && !layoutEnabled) { + radioGroup.check(idForMode(radioGroup, MODE_DOT)); + return; + } + boolean ok = writeMode(context, mode); + if (!ok) { + Toast.makeText( + context, + R.string.debug_notification_style_write_failed, + Toast.LENGTH_SHORT).show(); + bind(context, prefs, group, hint, layoutEnabled); + return; + } + prefs.edit().putString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, mode).apply(); + SbtSettings.ensureReadable(context); + if (onChanged != null) { + onChanged.run(); + } + }); + } + + static String currentMode(Context context, SharedPreferences prefs, boolean layoutEnabled) { + String stored = prefs.getString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_DOT); + NotificationDisplayStyleSettings.Style style = + NotificationDisplayStyleSettings.read(context).style(); + return currentMode(style, stored, layoutEnabled); + } + + private static String currentMode( + NotificationDisplayStyleSettings.Style androidStyle, + String stored, + boolean layoutEnabled + ) { + if (MODE_NONE.equals(stored) && layoutEnabled) { + return MODE_NONE; + } + if (androidStyle == NotificationDisplayStyleSettings.Style.CARDS) { + return MODE_CARDS; + } + if (androidStyle == NotificationDisplayStyleSettings.Style.ICONS) { + return MODE_ICONS; + } + return MODE_DOT; + } + + private static boolean writeMode(Context context, String mode) { + NotificationDisplayStyleSettings.Style style; + if (MODE_CARDS.equals(mode)) { + style = NotificationDisplayStyleSettings.Style.CARDS; + } else if (MODE_ICONS.equals(mode)) { + style = NotificationDisplayStyleSettings.Style.ICONS; + } else { + style = NotificationDisplayStyleSettings.Style.DOT; + } + return NotificationDisplayStyleSettings.write(context, style); + } + + private static RadioButton button(Context context, int text, String mode) { + RadioButton button = new RadioButton(context); + button.setId(View.generateViewId()); + button.setTag(mode); + button.setText(text); + return button; + } + + private static int idForMode(RadioGroup group, String mode) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (mode.equals(child.getTag())) { + return child.getId(); + } + } + return group.getChildCount() > 0 ? group.getChildAt(0).getId() : View.NO_ID; + } + + private static String modeForId(RadioGroup group, int id) { + View view = group.findViewById(id); + Object tag = view != null ? view.getTag() : null; + return tag instanceof String value ? value : MODE_DOT; + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java new file mode 100644 index 0000000..ec875a5 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockedSceneLayoutFragment.java @@ -0,0 +1,949 @@ +package se.ajpanton.statusbartweak.shell.ui; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.InputFilter; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioGroup; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.card.MaterialCardView; + +import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; +import se.ajpanton.statusbartweak.shell.settings.SbtSettings; + +abstract class LockedSceneLayoutFragment extends Fragment { + private final boolean aod; + private RadioGroup modeGroup; + private TextView modeHint; + private LinearLayout dynamicContent; + + LockedSceneLayoutFragment(boolean aod) { + this.aod = aod; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + Context context = requireContext(); + SharedPreferences prefs = context.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); + ScrollView scroll = new ScrollView(context); + LinearLayout root = vertical(context); + root.setPadding(dp(20), dp(20), dp(20), dp(20)); + scroll.addView(root); + + root.addView(title(context, aod ? R.string.section_aod : R.string.section_lockscreen)); + modeGroup = new RadioGroup(context); + modeGroup.setOrientation(RadioGroup.VERTICAL); + modeHint = body(context, R.string.layout_locked_notifications_hint); + LinearLayout modeContent = vertical(context); + modeContent.addView(modeHint); + modeContent.addView(modeGroup); + root.addView(card(context, R.string.layout_locked_notifications_title, modeContent)); + + dynamicContent = vertical(context); + root.addView(dynamicContent); + bindMode(context, prefs); + return scroll; + } + + private void bindMode(Context context, SharedPreferences prefs) { + boolean layoutEnabled = prefs.getBoolean(SbtSettings.KEY_CLOCK_ENABLED, SbtDefaults.CLOCK_ENABLED_DEFAULT); + LockedNotificationModeUi.bind( + context, + prefs, + modeGroup, + modeHint, + layoutEnabled, + () -> rebuildDynamic( + context, + prefs, + LockedNotificationModeUi.currentMode(context, prefs, layoutEnabled))); + rebuildDynamic(context, prefs, LockedNotificationModeUi.currentMode(context, prefs, layoutEnabled)); + } + + private void rebuildDynamic(Context context, SharedPreferences prefs, String mode) { + dynamicContent.removeAllViews(); + if (LockedNotificationModeUi.MODE_DOT.equals(mode) + || LockedNotificationModeUi.MODE_NONE.equals(mode)) { + return; + } + if (!aod) { + View clockCard = configuredPositionCardFromLayout( + context, + prefs, + R.id.layout_clock_card, + R.id.clock_position_mode_lockscreen, + SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN, + SbtDefaults.CLOCK_ENABLED_LOCKSCREEN_DEFAULT, + R.id.clock_position_group, + R.id.clock_position_middle_options_container, + R.id.clock_middle_auto_nearest, + R.id.clock_middle_side_prompt, + R.id.clock_middle_side_group, + R.id.clock_middle_side_right, + R.id.clock_vertical_offset_input, + R.id.clock_vertical_offset_minus_fast, + R.id.clock_vertical_offset_minus, + R.id.clock_vertical_offset_plus, + R.id.clock_vertical_offset_plus_fast, + R.id.clock_vertical_offset_reset, + sceneKey("clock_position"), + SbtDefaults.CLOCK_POSITION_DEFAULT, + sceneKey("clock_middle_auto_nearest_side"), + SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT, + sceneKey("clock_middle_side"), + SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT, + sceneKey("clock_vertical_offset_px"), + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT); + dynamicContent.addView(clockCard); + } + dynamicContent.addView(configuredIconContainersCardFromLayout( + context, + prefs, + R.id.layout_status_card, + "status", + aod ? R.id.status_position_mode_aod : R.id.status_position_mode_lockscreen, + R.id.status_position_group, + R.id.status_position_middle_options_container, + R.id.status_middle_auto_nearest, + R.id.status_middle_side_prompt, + R.id.status_middle_side_group, + R.id.status_middle_side_right, + R.id.status_vertical_offset_input, + R.id.status_vertical_offset_minus_fast, + R.id.status_vertical_offset_minus, + R.id.status_vertical_offset_plus, + R.id.status_vertical_offset_plus_fast, + R.id.status_vertical_offset_reset, + R.string.layout_status_position_title, + aod ? SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD : SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, + aod ? SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT : SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_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 (LockedNotificationModeUi.MODE_CARDS.equals(mode)) { + dynamicContent.addView(cardsNotificationCard(context, prefs)); + } else { + dynamicContent.addView(configuredIconContainersCardFromLayout( + context, + prefs, + R.id.layout_notif_card, + "notifications", + aod ? R.id.notif_position_mode_aod : R.id.notif_position_mode_lockscreen, + R.id.notif_position_group, + R.id.notif_position_middle_options_container, + R.id.notif_middle_auto_nearest, + R.id.notif_middle_side_prompt, + R.id.notif_middle_side_group, + R.id.notif_middle_side_right, + R.id.notif_vertical_offset_input, + R.id.notif_vertical_offset_minus_fast, + R.id.notif_vertical_offset_minus, + R.id.notif_vertical_offset_plus, + R.id.notif_vertical_offset_plus_fast, + R.id.notif_vertical_offset_reset, + R.string.layout_notification_position_title, + aod ? SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD : SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN, + aod ? SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT : SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_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 View configuredPositionCardFromLayout( + Context context, + SharedPreferences prefs, + int cardId, + int enabledCheckboxId, + String enabledKey, + boolean enabledDefault, + int positionGroupId, + int middleOptionsId, + int autoNearestId, + int promptId, + int middleSideGroupId, + int middleSideRightId, + int verticalInputId, + int verticalMinusFastId, + int verticalMinusId, + int verticalPlusId, + int verticalPlusFastId, + int verticalResetId, + String positionKey, + String positionDefault, + String autoNearestKey, + boolean autoNearestDefault, + String middleSideKey, + String middleSideDefault, + String verticalOffsetKey, + int verticalOffsetDefault + ) { + View card = inflateLayoutCard(context, cardId); + configureSingleEnabledCheckbox(card, prefs, enabledCheckboxId, enabledKey, enabledDefault); + bindXmlPositionSection( + card, + prefs, + positionGroupId, + middleOptionsId, + autoNearestId, + promptId, + middleSideGroupId, + middleSideRightId, + positionKey, + positionDefault, + autoNearestKey, + autoNearestDefault, + middleSideKey, + middleSideDefault); + bindXmlStepper( + prefs, + verticalOffsetKey, + card.findViewById(verticalInputId), + card.findViewById(verticalMinusFastId), + card.findViewById(verticalMinusId), + card.findViewById(verticalPlusId), + card.findViewById(verticalPlusFastId), + card.findViewById(verticalResetId), + verticalOffsetDefault, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + return card; + } + + private View configuredIconContainersCardFromLayout( + Context context, + SharedPreferences prefs, + int cardId, + String base, + int modeCheckboxId, + int positionGroupId, + int middleOptionsId, + int autoNearestId, + int promptId, + int middleSideGroupId, + int middleSideRightId, + int verticalInputId, + int verticalMinusFastId, + int verticalMinusId, + int verticalPlusId, + int verticalPlusFastId, + int verticalResetId, + int titleId, + String legacyEnabledKey, + boolean legacyEnabledDefault, + String positionDefault, + boolean autoNearestDefault, + String middleSideDefault, + int verticalOffsetDefault + ) { + LinearLayout wrapper = vertical(context); + String countKey = sceneKey(base + "_count"); + Runnable[] rebuild = new Runnable[1]; + rebuild[0] = () -> { + wrapper.removeAllViews(); + int count = iconContainerCount(prefs, countKey, legacyEnabledKey, legacyEnabledDefault); + View primary = configuredIconContainerCard( + context, + prefs, + cardId, + modeCheckboxId, + positionGroupId, + middleOptionsId, + autoNearestId, + promptId, + middleSideGroupId, + middleSideRightId, + verticalInputId, + verticalMinusFastId, + verticalMinusId, + verticalPlusId, + verticalPlusFastId, + verticalResetId, + titleId, + 0, + indexedSceneKey(base + "_position", 0), + positionDefault, + indexedSceneKey(base + "_middle_auto_nearest_side", 0), + autoNearestDefault, + indexedSceneKey(base + "_middle_side", 0), + middleSideDefault, + indexedSceneKey(base + "_vertical_offset_px", 0), + verticalOffsetDefault, + legacyEnabledKey, + countKey, + count, + rebuild[0]); + wrapper.addView(primary); + for (int i = 1; i < count; i++) { + wrapper.addView(configuredIconContainerCard( + context, + prefs, + cardId, + modeCheckboxId, + positionGroupId, + middleOptionsId, + autoNearestId, + promptId, + middleSideGroupId, + middleSideRightId, + verticalInputId, + verticalMinusFastId, + verticalMinusId, + verticalPlusId, + verticalPlusFastId, + verticalResetId, + titleId, + i, + indexedSceneKey(base + "_position", i), + positionDefault, + indexedSceneKey(base + "_middle_auto_nearest_side", i), + autoNearestDefault, + indexedSceneKey(base + "_middle_side", i), + middleSideDefault, + indexedSceneKey(base + "_vertical_offset_px", i), + verticalOffsetDefault, + null, + null, + count, + null)); + } + }; + rebuild[0].run(); + return wrapper; + } + + private View configuredIconContainerCard( + Context context, + SharedPreferences prefs, + int cardId, + int modeCheckboxId, + int positionGroupId, + int middleOptionsId, + int autoNearestId, + int promptId, + int middleSideGroupId, + int middleSideRightId, + int verticalInputId, + int verticalMinusFastId, + int verticalMinusId, + int verticalPlusId, + int verticalPlusFastId, + int verticalResetId, + int titleId, + int index, + String positionKey, + String positionDefault, + String autoNearestKey, + boolean autoNearestDefault, + String middleSideKey, + String middleSideDefault, + String verticalOffsetKey, + int verticalOffsetDefault, + @Nullable String legacyEnabledKey, + @Nullable String countKey, + int count, + @Nullable Runnable onCountChanged + ) { + View card = inflateLayoutCard(context, cardId); + removeSiblingModeCheckboxes(card, modeCheckboxId); + LinearLayout sectionContent = removeModeContainerForCheckbox(card, modeCheckboxId); + if (sectionContent != null && countKey != null && onCountChanged != null) { + int insertIndex = Math.min(1, sectionContent.getChildCount()); + sectionContent.addView( + iconContainerCountControl(context, prefs, countKey, legacyEnabledKey, count, onCountChanged), + insertIndex); + } + if (index > 0) { + TextView title = findCardTitle(card); + if (title != null) { + title.setText(getString(titleId) + " " + (index + 1)); + } + } + bindXmlPositionSection( + card, + prefs, + positionGroupId, + middleOptionsId, + autoNearestId, + promptId, + middleSideGroupId, + middleSideRightId, + positionKey, + positionDefault, + autoNearestKey, + autoNearestDefault, + middleSideKey, + middleSideDefault); + bindXmlStepper( + prefs, + verticalOffsetKey, + card.findViewById(verticalInputId), + card.findViewById(verticalMinusFastId), + card.findViewById(verticalMinusId), + card.findViewById(verticalPlusId), + card.findViewById(verticalPlusFastId), + card.findViewById(verticalResetId), + verticalOffsetDefault, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MIN, + SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_MAX); + return card; + } + + private View inflateLayoutCard(Context context, int cardId) { + View fullLayout = LayoutInflater.from(context).inflate(R.layout.fragment_layout, dynamicContent, false); + View card = fullLayout.findViewById(cardId); + if (card == null) { + return new View(context); + } + if (card.getParent() instanceof ViewGroup parent) { + parent.removeView(card); + } + return card; + } + + @Nullable + private TextView findCardTitle(View card) { + if (!(card instanceof ViewGroup cardGroup) || cardGroup.getChildCount() <= 0) { + return null; + } + View inner = cardGroup.getChildAt(0); + if (!(inner instanceof ViewGroup innerGroup) || innerGroup.getChildCount() <= 0) { + return null; + } + View title = innerGroup.getChildAt(0); + return title instanceof TextView textView ? textView : null; + } + + private void configureSingleEnabledCheckbox( + View root, + SharedPreferences prefs, + int enabledCheckboxId, + String key, + boolean defaultValue + ) { + CheckBox enabled = root.findViewById(enabledCheckboxId); + if (enabled == null) { + return; + } + hideSiblingCheckBoxes(enabled); + enabled.setText(R.string.layout_item_enabled); + enabled.setChecked(prefs.getBoolean(key, defaultValue)); + enabled.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(key, isChecked).apply(); + SbtSettings.ensureReadable(requireContext()); + }); + } + + private void removeSiblingModeCheckboxes(View root, int keptCheckboxId) { + CheckBox kept = root.findViewById(keptCheckboxId); + hideSiblingCheckBoxes(kept); + } + + private void hideSiblingCheckBoxes(@Nullable CheckBox kept) { + if (kept == null) { + return; + } + View modeContainer = directSectionChild(kept); + if (modeContainer instanceof ViewGroup group) { + hideOtherCheckBoxes(group, kept); + } + } + + private void hideOtherCheckBoxes(View view, CheckBox kept) { + if (view instanceof CheckBox checkBox && checkBox != kept) { + checkBox.setVisibility(View.GONE); + return; + } + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + hideOtherCheckBoxes(group.getChildAt(i), kept); + } + } + } + + @Nullable + private LinearLayout removeModeContainerForCheckbox(View root, int checkboxId) { + CheckBox checkbox = root.findViewById(checkboxId); + View modeContainer = directSectionChild(checkbox); + if (modeContainer == null || !(modeContainer.getParent() instanceof LinearLayout sectionContent)) { + return null; + } + sectionContent.removeView(modeContainer); + return sectionContent; + } + + @Nullable + private View directSectionChild(@Nullable View child) { + View current = child; + while (current != null && current.getParent() instanceof View parent) { + if (parent.getParent() instanceof MaterialCardView) { + return current; + } + current = parent; + } + return null; + } + + private View cardsNotificationCard(Context context, SharedPreferences prefs) { + LinearLayout content = vertical(context); + content.addView(stepper( + context, + prefs, + R.string.label_max_rows, + aod ? SbtSettings.KEY_CARDS_AOD_MAX_ROWS : SbtSettings.KEY_CARDS_LOCK_MAX_ROWS, + aod ? SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT : SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT, + aod ? SbtDefaults.CARDS_AOD_MAX_ROWS_MIN : SbtDefaults.CARDS_LOCK_MAX_ROWS_MIN, + aod ? SbtDefaults.CARDS_AOD_MAX_ROWS_MAX : SbtDefaults.CARDS_LOCK_MAX_ROWS_MAX)); + content.addView(stepper( + context, + prefs, + R.string.label_max_icons_per_row, + aod ? SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW : SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, + aod ? SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT : SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT, + aod ? SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MIN : SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MIN, + aod ? SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MAX : SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MAX)); + content.addView(checkBox( + context, + prefs, + R.string.label_even_distribution, + aod ? SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION : SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION, + aod ? SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT : SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT)); + content.addView(checkBox( + context, + prefs, + R.string.label_limit_screen_width, + aod ? SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH : SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, + aod ? SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT : SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT)); + return card(context, R.string.layout_notification_position_title, content); + } + + private View iconContainerCountControl( + Context context, + SharedPreferences prefs, + String countKey, + String legacyEnabledKey, + int current, + Runnable onChanged + ) { + LinearLayout outer = vertical(context); + outer.addView(body(context, R.string.layout_icon_container_count)); + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + MaterialButton minus = button(context, "-"); + MaterialButton plus = button(context, "+"); + EditText input = numericInput(context, false); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(1)}); + input.setText(String.valueOf(current)); + row.addView(minus, buttonParams()); + row.addView(input, inputParams()); + row.addView(plus, buttonParams()); + outer.addView(row); + + View.OnClickListener listener = view -> { + int next = clampIconContainerCount(parseInt(input, current) + (view == minus ? -1 : 1)); + persistIconContainerCount(prefs, countKey, legacyEnabledKey, next); + onChanged.run(); + }; + minus.setOnClickListener(listener); + plus.setOnClickListener(listener); + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clampIconContainerCount(parseInt(input, current)); + persistIconContainerCount(prefs, countKey, legacyEnabledKey, next); + onChanged.run(); + } + }); + return outer; + } + + private void bindXmlPositionSection( + View root, + SharedPreferences prefs, + int groupId, + int middleOptionsId, + int autoNearestId, + int promptId, + int middleSideGroupId, + int middleSideRightId, + String positionKey, + String positionDefault, + String autoNearestKey, + boolean autoNearestDefault, + String middleSideKey, + String middleSideDefault + ) { + RadioGroup position = root.findViewById(groupId); + View middleOptions = root.findViewById(middleOptionsId); + CheckBox autoNearest = root.findViewById(autoNearestId); + TextView prompt = root.findViewById(promptId); + RadioGroup side = root.findViewById(middleSideGroupId); + if (position == null || middleOptions == null || autoNearest == null || prompt == null || side == null) { + return; + } + + position.check(positionIdForValue(groupId, prefs.getString(positionKey, positionDefault))); + autoNearest.setChecked(prefs.getBoolean(autoNearestKey, autoNearestDefault)); + side.check("right".equals(prefs.getString(middleSideKey, middleSideDefault)) + ? middleSideRightId + : firstSideId(side, middleSideRightId)); + updateMiddleOptions(middleOptions, "middle".equals(positionValueForId(position.getCheckedRadioButtonId()))); + prompt.setText(autoNearest.isChecked() + ? R.string.layout_middle_side_prompt_centred + : R.string.layout_middle_side_prompt_attach_only); + + position.setOnCheckedChangeListener((group, checkedId) -> { + prefs.edit().putString(positionKey, positionValueForId(checkedId)).apply(); + updateMiddleOptions(middleOptions, "middle".equals(positionValueForId(checkedId))); + SbtSettings.ensureReadable(requireContext()); + }); + autoNearest.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(autoNearestKey, isChecked).apply(); + prompt.setText(isChecked + ? R.string.layout_middle_side_prompt_centred + : R.string.layout_middle_side_prompt_attach_only); + SbtSettings.ensureReadable(requireContext()); + }); + side.setOnCheckedChangeListener((group, checkedId) -> { + prefs.edit().putString(middleSideKey, checkedId == middleSideRightId ? "right" : "left").apply(); + SbtSettings.ensureReadable(requireContext()); + }); + } + + private void bindXmlStepper( + SharedPreferences prefs, + String key, + @Nullable EditText input, + @Nullable MaterialButton minusFast, + @Nullable MaterialButton minus, + @Nullable MaterialButton plus, + @Nullable MaterialButton plusFast, + @Nullable MaterialButton reset, + int defaultValue, + int min, + int max + ) { + if (input == null) { + return; + } + int start = clamp(prefs.getInt(key, defaultValue), min, max); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(4)}); + input.setText(String.valueOf(start)); + bindXmlStepperButton(prefs, key, input, minusFast, -5, min, max); + bindXmlStepperButton(prefs, key, input, minus, -1, min, max); + bindXmlStepperButton(prefs, key, input, plus, 1, min, max); + bindXmlStepperButton(prefs, key, input, plusFast, 5, min, max); + if (reset != null) { + reset.setOnClickListener(v -> { + input.setText(String.valueOf(defaultValue)); + persistIntPref(prefs, key, defaultValue); + }); + } + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clamp(parseInt(input, defaultValue), min, max); + input.setText(String.valueOf(next)); + persistIntPref(prefs, key, next); + } + }); + } + + private void bindXmlStepperButton( + SharedPreferences prefs, + String key, + EditText input, + @Nullable MaterialButton button, + int delta, + int min, + int max + ) { + if (button == null) { + return; + } + button.setOnClickListener(v -> { + int next = clamp(parseInt(input, 0) + delta, min, max); + input.setText(String.valueOf(next)); + persistIntPref(prefs, key, next); + }); + } + + private int positionIdForValue(int groupId, @Nullable String value) { + String normalized = value != null ? value : "left"; + if (groupId == R.id.clock_position_group) { + if ("right".equals(normalized)) { + return R.id.clock_position_right; + } + if ("middle".equals(normalized)) { + return R.id.clock_position_middle; + } + return R.id.clock_position_left; + } + if (groupId == R.id.notif_position_group) { + if ("right".equals(normalized)) { + return R.id.notif_position_right; + } + if ("middle".equals(normalized)) { + return R.id.notif_position_middle; + } + return R.id.notif_position_left; + } + if (groupId == R.id.status_position_group) { + if ("right".equals(normalized)) { + return R.id.status_position_right; + } + if ("middle".equals(normalized)) { + return R.id.status_position_middle; + } + return R.id.status_position_left; + } + return View.NO_ID; + } + + private String positionValueForId(int id) { + if (id == R.id.clock_position_right + || id == R.id.notif_position_right + || id == R.id.status_position_right) { + return "right"; + } + if (id == R.id.clock_position_middle + || id == R.id.notif_position_middle + || id == R.id.status_position_middle) { + return "middle"; + } + return "left"; + } + + private int firstSideId(RadioGroup group, int rightId) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (child.getId() != rightId) { + return child.getId(); + } + } + return View.NO_ID; + } + + private void updateMiddleOptions(View view, boolean enabled) { + if (view.getVisibility() != View.VISIBLE) { + view.setVisibility(View.VISIBLE); + } + view.setEnabled(enabled); + view.setAlpha(enabled ? 1f : 0.45f); + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + updateMiddleOptionChild(group.getChildAt(i), enabled); + } + } + } + + private void updateMiddleOptionChild(View view, boolean enabled) { + view.setEnabled(enabled); + if (view instanceof ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + updateMiddleOptionChild(group.getChildAt(i), enabled); + } + } + } + + private View stepper(Context context, SharedPreferences prefs, int label, String key, int def, int min, int max) { + LinearLayout outer = vertical(context); + outer.addView(body(context, label)); + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + MaterialButton minus = button(context, "-"); + MaterialButton plus = button(context, "+"); + EditText input = numericInput(context, true); + input.setText(String.valueOf(clamp(prefs.getInt(key, def), min, max))); + row.addView(minus, buttonParams()); + row.addView(input, inputParams()); + row.addView(plus, buttonParams()); + outer.addView(row); + View.OnClickListener listener = view -> { + int delta = view == minus ? -1 : 1; + int next = clamp(parseInt(input, def) + delta, min, max); + input.setText(String.valueOf(next)); + persistIntPref(prefs, key, next); + }; + minus.setOnClickListener(listener); + plus.setOnClickListener(listener); + input.setOnFocusChangeListener((v, hasFocus) -> { + if (!hasFocus) { + int next = clamp(parseInt(input, def), min, max); + input.setText(String.valueOf(next)); + persistIntPref(prefs, key, next); + } + }); + return outer; + } + + private EditText numericInput(Context context, boolean signed) { + EditText input = new EditText(context); + input.setSingleLine(true); + input.setGravity(Gravity.CENTER); + input.setInputType(signed + ? android.text.InputType.TYPE_CLASS_NUMBER | android.text.InputType.TYPE_NUMBER_FLAG_SIGNED + : android.text.InputType.TYPE_CLASS_NUMBER); + input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(4)}); + return input; + } + + private CheckBox checkBox(Context context, SharedPreferences prefs, int text, String key, boolean def) { + CheckBox checkBox = new CheckBox(context); + checkBox.setText(text); + checkBox.setChecked(prefs.getBoolean(key, def)); + checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefs.edit().putBoolean(key, isChecked).apply(); + SbtSettings.ensureReadable(context); + }); + return checkBox; + } + + private void persistIntPref(SharedPreferences prefs, String key, int value) { + prefs.edit().putInt(key, value).apply(); + SbtSettings.ensureReadable(requireContext()); + } + + private int iconContainerCount( + SharedPreferences prefs, + String countKey, + String legacyEnabledKey, + boolean legacyEnabledDefault + ) { + int fallback = prefs.getBoolean(legacyEnabledKey, legacyEnabledDefault) ? 1 : 0; + return clampIconContainerCount(prefs.getInt(countKey, fallback)); + } + + private void persistIconContainerCount( + SharedPreferences prefs, + String countKey, + String legacyEnabledKey, + int count + ) { + int clamped = clampIconContainerCount(count); + prefs.edit() + .putInt(countKey, clamped) + .putBoolean(legacyEnabledKey, clamped > 0) + .apply(); + SbtSettings.ensureReadable(requireContext()); + } + + private int clampIconContainerCount(int value) { + return clamp( + value, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MIN, + SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX); + } + + private String sceneKey(String suffix) { + return (aod ? "layout_aod_" : "layout_lock_") + suffix; + } + + private String indexedSceneKey(String suffix, int index) { + return index <= 0 ? sceneKey(suffix) : sceneKey(suffix) + "_" + (index + 1); + } + + private MaterialButton button(Context context, String text) { + MaterialButton button = new MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle); + button.setText(text); + button.setMinWidth(dp(40)); + button.setMinHeight(dp(40)); + button.setPadding(0, button.getPaddingTop(), 0, button.getPaddingBottom()); + return button; + } + + private LinearLayout.LayoutParams buttonParams() { + return new LinearLayout.LayoutParams(dp(40), ViewGroup.LayoutParams.WRAP_CONTENT); + } + + private LinearLayout.LayoutParams inputParams() { + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + dp(48), + ViewGroup.LayoutParams.WRAP_CONTENT); + params.leftMargin = dp(8); + params.rightMargin = dp(8); + return params; + } + + private MaterialCardView card(Context context, int titleId, View content) { + MaterialCardView card = new MaterialCardView(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = dp(16); + card.setLayoutParams(params); + card.setUseCompatPadding(true); + card.setStrokeColor(context.getColor(R.color.sbt_card_outline)); + card.setStrokeWidth(dp(1)); + LinearLayout inner = vertical(context); + inner.setPadding(dp(16), dp(16), dp(16), dp(16)); + inner.addView(sectionTitle(context, titleId)); + inner.addView(content); + card.addView(inner); + return card; + } + + private LinearLayout vertical(Context context) { + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + return layout; + } + + private TextView title(Context context, int text) { + TextView view = new TextView(context); + view.setText(text); + view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Headline5); + return view; + } + + private TextView sectionTitle(Context context, int text) { + TextView view = new TextView(context); + view.setText(text); + view.setAllCaps(true); + view.setTypeface(view.getTypeface(), android.graphics.Typeface.BOLD); + return view; + } + + private TextView body(Context context, int text) { + TextView view = new TextView(context); + view.setText(text); + view.setPadding(0, dp(8), 0, dp(6)); + return view; + } + + private int parseInt(EditText input, int fallback) { + try { + return Integer.parseInt(input.getText().toString().trim()); + } catch (Throwable ignored) { + return fallback; + } + } + + private int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } + + private int dp(int value) { + return Math.round(value * getResources().getDisplayMetrics().density); + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockscreenLayoutFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockscreenLayoutFragment.java new file mode 100644 index 0000000..1539ec8 --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/LockscreenLayoutFragment.java @@ -0,0 +1,7 @@ +package se.ajpanton.statusbartweak.shell.ui; + +public final class LockscreenLayoutFragment extends LockedSceneLayoutFragment { + public LockscreenLayoutFragment() { + super(false); + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java index 4641dfa..2f6b64a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/MainActivity.java @@ -85,13 +85,17 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On if (itemId == R.id.nav_system_icons) { fragment = new SystemIconsFragment(); } else if (itemId == R.id.nav_layout) { + fragment = new LayoutHomeFragment(); + } else if (itemId == R.id.nav_layout_unlocked) { fragment = new LayoutFragment(); + } else if (itemId == R.id.nav_layout_lockscreen) { + fragment = new LockscreenLayoutFragment(); + } else if (itemId == R.id.nav_layout_aod) { + fragment = new AodLayoutFragment(); } else if (itemId == R.id.nav_battery_bar) { fragment = new BatteryBarFragment(); } else if (itemId == R.id.nav_clock) { fragment = new ClockFragment(); - } else if (itemId == R.id.nav_notification_icons) { - fragment = new NotificationIconsFragment(); } else if (itemId == R.id.nav_block_app_notification_icons) { fragment = new BlockAppNotificationIconsFragment(); } else if (itemId == R.id.nav_status_chips) { @@ -113,8 +117,12 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On return R.string.nav_system_icons; } else if (itemId == R.id.nav_layout) { return R.string.nav_layout; - } else if (itemId == R.id.nav_notification_icons) { - return R.string.nav_notification_icons; + } else if (itemId == R.id.nav_layout_unlocked) { + return R.string.section_unlocked; + } else if (itemId == R.id.nav_layout_lockscreen) { + return R.string.section_lockscreen; + } else if (itemId == R.id.nav_layout_aod) { + return R.string.section_aod; } else if (itemId == R.id.nav_block_app_notification_icons) { return R.string.hidden_apps_page_title; } else if (itemId == R.id.nav_clock) { diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/NotificationIconsFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/NotificationIconsFragment.java deleted file mode 100644 index ecb923b..0000000 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/NotificationIconsFragment.java +++ /dev/null @@ -1,489 +0,0 @@ -package se.ajpanton.statusbartweak.shell.ui; - -import se.ajpanton.statusbartweak.R; -import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; -import se.ajpanton.statusbartweak.shell.settings.SbtSettings; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.text.Editable; -import android.text.InputFilter; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; -import java.util.Arrays; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import com.google.android.material.button.MaterialButton; -import com.google.android.material.slider.Slider; -import com.google.android.material.switchmaterial.SwitchMaterial; - -public class NotificationIconsFragment extends Fragment { - private static final int INF_VALUE = Integer.MAX_VALUE; - private static final String INF_LABEL = "\u221e"; - private static final int SLIDER_STEPS = 200; - private static final int INF_GAP_STEPS = 20; - private static final double SLIDER_EXPONENT = 2.0; - - @Nullable - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View root = inflater.inflate(R.layout.fragment_notification_icons, container, false); - SharedPreferences prefs = requireContext() - .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); - SbtSettings.ensureReadable(requireContext()); - bindNumberControl(root, prefs, - R.id.unlocked_max_icons_slider, - R.id.unlocked_max_icons_input, - R.id.unlocked_max_icons_minus, - R.id.unlocked_max_icons_plus, - SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW, - SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT, - SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN, - SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MAX, - true); - - bindNumberControl(root, prefs, - R.id.icons_aod_max_icons_slider, - R.id.icons_aod_max_icons_input, - R.id.icons_aod_max_icons_minus, - R.id.icons_aod_max_icons_plus, - SbtSettings.KEY_ICONS_AOD_MAX_ICONS_PER_ROW, - SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_DEFAULT, - SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_MIN, - SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_MAX, - true); - bindNumberControl(root, prefs, - R.id.icons_aod_max_rows_slider, - R.id.icons_aod_max_rows_input, - R.id.icons_aod_max_rows_minus, - R.id.icons_aod_max_rows_plus, - SbtSettings.KEY_ICONS_AOD_MAX_ROWS, - SbtDefaults.ICONS_AOD_MAX_ROWS_DEFAULT, - SbtDefaults.ICONS_AOD_MAX_ROWS_MIN, - SbtDefaults.ICONS_AOD_MAX_ROWS_MAX, - true); - bindSwitch(root, prefs, - R.id.icons_aod_even_switch, - SbtSettings.KEY_ICONS_AOD_EVEN_DISTRIBUTION, - SbtDefaults.ICONS_AOD_EVEN_DISTRIBUTION_DEFAULT); - bindSwitch(root, prefs, - R.id.icons_aod_cutout_switch, - SbtSettings.KEY_ICONS_AOD_CUTOUT_LIMIT, - SbtDefaults.ICONS_AOD_CUTOUT_LIMIT_DEFAULT); - - bindNumberControl(root, prefs, - R.id.icons_lock_max_icons_slider, - R.id.icons_lock_max_icons_input, - R.id.icons_lock_max_icons_minus, - R.id.icons_lock_max_icons_plus, - SbtSettings.KEY_ICONS_LOCK_MAX_ICONS_PER_ROW, - SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_DEFAULT, - SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_MIN, - SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_MAX, - true); - bindNumberControl(root, prefs, - R.id.icons_lock_max_rows_slider, - R.id.icons_lock_max_rows_input, - R.id.icons_lock_max_rows_minus, - R.id.icons_lock_max_rows_plus, - SbtSettings.KEY_ICONS_LOCK_MAX_ROWS, - SbtDefaults.ICONS_LOCK_MAX_ROWS_DEFAULT, - SbtDefaults.ICONS_LOCK_MAX_ROWS_MIN, - SbtDefaults.ICONS_LOCK_MAX_ROWS_MAX, - true); - bindSwitch(root, prefs, - R.id.icons_lock_even_switch, - SbtSettings.KEY_ICONS_LOCK_EVEN_DISTRIBUTION, - SbtDefaults.ICONS_LOCK_EVEN_DISTRIBUTION_DEFAULT); - bindSwitch(root, prefs, - R.id.icons_lock_cutout_switch, - SbtSettings.KEY_ICONS_LOCK_CUTOUT_LIMIT, - SbtDefaults.ICONS_LOCK_CUTOUT_LIMIT_DEFAULT); - - bindNumberControl(root, prefs, - R.id.cards_aod_max_icons_slider, - R.id.cards_aod_max_icons_input, - R.id.cards_aod_max_icons_minus, - R.id.cards_aod_max_icons_plus, - SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW, - SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT, - SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MIN, - SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MAX, - true); - bindNumberControl(root, prefs, - R.id.cards_aod_max_rows_slider, - R.id.cards_aod_max_rows_input, - R.id.cards_aod_max_rows_minus, - R.id.cards_aod_max_rows_plus, - SbtSettings.KEY_CARDS_AOD_MAX_ROWS, - SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT, - SbtDefaults.CARDS_AOD_MAX_ROWS_MIN, - SbtDefaults.CARDS_AOD_MAX_ROWS_MAX, - true); - bindSwitch(root, prefs, - R.id.cards_aod_even_switch, - SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION, - SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT); - bindSwitch(root, prefs, - R.id.cards_aod_limit_width_switch, - SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH, - SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT); - - bindNumberControl(root, prefs, - R.id.cards_lock_max_icons_slider, - R.id.cards_lock_max_icons_input, - R.id.cards_lock_max_icons_minus, - R.id.cards_lock_max_icons_plus, - SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, - SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT, - SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MIN, - SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MAX, - true); - bindNumberControl(root, prefs, - R.id.cards_lock_max_rows_slider, - R.id.cards_lock_max_rows_input, - R.id.cards_lock_max_rows_minus, - R.id.cards_lock_max_rows_plus, - SbtSettings.KEY_CARDS_LOCK_MAX_ROWS, - SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT, - SbtDefaults.CARDS_LOCK_MAX_ROWS_MIN, - SbtDefaults.CARDS_LOCK_MAX_ROWS_MAX, - true); - bindSwitch(root, prefs, - R.id.cards_lock_even_switch, - SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION, - SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT); - bindSwitch(root, prefs, - R.id.cards_lock_limit_width_switch, - SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, - SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT); - applyLayoutAvailability(root, prefs); - return root; - } - - private void applyLayoutAvailability(View root, SharedPreferences prefs) { - if (root == null || prefs == null) { - return; - } - boolean layoutEnabled = prefs.getBoolean( - SbtSettings.KEY_CLOCK_ENABLED, - SbtDefaults.CLOCK_ENABLED_DEFAULT); - View unlockedSection = root.findViewById(R.id.notification_icons_unlocked_section); - View layoutSections = root.findViewById(R.id.notification_icons_layout_sections); - View layoutRequiredHint = root.findViewById(R.id.notification_icons_layout_required_hint); - View unlockedObsoleteHint = root.findViewById(R.id.notification_icons_unlocked_obsolete_hint); - if (unlockedSection != null) { - setViewTreeEnabled(unlockedSection, !layoutEnabled); - unlockedSection.setAlpha(layoutEnabled ? 0.45f : 1f); - } - if (layoutSections != null) { - setViewTreeEnabled(layoutSections, layoutEnabled); - layoutSections.setAlpha(layoutEnabled ? 1f : 0.45f); - } - if (layoutRequiredHint != null) { - layoutRequiredHint.setVisibility(layoutEnabled ? View.GONE : View.VISIBLE); - } - if (unlockedObsoleteHint != null) { - unlockedObsoleteHint.setVisibility(layoutEnabled ? View.VISIBLE : View.GONE); - } - } - - private void setViewTreeEnabled(View view, boolean enabled) { - if (view == null) { - return; - } - view.setEnabled(enabled); - if (view instanceof ViewGroup group) { - for (int i = 0; i < group.getChildCount(); i++) { - setViewTreeEnabled(group.getChildAt(i), enabled); - } - } - } - - private void bindNumberControl(View root, - SharedPreferences prefs, - int sliderId, - int inputId, - int minusId, - int plusId, - String key, - int defaultValue, - int min, - int max, - boolean allowInf) { - Slider slider = root.findViewById(sliderId); - EditText input = root.findViewById(inputId); - MaterialButton minus = root.findViewById(minusId); - MaterialButton plus = root.findViewById(plusId); - - int[] positions = buildPositions(min, max); - int maxPos = SLIDER_STEPS - INF_GAP_STEPS; - - slider.setValueFrom(0); - slider.setValueTo(SLIDER_STEPS); - slider.setStepSize(1f); - slider.setTickVisible(false); - slider.setLabelFormatter(value -> labelForSliderValue(Math.round(value), min, max, allowInf, positions)); - - final boolean[] updating = new boolean[]{false}; - - int current = clampInt(prefs.getInt(key, defaultValue), min, Integer.MAX_VALUE); - applyValueToUi(slider, input, min, max, allowInf, positions, current, updating); - input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)}); - - slider.addOnChangeListener((s, v, fromUser) -> { - if (updating[0]) { - return; - } - int pos = Math.round(v); - int snapped = snapPosition(pos, positions, allowInf, maxPos); - if (snapped != pos) { - updating[0] = true; - slider.setValue(snapped); - updating[0] = false; - } - int mapped = positionToValue(snapped, min, max, allowInf, positions, maxPos); - int nextValue = mapped; - updating[0] = true; - if (mapped == INF_VALUE) { - input.setText(INF_LABEL); - } else { - input.setText(String.valueOf(mapped)); - } - updating[0] = false; - if (fromUser) { - prefs.edit().putInt(key, nextValue).apply(); - SbtSettings.ensureReadable(requireContext()); - } - }); - - minus.setOnClickListener(v -> { - int parsed = parseInput(input, current); - int next; - if (parsed == INF_VALUE) { - next = max; - } else { - next = clampInt(parsed - 1, min, Integer.MAX_VALUE); - } - applyValueToUi(slider, input, min, max, allowInf, positions, next, updating); - prefs.edit().putInt(key, next).apply(); - SbtSettings.ensureReadable(requireContext()); - }); - - plus.setOnClickListener(v -> { - int parsed = parseInput(input, current); - int next; - if (parsed == INF_VALUE) { - next = INF_VALUE; - } else { - next = clampInt(parsed + 1, min, Integer.MAX_VALUE); - } - applyValueToUi(slider, input, min, max, allowInf, positions, next, updating); - prefs.edit().putInt(key, next).apply(); - SbtSettings.ensureReadable(requireContext()); - }); - - input.setOnEditorActionListener((v, actionId, event) -> { - if (actionId == EditorInfo.IME_ACTION_DONE) { - int next = clampInt(parseInput(input, current), min, Integer.MAX_VALUE); - applyValueToUi(slider, input, min, max, allowInf, positions, next, updating); - prefs.edit().putInt(key, next).apply(); - SbtSettings.ensureReadable(requireContext()); - } - return false; - }); - - input.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { } - - @Override - public void afterTextChanged(Editable s) { - if (updating[0]) { - return; - } - String text = s != null ? s.toString().trim() : ""; - if (text.isEmpty()) { - return; - } - int parsed = parseInput(input, current); - int next = clampInt(parsed, min, Integer.MAX_VALUE); - if (next != parsed) { - applyValueToUi(slider, input, min, max, allowInf, positions, next, updating); - } else { - updateSliderValue(slider, min, max, allowInf, positions, next, updating); - } - prefs.edit().putInt(key, next).apply(); - SbtSettings.ensureReadable(requireContext()); - } - }); - } - - private void bindSwitch(View root, - SharedPreferences prefs, - int switchId, - String key, - boolean defaultValue) { - SwitchMaterial toggle = root.findViewById(switchId); - boolean current = prefs.getBoolean(key, defaultValue); - toggle.setChecked(current); - toggle.setOnCheckedChangeListener((buttonView, isChecked) -> - { - prefs.edit().putBoolean(key, isChecked).apply(); - SbtSettings.ensureReadable(requireContext()); - }); - } - - private int clampInt(int value, int min, int max) { - if (value < min) { - return min; - } - if (value > max) { - return max; - } - return value; - } - - private int parseInput(EditText input, int fallback) { - if (input == null) { - return fallback; - } - - String text = input.getText().toString().trim(); - if (text.isEmpty()) { - return fallback; - } - if (INF_LABEL.equals(text) || "inf".equalsIgnoreCase(text)) { - return INF_VALUE; - } - return Integer.parseInt(text); - } - - private void applyValueToUi(Slider slider, - EditText input, - int min, - int max, - boolean allowInf, - int[] positions, - int value, - boolean[] updating) { - updateSliderValue(slider, min, max, allowInf, positions, value, updating); - updating[0] = true; - if (value == INF_VALUE) { - input.setText(INF_LABEL); - } else { - input.setText(String.valueOf(value)); - } - updating[0] = false; - } - - private void updateSliderValue(Slider slider, - int min, - int max, - boolean allowInf, - int[] positions, - int value, - boolean[] updating) { - int idx = valueToPosition(value, min, max, allowInf, positions); - updating[0] = true; - slider.setValue(idx); - updating[0] = false; - } - - private int[] buildPositions(int min, int max) { - int count = max - min + 1; - int maxPos = SLIDER_STEPS - INF_GAP_STEPS; - int[] positions = new int[count]; - if (count <= 1) { - positions[0] = 0; - return positions; - } - for (int i = 0; i < count; i++) { - double t = (double) i / (double) (count - 1); - double shaped = Math.pow(t, 1.0 / SLIDER_EXPONENT); - positions[i] = (int) Math.round(shaped * maxPos); - } - positions[0] = 0; - positions[count - 1] = maxPos; - for (int i = 1; i < count; i++) { - if (positions[i] <= positions[i - 1]) { - positions[i] = positions[i - 1] + 1; - } - } - positions[count - 1] = maxPos; - for (int i = count - 2; i >= 0; i--) { - if (positions[i] >= positions[i + 1]) { - positions[i] = positions[i + 1] - 1; - } - } - return positions; - } - - private int valueToPosition(int value, int min, int max, boolean allowInf, int[] positions) { - if (allowInf && value == INF_VALUE) { - return SLIDER_STEPS; - } - if (allowInf && value > max) { - return SLIDER_STEPS; - } - int clamped = clampInt(value, min, max); - return positions[clamped - min]; - } - - private int positionToValue(int pos, - int min, - int max, - boolean allowInf, - int[] positions, - int maxPos) { - if (allowInf && pos >= maxPos + (INF_GAP_STEPS / 2)) { - return INF_VALUE; - } - int idx = nearestIndex(pos, positions); - int value = min + idx; - return clampInt(value, min, max); - } - - private int nearestIndex(int pos, int[] positions) { - int idx = Arrays.binarySearch(positions, pos); - if (idx >= 0) { - return idx; - } - int insert = -idx - 1; - if (insert <= 0) { - return 0; - } - if (insert >= positions.length) { - return positions.length - 1; - } - int left = positions[insert - 1]; - int right = positions[insert]; - return (pos - left <= right - pos) ? (insert - 1) : insert; - } - - private int snapPosition(int pos, int[] positions, boolean allowInf, int maxPos) { - if (allowInf && pos >= maxPos + (INF_GAP_STEPS / 2)) { - return SLIDER_STEPS; - } - int idx = nearestIndex(pos, positions); - return positions[idx]; - } - - private String labelForSliderValue(int pos, int min, int max, boolean allowInf, int[] positions) { - int maxPos = SLIDER_STEPS - INF_GAP_STEPS; - int value = positionToValue(pos, min, max, allowInf, positions, maxPos); - return value == INF_VALUE ? INF_LABEL : String.valueOf(value); - } -} diff --git a/app/src/main/res/layout/fragment_icons_debug.xml b/app/src/main/res/layout/fragment_icons_debug.xml index de05698..865f1c3 100644 --- a/app/src/main/res/layout/fragment_icons_debug.xml +++ b/app/src/main/res/layout/fragment_icons_debug.xml @@ -73,6 +73,81 @@ + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/drawer_menu.xml b/app/src/main/res/menu/drawer_menu.xml index a92f861..ac573f5 100644 --- a/app/src/main/res/menu/drawer_menu.xml +++ b/app/src/main/res/menu/drawer_menu.xml @@ -6,9 +6,17 @@ android:checkable="true" android:title="@string/nav_layout" /> + android:title="@string/nav_layout_unlocked" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c5517f8..5505499 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5,13 +5,14 @@ Close navigation Debugging Hide status icons -     Notification icons     Hide notification icons     Hide status icons Clock Layout +     Unlocked +     Lockscreen +     AOD Battery bar - Notification icons layout Hide notification icons Status chips Misc @@ -29,6 +30,15 @@ Restart SystemUI SystemUI restart requested. Could not restart SystemUI. Root shell failed. + Android notification style + Current: %1$s + Temporary control for Samsung lockscreen/AOD notification style. Writes use root shell. + Cards + Icons + Dot + unknown + Could not write notification style. Root shell failed. + Notification style changed. Notification icons Cycle debug icons Cycles visible debug icons between 0 and the configured count every 2 seconds. @@ -151,21 +161,27 @@ That threshold moved to a new position in the list. Leave the field empty and press OK for \"Same as discharge\", or use the button below for \"Same as default\". Clock - Layout + Layout + Unlocked Master toggles Master toggle Use custom clock Use custom layout Clock position - Clock position - Status chip position - Notification icons position - Status icons position - Show dot if truncated + Clock + Status chip + Notification icons + Status icons + Enabled + Number of containers + Locked notifications mode + Mirrors the Samsung notification display style for AOD and lockscreen. + None + None is remembered, but Layout OFF uses Dot. + Misc + Max unlocked icons per row Padding Requires Layout to be ON - Requires Layout to be ON - Obsolete when Layout is ON Gap from cutout (px) Left padding (px) Right padding (px) diff --git a/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java b/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java new file mode 100644 index 0000000..6c03cbf --- /dev/null +++ b/app/src/test/java/se/ajpanton/statusbartweak/runtime/layoutsolver/PackedStatusBarLayoutSolverTest.java @@ -0,0 +1,296 @@ +package se.ajpanton.statusbartweak.runtime.layoutsolver; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Box; +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Fallback; +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.LayoutItem; +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Position; +import se.ajpanton.statusbartweak.runtime.layoutsolver.PackedStatusBarLayoutSolver.Spec; + +public final class PackedStatusBarLayoutSolverTest { + @Test + public void sharedStatusPoolDoesNotExpandAcrossCutout() { + ArrayList items = baseItems(30, 2); + + PackedStatusBarLayoutSolver.solve(cutoutSpec(), items); + + LayoutItem status0 = find(items, "Status0"); + LayoutItem status1 = find(items, "Status1"); + assertFalse(overlapsCutout(status0)); + assertFalse(overlapsCutout(status1)); + assertTrue(status0.visibleIcons > 0); + assertTrue(status1.visibleIcons > 0); + assertTrue(status1.dot); + } + + @Test + public void sharedNotificationPoolDoesNotExpandAcrossCutout() { + ArrayList items = baseItems(3, 40); + + PackedStatusBarLayoutSolver.solve(cutoutSpec(), items); + + LayoutItem notifs0 = find(items, "Notifs0"); + LayoutItem notifs1 = find(items, "Notifs1"); + LayoutItem notifs2 = find(items, "Notifs2"); + assertFalse(overlapsCutout(notifs0)); + assertFalse(overlapsCutout(notifs1)); + assertFalse(overlapsCutout(notifs2)); + } + + @Test + public void zeroTruncationDoesNotExpandMultiBoxClock() { + LayoutItem clock = new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(102, 14, 33, 38), + new Box(0, 32, 134, 47))); + double beforeWidth = clock.width(); + + double actual = clock.truncateAtLeast(0); + + assertTrue(actual == 0); + assertTrue(clock.width() == beforeWidth); + assertTrue(clock.boxes.get(0).width == 33); + assertTrue(clock.boxes.get(1).width == 134); + } + + @Test + public void discreteTruncationUsesRealIconWidthsAndWallCompactDot() { + ArrayList items = new ArrayList<>(); + items.add(new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(102, 14, 33, 38), + new Box(0, 32, 134, 47)))); + LayoutItem status = iconItem("Status", "Status", "status", 5, 35, 197, 18, 35); + status.iconWidths = new ArrayList<>(Arrays.asList(35, 35, 35, 35, 57)); + status.widthLimit = 197.0; + LayoutItem notifications = iconItem("Notifs", "Notifs", "notifications", 5, 39, 195, 36, 39); + notifications.compactDotWidth = 27; + items.add(status); + items.add(notifications); + + PackedStatusBarLayoutSolver.solve(productionSpec(834, 387, 59, 11), items); + + assertTrue(status.visibleIcons == 5); + assertFalse(status.dot); + assertTrue(notifications.visibleIcons == 0); + assertTrue(notifications.dot); + assertTrue(notifications.right() <= 387); + assertTrue(notifications.left() - status.right() >= 11); + } + + @Test + public void middleLeftItemStacksAgainstCutoutWallInSplitRegion() { + ArrayList items = new ArrayList<>(); + items.add(new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(102, 14, 33, 38), + new Box(0, 32, 134, 47)))); + LayoutItem status = iconItem("Status", "Status", "status", 5, 35, 197, 18, 35); + status.iconWidths = new ArrayList<>(Arrays.asList(35, 35, 40, 34, 53)); + status.widthLimit = 197.0; + LayoutItem notifications = new LayoutItem( + "Notifs", + "Notifs", + Position.MIDDLE, + Fallback.LEFT, + LayoutItem.boxes(new Box(0, 36, 156, 39))); + notifications.iconGroup = "notifications"; + notifications.iconCount = 4; + notifications.visibleIcons = 4; + notifications.poolRemaining = 4; + notifications.iconWidth = 39; + notifications.iconWidths = new ArrayList<>(Arrays.asList(39, 39, 39, 39)); + notifications.dotWidth = 39; + notifications.compactDotWidth = 27; + notifications.allowDot = true; + notifications.allowZero = false; + notifications.widthLimit = 156.0; + items.add(status); + items.add(notifications); + + PackedStatusBarLayoutSolver.solve(productionSpec(834, 387, 59, 11), items); + + assertTrue(notifications.right() <= 387); + assertTrue(notifications.left() >= status.right() + 11); + } + + @Test + public void truncatedDotUsesAvailableIconSlotAndWallCroppedDot() { + LayoutItem notifications = iconItem( + "Notifs", + "Notifs", + "notifications", + 2, + 39, + 78, + 36, + 39); + notifications.direction = PackedStatusBarLayoutSolver.Direction.RIGHT; + notifications.widthLimit = 70.0; + + notifications.recomputeIconState(); + + assertTrue(notifications.visibleIcons == 1); + assertTrue(notifications.dot); + assertTrue(notifications.width() == 65.0); + } + + private static Spec cutoutSpec() { + return new Spec( + 1200, + 850, + 100, + new ArrayList<>(Arrays.asList("Notifs", "Status", "Chip"))); + } + + private static Spec productionSpec(int screenWidth, int cutoutLeft, int cutoutWidth, int padding) { + return new Spec( + screenWidth, + cutoutLeft, + cutoutWidth, + padding, + new ArrayList<>(Arrays.asList("Notifs", "Status", "Chip", "Clock"))); + } + + private static LayoutItem iconItem( + String name, + String kind, + String group, + int count, + int iconWidth, + int naturalWidth, + int top, + int height + ) { + LayoutItem item = new LayoutItem( + name, + kind, + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes(new Box(0, top, naturalWidth, height))); + item.iconGroup = group; + item.iconCount = count; + item.visibleIcons = count; + item.poolRemaining = count; + item.iconWidth = iconWidth; + item.dotWidth = iconWidth; + item.compactDotWidth = iconWidth; + item.allowDot = true; + item.allowZero = false; + item.widthLimit = count * (double) iconWidth; + return item; + } + + private static ArrayList baseItems(int statusIcons, int notificationIcons) { + ArrayList items = new ArrayList<>(); + LayoutItem clock = new LayoutItem( + "Clock", + "Clock", + Position.LEFT, + Fallback.LEFT, + LayoutItem.boxes( + new Box(0, 0, 240, 40), + new Box(80, -20, 30, 30))); + clock.y = 100; + items.add(clock); + + items.add(LayoutItem.fixed( + "Chip", + "Chip", + Position.MIDDLE, + Fallback.RIGHT, + 400, + 40, + 90)); + + addIcon(items, "Notifs0", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 0); + addIcon(items, "Notifs1", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 50); + addIcon(items, "Notifs2", "Notifs", Position.LEFT, Fallback.LEFT, "notifications", notificationIcons, 30); + addIcon(items, "Status0", "Status", Position.RIGHT, Fallback.RIGHT, "status", statusIcons, 50); + addIcon(items, "Status1", "Status", Position.RIGHT, Fallback.RIGHT, "status", statusIcons, 100); + + items.add(LayoutItem.fixed("Dummy0", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 50)); + items.add(LayoutItem.fixed("Dummy1", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 0)); + items.add(LayoutItem.fixed("Dummy2", "Dummy", Position.MIDDLE, Fallback.LEFT, 100, 30, 20)); + configureIconGroups(items); + return sortByKindOrder(items, Arrays.asList("Clock", "Chip", "Status", "Notifs", "Dummy")); + } + + private static void addIcon( + ArrayList items, + String name, + String kind, + Position position, + Fallback fallback, + String group, + int count, + int y + ) { + items.add(LayoutItem.iconStrip(name, kind, position, fallback, group, count, 50, y)); + } + + private static void configureIconGroups(ArrayList items) { + ArrayList status = new ArrayList<>(); + ArrayList notifications = new ArrayList<>(); + for (LayoutItem item : items) { + if ("status".equals(item.iconGroup)) { + status.add(item); + } else if ("notifications".equals(item.iconGroup)) { + notifications.add(item); + } + } + configureGroup(status); + configureGroup(notifications); + } + + private static void configureGroup(ArrayList items) { + for (int i = 0; i < items.size(); i++) { + LayoutItem item = items.get(i); + item.allowZero = i < items.size() - 1; + item.allowDot = i == items.size() - 1; + } + } + + private static ArrayList sortByKindOrder(ArrayList items, List order) { + ArrayList sorted = new ArrayList<>(items); + sorted.sort((a, b) -> { + int kind = Integer.compare(order.indexOf(a.kind), order.indexOf(b.kind)); + return kind != 0 ? kind : a.name.compareTo(b.name); + }); + return sorted; + } + + private static LayoutItem find(ArrayList items, String name) { + for (LayoutItem item : items) { + if (name.equals(item.name)) { + return item; + } + } + throw new AssertionError("Missing item " + name); + } + + private static boolean overlapsCutout(LayoutItem item) { + return Math.max(item.left(), 850) < Math.min(item.right(), 950); + } + +} diff --git a/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java b/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java new file mode 100644 index 0000000..a68d339 --- /dev/null +++ b/app/src/test/java/se/ajpanton/statusbartweak/runtime/render/UnlockedLayoutBandTest.java @@ -0,0 +1,85 @@ +package se.ajpanton.statusbartweak.runtime.render; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; + +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutItemKind; +import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition; + +public final class UnlockedLayoutBandTest { + @Test + public void compactNotificationDotUsesSolverWidthAfterIconShrinking() { + UnlockedLayoutBand band = UnlockedLayoutBand.icons( + LayoutItemKind.NOTIFICATIONS, + null, + null, + placements(6, 39, 39), + LayoutPosition.LEFT, + false, + AnchorSide.LEFT, + true); + + band.applyPackedIconState(0, 0, true, 14); + band.place(388); + + assertEquals(1, band.placements.size()); + SnapshotPlacement dot = band.placements.get(0); + assertTrue(dot.overflowDot); + assertEquals(14, dot.bounds.width); + assertEquals(388, dot.bounds.left); + assertEquals(14, band.placedBounds.width); + } + + @Test + public void packedIconStateUsesRequestedShrinkableIconSlice() { + ArrayList placements = placements(6, 39, 39); + placements.add(1, new SnapshotPlacement( + null, + new Bounds(0, 36, 39, 39), + "existing-dot", + false, + true, + null, + new Bounds(0, 0, 39, 39), + 0)); + UnlockedLayoutBand band = UnlockedLayoutBand.icons( + LayoutItemKind.NOTIFICATIONS, + null, + null, + placements, + LayoutPosition.LEFT, + false, + AnchorSide.LEFT, + true); + + band.applyPackedIconState(2, 3, false, 117); + + assertEquals(3, band.placements.size()); + assertEquals("icon2", band.placements.get(0).key); + assertEquals("icon3", band.placements.get(1).key); + assertEquals("icon4", band.placements.get(2).key); + } + + private static ArrayList placements(int count, int width, int height) { + ArrayList result = new ArrayList<>(); + for (int i = 0; i < count; i++) { + Bounds bounds = new Bounds(0, 36, width, height); + result.add(new SnapshotPlacement( + null, + bounds, + "icon" + i, + false, + false, + null, + new Bounds(0, 0, width, height), + 0)); + } + return result; + } +}