13 Commits
40 changed files with 1882 additions and 1015 deletions
@@ -9,12 +9,13 @@ public final class SystemUiCapabilities {
public static final String PACKAGE_SYSTEM = "system"; public static final String PACKAGE_SYSTEM = "system";
public static final String PACKAGE_SYSTEMUI = "com.android.systemui"; public static final String PACKAGE_SYSTEMUI = "com.android.systemui";
public static final String PACKAGE_SAMSUNG_AOD_SERVICE = "com.samsung.android.app.aodservice"; public static final String PACKAGE_SAMSUNG_AOD_SERVICE = "com.samsung.android.app.aodservice";
private static final boolean SAMSUNG_ONE_UI = "samsung".equalsIgnoreCase(Build.MANUFACTURER);
private SystemUiCapabilities() { private SystemUiCapabilities() {
} }
public static boolean isSamsungOneUi() { public static boolean isSamsungOneUi() {
return "samsung".equalsIgnoreCase(Build.MANUFACTURER); return SAMSUNG_ONE_UI;
} }
public static boolean supportsLockscreenShadeWindowRoot() { public static boolean supportsLockscreenShadeWindowRoot() {
@@ -1,5 +1,6 @@
package se.ajpanton.statusbartweak.runtime; package se.ajpanton.statusbartweak.runtime;
import android.annotation.SuppressLint;
import android.content.res.Resources; import android.content.res.Resources;
import android.view.View; import android.view.View;
@@ -7,6 +8,7 @@ public final class ViewIdNames {
private ViewIdNames() { private ViewIdNames() {
} }
@SuppressLint("ResourceType") // Foreign SystemUI ids are opaque integers, not this app's R.id values.
public static String idName(View view) { public static String idName(View view) {
if (view == null || view.getId() == View.NO_ID || (view.getId() >>> 24) == 0) { if (view == null || view.getId() == View.NO_ID || (view.getId() >>> 24) == 0) {
return ""; return "";
@@ -59,8 +59,8 @@ final class BatteryBarController {
private final Map<View, OverlayBatteryBar> overlayBatteryBars = new WeakHashMap<>(); private final Map<View, OverlayBatteryBar> overlayBatteryBars = new WeakHashMap<>();
private final Map<View, View.OnLayoutChangeListener> rootLayoutListeners = new WeakHashMap<>(); private final Map<View, View.OnLayoutChangeListener> rootLayoutListeners = new WeakHashMap<>();
private final Set<View> pendingOverlayRetries = Collections.newSetFromMap(new WeakHashMap<>()); private final Set<View> pendingOverlayRetries = Collections.newSetFromMap(new WeakHashMap<>());
private final Map<View, String> lastSignatures = new WeakHashMap<>(); private final Map<View, RenderSignature> lastSignatures = new WeakHashMap<>();
private final Map<View, String> lastOverlaySignatures = new WeakHashMap<>(); private final Map<View, RenderSignature> lastOverlaySignatures = new WeakHashMap<>();
private final Map<View, Integer> lastRootTransformSignatures = new WeakHashMap<>(); private final Map<View, Integer> lastRootTransformSignatures = new WeakHashMap<>();
private int systemBarAppearance; private int systemBarAppearance;
private int systemBarBehavior; private int systemBarBehavior;
@@ -87,7 +87,7 @@ final class BatteryBarController {
installRootTransformHooks(); installRootTransformHooks();
installSystemBarAttributeListener(classLoader); installSystemBarAttributeListener(classLoader);
installTransientBarListener(classLoader); installTransientBarListener(classLoader);
runtimeContext.getModeStateRepository().addSceneListener((previous, current) -> refreshAllRoots()); runtimeContext.getModeStateRepository().addSceneListener((previous, current) -> postRefreshAllRoots());
hooksInstalled = true; hooksInstalled = true;
} }
@@ -149,14 +149,6 @@ final class BatteryBarController {
} }
return result; return result;
}); });
XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setY", chain -> {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof View root && roots.contains(root)) {
applyBatteryBarAfterRootTransform(root);
}
return result;
});
XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setAlpha", chain -> { XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setAlpha", chain -> {
Object target = chain.getThisObject(); Object target = chain.getThisObject();
Object alphaArg = chain.getArgs() != null && !chain.getArgs().isEmpty() Object alphaArg = chain.getArgs() != null && !chain.getArgs().isEmpty()
@@ -374,10 +366,6 @@ final class BatteryBarController {
} }
private void applyBatteryBar(View root) { private void applyBatteryBar(View root) {
applyBatteryBarInternal(root);
}
private void applyBatteryBarInternal(View root) {
if (!(root instanceof ViewGroup rootGroup)) { if (!(root instanceof ViewGroup rootGroup)) {
removeBatteryBarView(root); removeBatteryBarView(root);
return; return;
@@ -414,15 +402,15 @@ final class BatteryBarController {
return; return;
} }
removeOverlayBatteryBar(root); removeOverlayBatteryBar(root);
String signature = buildSignature(root, settings, color, geometry, scenario); RenderSignature signature = RenderSignature.of(
root, scenario, batteryLevel, plugged, color, geometry, settings);
BatteryBarView existingView = batteryBarViews.get(root); BatteryBarView existingView = batteryBarViews.get(root);
String lastSignature = lastSignatures.get(root); RenderSignature lastSignature = lastSignatures.get(root);
if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) { if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) {
existingView.bringToFront(); existingView.bringToFront();
return; return;
} }
applyEmbeddedBatteryBar(rootGroup, settings, geometry, scenario, color); applyEmbeddedBatteryBar(rootGroup, settings, geometry, scenario, color);
lastSignatures.put(root, signature);
} }
private void applyEmbeddedBatteryBar( private void applyEmbeddedBatteryBar(
@@ -437,7 +425,9 @@ final class BatteryBarController {
barView.update(rootGroup, rootGroup, settings, geometry, batteryLevel, plugged, color); barView.update(rootGroup, rootGroup, settings, geometry, batteryLevel, plugged, color);
barView.bringToFront(); barView.bringToFront();
barView.invalidate(); barView.invalidate();
lastSignatures.put(rootGroup, buildSignature(rootGroup, settings, color, geometry, scenario)); lastSignatures.put(
rootGroup,
RenderSignature.of(rootGroup, scenario, batteryLevel, plugged, color, geometry, settings));
} }
private void applyOverlayBatteryBar( private void applyOverlayBatteryBar(
@@ -447,7 +437,8 @@ final class BatteryBarController {
BatteryBarGeometry.Scenario scenario, BatteryBarGeometry.Scenario scenario,
int color int color
) { ) {
String signature = buildSignature(root, settings, color, geometry, scenario); RenderSignature signature = RenderSignature.of(
root, scenario, batteryLevel, plugged, color, geometry, settings);
OverlayBatteryBar overlay = overlayBatteryBars.get(root); OverlayBatteryBar overlay = overlayBatteryBars.get(root);
if (overlay == null) { if (overlay == null) {
overlay = new OverlayBatteryBar(root.getContext()); overlay = new OverlayBatteryBar(root.getContext());
@@ -534,38 +525,19 @@ final class BatteryBarController {
); );
} }
private String buildSignature(
View root,
SbtSettings settings,
int color,
BatteryBarGeometry geometry,
BatteryBarGeometry.Scenario scenario
) {
return root.getWidth() + "|"
+ root.getHeight() + "|"
+ scenario + "|"
+ batteryLevel + "|"
+ plugged + "|"
+ color + "|"
+ settings.batteryBarEnabled + "|"
+ settings.batteryBarScenarioEnabled + "|"
+ settings.batteryBarPosition + "|"
+ settings.batteryBarAlignment + "|"
+ settings.batteryBarThicknessDp + "|"
+ settings.batteryBarEdgeOffsetDp + "|"
+ settings.batteryBarCurvedGeometryMode + "|"
+ (geometry != null ? geometry.signature() : "") + "|"
+ settings.batteryBarMinLevel + "|"
+ settings.batteryBarMaxLevel + "|"
+ settings.batteryBarDefaultDischargeColor + "|"
+ settings.batteryBarDefaultChargeColor + "|"
+ BatteryBarStyle.encodeThresholds(settings.batteryBarThresholds);
}
private BatteryBarGeometry.Scenario scenarioForRoot(View root) { private BatteryBarGeometry.Scenario scenarioForRoot(View root) {
if (isKeyguardRoot(root)) { if (isKeyguardRoot(root)) {
return BatteryBarGeometry.Scenario.LOCKSCREEN; return BatteryBarGeometry.Scenario.LOCKSCREEN;
} }
SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene();
if (isPhoneRoot(root)
&& scene != null
&& scene.isLockscreen()
&& isKeyguardLocked(root.getContext())
&& !isStatusBarSurfaceHidden(root)
&& !isFullscreenSystemBarState()) {
return BatteryBarGeometry.Scenario.LOCKSCREEN;
}
boolean fullscreen = isPhoneRoot(root) boolean fullscreen = isPhoneRoot(root)
&& (isUnlockedScene() || isLockedPhoneFullscreenRoot(root)) && (isUnlockedScene() || isLockedPhoneFullscreenRoot(root))
&& (isStatusBarSurfaceHidden(root) || isFullscreenSystemBarState()); && (isStatusBarSurfaceHidden(root) || isFullscreenSystemBarState());
@@ -623,10 +595,14 @@ final class BatteryBarController {
return scene == null || !scene.isUnlocked(); return scene == null || !scene.isUnlocked();
} }
if (isPhoneRoot(root)) { if (isPhoneRoot(root)) {
if (isLockedPhoneStatusBarRoot(root)) { if (scene == null || scene.isUnlocked()) {
return isLockedPhoneFullscreenRoot(root); return true;
} }
return scene == null || scene.isUnlocked() || isLockedPhoneFullscreenRoot(root); if (isLockedPhoneStatusBarRoot(root)) {
return isLockedPhoneFullscreenRoot(root)
|| (!isStatusBarSurfaceHidden(root) && !isFullscreenSystemBarState());
}
return false;
} }
return true; return true;
} }
@@ -695,7 +671,15 @@ final class BatteryBarController {
} }
private boolean shouldRenderVisibleFullscreenBar(View root) { private boolean shouldRenderVisibleFullscreenBar(View root) {
return root != null && isFullscreenSystemBarState() && statusBarTransientShown; return root != null
&& isFullscreenSystemBarState()
&& statusBarTransientShown
&& isStatusBarWindowVisible(root);
}
private boolean isStatusBarWindowVisible(View root) {
WindowInsets insets = root.getRootWindowInsets();
return insets == null || insets.isVisible(WindowInsets.Type.statusBars());
} }
private void updateTransientBarState(java.util.List<Object> args, boolean visible) { private void updateTransientBarState(java.util.List<Object> args, boolean visible) {
@@ -733,13 +717,7 @@ final class BatteryBarController {
} }
private OverlayBounds fullscreenOverlayBounds(View root) { private OverlayBounds fullscreenOverlayBounds(View root) {
int width = root.getWidth(); return new OverlayBounds(root.getWidth(), root.getHeight());
int height = root.getHeight();
Rect bounds = currentWindowBounds(root.getContext());
if (bounds.width() > 0) {
width = bounds.width();
}
return new OverlayBounds(width, height);
} }
private static Rect currentWindowBounds(Context context) { private static Rect currentWindowBounds(Context context) {
@@ -761,6 +739,47 @@ final class BatteryBarController {
private record OverlayBounds(int width, int height) { private record OverlayBounds(int width, int height) {
} }
private record RenderSignature(
int width,
int height,
BatteryBarGeometry.Scenario scenario,
int batteryLevel,
boolean plugged,
int color,
String position,
String alignment,
int thicknessDp,
int edgeOffsetDp,
String curvedGeometryMode,
int minLevel,
int maxLevel
) {
static RenderSignature of(
View root,
BatteryBarGeometry.Scenario scenario,
int batteryLevel,
boolean plugged,
int color,
BatteryBarGeometry geometry,
SbtSettings settings
) {
return new RenderSignature(
root.getWidth(),
root.getHeight(),
scenario,
batteryLevel,
plugged,
color,
geometry.position,
geometry.alignment,
geometry.thicknessDp,
geometry.edgeOffsetDp,
geometry.curvedGeometryMode,
settings.batteryBarMinLevel,
settings.batteryBarMaxLevel);
}
}
private void updateSystemBarState(java.util.List<Object> args) { private void updateSystemBarState(java.util.List<Object> args) {
if (args == null || args.size() < 2) { if (args == null || args.size() < 2) {
return; return;
@@ -104,6 +104,10 @@ final class DrawerClockTextController {
untrackDisabledRoles(clockEnabled, dateEnabled); untrackDisabledRoles(clockEnabled, dateEnabled);
SurfaceMode surfaceMode = scene != null ? scene.surfaceMode() : null; SurfaceMode surfaceMode = scene != null ? scene.surfaceMode() : null;
if (!root.isLayoutRequested()
&& hasAttachedCachedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) {
return;
}
if (hasValidTrackedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) { if (hasValidTrackedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) {
return; return;
} }
@@ -200,6 +204,28 @@ final class DrawerClockTextController {
return false; return false;
} }
private boolean hasAttachedCachedCandidates(
View root,
boolean clockEnabled,
boolean dateEnabled,
SurfaceMode surfaceMode
) {
CandidateCache cache = candidateCacheByRoot.get(root);
if (cache == null || cache.surfaceMode != surfaceMode) {
return false;
}
return (!clockEnabled || isAttachedCachedCandidate(cache.clockView, Role.CLOCK))
&& (!dateEnabled || isAttachedCachedCandidate(cache.dateView, Role.DATE));
}
private boolean isAttachedCachedCandidate(WeakReference<TextView> reference, Role role) {
TextView textView = reference != null ? reference.get() : null;
return textView != null
&& textView.isAttachedToWindow()
&& trackedRoles.get(textView) == role
&& ownedTexts.contains(textView);
}
private void track(TextView textView, Role role) { private void track(TextView textView, Role role) {
if (textView == null || role == null) { if (textView == null || role == null) {
return; return;
@@ -1,43 +0,0 @@
package se.ajpanton.statusbartweak.runtime.hooks;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Replacement for legacy Xposed additional-instance-field helpers.
*/
public final class InstanceStateStore {
private final WeakHashMap<Object, Map<String, Object>> state = new WeakHashMap<>();
public synchronized Object get(Object target, String key) {
if (target == null || key == null) {
return null;
}
Map<String, Object> fields = state.get(target);
return fields != null ? fields.get(key) : null;
}
public synchronized void put(Object target, String key, Object value) {
if (target == null || key == null) {
return;
}
Map<String, Object> fields = state.computeIfAbsent(target, ignored -> new HashMap<>());
fields.put(key, value);
}
public synchronized void remove(Object target, String key) {
if (target == null || key == null) {
return;
}
Map<String, Object> fields = state.get(target);
if (fields == null) {
return;
}
fields.remove(key);
if (fields.isEmpty()) {
state.remove(target);
}
}
}
@@ -17,6 +17,7 @@ import se.ajpanton.statusbartweak.runtime.render.StatusBarTintTarget;
final class DarkIconDispatcherGuard { final class DarkIconDispatcherGuard {
private final RuntimeContext runtimeContext; private final RuntimeContext runtimeContext;
private final BooleanSupplier lockscreenSceneSupplier; private final BooleanSupplier lockscreenSceneSupplier;
private final BooleanSupplier lockscreenDarkIconsSupplier;
private final Runnable refreshAllRoots; private final Runnable refreshAllRoots;
private final Set<Object> dispatchers = private final Set<Object> dispatchers =
Collections.newSetFromMap(new WeakHashMap<>()); Collections.newSetFromMap(new WeakHashMap<>());
@@ -25,10 +26,12 @@ final class DarkIconDispatcherGuard {
DarkIconDispatcherGuard( DarkIconDispatcherGuard(
RuntimeContext runtimeContext, RuntimeContext runtimeContext,
BooleanSupplier lockscreenSceneSupplier, BooleanSupplier lockscreenSceneSupplier,
BooleanSupplier lockscreenDarkIconsSupplier,
Runnable refreshAllRoots Runnable refreshAllRoots
) { ) {
this.runtimeContext = runtimeContext; this.runtimeContext = runtimeContext;
this.lockscreenSceneSupplier = lockscreenSceneSupplier; this.lockscreenSceneSupplier = lockscreenSceneSupplier;
this.lockscreenDarkIconsSupplier = lockscreenDarkIconsSupplier;
this.refreshAllRoots = refreshAllRoots; this.refreshAllRoots = refreshAllRoots;
} }
@@ -48,7 +51,7 @@ final class DarkIconDispatcherGuard {
chain -> { chain -> {
Object dispatcher = chain.getThisObject(); Object dispatcher = chain.getThisObject();
track(dispatcher); track(dispatcher);
if (isLockscreenScene()) { if (isLockscreenScene() && !shouldUseDarkLockscreenIcons()) {
forceLightState(dispatcher); forceLightState(dispatcher);
} }
Object result = chain.proceed(); Object result = chain.proceed();
@@ -63,7 +66,11 @@ final class DarkIconDispatcherGuard {
return; return;
} }
for (Object dispatcher : new ArrayList<>(dispatchers)) { for (Object dispatcher : new ArrayList<>(dispatchers)) {
if (shouldUseDarkLockscreenIcons()) {
restoreState(dispatcher, forcedStates.remove(dispatcher));
} else {
forceLightState(dispatcher); forceLightState(dispatcher);
}
ReflectionSupport.invokeMethod(dispatcher, "applyIconTint"); ReflectionSupport.invokeMethod(dispatcher, "applyIconTint");
} }
} }
@@ -85,6 +92,10 @@ final class DarkIconDispatcherGuard {
return lockscreenSceneSupplier.getAsBoolean(); return lockscreenSceneSupplier.getAsBoolean();
} }
private boolean shouldUseDarkLockscreenIcons() {
return lockscreenDarkIconsSupplier.getAsBoolean();
}
private void track(Object dispatcher) { private void track(Object dispatcher) {
if (dispatcher != null) { if (dispatcher != null) {
dispatchers.add(dispatcher); dispatchers.add(dispatcher);
@@ -92,6 +103,10 @@ final class DarkIconDispatcherGuard {
} }
private void updateTintTarget(Object dispatcher) { private void updateTintTarget(Object dispatcher) {
if (isLockscreenScene() && shouldUseDarkLockscreenIcons()) {
StatusBarTintTarget.setDarkIcons(true);
return;
}
Object intensity = ReflectionSupport.getFieldValue(dispatcher, "mDarkIntensity"); Object intensity = ReflectionSupport.getFieldValue(dispatcher, "mDarkIntensity");
if (intensity instanceof Number number) { if (intensity instanceof Number number) {
StatusBarTintTarget.setDarkIcons(number.floatValue() > 0.5f); StatusBarTintTarget.setDarkIcons(number.floatValue() > 0.5f);
@@ -1,5 +1,6 @@
package se.ajpanton.statusbartweak.runtime.layout; package se.ajpanton.statusbartweak.runtime.layout;
import android.annotation.SuppressLint;
import android.app.Notification; import android.app.Notification;
import android.app.KeyguardManager; import android.app.KeyguardManager;
import android.content.Context; import android.content.Context;
@@ -10,7 +11,6 @@ import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.os.PowerManager; import android.os.PowerManager;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets; import android.view.WindowInsets;
@@ -53,6 +53,7 @@ import se.ajpanton.statusbartweak.runtime.render.LockscreenCardsNotificationStri
import se.ajpanton.statusbartweak.runtime.notifications.NotificationActiveKeyStore; import se.ajpanton.statusbartweak.runtime.notifications.NotificationActiveKeyStore;
import se.ajpanton.statusbartweak.runtime.notifications.NotificationUnreadTracker; import se.ajpanton.statusbartweak.runtime.notifications.NotificationUnreadTracker;
import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager; import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager;
import se.ajpanton.statusbartweak.runtime.render.StatusChipCallSupport;
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController; import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController;
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult; import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult;
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
@@ -80,6 +81,7 @@ final class StockLayoutCanvasController {
"mEntries", "mEntries",
"entries" "entries"
}; };
private static final int APPEARANCE_LIGHT_STATUS_BARS = 8;
private static final Set<String> TARGET_ID_NAMES = Set.of( private static final Set<String> TARGET_ID_NAMES = Set.of(
"clock", "clock",
"left_clock_container", "left_clock_container",
@@ -198,6 +200,7 @@ final class StockLayoutCanvasController {
private int notificationDrawerCloseGeneration; private int notificationDrawerCloseGeneration;
private boolean notificationDrawerClosePending; private boolean notificationDrawerClosePending;
private int aodVisualAlphaGeneration; private int aodVisualAlphaGeneration;
private int systemBarAppearance;
private int systemBarRequestedVisibleTypes = WindowInsets.Type.statusBars(); private int systemBarRequestedVisibleTypes = WindowInsets.Type.statusBars();
private boolean statusBarTransientShown; private boolean statusBarTransientShown;
@@ -207,6 +210,7 @@ final class StockLayoutCanvasController {
darkIconDispatcherGuard = new DarkIconDispatcherGuard( darkIconDispatcherGuard = new DarkIconDispatcherGuard(
runtimeContext, runtimeContext,
this::isLockscreenScene, this::isLockscreenScene,
this::shouldUseDarkLockscreenStatusbarTint,
this::scheduleUnlockedAppearanceRefreshForAllRoots); this::scheduleUnlockedAppearanceRefreshForAllRoots);
lockscreenCardsNotificationStripRenderer = lockscreenCardsNotificationStripRenderer =
new LockscreenCardsNotificationStripRenderer(); new LockscreenCardsNotificationStripRenderer();
@@ -247,29 +251,39 @@ final class StockLayoutCanvasController {
"onSystemBarAttributesChanged", "onSystemBarAttributesChanged",
chain -> { chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
updateSystemBarRequestedVisibleTypes(chain.getArgs()); updateSystemBarAttributes(chain.getArgs());
return result; return result;
}); });
} }
private void updateSystemBarRequestedVisibleTypes(List<Object> args) { private void updateSystemBarAttributes(List<Object> args) {
if (args == null || args.size() <= 5) { if (args == null || args.size() < 2) {
return; return;
} }
Integer displayId = asInteger(args.get(0)); Integer displayId = asInteger(args.get(0));
Integer requestedVisibleTypes = asInteger(args.get(5)); Integer appearance = asInteger(args.get(1));
if (displayId == null || displayId != 0 || requestedVisibleTypes == null) { Integer requestedVisibleTypes = args.size() > 5 ? asInteger(args.get(5)) : null;
return; if (displayId == null || displayId != 0 || appearance == null) {
}
if (systemBarRequestedVisibleTypes == requestedVisibleTypes) {
return; return;
} }
boolean appearanceChanged = systemBarAppearance != appearance;
systemBarAppearance = appearance;
if (requestedVisibleTypes != null
&& systemBarRequestedVisibleTypes != requestedVisibleTypes) {
systemBarRequestedVisibleTypes = requestedVisibleTypes; systemBarRequestedVisibleTypes = requestedVisibleTypes;
if (statusBarsRequestedVisible()) { if (statusBarsRequestedVisible()) {
releaseSuppressedHiddenStatusBarRoots(); releaseSuppressedHiddenStatusBarRoots();
privacyOverlayHiddenStatusBarRoots.clear(); privacyOverlayHiddenStatusBarRoots.clear();
} }
} }
if (appearanceChanged) {
darkIconDispatcherGuard.forceLightStateForAll();
requestUnlockedLayoutRefreshForAllRoots();
} else if (statusBarsRequestedVisible()) {
releaseSuppressedHiddenStatusBarRoots();
privacyOverlayHiddenStatusBarRoots.clear();
}
}
private void installTransientBarListener(ClassLoader classLoader) { private void installTransientBarListener(ClassLoader classLoader) {
Class<?> commandQueueClass = ReflectionSupport.findClassIfExists( Class<?> commandQueueClass = ReflectionSupport.findClassIfExists(
@@ -1807,9 +1821,18 @@ final class StockLayoutCanvasController {
return; return;
} }
dirtyUnlockedLayoutRoots.add(root); dirtyUnlockedLayoutRoots.add(root);
Runnable refresh = () -> {
if (root.isAttachedToWindow()) {
root.requestLayout(); root.requestLayout();
root.invalidate(); root.invalidate();
} }
};
if (isViewThread(root)) {
refresh.run();
} else if (root.getHandler() != null) {
root.getHandler().post(refresh);
}
}
private View rootForShadeTarget(Object target) { private View rootForShadeTarget(Object target) {
if (!(target instanceof View view)) { if (!(target instanceof View view)) {
@@ -2157,6 +2180,15 @@ final class StockLayoutCanvasController {
return scene != null && scene.isLockscreen(); return scene != null && scene.isLockscreen();
} }
private boolean statusBarTintTargetEnabledForScene(SceneKey scene) {
return scene != null && (scene.isUnlocked() || shouldUseDarkLockscreenStatusbarTint());
}
private boolean shouldUseDarkLockscreenStatusbarTint() {
return isLockscreenScene()
&& (systemBarAppearance & APPEARANCE_LIGHT_STATUS_BARS) != 0;
}
private void scheduleUnlockedAppearanceRefreshForRoot(View root) { private void scheduleUnlockedAppearanceRefreshForRoot(View root) {
scheduleUnlockedAppearanceRefreshForRoot(root, false); scheduleUnlockedAppearanceRefreshForRoot(root, false);
} }
@@ -2282,7 +2314,8 @@ final class StockLayoutCanvasController {
if (unlockedIconRenderController.refreshUnlockedClockText( if (unlockedIconRenderController.refreshUnlockedClockText(
root, root,
unlockedHosts.clockHost, unlockedHosts.clockHost,
activeScene)) { activeScene,
statusBarTintTargetEnabledForScene(activeScene))) {
bringUnlockedHostsAndDebugOverlayToFront(root); bringUnlockedHostsAndDebugOverlayToFront(root);
} }
} }
@@ -2402,6 +2435,16 @@ final class StockLayoutCanvasController {
boolean unlockedScene = activeScene != null && activeScene.isUnlocked(); boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
boolean aodScene = activeScene != null && activeScene.isAod(); boolean aodScene = activeScene != null && activeScene.isAod();
boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root); boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root);
if (canSkipStableDrawOnlyApply(root, activeScene, rootDirty)) {
if (unlockedScene) {
syncUnlockedHostsToStockStatusBar(root, activeScene);
hideTrackedUnlockedStockSurfaces(root);
hideTrackedStatusChipSources(root);
} else {
bringUnlockedHostsAndDebugOverlayToFront(root);
}
return;
}
boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root, activeScene); boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root, activeScene);
boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root); boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root);
boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot; boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot;
@@ -2481,6 +2524,7 @@ final class StockLayoutCanvasController {
unlockedHosts.statusHost, unlockedHosts.statusHost,
unlockedHosts.notificationHost, unlockedHosts.notificationHost,
activeScene, activeScene,
statusBarTintTargetEnabledForScene(activeScene),
rootDirty, rootDirty,
lockedCarrierTextSource()); lockedCarrierTextSource());
boolean shouldShowUnlockedHosts = unlockedScene boolean shouldShowUnlockedHosts = unlockedScene
@@ -2551,6 +2595,35 @@ final class StockLayoutCanvasController {
}); });
} }
private boolean canSkipStableDrawOnlyApply(
View root,
SceneKey scene,
boolean rootDirty
) {
if (root == null
|| scene == null
|| (!scene.isUnlocked() && !scene.isLockscreen())
|| root.isLayoutRequested()) {
return false;
}
if (rootDirty) {
return false;
}
if (!hasDirectUnlockedLayoutHost(root)) {
return false;
}
if (!unlockedIconRenderController.hasLayoutPlan(root)) {
return false;
}
if (isKnownRootGeometryChanged(root)) {
return false;
}
if (scene.isUnlocked() && !hasTrackedUnlockedStockSurfaces(root)) {
return false;
}
return true;
}
private void hideTrackedUnlockedStockSurfaces(View root) { private void hideTrackedUnlockedStockSurfaces(View root) {
if (root == null || trackedUnlockedStockSurfaces.isEmpty()) { if (root == null || trackedUnlockedStockSurfaces.isEmpty()) {
return; return;
@@ -2737,8 +2810,9 @@ final class StockLayoutCanvasController {
staleViews.add(view); staleViews.add(view);
continue; continue;
} }
if (!isTrackableStatusChipSurface(view) boolean trackable = isTrackableStatusChipSurface(view);
&& !isTrackedHiddenStatusChipSource(root, view)) { boolean retainedHidden = !trackable && isTrackedHiddenStatusChipSource(root, view);
if (!trackable && !retainedHidden) {
forgetTrackedStatusChipState(view); forgetTrackedStatusChipState(view);
staleViews.add(view); staleViews.add(view);
continue; continue;
@@ -3290,7 +3364,7 @@ final class StockLayoutCanvasController {
return visibility != View.VISIBLE return visibility != View.VISIBLE
&& isTrackedStatusChipWriteTarget(view) && isTrackedStatusChipWriteTarget(view)
&& hasVisibleStatusChipContent(view) && hasVisibleStatusChipContent(view)
&& !isCallStatusChip(view); && !StatusChipCallSupport.isCallChip(view);
} }
private boolean isTrackedStatusChipWriteTarget(View view) { private boolean isTrackedStatusChipWriteTarget(View view) {
@@ -3454,8 +3528,13 @@ final class StockLayoutCanvasController {
boolean shelfSurface = isCardsNotificationIconSurface(view); boolean shelfSurface = isCardsNotificationIconSurface(view);
boolean shelfBackgroundSurface = isCardsNotificationShelfBackgroundSurface(view); boolean shelfBackgroundSurface = isCardsNotificationShelfBackgroundSurface(view);
boolean shadeIconOnlySurface = "keyguard_icononly_container_view".equals(idName); boolean shadeIconOnlySurface = "keyguard_icononly_container_view".equals(idName);
boolean clockSurface = "clock".equals(idName)
|| "left_clock_container".equals(idName)
|| viewClassName.contains("QSClockIndicatorView")
|| viewClassName.contains("statusbar.policy.Clock");
if (!("system_icons".equals(idName) if (!("system_icons".equals(idName)
|| carrierSurface || carrierSurface
|| clockSurface
|| "system_icon_area".equals(idName) || "system_icon_area".equals(idName)
|| "statusIcons".equals(idName) || "statusIcons".equals(idName)
|| "battery".equals(idName) || "battery".equals(idName)
@@ -3788,6 +3867,9 @@ final class StockLayoutCanvasController {
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) { if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
return false; return false;
} }
if (StatusChipCallSupport.isInactiveCallChip(view)) {
return false;
}
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
if (width <= 0 || height <= 0) { if (width <= 0 || height <= 0) {
@@ -3824,6 +3906,9 @@ final class StockLayoutCanvasController {
if (view.getVisibility() != View.VISIBLE) { if (view.getVisibility() != View.VISIBLE) {
return false; return false;
} }
if (StatusChipCallSupport.isInactiveCallChip(view)) {
return false;
}
String idName = ViewIdNames.idName(view); String idName = ViewIdNames.idName(view);
if (!"ongoing_activity_capsule".equals(idName)) { if (!"ongoing_activity_capsule".equals(idName)) {
return false; return false;
@@ -3852,26 +3937,12 @@ final class StockLayoutCanvasController {
|| !isStatusChipCandidate(view)) { || !isStatusChipCandidate(view)) {
return false; return false;
} }
if (isCallStatusChip(view) if (StatusChipCallSupport.isInactiveCallChip(view)) {
&& view.getVisibility() != View.VISIBLE
&& !isPhoneCallActive(view)) {
return false; return false;
} }
return hasVisibleStatusChipContent(view); return hasVisibleStatusChipContent(view);
} }
private boolean isPhoneCallActive(View view) {
if (view == null || view.getContext() == null) {
return false;
}
try {
TelecomManager telecomManager = view.getContext().getSystemService(TelecomManager.class);
return telecomManager != null && telecomManager.isInCall();
} catch (RuntimeException ignored) {
return false;
}
}
private boolean hasVisibleStatusChipContent(View source) { private boolean hasVisibleStatusChipContent(View source) {
if (!(source instanceof ViewGroup group)) { if (!(source instanceof ViewGroup group)) {
return false; return false;
@@ -3901,42 +3972,6 @@ final class StockLayoutCanvasController {
return false; return false;
} }
private boolean isCallStatusChip(View source) {
if (source == null) {
return false;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(source);
int scanned = 0;
while (!queue.isEmpty() && scanned++ < 48) {
View view = queue.removeFirst();
String className = view.getClass().getName().toLowerCase(Locale.ROOT);
String idName = ViewIdNames.idName(view).toLowerCase(Locale.ROOT);
if (className.contains("call") || idName.contains("call")) {
return true;
}
if (containsCallToken(view.getContentDescription())) {
return true;
}
if (view instanceof TextView textView && containsCallToken(textView.getText())) {
return true;
}
if (view instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
queue.addLast(childGroup.getChildAt(i));
}
}
}
return false;
}
private boolean containsCallToken(CharSequence value) {
if (value == null || value.length() == 0) {
return false;
}
String text = value.toString().toLowerCase(Locale.ROOT);
return text.contains("call") || text.contains("phone");
}
private boolean isStatusChipCandidate(View view) { private boolean isStatusChipCandidate(View view) {
String className = view.getClass().getName().toLowerCase(Locale.ROOT); String className = view.getClass().getName().toLowerCase(Locale.ROOT);
@@ -4877,7 +4912,8 @@ final class StockLayoutCanvasController {
lockscreenCardsNotificationRenderGenerationByRoot.remove(root); lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
return; return;
} }
if (runtimeContext.getModeStateRepository().isSystemUiShadeLocked()) { if (runtimeContext.getModeStateRepository().isSystemUiShadeLocked()
|| hasVisibleLockscreenShadeHeader(root)) {
removeLockscreenCardsNotificationHost(root); removeLockscreenCardsNotificationHost(root);
lockscreenCardsNotificationRenderGenerationByRoot.remove(root); lockscreenCardsNotificationRenderGenerationByRoot.remove(root);
return; return;
@@ -4917,6 +4953,27 @@ final class StockLayoutCanvasController {
} }
} }
private boolean hasVisibleLockscreenShadeHeader(View root) {
if (root == null || !isNotificationShadeWindowRoot(root)) {
return false;
}
final boolean[] found = {false};
walkTree(root, view -> {
if (found[0] || !(view instanceof TextView textView)) {
return;
}
if (!textView.isShown() || textView.getAlpha() <= 0f) {
return;
}
String idName = ViewIdNames.idName(textView);
String className = textView.getClass().getName();
if ("header_clock".equals(idName) || className.contains("QSClockHeaderView")) {
found[0] = true;
}
});
return found[0];
}
private void renderAodCardsNotificationStripIfNeeded( private void renderAodCardsNotificationStripIfNeeded(
View root, View root,
SbtSettings settings, SbtSettings settings,
@@ -0,0 +1,130 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Spannable;
import android.text.style.ForegroundColorSpan;
import java.util.ArrayList;
import androidx.appcompat.widget.AppCompatTextView;
/** Draws the outline with one colour, then restores the original span colours for the text fill. */
final class ClockOutlineTextView extends AppCompatTextView {
private int sourcePaddingLeft;
private int sourcePaddingTop;
private int sourcePaddingRight;
private int sourcePaddingBottom;
private int outlineColor;
private float outlineThicknessPx;
private ForegroundColorSpan outlineSpan;
ClockOutlineTextView(Context context) {
super(context);
}
boolean setSourcePadding(int left, int top, int right, int bottom) {
if (sourcePaddingLeft == left
&& sourcePaddingTop == top
&& sourcePaddingRight == right
&& sourcePaddingBottom == bottom) {
return false;
}
sourcePaddingLeft = left;
sourcePaddingTop = top;
sourcePaddingRight = right;
sourcePaddingBottom = bottom;
applyPaddedInsets();
return true;
}
boolean setOutline(ClockOutlineSpec outline) {
int color = outline != null ? outline.color : 0;
float thickness = outline != null ? outline.thicknessPx : 0f;
if (outlineColor == color && outlineThicknessPx == thickness) {
return false;
}
outlineColor = color;
outlineThicknessPx = thickness;
outlineSpan = new ForegroundColorSpan(color);
applyPaddedInsets();
return true;
}
int outlineInsetPx() {
return enabled() ? (int) Math.ceil(outlineThicknessPx) : 0;
}
@Override
protected void onDraw(Canvas canvas) {
if (!enabled()) {
super.onDraw(canvas);
return;
}
Paint paint = getPaint();
Paint.Style style = paint.getStyle();
float strokeWidth = paint.getStrokeWidth();
int color = paint.getColor();
ArrayList<SpanState> foregrounds = replaceForegroundColors();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(outlineThicknessPx * 2f);
paint.setColor(outlineColor);
try {
super.onDraw(canvas);
} finally {
restoreForegroundColors(foregrounds);
paint.setStyle(style);
paint.setStrokeWidth(strokeWidth);
paint.setColor(color);
}
super.onDraw(canvas);
}
private ArrayList<SpanState> replaceForegroundColors() {
if (!(getText() instanceof Spannable text) || text.length() == 0) {
return null;
}
ForegroundColorSpan[] spans = text.getSpans(0, text.length(), ForegroundColorSpan.class);
ArrayList<SpanState> states = new ArrayList<>(spans.length);
for (ForegroundColorSpan span : spans) {
states.add(new SpanState(
span,
text.getSpanStart(span),
text.getSpanEnd(span),
text.getSpanFlags(span)));
text.removeSpan(span);
}
text.setSpan(outlineSpan, 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return states;
}
private void restoreForegroundColors(ArrayList<SpanState> states) {
if (!(getText() instanceof Spannable text) || outlineSpan == null) {
return;
}
text.removeSpan(outlineSpan);
if (states == null) {
return;
}
for (SpanState state : states) {
text.setSpan(state.span, state.start, state.end, state.flags);
}
}
private boolean enabled() {
return outlineThicknessPx > 0f && ((outlineColor >>> 24) & 0xff) > 0;
}
private void applyPaddedInsets() {
int inset = outlineInsetPx();
setPadding(
sourcePaddingLeft + inset,
sourcePaddingTop + inset,
sourcePaddingRight + inset,
sourcePaddingBottom + inset);
}
private record SpanState(ForegroundColorSpan span, int start, int end, int flags) {
}
}
@@ -1,7 +1,7 @@
package se.ajpanton.statusbartweak.runtime.render; package se.ajpanton.statusbartweak.runtime.render;
import android.text.TextUtils;
import android.text.Spanned; import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -19,6 +19,10 @@ final class ClockProxyRenderer {
private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>(); private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>();
void render(ViewGroup host, ClockPlacement placement) { void render(ViewGroup host, ClockPlacement placement) {
render(host, placement, ClockOutlineSpec.NONE);
}
void render(ViewGroup host, ClockPlacement placement, ClockOutlineSpec outline) {
if (host == null || placement == null || placement.rows.isEmpty()) { if (host == null || placement == null || placement.rows.isEmpty()) {
clear(host); clear(host);
return; return;
@@ -36,13 +40,8 @@ final class ClockProxyRenderer {
views.clear(); views.clear();
hostChanged = true; hostChanged = true;
} }
while (views.size() > placement.rows.size()) {
TextView view = views.remove(views.size() - 1);
detachFromParent(view);
hostChanged = true;
}
while (views.size() < placement.rows.size()) { while (views.size() < placement.rows.size()) {
TextView view = new TextView(host.getContext()); TextView view = new ClockOutlineTextView(host.getContext());
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
views.add(view); views.add(view);
host.addView(view); host.addView(view);
@@ -50,10 +49,13 @@ final class ClockProxyRenderer {
} }
for (int i = 0; i < placement.rows.size(); i++) { for (int i = 0; i < placement.rows.size(); i++) {
ClockRow row = placement.rows.get(i); ClockRow row = placement.rows.get(i);
TextView view = views.get(i); ClockOutlineTextView view = (ClockOutlineTextView) views.get(i);
if (copyTextStyleIfChanged(placement.source, view, placement.textSizePx)) { if (copyTextStyleIfChanged(placement.source, view, placement.textSizePx)) {
hostChanged = true; hostChanged = true;
} }
if (view.setOutline(outline)) {
hostChanged = true;
}
if (applyTextColorOverrideIfNeeded(view, placement.textColorOverride)) { if (applyTextColorOverrideIfNeeded(view, placement.textColorOverride)) {
hostChanged = true; hostChanged = true;
} }
@@ -61,7 +63,7 @@ final class ClockProxyRenderer {
hostChanged = true; hostChanged = true;
} }
if (!sameTextAndColorSpans(view.getText(), row.text)) { if (!sameTextAndColorSpans(view.getText(), row.text)) {
view.setText(row.text); view.setText(row.text, TextView.BufferType.SPANNABLE);
hostChanged = true; hostChanged = true;
} }
if (!Objects.equals(view.getContentDescription(), placement.contentDescription)) { if (!Objects.equals(view.getContentDescription(), placement.contentDescription)) {
@@ -76,21 +78,24 @@ final class ClockProxyRenderer {
view.setAlpha(1f); view.setAlpha(1f);
hostChanged = true; hostChanged = true;
} }
int left = placement.bounds.left + row.x; int inset = view.outlineInsetPx();
int top = placement.bounds.top + row.y; int left = placement.bounds.left + row.x - inset;
int top = placement.bounds.top + row.y - inset;
int width = row.width + 2 * inset;
int height = row.height + 2 * inset;
int scrollX = Math.max(0, row.clipLeft); int scrollX = Math.max(0, row.clipLeft);
if (view.getScrollX() != scrollX) { if (view.getScrollX() != scrollX) {
view.setScrollX(scrollX); view.setScrollX(scrollX);
hostChanged = true; hostChanged = true;
} }
if (applyTextLayoutParams(host, view, left, top, row.width, row.height)) { if (applyTextLayoutParams(host, view, left, top, width, height)) {
hostChanged = true; hostChanged = true;
} }
if (view.getLeft() != left if (view.getLeft() != left
|| view.getTop() != top || view.getTop() != top
|| view.getRight() != left + row.width || view.getRight() != left + width
|| view.getBottom() != top + row.height) { || view.getBottom() != top + height) {
view.layout(left, top, left + row.width, top + row.height); view.layout(left, top, left + width, top + height);
hostChanged = true; hostChanged = true;
} }
} }
@@ -211,7 +216,13 @@ final class ClockProxyRenderer {
target.setHorizontallyScrolling(true); target.setHorizontallyScrolling(true);
changed = true; changed = true;
} }
if (target.getPaddingLeft() != source.getPaddingLeft() if (target instanceof ClockOutlineTextView outlineTarget) {
changed |= outlineTarget.setSourcePadding(
source.getPaddingLeft(),
source.getPaddingTop(),
source.getPaddingRight(),
source.getPaddingBottom());
} else if (target.getPaddingLeft() != source.getPaddingLeft()
|| target.getPaddingTop() != source.getPaddingTop() || target.getPaddingTop() != source.getPaddingTop()
|| target.getPaddingRight() != source.getPaddingRight() || target.getPaddingRight() != source.getPaddingRight()
|| target.getPaddingBottom() != source.getPaddingBottom()) { || target.getPaddingBottom() != source.getPaddingBottom()) {
@@ -234,10 +245,7 @@ final class ClockProxyRenderer {
} }
private static boolean applyTextColorOverrideIfNeeded(TextView view, int color) { private static boolean applyTextColorOverrideIfNeeded(TextView view, int color) {
if (view == null || (color >>> 24) == 0) { if (view == null || (color >>> 24) == 0 || view.getCurrentTextColor() == color) {
return false;
}
if (view.getCurrentTextColor() == color) {
return false; return false;
} }
view.setTextColor(color); view.setTextColor(color);
@@ -276,3 +284,15 @@ final class ClockProxyRenderer {
} }
} }
} }
final class ClockOutlineSpec {
static final ClockOutlineSpec NONE = new ClockOutlineSpec(0, 0f);
final int color;
final float thicknessPx;
ClockOutlineSpec(int color, float thicknessPx) {
this.color = color;
this.thicknessPx = thicknessPx;
}
}
@@ -484,4 +484,13 @@ final class LayoutPlan {
notificationPlacements, notificationPlacements,
carrierPlacement); carrierPlacement);
} }
LayoutPlan withCarrierPlacement(ClockPlacement placement) {
return new LayoutPlan(
clockPlacement,
chipPlacements,
statusPlacements,
notificationPlacements,
placement);
}
} }
@@ -0,0 +1,64 @@
package se.ajpanton.statusbartweak.runtime.render;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.telecom.TelecomManager;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayDeque;
import java.util.Locale;
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
/** Shared call-chip detection for source suppression and snapshot collection. */
public final class StatusChipCallSupport {
private StatusChipCallSupport() {
}
public static boolean isInactiveCallChip(View source) {
return isCallChip(source) && !isPhoneCallActive(source);
}
public static boolean isCallChip(View source) {
if (source == null) {
return false;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(source);
int scanned = 0;
while (!queue.isEmpty() && scanned++ < 48) {
View view = queue.removeFirst();
String className = view.getClass().getName().toLowerCase(Locale.ROOT);
String idName = ViewIdNames.idName(view).toLowerCase(Locale.ROOT);
if (className.contains("ongoingcall")
|| className.contains("ongoing_call")
|| idName.contains("ongoing_call")) {
return true;
}
if (view instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
queue.addLast(childGroup.getChildAt(i));
}
}
}
return false;
}
@SuppressLint("MissingPermission")
public static boolean isPhoneCallActive(View view) {
if (view == null || view.getContext() == null
|| view.getContext().checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
!= PackageManager.PERMISSION_GRANTED) {
return false;
}
try {
TelecomManager telecomManager = view.getContext().getSystemService(TelecomManager.class);
return telecomManager != null && telecomManager.isInCall();
} catch (RuntimeException ignored) {
return false;
}
}
}
@@ -12,6 +12,7 @@ import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities; import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory; import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
import se.ajpanton.statusbartweak.runtime.features.clock.ClockColorResolver;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
@@ -29,6 +30,7 @@ public final class UnlockedIconSnapshotRenderController {
private final SnapshotRenderer notificationRenderer; private final SnapshotRenderer notificationRenderer;
private final SnapshotRenderer chipRenderer; private final SnapshotRenderer chipRenderer;
private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer(); private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer();
private final ClockColorResolver clockColorResolver = new ClockColorResolver();
private final ClockProxyRenderer carrierRenderer = new ClockProxyRenderer(); private final ClockProxyRenderer carrierRenderer = new ClockProxyRenderer();
private final UnlockedStockSourceCollector sourceCollector; private final UnlockedStockSourceCollector sourceCollector;
private final TextVerticalOffsetScaler textOffsetScaler = new TextVerticalOffsetScaler(); private final TextVerticalOffsetScaler textOffsetScaler = new TextVerticalOffsetScaler();
@@ -65,6 +67,7 @@ public final class UnlockedIconSnapshotRenderController {
ViewGroup statusHost, ViewGroup statusHost,
ViewGroup notificationHost, ViewGroup notificationHost,
SceneKey scene, SceneKey scene,
boolean statusBarTintTargetEnabled,
boolean forceStockRefresh, boolean forceStockRefresh,
TextView externalCarrierView TextView externalCarrierView
) { ) {
@@ -80,7 +83,7 @@ public final class UnlockedIconSnapshotRenderController {
return new RenderResult(true, true, true); return new RenderResult(true, true, true);
} }
SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
setStatusBarTintTargetEnabled(scene.isUnlocked()); setStatusBarTintTargetEnabled(statusBarTintTargetEnabled);
layoutHostToRoot(root, clockHost); layoutHostToRoot(root, clockHost);
layoutHostToRoot(root, carrierHost); layoutHostToRoot(root, carrierHost);
layoutHostToRoot(root, chipHost); layoutHostToRoot(root, chipHost);
@@ -153,7 +156,7 @@ public final class UnlockedIconSnapshotRenderController {
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock); LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
lastLayoutPlanByRoot.put(root, updatedPlan); lastLayoutPlanByRoot.put(root, updatedPlan);
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry); lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
clockRenderer.render(clockHost, updatedClock); renderClock(clockHost, updatedClock, settings);
return new RenderResult(false, false, false); return new RenderResult(false, false, false);
} }
} }
@@ -180,7 +183,7 @@ public final class UnlockedIconSnapshotRenderController {
updatedClock)) { updatedClock)) {
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock); LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
lastLayoutPlanByRoot.put(root, updatedPlan); lastLayoutPlanByRoot.put(root, updatedPlan);
clockRenderer.render(clockHost, updatedClock); renderClock(clockHost, updatedClock, settings);
return new RenderResult(false, false, false); return new RenderResult(false, false, false);
} }
} }
@@ -235,9 +238,10 @@ public final class UnlockedIconSnapshotRenderController {
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry); lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons); layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons);
layoutPlan = withCurrentClockAppearance(root, layoutPlan, collectedIcons, settings); layoutPlan = withCurrentClockAppearance(root, layoutPlan, collectedIcons, settings);
layoutPlan = withCurrentCarrierAppearance(layoutPlan);
lastLayoutPlanByRoot.put(root, layoutPlan); lastLayoutPlanByRoot.put(root, layoutPlan);
if (clockEnabledForScene(settings, scene) && layoutPlan.clockPlacement != null) { if (clockEnabledForScene(settings, scene) && layoutPlan.clockPlacement != null) {
clockRenderer.render(clockHost, layoutPlan.clockPlacement); renderClock(clockHost, layoutPlan.clockPlacement, settings);
} else { } else {
clockRenderer.clear(clockHost); clockRenderer.clear(clockHost);
} }
@@ -313,7 +317,7 @@ public final class UnlockedIconSnapshotRenderController {
layoutPlan = updatedPlan; layoutPlan = updatedPlan;
} }
if (settings.clockEnabledUnlocked && layoutPlan.clockPlacement != null) { if (settings.clockEnabledUnlocked && layoutPlan.clockPlacement != null) {
clockRenderer.render(clockHost, layoutPlan.clockPlacement); renderClock(clockHost, layoutPlan.clockPlacement, settings);
} }
} }
@@ -347,7 +351,8 @@ public final class UnlockedIconSnapshotRenderController {
public boolean refreshUnlockedClockText( public boolean refreshUnlockedClockText(
View root, View root,
ViewGroup clockHost, ViewGroup clockHost,
SceneKey scene SceneKey scene,
boolean statusBarTintTargetEnabled
) { ) {
if (root == null if (root == null
|| clockHost == null || clockHost == null
@@ -361,7 +366,7 @@ public final class UnlockedIconSnapshotRenderController {
return false; return false;
} }
SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
setStatusBarTintTargetEnabled(scene.isUnlocked()); setStatusBarTintTargetEnabled(statusBarTintTargetEnabled);
if (!clockEnabledForScene(settings, scene)) { if (!clockEnabledForScene(settings, scene)) {
return false; return false;
} }
@@ -378,7 +383,7 @@ public final class UnlockedIconSnapshotRenderController {
LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock); LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock);
lastLayoutPlanByRoot.put(root, updatedPlan); lastLayoutPlanByRoot.put(root, updatedPlan);
lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings)); lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings));
clockRenderer.render(clockHost, updatedClock); renderClock(clockHost, updatedClock, settings);
return true; return true;
} }
@@ -595,6 +600,24 @@ public final class UnlockedIconSnapshotRenderController {
return plan.withClockPlacement(updatedClock); return plan.withClockPlacement(updatedClock);
} }
private LayoutPlan withCurrentCarrierAppearance(LayoutPlan plan) {
int targetColor = statusBarTintTargetColor();
if (plan == null
|| plan.carrierPlacement == null
|| !hasAlpha(targetColor)
|| plan.carrierPlacement.textColorOverride == targetColor) {
return plan;
}
ClockPlacement previous = plan.carrierPlacement;
return plan.withCarrierPlacement(new ClockPlacement(
previous.source,
previous.contentDescription,
previous.rows,
previous.bounds,
targetColor,
previous.textSizePx));
}
private int clockReferenceColor(ClockPlacement placement, CollectedIcons icons) { private int clockReferenceColor(ClockPlacement placement, CollectedIcons icons) {
int targetColor = statusBarTintTargetColor(); int targetColor = statusBarTintTargetColor();
if (hasAlpha(targetColor)) { if (hasAlpha(targetColor)) {
@@ -633,6 +656,51 @@ public final class UnlockedIconSnapshotRenderController {
return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : android.graphics.Color.TRANSPARENT; return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : android.graphics.Color.TRANSPARENT;
} }
private void renderClock(
ViewGroup clockHost,
ClockPlacement placement,
SbtSettings settings
) {
clockRenderer.render(clockHost, placement, clockOutlineSpec(clockHost, placement, settings));
}
private ClockOutlineSpec clockOutlineSpec(
ViewGroup host,
ClockPlacement placement,
SbtSettings settings
) {
if (host == null
|| placement == null
|| settings == null
|| !settings.clockOutlineEnabled
|| settings.clockOutlineThicknessDp <= 0
|| settings.clockOutlineColor == null
|| settings.clockOutlineColor.trim().isEmpty()) {
return ClockOutlineSpec.NONE;
}
int referenceColor = hasAlpha(placement.textColorOverride)
? placement.textColorOverride
: placement.source != null
? placement.source.getCurrentTextColor()
: android.graphics.Color.WHITE;
try {
int color = clockColorResolver.resolveColor(
host.getContext(),
settings.clockOutlineColor,
referenceColor,
android.graphics.Color.TRANSPARENT,
ClockColorResolver.Options.clock());
if (!hasAlpha(color)) {
return ClockOutlineSpec.NONE;
}
return new ClockOutlineSpec(
color,
settings.clockOutlineThicknessDp * host.getResources().getDisplayMetrics().density);
} catch (IllegalArgumentException ignored) {
return ClockOutlineSpec.NONE;
}
}
private boolean hasAlpha(int color) { private boolean hasAlpha(int color) {
return (color >>> 24) != 0; return (color >>> 24) != 0;
} }
@@ -668,7 +736,10 @@ public final class UnlockedIconSnapshotRenderController {
settings.clockShowSeconds, settings.clockShowSeconds,
settings.clockShowDatePrefix, settings.clockShowDatePrefix,
settings.clockCustomFormatEnabled, settings.clockCustomFormatEnabled,
settings.clockCustomFormat); settings.clockCustomFormat,
settings.clockOutlineEnabled,
settings.clockOutlineThicknessDp,
settings.clockOutlineColor);
} }
public void clearAll() { public void clearAll() {
@@ -859,7 +859,7 @@ final class UnlockedLayoutPlanner {
} }
if (style != NotificationDisplayStyle.ICONS if (style != NotificationDisplayStyle.ICONS
&& !(style == NotificationDisplayStyle.UNKNOWN && !(style == NotificationDisplayStyle.UNKNOWN
&& isExperimentalGenericAod(settings))) { && SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings))) {
return 0; return 0;
} }
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0; return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
@@ -873,10 +873,6 @@ final class UnlockedLayoutPlanner {
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT; && scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT;
} }
private boolean isExperimentalGenericAod(SbtSettings settings) {
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
}
private ArrayList<SnapshotPlacement> unreadNotificationPlacements( private ArrayList<SnapshotPlacement> unreadNotificationPlacements(
ArrayList<SnapshotPlacement> placements ArrayList<SnapshotPlacement> placements
) { ) {
@@ -92,7 +92,8 @@ final class UnlockedStockSourceCollector {
LockscreenCardsNotificationIconSourceStore LockscreenCardsNotificationIconSourceStore
.snapshotAodNotificationPackageSources(root)); .snapshotAodNotificationPackageSources(root));
} }
if (notificationIcons.isEmpty() && isExperimentalGenericAod(settings)) { if (notificationIcons.isEmpty()
&& SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings)) {
View notificationRoot = anchors != null && anchors.notificationRoot != null View notificationRoot = anchors != null && anchors.notificationRoot != null
? anchors.notificationRoot ? anchors.notificationRoot
: root; : root;
@@ -146,10 +147,6 @@ final class UnlockedStockSourceCollector {
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings)); && SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings));
} }
private boolean isExperimentalGenericAod(SbtSettings settings) {
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
}
private String reflectString(View view, String methodName, String... fieldNames) { private String reflectString(View view, String methodName, String... fieldNames) {
Object methodValue = ReflectionSupport.invokeMethod(view, methodName); Object methodValue = ReflectionSupport.invokeMethod(view, methodName);
if (methodValue != null) { if (methodValue != null) {
@@ -1223,6 +1220,7 @@ final class UnlockedStockSourceCollector {
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
|| !ViewGeometry.isDescendantOf(view, root) || !ViewGeometry.isDescendantOf(view, root)
|| ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false) || ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)
|| StatusChipCallSupport.isInactiveCallChip(view)
|| !isStatusChipCandidate(view) || !isStatusChipCandidate(view)
|| !hasRenderableStatusChipContent(view)) { || !hasRenderableStatusChipContent(view)) {
return false; return false;
@@ -1248,6 +1246,9 @@ final class UnlockedStockSourceCollector {
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) { if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
return false; return false;
} }
if (StatusChipCallSupport.isInactiveCallChip(view)) {
return false;
}
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
if (width <= 0 || height <= 0 || root == null || width >= root.getWidth() / 2) { if (width <= 0 || height <= 0 || root == null || width >= root.getWidth() / 2) {
@@ -1339,6 +1340,9 @@ final class UnlockedStockSourceCollector {
if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) { if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) {
return false; return false;
} }
if (StatusChipCallSupport.isInactiveCallChip(view)) {
return false;
}
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth(); int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight(); int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) { if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) {
@@ -107,10 +107,6 @@ public final class BatteryBarGeometry {
return position + "|" + alignment + "|" + thicknessDp + "|" + edgeOffsetDp + "|" + curvedGeometryMode; return position + "|" + alignment + "|" + thicknessDp + "|" + edgeOffsetDp + "|" + curvedGeometryMode;
} }
public String signature() {
return encode();
}
public static String enabledKey(Scenario scenario) { public static String enabledKey(Scenario scenario) {
return SbtSettings.batteryBarGeometryOverrideEnabledKey(scenario.id); return SbtSettings.batteryBarGeometryOverrideEnabledKey(scenario.id);
} }
@@ -87,6 +87,11 @@ public final class SbtDefaults {
public static final boolean CLOCK_SHOW_DATE_PREFIX_DEFAULT = false; public static final boolean CLOCK_SHOW_DATE_PREFIX_DEFAULT = false;
public static final boolean CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false; public static final boolean CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
public static final String CLOCK_CUSTOM_FORMAT_DEFAULT = ""; public static final String CLOCK_CUSTOM_FORMAT_DEFAULT = "";
public static final boolean CLOCK_OUTLINE_ENABLED_DEFAULT = false;
public static final int CLOCK_OUTLINE_THICKNESS_DP_DEFAULT = 1;
public static final int CLOCK_OUTLINE_THICKNESS_DP_MIN = 1;
public static final int CLOCK_OUTLINE_THICKNESS_DP_MAX = 6;
public static final String CLOCK_OUTLINE_COLOR_DEFAULT = "#000 ; #FFF";
public static final boolean DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false; public static final boolean DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
public static final String DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT = ""; public static final String DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT = "";
public static final boolean DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT = false; public static final boolean DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
File diff suppressed because it is too large Load Diff
@@ -155,6 +155,15 @@ public class SbtSettingsProvider extends ContentProvider {
prefs.getBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT)); prefs.getBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT));
out.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, out.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
prefs.getString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT)); prefs.getString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT));
out.putBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
prefs.getBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
SbtDefaults.CLOCK_OUTLINE_ENABLED_DEFAULT));
out.putInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
prefs.getInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_DEFAULT));
out.putString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
prefs.getString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
SbtDefaults.CLOCK_OUTLINE_COLOR_DEFAULT));
out.putBoolean(SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED, out.putBoolean(SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
prefs.getBoolean( prefs.getBoolean(
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
@@ -839,14 +839,14 @@ public final class BatteryBarFragment extends Fragment {
final boolean[] updating = new boolean[]{false}; final boolean[] updating = new boolean[]{false};
if (minus != null) { if (minus != null) {
minus.setOnClickListener(v -> { minus.setOnClickListener(v -> {
int current = scenarioNumberValue(input, controls, prefs, edgeOffset); int current = scenarioNumberValue(input, prefs, edgeOffset);
setScenarioNumberValue(input, current - 1, edgeOffset, updating); setScenarioNumberValue(input, current - 1, edgeOffset, updating);
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs)); persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
}); });
} }
if (plus != null) { if (plus != null) {
plus.setOnClickListener(v -> { plus.setOnClickListener(v -> {
int current = scenarioNumberValue(input, controls, prefs, edgeOffset); int current = scenarioNumberValue(input, prefs, edgeOffset);
setScenarioNumberValue(input, current + 1, edgeOffset, updating); setScenarioNumberValue(input, current + 1, edgeOffset, updating);
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs)); persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
}); });
@@ -870,7 +870,7 @@ public final class BatteryBarFragment extends Fragment {
} }
setScenarioNumberValue( setScenarioNumberValue(
input, input,
scenarioNumberValue(input, controls, prefs, edgeOffset), scenarioNumberValue(input, prefs, edgeOffset),
edgeOffset, edgeOffset,
updating); updating);
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs)); persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
@@ -882,7 +882,7 @@ public final class BatteryBarFragment extends Fragment {
} }
setScenarioNumberValue( setScenarioNumberValue(
input, input,
scenarioNumberValue(input, controls, prefs, edgeOffset), scenarioNumberValue(input, prefs, edgeOffset),
edgeOffset, edgeOffset,
updating); updating);
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs)); persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
@@ -891,7 +891,6 @@ public final class BatteryBarFragment extends Fragment {
private int scenarioNumberValue( private int scenarioNumberValue(
EditText input, EditText input,
GeometryControls controls,
SharedPreferences prefs, SharedPreferences prefs,
boolean edgeOffset boolean edgeOffset
) { ) {
@@ -950,8 +949,8 @@ public final class BatteryBarFragment extends Fragment {
return new BatteryBarGeometry( return new BatteryBarGeometry(
positionValue(checkedPosition), positionValue(checkedPosition),
alignment, alignment,
scenarioNumberValue(controls.thicknessInput, controls, prefs, false), scenarioNumberValue(controls.thicknessInput, prefs, false),
scenarioNumberValue(controls.edgeOffsetInput, controls, prefs, true), scenarioNumberValue(controls.edgeOffsetInput, prefs, true),
curvedGeometryValue(checkedPosition)); curvedGeometryValue(checkedPosition));
} }
@@ -40,6 +40,7 @@ import androidx.fragment.app.Fragment;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.card.MaterialCardView; import com.google.android.material.card.MaterialCardView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.slider.Slider;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
@@ -93,6 +94,11 @@ public final class ClockFragment extends Fragment {
EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input); EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input);
ClockPatternPreviewView customFormatPreview = ClockPatternPreviewView customFormatPreview =
root.findViewById(R.id.clock_custom_format_preview); root.findViewById(R.id.clock_custom_format_preview);
CheckBox clockOutlineEnabled = root.findViewById(R.id.clock_outline_enabled);
View clockOutlineContainer = root.findViewById(R.id.clock_outline_container);
Slider clockOutlineThickness = root.findViewById(R.id.clock_outline_thickness);
TextView clockOutlineThicknessLabel = root.findViewById(R.id.clock_outline_thickness_value);
EditText clockOutlineColor = root.findViewById(R.id.clock_outline_color);
MaterialCardView drawerClockCustomFormatCard = MaterialCardView drawerClockCustomFormatCard =
root.findViewById(R.id.drawer_clock_custom_format_card); root.findViewById(R.id.drawer_clock_custom_format_card);
CheckBox drawerClockCustomFormatEnabled = CheckBox drawerClockCustomFormatEnabled =
@@ -129,6 +135,12 @@ public final class ClockFragment extends Fragment {
String customFormatValue = prefs.getString( String customFormatValue = prefs.getString(
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT); SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT);
int clockOutlineThicknessDp = prefs.getInt(
SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_DEFAULT);
String clockOutlineColorValue = prefs.getString(
SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
SbtDefaults.CLOCK_OUTLINE_COLOR_DEFAULT);
boolean drawerClockCustomFormatEnabledValue = prefs.getBoolean( boolean drawerClockCustomFormatEnabledValue = prefs.getBoolean(
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED, SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT); SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT);
@@ -160,6 +172,15 @@ public final class ClockFragment extends Fragment {
customFormatValue, customFormatValue,
18f, 18f,
true); true);
bindClockOutline(
prefs,
clockOutlineEnabled,
clockOutlineContainer,
clockOutlineThickness,
clockOutlineThicknessLabel,
clockOutlineThicknessDp,
clockOutlineColor,
clockOutlineColorValue);
bindCustomFormat( bindCustomFormat(
prefs, prefs,
drawerClockCustomFormatCard, drawerClockCustomFormatCard,
@@ -218,6 +239,69 @@ public final class ClockFragment extends Fragment {
return root; return root;
} }
private void bindClockOutline(
SharedPreferences prefs,
CheckBox enabledView,
View container,
Slider thicknessView,
TextView thicknessValueView,
int thicknessDp,
EditText colorView,
String colorExpression
) {
if (enabledView == null || thicknessView == null || colorView == null) {
return;
}
int clampedThickness = Math.max(
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_MIN,
Math.min(thicknessDp, SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_MAX));
enabledView.setChecked(prefs.getBoolean(
SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
SbtDefaults.CLOCK_OUTLINE_ENABLED_DEFAULT));
thicknessView.setValue(clampedThickness);
updateClockOutlineThicknessLabel(thicknessValueView, clampedThickness);
colorView.setText(colorExpression != null ? colorExpression : "");
updateCustomFormatVisibility(container, enabledView.isChecked());
enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> {
updateCustomFormatVisibility(container, isChecked);
prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED, isChecked).apply();
SbtSettings.ensureReadable(requireContext());
});
thicknessView.addOnChangeListener((slider, value, fromUser) -> {
int thickness = Math.round(value);
updateClockOutlineThicknessLabel(thicknessValueView, thickness);
if (!fromUser) {
return;
}
prefs.edit().putInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP, thickness).apply();
SbtSettings.ensureReadable(requireContext());
});
colorView.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) {
prefs.edit()
.putString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
s != null ? s.toString() : "")
.apply();
SbtSettings.ensureReadable(requireContext());
}
});
}
private void updateClockOutlineThicknessLabel(TextView view, int thicknessDp) {
if (view != null) {
view.setText(getString(R.string.clock_outline_thickness_value, thicknessDp));
}
}
private void updateCustomFormatVisibility(View customFormatContainer, boolean enabled) { private void updateCustomFormatVisibility(View customFormatContainer, boolean enabled) {
if (customFormatContainer == null) { if (customFormatContainer == null) {
return; return;
@@ -40,7 +40,7 @@ public final class LayoutFragment extends Fragment {
View root = inflater.inflate(R.layout.fragment_layout, container, false); View root = inflater.inflate(R.layout.fragment_layout, container, false);
SharedPreferences prefs = requireContext() SharedPreferences prefs = requireContext()
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
hideUnlockedSubpageObsoleteControls(root); configureUnlockedOnlyModeControls(root);
reorderItemCards(root, prefs); reorderItemCards(root, prefs);
setupPositionSection(root, setupPositionSection(root,
@@ -508,7 +508,7 @@ public final class LayoutFragment extends Fragment {
} }
} }
private void hideUnlockedSubpageObsoleteControls(View root) { private void configureUnlockedOnlyModeControls(View root) {
hide(root, R.id.clock_position_mode_lockscreen); hide(root, R.id.clock_position_mode_lockscreen);
hide(root, R.id.notif_position_mode_aod); hide(root, R.id.notif_position_mode_aod);
hide(root, R.id.notif_position_mode_lockscreen); hide(root, R.id.notif_position_mode_lockscreen);
@@ -106,6 +106,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:baselineAligned="false"
android:orientation="horizontal"> android:orientation="horizontal">
<LinearLayout <LinearLayout
@@ -79,6 +79,71 @@
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:cardUseCompatPadding="true"
app:strokeColor="@color/sbt_card_outline"
app:strokeWidth="1dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.03"
android:text="@string/clock_outline_title"
android:textAllCaps="true"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<CheckBox
android:id="@+id/clock_outline_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/clock_outline_enabled" />
<LinearLayout
android:id="@+id/clock_outline_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/clock_outline_thickness_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceBody1" />
<com.google.android.material.slider.Slider
android:id="@+id/clock_outline_thickness"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:valueFrom="1"
android:valueTo="6"
android:stepSize="1" />
<EditText
android:id="@+id/clock_outline_color"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/clock_outline_color_hint"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:singleLine="true" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <com.google.android.material.card.MaterialCardView
android:id="@+id/drawer_clock_custom_format_card" android:id="@+id/drawer_clock_custom_format_card"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -3,6 +3,7 @@
android:id="@+id/hidden_item_root" android:id="@+id/hidden_item_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:baselineAligned="false"
android:foreground="?attr/selectableItemBackground" android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical" android:gravity="center_vertical"
android:minHeight="56dp" android:minHeight="56dp"
-1
View File
@@ -8,7 +8,6 @@
<color name="sbt_background_top">#FF110B07</color> <color name="sbt_background_top">#FF110B07</color>
<color name="sbt_background_bottom">#FF1B0F08</color> <color name="sbt_background_bottom">#FF1B0F08</color>
<color name="sbt_surface">#FF20140D</color> <color name="sbt_surface">#FF20140D</color>
<color name="sbt_surface_variant">#FF302012</color>
<color name="sbt_on_primary">#FFFFFFFF</color> <color name="sbt_on_primary">#FFFFFFFF</color>
<color name="sbt_on_surface">#FFEAF0F3</color> <color name="sbt_on_surface">#FFEAF0F3</color>
<color name="sbt_on_surface_variant">#FFE3D3B6</color> <color name="sbt_on_surface_variant">#FFE3D3B6</color>
-23
View File
@@ -1,23 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.StatusBarTweak" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/sbt_primary</item>
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
<item name="colorOnPrimary">@color/sbt_on_primary</item>
<item name="colorSecondary">@color/sbt_accent</item>
<item name="colorSecondaryVariant">@color/sbt_accent_dark</item>
<item name="colorOnSecondary">@color/black</item>
<item name="colorSurface">@color/sbt_surface</item>
<item name="colorOnSurface">@color/sbt_on_surface</item>
<item name="android:colorAccent">@color/sbt_accent</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:windowBackground">@drawable/sbt_window_background</item>
<item name="android:navigationBarColor">@color/sbt_primary_dark</item>
<item name="android:statusBarColor">@color/sbt_primary_dark</item>
<item name="android:windowLightStatusBar">false</item>
<item name="materialCardViewStyle">@style/Widget.StatusBarTweak.Card</item>
<item name="materialButtonOutlinedStyle">@style/Widget.StatusBarTweak.OutlinedButton</item>
<item name="materialAlertDialogTheme">@style/ThemeOverlay.StatusBarTweak.MaterialAlertDialog</item>
<item name="sliderStyle">@style/Widget.StatusBarTweak.Slider</item>
<item name="switchStyle">@style/Widget.StatusBarTweak.Switch</item>
</style>
</resources>
-2
View File
@@ -8,12 +8,10 @@
<color name="sbt_background_top">#FFFFF4E8</color> <color name="sbt_background_top">#FFFFF4E8</color>
<color name="sbt_background_bottom">#FFF5E5D2</color> <color name="sbt_background_bottom">#FFF5E5D2</color>
<color name="sbt_surface">#FFFFFFFF</color> <color name="sbt_surface">#FFFFFFFF</color>
<color name="sbt_surface_variant">#FFFFF1E0</color>
<color name="sbt_on_primary">#FFFFFFFF</color> <color name="sbt_on_primary">#FFFFFFFF</color>
<color name="sbt_on_surface">#FF111820</color> <color name="sbt_on_surface">#FF111820</color>
<color name="sbt_on_surface_variant">#FF62553F</color> <color name="sbt_on_surface_variant">#FF62553F</color>
<color name="black">#FF000000</color> <color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="sbt_card_outline">#24000000</color> <color name="sbt_card_outline">#24000000</color>
<color name="sbt_panel_outline">#4D000000</color> <color name="sbt_panel_outline">#4D000000</color>
<color name="sbt_hidden_item_active_bg">#1AE65100</color> <color name="sbt_hidden_item_active_bg">#1AE65100</color>
+4 -29
View File
@@ -14,7 +14,6 @@
<string name="nav_layout_lockscreen">&#160;&#160;&#160;&#160;Lockscreen</string> <string name="nav_layout_lockscreen">&#160;&#160;&#160;&#160;Lockscreen</string>
<string name="nav_layout_aod">&#160;&#160;&#160;&#160;AOD</string> <string name="nav_layout_aod">&#160;&#160;&#160;&#160;AOD</string>
<string name="nav_battery_bar">Battery bar</string> <string name="nav_battery_bar">Battery bar</string>
<string name="nav_block_app_notification_icons">Hide notification icons</string>
<string name="nav_status_chips">Status chips</string> <string name="nav_status_chips">Status chips</string>
<string name="nav_misc">Misc</string> <string name="nav_misc">Misc</string>
<string name="debug_title">Debugging</string> <string name="debug_title">Debugging</string>
@@ -51,7 +50,6 @@
<string name="debug_camera_action_undo"></string> <string name="debug_camera_action_undo"></string>
<string name="debug_camera_other_item">%1$s • %2$s</string> <string name="debug_camera_other_item">%1$s • %2$s</string>
<string name="debug_camera_unknown">unknown</string> <string name="debug_camera_unknown">unknown</string>
<string name="notification_icons_title">Notification icons</string>
<string name="system_icons_title">Hide status icons</string> <string name="system_icons_title">Hide status icons</string>
<string name="system_icons_hint">Status-icon rules for the custom layout engine.</string> <string name="system_icons_hint">Status-icon rules for the custom layout engine.</string>
<string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string> <string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
@@ -72,43 +70,27 @@
<string name="system_icon_bluetooth">Bluetooth</string> <string name="system_icon_bluetooth">Bluetooth</string>
<string name="system_icon_data_saver">Data saver</string> <string name="system_icon_data_saver">Data saver</string>
<string name="system_icon_do_not_disturb">Do not disturb</string> <string name="system_icon_do_not_disturb">Do not disturb</string>
<string name="system_icon_enhanced_processing">Enhanced processing</string>
<string name="system_icon_home_carrier">Homescreen carrier</string>
<string name="system_icon_hotspot">Mobile hotspot</string> <string name="system_icon_hotspot">Mobile hotspot</string>
<string name="system_icon_ims">IMS network icons</string>
<string name="system_icon_location">Location</string> <string name="system_icon_location">Location</string>
<string name="system_icon_lock_carrier">Lockscreen carrier</string>
<string name="system_icon_lockscreen_mum">Two phone user icon</string>
<string name="system_icon_managed_profile">Managed profile</string>
<string name="system_icon_mobile">Mobile data signal</string> <string name="system_icon_mobile">Mobile data signal</string>
<string name="system_icon_modes">Modes</string>
<string name="system_icon_nfc">NFC</string> <string name="system_icon_nfc">NFC</string>
<string name="system_icon_panel_carrier">Expanded panel carrier</string>
<string name="system_icon_power_saver">Power saver</string>
<string name="system_icon_rcs">RCS</string>
<string name="system_icon_two_phone_mode">Two phone mode icon</string>
<string name="system_icon_volume">Volume</string> <string name="system_icon_volume">Volume</string>
<string name="system_icon_vpn">VPN</string> <string name="system_icon_vpn">VPN</string>
<string name="system_icon_wifi">Wi-Fi</string> <string name="system_icon_wifi">Wi-Fi</string>
<string name="section_unlocked">Unlocked</string> <string name="section_unlocked">Unlocked</string>
<string name="section_icons_mode">Icons mode</string>
<string name="section_cards_mode">Cards mode</string>
<string name="section_aod">AOD</string> <string name="section_aod">AOD</string>
<string name="section_lockscreen">Lockscreen</string> <string name="section_lockscreen">Lockscreen</string>
<string name="label_max_icons_per_row">Max icons per row</string> <string name="label_max_icons_per_row">Max icons per row</string>
<string name="label_max_rows">Max rows</string> <string name="label_max_rows">Max rows</string>
<string name="label_even_distribution">Even distribution</string> <string name="label_even_distribution">Even distribution</string>
<string name="label_limit_screen_width">Limit to screen width</string> <string name="label_limit_screen_width">Limit to screen width</string>
<string name="label_cutout_aware">Cutout aware</string>
<string name="label_aod_keep_visible_during_call">Keep AOD visible during calls</string> <string name="label_aod_keep_visible_during_call">Keep AOD visible during calls</string>
<string name="label_aod_keep_visible_during_call_hint">Prevents SystemUI from suppressing AOD while an active call is in progress.</string> <string name="label_aod_keep_visible_during_call_hint">Prevents SystemUI from suppressing AOD while an active call is in progress.</string>
<string name="hidden_apps_dialog_title">Notification icon app blocks</string>
<string name="hidden_apps_page_title">Hide notification icons</string> <string name="hidden_apps_page_title">Hide notification icons</string>
<string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string> <string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string>
<string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string> <string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
<string name="hidden_apps_clear_session">Clear session list</string> <string name="hidden_apps_clear_session">Clear session list</string>
<string name="hidden_apps_empty">No apps yet. Generate a notification first.</string> <string name="hidden_apps_empty">No apps yet. Generate a notification first.</string>
<string name="hidden_apps_close">Close</string>
<string name="hidden_apps_exception_regex_input_hint">Regex</string> <string name="hidden_apps_exception_regex_input_hint">Regex</string>
<plurals name="hidden_apps_package_with_exceptions"> <plurals name="hidden_apps_package_with_exceptions">
<item quantity="one">%1$s \u2022 %2$d exception</item> <item quantity="one">%1$s \u2022 %2$d exception</item>
@@ -126,7 +108,6 @@
<string name="hidden_apps_mode_unlock">Unlocked</string> <string name="hidden_apps_mode_unlock">Unlocked</string>
<string name="hidden_apps_package_unknown">(unknown package)</string> <string name="hidden_apps_package_unknown">(unknown package)</string>
<string name="status_chips_title">Status chips</string> <string name="status_chips_title">Status chips</string>
<string name="status_chips_hint">Hide Samsung/SystemUI status chips (music, maps/navigation, call) in the top status area.</string>
<string name="status_chips_hide_all_label">Hide other status chips (unlocked)</string> <string name="status_chips_hide_all_label">Hide other status chips (unlocked)</string>
<string name="status_chips_hide_all_hint">Hides detected status chips that are not media, navigation or call. Privacy indicators are excluded.</string> <string name="status_chips_hide_all_hint">Hides detected status chips that are not media, navigation or call. Privacy indicators are excluded.</string>
<string name="status_chips_hide_media_unlocked_label">Hide media chip (unlocked)</string> <string name="status_chips_hide_media_unlocked_label">Hide media chip (unlocked)</string>
@@ -142,9 +123,6 @@
<string name="battery_bar_position_top">Top edge of screen</string> <string name="battery_bar_position_top">Top edge of screen</string>
<string name="battery_bar_position_bottom">Bottom of status bar</string> <string name="battery_bar_position_bottom">Bottom of status bar</string>
<string name="battery_bar_alignment_title">Alignment</string> <string name="battery_bar_alignment_title">Alignment</string>
<string name="battery_bar_alignment_ltr">&#8594;</string>
<string name="battery_bar_alignment_rtl">&#8592;</string>
<string name="battery_bar_alignment_center">&#8596;</string>
<string name="battery_bar_thickness_title">Thickness (dp)</string> <string name="battery_bar_thickness_title">Thickness (dp)</string>
<string name="battery_bar_edge_offset_title">Edge offset (dp)</string> <string name="battery_bar_edge_offset_title">Edge offset (dp)</string>
<string name="battery_bar_curved_geometry_length">Follow screen edges</string> <string name="battery_bar_curved_geometry_length">Follow screen edges</string>
@@ -163,15 +141,12 @@
<string name="battery_bar_thresholds_title">Threshold colours</string> <string name="battery_bar_thresholds_title">Threshold colours</string>
<string name="battery_bar_thresholds_hint">Below a threshold, the first matching colour overrides the default.</string> <string name="battery_bar_thresholds_hint">Below a threshold, the first matching colour overrides the default.</string>
<string name="battery_bar_add_threshold">Add threshold</string> <string name="battery_bar_add_threshold">Add threshold</string>
<string name="battery_bar_colour_dialog_title">Set colour</string>
<string name="battery_bar_colour_dialog_hint">Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB</string> <string name="battery_bar_colour_dialog_hint">Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB</string>
<string name="battery_bar_threshold_percent_title">Threshold percent</string> <string name="battery_bar_threshold_percent_title">Threshold percent</string>
<string name="battery_bar_threshold_percent_hint">Applies when battery is at or below this percent.</string> <string name="battery_bar_threshold_percent_hint">Applies when battery is at or below this percent.</string>
<string name="battery_bar_threshold_change_percent">Change threshold %</string>
<string name="battery_bar_threshold_label">Below %1$d%%</string> <string name="battery_bar_threshold_label">Below %1$d%%</string>
<string name="battery_bar_threshold_discharge_colour">Discharge colour</string> <string name="battery_bar_threshold_discharge_colour">Discharge colour</string>
<string name="battery_bar_threshold_charge_colour">Charge colour</string> <string name="battery_bar_threshold_charge_colour">Charge colour</string>
<string name="battery_bar_threshold_remove">Remove threshold</string>
<string name="battery_bar_colour_auto">Auto</string> <string name="battery_bar_colour_auto">Auto</string>
<string name="battery_bar_colour_same_as_discharge">Same as discharge</string> <string name="battery_bar_colour_same_as_discharge">Same as discharge</string>
<string name="battery_bar_colour_same_as_default">Default</string> <string name="battery_bar_colour_same_as_default">Default</string>
@@ -185,9 +160,7 @@
<string name="layout_home_title">Layout</string> <string name="layout_home_title">Layout</string>
<string name="layout_title">Unlocked</string> <string name="layout_title">Unlocked</string>
<string name="layout_master_toggle_title">Master toggle</string> <string name="layout_master_toggle_title">Master toggle</string>
<string name="clock_enabled">Use custom clock</string>
<string name="layout_enabled">Use custom layout</string> <string name="layout_enabled">Use custom layout</string>
<string name="clock_position_title">Clock position</string>
<string name="layout_clock_position_title">Clock</string> <string name="layout_clock_position_title">Clock</string>
<string name="layout_carrier_position_title">Carrier</string> <string name="layout_carrier_position_title">Carrier</string>
<string name="layout_chip_position_title">Status chip</string> <string name="layout_chip_position_title">Status chip</string>
@@ -234,12 +207,15 @@
<string name="clock_vertical_offset_label">Vertical position</string> <string name="clock_vertical_offset_label">Vertical position</string>
<string name="clock_middle_side_left">Left side</string> <string name="clock_middle_side_left">Left side</string>
<string name="clock_middle_side_right">Right side</string> <string name="clock_middle_side_right">Right side</string>
<string name="clock_custom_format_title">Custom format</string>
<string name="clock_custom_format_statusbar_title">Statusbar clock</string> <string name="clock_custom_format_statusbar_title">Statusbar clock</string>
<string name="clock_custom_format_drawer_clock_title">Drawer clock</string> <string name="clock_custom_format_drawer_clock_title">Drawer clock</string>
<string name="clock_custom_format_drawer_date_title">Drawer date</string> <string name="clock_custom_format_drawer_date_title">Drawer date</string>
<string name="clock_custom_format_tools_title">Tools and instructions</string> <string name="clock_custom_format_tools_title">Tools and instructions</string>
<string name="clock_custom_format_enabled">Enable custom date/time pattern</string> <string name="clock_custom_format_enabled">Enable custom date/time pattern</string>
<string name="clock_outline_title">Statusbar clock outline</string>
<string name="clock_outline_enabled">Add outline</string>
<string name="clock_outline_thickness_value">Thickness: %1$d dp</string>
<string name="clock_outline_color_hint">Outline colour, for example #000 ; #FFF</string>
<string name="drawer_clock_custom_format_enabled">Enable custom date/time pattern</string> <string name="drawer_clock_custom_format_enabled">Enable custom date/time pattern</string>
<string name="drawer_date_custom_format_enabled">Enable custom date/time pattern</string> <string name="drawer_date_custom_format_enabled">Enable custom date/time pattern</string>
<string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: &lt;i&gt;, &lt;u&gt;, &lt;small&gt;, &lt;big&gt;, &lt;font color=\'...\'&gt;, &lt;font face=\'...\'&gt;\n\u2022 &lt;b&gt; is not supported\n\u2022 Custom font tags: &lt;sans&gt;, &lt;serif&gt;, &lt;mono&gt;, &lt;condensed&gt;\n\u2022 {/font} closes any open &lt;font ...&gt; tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string> <string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: &lt;i&gt;, &lt;u&gt;, &lt;small&gt;, &lt;big&gt;, &lt;font color=\'...\'&gt;, &lt;font face=\'...\'&gt;\n\u2022 &lt;b&gt; is not supported\n\u2022 Custom font tags: &lt;sans&gt;, &lt;serif&gt;, &lt;mono&gt;, &lt;condensed&gt;\n\u2022 {/font} closes any open &lt;font ...&gt; tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string>
@@ -257,7 +233,6 @@
<string name="clock_font_picker_hint">Search font family</string> <string name="clock_font_picker_hint">Search font family</string>
<string name="clock_font_picker_copied">Copied: %1$s</string> <string name="clock_font_picker_copied">Copied: %1$s</string>
<string name="clock_custom_format_help_link">Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html</string> <string name="clock_custom_format_help_link">Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html</string>
<string name="clock_custom_format_invalid">Invalid pattern</string>
<string name="label_battery_lockscreen_unplugged_percent">Unplugged battery % on lockscreen</string> <string name="label_battery_lockscreen_unplugged_percent">Unplugged battery % on lockscreen</string>
<string name="label_battery_lockscreen_unplugged_percent_hint">Shows the plain battery percentage on the lockscreen when unplugged.</string> <string name="label_battery_lockscreen_unplugged_percent_hint">Shows the plain battery percentage on the lockscreen when unplugged.</string>
<string name="label_battery_aod_unplugged_percent">Unplugged battery % on AOD</string> <string name="label_battery_aod_unplugged_percent">Unplugged battery % on AOD</string>
-22
View File
@@ -1,26 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="Theme.StatusBarTweak" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/sbt_primary</item>
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
<item name="colorOnPrimary">@color/sbt_on_primary</item>
<item name="colorSecondary">@color/sbt_accent</item>
<item name="colorSecondaryVariant">@color/sbt_accent_dark</item>
<item name="colorOnSecondary">@color/black</item>
<item name="colorSurface">@color/sbt_surface</item>
<item name="colorOnSurface">@color/sbt_on_surface</item>
<item name="android:colorAccent">@color/sbt_accent</item>
<item name="android:fontFamily">sans-serif</item>
<item name="android:windowBackground">@drawable/sbt_window_background</item>
<item name="android:navigationBarColor">@color/sbt_primary_dark</item>
<item name="android:statusBarColor">@color/sbt_primary_dark</item>
<item name="android:windowLightStatusBar">false</item>
<item name="materialCardViewStyle">@style/Widget.StatusBarTweak.Card</item>
<item name="materialButtonOutlinedStyle">@style/Widget.StatusBarTweak.OutlinedButton</item>
<item name="materialAlertDialogTheme">@style/ThemeOverlay.StatusBarTweak.MaterialAlertDialog</item>
<item name="sliderStyle">@style/Widget.StatusBarTweak.Slider</item>
<item name="switchStyle">@style/Widget.StatusBarTweak.Switch</item>
</style>
<style name="Theme.StatusBarTweak.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="Theme.StatusBarTweak.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/sbt_primary</item> <item name="colorPrimary">@color/sbt_primary</item>
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item> <item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
@@ -0,0 +1,115 @@
package se.ajpanton.statusbartweak.shell.settings;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import org.junit.Test;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public final class SbtSettingsTest {
@Test
public void emptySourceMatchesRepresentativeDefaults() {
SbtSettings defaults = SbtSettings.defaults();
SbtSettings decoded = SbtSettings.fromSource(new MapSource());
assertEquals(SbtDefaults.CLOCK_ENABLED_DEFAULT, defaults.clockEnabled);
assertEquals(SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT, defaults.clockCustomFormat);
assertEquals(SbtDefaults.BATTERY_BAR_POSITION_DEFAULT, defaults.batteryBarPosition);
assertEquals(SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT, defaults.batteryBarThicknessDp);
assertEquals(
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
defaults.layoutNotifUnlockedCount);
assertEquals(
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
defaults.layoutStatusUnlockedCount);
assertEquals(defaults.clockEnabled, decoded.clockEnabled);
assertEquals(defaults.clockCustomFormat, decoded.clockCustomFormat);
assertEquals(defaults.batteryBarPosition, decoded.batteryBarPosition);
assertEquals(defaults.batteryBarThicknessDp, decoded.batteryBarThicknessDp);
assertEquals(defaults.layoutNotifUnlockedCount, decoded.layoutNotifUnlockedCount);
assertEquals(defaults.layoutStatusUnlockedCount, decoded.layoutStatusUnlockedCount);
assertEquals(defaults.systemIconBlockedModes, decoded.systemIconBlockedModes);
}
@Test
public void sourceDecodingPreservesSettingsNormalization() {
MapSource source = new MapSource();
source.put(SbtSettings.KEY_CLOCK_ENABLED, false);
source.put(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, "H:mm:ss");
source.put(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW, 0);
source.put(SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX, 500);
source.put(SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, "unsupported");
source.put(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, 100);
source.put(SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, "unsupported");
source.put(
SystemIconRules.PREF_KEY_BLOCKED_MODES,
setOf("wifi|5", "invalid", "wifi|not-a-number"));
source.put(
SbtSettings.KEY_APP_ICON_BLOCKED_MODES,
setOf("app.example|1|0|1", "bad-entry"));
SbtSettings decoded = SbtSettings.fromSource(source);
assertFalse(decoded.clockEnabled);
assertEquals("H:mm:ss", decoded.clockCustomFormat);
assertEquals(SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN, decoded.unlockedMaxIconsPerRow);
assertEquals(500, decoded.clockVerticalOffsetPx);
assertEquals(SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF, decoded.batteryBarCurvedGeometryMode);
assertEquals(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, decoded.layoutNotifUnlockedCount);
assertEquals(
SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT,
decoded.systemIconDualSimSignalMode);
assertEquals(Integer.valueOf(5), decoded.systemIconBlockedModes.get("wifi"));
assertEquals(Integer.valueOf(AppIconRules.MODE_AOD | AppIconRules.MODE_UNLOCK),
decoded.appIconBlockedModes.get("app.example"));
}
private static Set<String> setOf(String... values) {
HashSet<String> result = new HashSet<>();
Collections.addAll(result, values);
return result;
}
private static final class MapSource implements SbtSettings.SettingsSource {
private final Map<String, Object> values = new HashMap<>();
void put(String key, Object value) {
values.put(key, value);
}
@Override
public boolean getBoolean(String key, boolean defaultValue) {
Object value = values.get(key);
return value instanceof Boolean ? (Boolean) value : defaultValue;
}
@Override
public int getInt(String key, int defaultValue) {
Object value = values.get(key);
return value instanceof Integer ? (Integer) value : defaultValue;
}
@Override
public String getString(String key, String defaultValue) {
Object value = values.get(key);
return value instanceof String ? (String) value : defaultValue;
}
@Override
@SuppressWarnings("unchecked")
public Set<String> getStringSet(String key) {
Object value = values.get(key);
return value instanceof Set ? (Set<String>) value : Collections.emptySet();
}
@Override
public boolean contains(String key) {
return values.containsKey(key);
}
}
}
+186
View File
@@ -0,0 +1,186 @@
"""Editable status-bar layout playground.
Run from this directory with ``python3 playground.py``. Everything below is
intended to be edited freely; the model and renderer live in separate files.
Coordinates use the same convention as the module's layout solver:
horizontal zero is the status bar's left edge and vertical zero is its bottom.
"""
from statusbar_lab import StatusBarLab, containers_overlap, vertically_overlaps
from statusbar_renderer import render
# ---------------------------------------------------------------------------
# Virtual status-bar settings
# ---------------------------------------------------------------------------
STATUSBAR_WIDTH = 1200
STATUSBAR_HEIGHT = 100
# These are inputs for the algorithm you will write. The playground itself
# does not apply them automatically.
EDGE_PADDING = 0
CONTAINER_PADDING = 8
CAMERA_PADDING = 0
TEXT_HORIZONTAL_PADDING = 6
ICON_DOT_GAP = 0
DOT_WIDTH_FACTOR = 0.75
# Keep these in the same terms as the module settings. They are intentionally
# passive until the positioning/truncation algorithm below uses them.
CONTAINER_ORDER = ["clock", "chip", "carrier", "notification", "status"]
SHRINK_ORDER = ["notification", "status", "chip", "carrier"]
# Rendering scale only affects the visualizer, never the simulated geometry.
RENDER_SCALE = 1.0
VERTICAL_RENDER_SCALE = 2.0
# ---------------------------------------------------------------------------
# Scenario API
# ---------------------------------------------------------------------------
LAB = StatusBarLab()
# These lists remain valid when items are added. Clock text lines belong to
# one CLOCK group because the group moves them by their relative x offsets.
CLOCK = LAB.clock
CLOCK_LINES = CLOCK.lines
NOTIFICATIONS = LAB.notifications
STATUSES = LAB.statuses
CHIPS = LAB.chips
CARRIERS = LAB.carriers
CAMERAS = LAB.cameras
def _sync_settings() -> None:
LAB.text_horizontal_padding = TEXT_HORIZONTAL_PADDING
LAB.icon_dot_gap = ICON_DOT_GAP
LAB.dot_width_factor = DOT_WIDTH_FACTOR
def AddClock(
text: str,
height: float,
vertical_offset: float = 0,
horizontal_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
):
"""Add one text line to the single movable CLOCK group.
Horizontal offsets are normalized relative to all clock lines. Therefore
offsets ``5, 10, -20`` produce the same clock geometry as ``105, 110, 80``.
``position`` is ``left``, ``middle`` or ``right``; ``cutout_side`` is the
fallback side for a middle clock when a camera splits the status bar.
"""
_sync_settings()
return CLOCK.add_line(text, height, vertical_offset, horizontal_offset, position, cutout_side)
def AddNotification(
height: float,
vertical_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
direction: str = "right",
):
_sync_settings()
return LAB.add_notification(height, vertical_offset, position, cutout_side, direction)
def SetNotificationIcons(number_of_notifications: int) -> None:
"""Replace the notification-icon pool and empty every notification container."""
LAB.configure_notification_icons(number_of_notifications)
def AddStatus(
height: float,
vertical_offset: float = 0,
position: str = "right",
cutout_side: str = "right",
direction: str = "left",
):
_sync_settings()
return LAB.add_status(height, vertical_offset, position, cutout_side, direction)
def SetStatusIcons(number_of_icons: int, widths: list[float] | None = None) -> None:
"""Replace the status-icon pool and empty every status container.
Missing widths are padded using the first status container's height. Add
status containers before calling this function when relying on that default.
"""
LAB.configure_status_icons(number_of_icons, widths or [])
def AddChip(
text: str,
height: float,
vertical_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
):
_sync_settings()
return LAB.add_chip(text, height, vertical_offset, position, cutout_side)
def AddCarrier(
text: str,
height: float,
vertical_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
):
_sync_settings()
return LAB.add_carrier(text, height, vertical_offset, position, cutout_side)
def AddCamera(width: float, height: float, horizontal_position: float, vertical_position: float):
return LAB.add_camera(width, height, horizontal_position, vertical_position)
# ---------------------------------------------------------------------------
# Simulated app settings: add or remove as many containers as you need.
# ---------------------------------------------------------------------------
AddClock("12:34:56", height=40, vertical_offset=0, horizontal_offset=0)
AddClock("Mon, 1 Jan", height=24, vertical_offset=42, horizontal_offset=10)
AddNotification(height=32, vertical_offset=0)
AddNotification(height=32, vertical_offset=40)
SetNotificationIcons(8)
AddStatus(height=30, vertical_offset=0)
AddStatus(height=30, vertical_offset=38)
SetStatusIcons(6, [20, 30, 30, 45])
AddChip("Navigation 12:34", height=38, vertical_offset=0)
AddCarrier("Example carrier", height=30, vertical_offset=45)
AddCamera(width=90, height=100, horizontal_position=555, vertical_position=0)
# ---------------------------------------------------------------------------
# Positioning and truncation algorithm
# ---------------------------------------------------------------------------
# All containers currently start at horizontal position zero. Add your layout
# algorithm here. Useful operations include:
#
# CLOCK.set_left(EDGE_PADDING)
# NOTIFICATIONS[0].set_right(STATUSBAR_WIDTH - EDGE_PADDING)
# CHIPS[0].set_width(120) # clock widths cannot be overridden
# NOTIFICATIONS[0].add_icons(3) # takes IDs from the shared pool
# NOTIFICATIONS[0].return_icons(1) # returns the newest assigned ID
# NOTIFICATIONS[0].set_dot(True)
# CLOCK.right_edge_between(10, 50) # ignores clock lines outside this band
# containers_overlap(CLOCK, CHIPS[0])
# vertically_overlaps(NOTIFICATIONS[0].bounds, CHIPS[0].bounds)
# Keep the renderer call at the end so the editable scenario and algorithm stay
# together above it.
_sync_settings()
render(LAB, STATUSBAR_WIDTH, STATUSBAR_HEIGHT, RENDER_SCALE, VERTICAL_RENDER_SCALE)
+428
View File
@@ -0,0 +1,428 @@
"""Container model for the status-bar layout playground.
This module deliberately contains no placement policy. ``playground.py`` owns
the scenario and is the only file intended for interactive editing.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Iterable, Optional
def text_width(text: str, height: float, horizontal_padding: float) -> float:
"""Use stable approximate glyph widths so geometry is available before Tk starts."""
narrow = set(" ilI.,:;!|'`")
wide = set("MW@#%&")
units = sum(0.35 if char in narrow else 0.9 if char in wide else 0.62 for char in text)
return max(0.0, units * height + horizontal_padding * 2)
def _bounds_share_vertical_space(first: "Bounds", second: "Bounds") -> bool:
return first.bottom < second.top and second.bottom < first.top
def rectangles_overlap(first: "Bounds", second: "Bounds") -> bool:
return (
_bounds_share_vertical_space(first, second)
and first.left < second.right
and second.left < first.right
)
@dataclass(frozen=True)
class Bounds:
left: float
bottom: float
right: float
top: float
def vertically_overlaps(
first: "Bounds | Container | ClockGroup",
second: "Bounds | Container | ClockGroup",
) -> bool:
"""Return whether two containers can collide through horizontal movement.
A composite clock is expanded to its individual lines, so protruding clock
text outside the tested vertical band cannot produce a false collision.
"""
first_items = _collision_items(first)
second_items = _collision_items(second)
return any(
_bounds_share_vertical_space(first_item.bounds, second_item.bounds)
for first_item in first_items
for second_item in second_items
)
def shares_vertical_space(
first: "Bounds | Container | ClockGroup",
second: "Bounds | Container | ClockGroup",
) -> bool:
return vertically_overlaps(first, second)
class Container:
"""A mutable, bottom-origin status-bar rectangle."""
def __init__(
self,
lab: "StatusBarLab",
kind: str,
height: float,
vertical_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
) -> None:
self.lab = lab
self.kind = kind
self.height = float(height)
self.y = float(vertical_offset)
self.position = position
self.cutout_side = cutout_side
self.x = 0.0
@property
def width(self) -> float:
raise NotImplementedError
@property
def left(self) -> float:
return self.x
@property
def right(self) -> float:
return self.x + self.width
@property
def bottom(self) -> float:
return self.y
@property
def top(self) -> float:
return self.y + self.height
@property
def bounds(self) -> Bounds:
return Bounds(self.left, self.bottom, self.right, self.top)
def set_left(self, left: float) -> None:
self.x = float(left)
def set_right(self, right: float) -> None:
self.x = float(right) - self.width
def shift_x(self, amount: float) -> None:
self.x += float(amount)
def vertically_overlaps(self, other: "Bounds") -> bool:
return vertically_overlaps(self.bounds, other)
def overlaps(self, other: "Container | ClockGroup") -> bool:
return containers_overlap(self, other)
class TextContainer(Container):
def __init__(self, text: str, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.text = text
self._width_override: Optional[float] = None
@property
def natural_width(self) -> float:
return text_width(self.text, self.height, self.lab.text_horizontal_padding)
@property
def width(self) -> float:
return self.natural_width if self._width_override is None else self._width_override
def set_width(self, width: float) -> None:
self._width_override = max(0.0, float(width))
def restore_natural_width(self) -> None:
self._width_override = None
class ClockLine(TextContainer):
"""A clock line whose width always follows its text."""
def __init__(self, horizontal_offset: float, text: str, *args, **kwargs) -> None:
super().__init__(text, *args, **kwargs)
self.raw_horizontal_offset = float(horizontal_offset)
self.relative_x = 0.0
def set_width(self, width: float) -> None:
raise ValueError("Clock line widths are derived from their text and cannot be changed.")
class ClockGroup:
"""One movable clock made from one or more independently-sized text lines."""
def __init__(self, lab: "StatusBarLab") -> None:
self.lab = lab
self.lines: list[ClockLine] = []
self.x = 0.0
def add_line(
self,
text: str,
height: float,
vertical_offset: float = 0,
horizontal_offset: float = 0,
position: str = "left",
cutout_side: str = "left",
) -> ClockLine:
line = ClockLine(
horizontal_offset,
text,
self.lab,
"clock",
height,
vertical_offset,
position,
cutout_side,
)
self.lines.append(line)
self._apply_relative_offsets()
return line
def _apply_relative_offsets(self) -> None:
if not self.lines:
return
origin = min(line.raw_horizontal_offset for line in self.lines)
for line in self.lines:
line.relative_x = line.raw_horizontal_offset - origin
line.x = self.x + line.relative_x
@property
def left(self) -> float:
return min((line.left for line in self.lines), default=self.x)
@property
def right(self) -> float:
return max((line.right for line in self.lines), default=self.x)
@property
def width(self) -> float:
return self.right - self.left
def set_left(self, left: float) -> None:
self.x += float(left) - self.left
self._apply_relative_offsets()
def set_right(self, right: float) -> None:
self.x += float(right) - self.right
self._apply_relative_offsets()
def shift_x(self, amount: float) -> None:
self.x += float(amount)
self._apply_relative_offsets()
def bounds_between(self, bottom: float, top: float) -> Optional[Bounds]:
"""Return clock bounds limited to lines that occupy ``bottom..top``."""
band = Bounds(float("-inf"), bottom, float("inf"), top)
lines = [line for line in self.lines if vertically_overlaps(line.bounds, band)]
if not lines:
return None
return Bounds(
min(line.left for line in lines),
min(line.bottom for line in lines),
max(line.right for line in lines),
max(line.top for line in lines),
)
def left_edge_between(self, bottom: float, top: float) -> Optional[float]:
bounds = self.bounds_between(bottom, top)
return bounds.left if bounds is not None else None
def right_edge_between(self, bottom: float, top: float) -> Optional[float]:
bounds = self.bounds_between(bottom, top)
return bounds.right if bounds is not None else None
def overlaps(self, other: Container | "ClockGroup") -> bool:
return containers_overlap(self, other)
class IconPool:
def __init__(self, kind: str) -> None:
self.kind = kind
self.widths: list[Optional[float]] = []
self.available_ids: list[int] = []
def configure(self, count: int, widths: Iterable[float]) -> None:
count = max(0, int(count))
supplied: list[Optional[float]] = [float(width) for width in widths][:count]
supplied.extend([None] * (count - len(supplied)))
self.widths = supplied
self.available_ids = list(range(count))
def take(self, count: int) -> list[int]:
count = max(0, int(count))
assigned = self.available_ids[:count]
del self.available_ids[:len(assigned)]
return assigned
def release(self, ids: Iterable[int]) -> None:
self.available_ids = sorted(set(self.available_ids).union(ids))
def width_for(self, icon_id: int, fallback_width: float) -> float:
width = self.widths[icon_id]
return fallback_width if width is None else width
class IconContainer(Container):
def __init__(self, pool: IconPool, direction: str = "right", *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
if direction not in ("left", "right"):
raise ValueError("Icon direction must be 'left' or 'right'.")
self.pool = pool
self.direction = direction
self.icon_ids: list[int] = []
self.dot = False
@property
def icon_count(self) -> int:
return len(self.icon_ids)
@property
def start_number(self) -> Optional[int]:
return self.icon_ids[0] if self.icon_ids else None
@property
def dot_width(self) -> float:
return self.height * self.lab.dot_width_factor
@property
def width(self) -> float:
icon_width = sum(self.icon_width(icon_id) for icon_id in self.icon_ids)
if not self.dot:
return icon_width
gap = self.lab.icon_dot_gap if self.icon_ids else 0.0
return icon_width + gap + self.dot_width
def add_icons(self, count: int) -> int:
assigned = self.pool.take(count)
self.icon_ids.extend(assigned)
return len(assigned)
def return_icons(self, count: int) -> int:
count = min(max(0, int(count)), len(self.icon_ids))
returned = self.icon_ids[-count:] if count else []
if count:
del self.icon_ids[-count:]
self.pool.release(returned)
return len(returned)
def clear_icons(self) -> None:
self.return_icons(len(self.icon_ids))
def set_dot(self, enabled: bool) -> None:
self.dot = bool(enabled)
def toggle_dot(self) -> bool:
self.dot = not self.dot
return self.dot
def segments(self) -> list[tuple[str, Optional[int], float]]:
icons = [("icon", icon_id, self.icon_width(icon_id)) for icon_id in self.icon_ids]
dot = [("dot", None, self.dot_width)] if self.dot else []
if self.dot and self.icon_ids and self.lab.icon_dot_gap > 0:
dot.insert(0, ("gap", None, self.lab.icon_dot_gap))
return icons + dot if self.direction == "right" else dot + list(reversed(icons))
def icon_width(self, icon_id: int) -> float:
return self.pool.width_for(icon_id, self.height)
class CameraCutout(Container):
def __init__(self, lab: "StatusBarLab", width: float, height: float, horizontal_position: float, vertical_position: float) -> None:
super().__init__(lab, "camera", height, vertical_position)
self._width = float(width)
self.x = float(horizontal_position)
@property
def width(self) -> float:
return self._width
def set_width(self, width: float) -> None:
self._width = max(0.0, float(width))
class StatusBarLab:
def __init__(self) -> None:
self.text_horizontal_padding = 6.0
self.icon_dot_gap = 0.0
self.dot_width_factor = 0.75
self.clock = ClockGroup(self)
self.notifications: list[IconContainer] = []
self.statuses: list[IconContainer] = []
self.chips: list[TextContainer] = []
self.carriers: list[TextContainer] = []
self.cameras: list[CameraCutout] = []
self.notification_pool = IconPool("notification")
self.status_pool = IconPool("status")
def add_notification(self, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left", direction: str = "right") -> IconContainer:
item = IconContainer(self.notification_pool, direction, self, "notification", height, vertical_offset, position, cutout_side)
self.notifications.append(item)
return item
def add_status(self, height: float, vertical_offset: float = 0, position: str = "right", cutout_side: str = "right", direction: str = "left") -> IconContainer:
item = IconContainer(self.status_pool, direction, self, "status", height, vertical_offset, position, cutout_side)
self.statuses.append(item)
return item
def add_chip(self, text: str, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left") -> TextContainer:
item = TextContainer(text, self, "chip", height, vertical_offset, position, cutout_side)
self.chips.append(item)
return item
def add_carrier(self, text: str, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left") -> TextContainer:
item = TextContainer(text, self, "carrier", height, vertical_offset, position, cutout_side)
self.carriers.append(item)
return item
def add_camera(self, width: float, height: float, horizontal_position: float, vertical_position: float) -> CameraCutout:
item = CameraCutout(self, width, height, horizontal_position, vertical_position)
self.cameras.append(item)
return item
def configure_notification_icons(self, count: int) -> None:
self.notification_pool.configure(count, [])
for item in self.notifications:
item.icon_ids.clear()
def configure_status_icons(self, count: int, widths: Iterable[float]) -> None:
self.status_pool.configure(count, widths)
for item in self.statuses:
item.icon_ids.clear()
def render_containers(self) -> list[Container]:
return [*self.clock.lines, *self.notifications, *self.statuses, *self.chips, *self.carriers]
def containers_overlap(first: Container | ClockGroup, second: Container | ClockGroup) -> bool:
"""Check horizontal and vertical overlap, expanding a clock into its lines."""
first_items = _collision_items(first)
second_items = _collision_items(second)
for first_item in first_items:
for second_item in second_items:
if first_item is second_item:
continue
if rectangles_overlap(first_item.bounds, second_item.bounds):
return True
return False
def _collision_items(item: Bounds | Container | ClockGroup) -> list[Container]:
if isinstance(item, Bounds):
return [_BoundsContainer(item)]
return item.lines if isinstance(item, ClockGroup) else [item]
class _BoundsContainer:
"""Adapter so the public helpers can accept a raw Bounds instance too."""
def __init__(self, bounds: Bounds) -> None:
self.bounds = bounds
+139
View File
@@ -0,0 +1,139 @@
"""Tk renderer for the status-bar layout playground."""
from __future__ import annotations
import tkinter as tk
from tkinter import font as tkfont
from typing import Iterable
from statusbar_lab import CameraCutout, Container, IconContainer, StatusBarLab, rectangles_overlap
TYPE_COLOURS = {
"clock": "#9fd7ff",
"notification": "#a7efaa",
"status": "#ffd596",
"chip": "#e2b0ff",
"carrier": "#ffb4c4",
}
def render(
lab: StatusBarLab,
statusbar_width: float,
statusbar_height: float,
render_scale: float = 1.0,
vertical_scale: float = 2.0,
margin: int = 40,
) -> None:
root = tk.Tk()
root.title("StatusBarTweak layout playground")
width = int(statusbar_width * render_scale + margin * 2)
height = int(statusbar_height * vertical_scale + margin * 2)
canvas = tk.Canvas(root, width=width, height=height, bg="#242424", highlightthickness=0)
canvas.pack()
def point(x: float, y: float) -> tuple[float, float]:
return margin + x * render_scale, margin + (statusbar_height - y) * vertical_scale
left, bottom = point(0, 0)
right, top = point(statusbar_width, statusbar_height)
canvas.create_rectangle(left, top, right, bottom, fill="white", outline="#4a4a4a", width=5)
containers = lab.render_containers()
for container in containers:
_draw_container(canvas, point, container, render_scale, vertical_scale)
_draw_overlaps(canvas, point, containers)
for camera in lab.cameras:
_draw_camera(canvas, point, camera)
root.mainloop()
def _draw_container(canvas: tk.Canvas, point, container: Container, render_scale: float, vertical_scale: float) -> None:
if isinstance(container, IconContainer):
_draw_icons(canvas, point, container)
return
x1, y1 = point(container.left, container.bottom)
x2, y2 = point(container.right, container.top)
canvas.create_rectangle(x1, y2, x2, y1, fill=TYPE_COLOURS[container.kind], outline="black", width=1)
_draw_clipped_text(canvas, container.text, x1 + 3, (y1 + y2) / 2, max(0, x2 - x1 - 6), container.height * vertical_scale)
def _draw_icons(canvas: tk.Canvas, point, container: IconContainer) -> None:
colour = TYPE_COLOURS[container.kind]
if not container.segments():
x, y1 = point(container.left, container.bottom)
_, y2 = point(container.left, container.top)
canvas.create_line(x, y1, x, y2, fill="black", width=1)
return
cursor = container.left
for segment_kind, icon_id, segment_width in container.segments():
x1, y1 = point(cursor, container.bottom)
x2, y2 = point(cursor + segment_width, container.top)
if segment_kind == "gap":
cursor += segment_width
continue
if segment_kind == "dot":
canvas.create_rectangle(x1, y2, x2, y1, fill=colour, outline="black", width=1)
radius = min(abs(x2 - x1), abs(y1 - y2)) * 0.24
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
canvas.create_oval(cx - radius, cy - radius, cx + radius, cy + radius, fill="black", outline="black")
else:
canvas.create_rectangle(x1, y2, x2, y1, fill=colour, outline="black", width=1)
_draw_clipped_text(canvas, str(icon_id), (x1 + x2) / 2, (y1 + y2) / 2, abs(x2 - x1) - 2, abs(y1 - y2), anchor="center")
cursor += segment_width
def _draw_camera(canvas: tk.Canvas, point, camera: CameraCutout) -> None:
x1, y1 = point(camera.left, camera.bottom)
x2, y2 = point(camera.right, camera.top)
canvas.create_rectangle(x1, y2, x2, y1, fill="black", outline="black")
def _draw_overlaps(canvas: tk.Canvas, point, containers: Iterable[Container]) -> None:
items = list(containers)
for index, first in enumerate(items):
for second in items[index + 1:]:
if first.kind == "clock" and second.kind == "clock":
continue
if not rectangles_overlap(first.bounds, second.bounds):
continue
left = max(first.left, second.left)
right = min(first.right, second.right)
bottom = max(first.bottom, second.bottom)
top = min(first.top, second.top)
x1, y1 = point(left, bottom)
x2, y2 = point(right, top)
canvas.create_rectangle(x1, y2, x2, y1, fill="red", outline="red")
radius = max(abs(x2 - x1), abs(y1 - y2)) / 2 + 12
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
canvas.create_oval(cx - radius, cy - radius, cx + radius, cy + radius, outline="red", width=3)
def _draw_clipped_text(
canvas: tk.Canvas,
text: str,
x: float,
y: float,
width: float,
height: float,
anchor: str = "w",
) -> None:
if width <= 0:
return
size = max(7, int(height * 0.42))
text_font = tkfont.Font(family="TkDefaultFont", size=size)
clipped = _clip_text(text_font, text, width)
canvas.create_text(x, y, text=clipped, font=text_font, fill="black", anchor=anchor)
def _clip_text(text_font: tkfont.Font, text: str, width: float) -> str:
if text_font.measure(text) <= width:
return text
ellipsis = "..."
while text and text_font.measure(text + ellipsis) > width:
text = text[:-1]
return text + ellipsis if text else ""
@@ -125,9 +125,6 @@ public final class HarnessForegroundService extends Service {
} }
private void ensureChannel() { private void ensureChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
NotificationManager nm = getSystemService(NotificationManager.class); NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null) { if (nm == null) {
return; return;
@@ -315,11 +315,7 @@ public final class MainActivity extends Activity {
} }
Intent intent = new Intent(this, HarnessForegroundService.class); Intent intent = new Intent(this, HarnessForegroundService.class);
intent.setAction(action); intent.setAction(action);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent); startForegroundService(intent);
} else {
startService(intent);
}
} }
private void stopHarnessService() { private void stopHarnessService() {
@@ -337,9 +333,6 @@ public final class MainActivity extends Activity {
} }
private void ensureChannel(NotificationManager nm) { private void ensureChannel(NotificationManager nm) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
NotificationChannel channel = new NotificationChannel( NotificationChannel channel = new NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"StatusBarTweak harness", "StatusBarTweak harness",
@@ -444,19 +444,15 @@ public final class ScenarioActivity extends Activity {
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; | WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
controller.setSystemBarsAppearance(lightBars ? mask : 0, mask); controller.setSystemBarsAppearance(lightBars ? mask : 0, mask);
} }
} else if (Build.VERSION.SDK_INT >= 23) { } else {
int flags = getWindow().getDecorView().getSystemUiVisibility(); int flags = getWindow().getDecorView().getSystemUiVisibility();
if (lightBars) { if (lightBars) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
if (Build.VERSION.SDK_INT >= 26) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
} else { } else {
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR; flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
if (Build.VERSION.SDK_INT >= 26) {
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR; flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
} }
}
getWindow().getDecorView().setSystemUiVisibility(flags); getWindow().getDecorView().setSystemUiVisibility(flags);
} }
} }
+1 -1
View File
@@ -1 +1 @@
version=0.6 version=0.8