From 37d793a1bef5ca0e86e51d189d16ca7c7bb8e2ba Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sat, 6 Jun 2026 14:48:45 +0000 Subject: [PATCH] Add fullscreen battery bar scenarios and dual SIM signal handling --- .../battery/BatteryBarController.java | 567 +++++++++++++++++- .../runtime/render/SnapshotRenderer.java | 6 + .../UnlockedIconSnapshotRenderController.java | 2 +- .../render/UnlockedStockSourceCollector.java | 289 ++++++++- .../shell/settings/BatteryBarGeometry.java | 118 ++++ .../shell/settings/SbtDefaults.java | 1 + .../shell/settings/SbtSettings.java | 154 +++++ .../shell/settings/SbtSettingsProvider.java | 10 + .../shell/ui/BatteryBarFragment.java | 251 ++++++++ .../shell/ui/SystemIconsFragment.java | 199 +++++- .../main/res/layout/fragment_battery_bar.xml | 39 ++ app/src/main/res/values/strings.xml | 20 + 12 files changed, 1641 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/shell/settings/BatteryBarGeometry.java diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java index b861787..cc86156 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/battery/BatteryBarController.java @@ -3,14 +3,20 @@ package se.ajpanton.statusbartweak.runtime.features.battery; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.PixelFormat; import android.graphics.Rect; import android.os.BatteryManager; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.WindowMetrics; import android.widget.FrameLayout; import android.widget.TextView; @@ -25,13 +31,17 @@ import se.ajpanton.statusbartweak.runtime.ViewIdNames; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport; +import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.settings.BroadcastReceiverSupport; import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache; +import se.ajpanton.statusbartweak.shell.settings.BatteryBarGeometry; import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class BatteryBarController { + private static final int APPEARANCE_LIGHT_STATUS_BARS = 8; + private static final String[] ROOT_CLASSES = new String[] { "com.android.systemui.statusbar.phone.PhoneStatusBarView", "com.android.systemui.statusbar.phone.KeyguardStatusBarView" @@ -40,8 +50,16 @@ final class BatteryBarController { private final RuntimeContext runtimeContext; private final Set roots = Collections.newSetFromMap(new WeakHashMap<>()); private final Map batteryBarViews = new WeakHashMap<>(); + private final Map overlayBatteryBars = new WeakHashMap<>(); private final Map rootLayoutListeners = new WeakHashMap<>(); + private final Set pendingOverlayRetries = Collections.newSetFromMap(new WeakHashMap<>()); private final Map lastSignatures = new WeakHashMap<>(); + private final Map lastOverlaySignatures = new WeakHashMap<>(); + private int systemBarAppearance; + private int systemBarBehavior; + private int systemBarRequestedVisibleTypes = WindowInsets.Type.statusBars(); + private boolean statusBarTransientShown; + private String systemBarPackageName = ""; private boolean hooksInstalled; private boolean receiverRegistered; @@ -59,6 +77,10 @@ final class BatteryBarController { for (String className : ROOT_CLASSES) { installHooksForClass(classLoader, className); } + installRootTransformHooks(); + installSystemBarAttributeListener(classLoader); + installTransientBarListener(classLoader); + runtimeContext.getModeStateRepository().addSceneListener((previous, current) -> refreshAllRoots()); hooksInstalled = true; } @@ -100,6 +122,113 @@ final class BatteryBarController { } return result; }); + XposedHookSupport.hookAllMethodsIfExists(framework, rootClass, "onApplyWindowInsets", chain -> { + Object result = chain.proceed(); + Object target = chain.getThisObject(); + if (target instanceof View root) { + applyBatteryBar(root); + } + return result; + }); + } + + private void installRootTransformHooks() { + XposedInterface framework = runtimeContext.getFramework(); + XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setTranslationY", chain -> { + Object result = chain.proceed(); + Object target = chain.getThisObject(); + if (target instanceof View root && roots.contains(root)) { + applyBatteryBar(root); + } + 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)) { + applyBatteryBar(root); + } + return result; + }); + XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setAlpha", chain -> { + Object result = chain.proceed(); + Object target = chain.getThisObject(); + if (target instanceof View root && roots.contains(root)) { + applyBatteryBar(root); + } + return result; + }); + XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setVisibility", chain -> { + Object result = chain.proceed(); + Object target = chain.getThisObject(); + if (target instanceof View root && roots.contains(root)) { + applyBatteryBar(root); + } + return result; + }); + } + + private void installSystemBarAttributeListener(ClassLoader classLoader) { + Class commandQueueClass = ReflectionSupport.findClassIfExists( + "com.android.systemui.statusbar.CommandQueue", + classLoader); + if (commandQueueClass == null) { + return; + } + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + commandQueueClass, + "onSystemBarAttributesChanged", + chain -> { + Object result = chain.proceed(); + updateSystemBarState(chain.getArgs()); + return result; + }); + } + + private void installTransientBarListener(ClassLoader classLoader) { + Class commandQueueClass = ReflectionSupport.findClassIfExists( + "com.android.systemui.statusbar.CommandQueue", + classLoader); + if (commandQueueClass != null) { + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + commandQueueClass, + "showTransient", + chain -> { + Object result = chain.proceed(); + updateTransientBarState(chain.getArgs(), true); + return result; + }); + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + commandQueueClass, + "abortTransient", + chain -> { + Object result = chain.proceed(); + updateTransientBarState(chain.getArgs(), false); + return result; + }); + } + installStatusBarAutoHideElementListener(classLoader); + } + + private void installStatusBarAutoHideElementListener(ClassLoader classLoader) { + Class statusBarAutoHideElementClass = ReflectionSupport.findClassIfExists( + "com.android.systemui.statusbar.phone.CentralSurfacesImpl$5", + classLoader); + if (statusBarAutoHideElementClass == null) { + return; + } + XposedHookSupport.hookAllMethodsIfExists( + runtimeContext.getFramework(), + statusBarAutoHideElementClass, + "hide", + chain -> { + Object result = chain.proceed(); + updateStatusBarTransientShown(false); + return result; + }); } private void trackAndApplyRoot(View root) { @@ -115,6 +244,7 @@ final class BatteryBarController { root.removeOnLayoutChangeListener(listener); } roots.remove(root); + pendingOverlayRetries.remove(root); removeBatteryBarView(root); } @@ -210,19 +340,88 @@ final class BatteryBarController { return; } int color = resolveBarColor(root, settings); - String signature = buildSignature(root, settings, color); + BatteryBarGeometry.Scenario scenario = scenarioForRoot(root); + if (!shouldRenderOnRoot(root)) { + removeBatteryBarView(root); + return; + } + boolean transientStatusBarReveal = isFullscreenScenario(scenario) + && shouldRenderVisibleFullscreenBar(root); + if (transientStatusBarReveal) { + scenario = visibleStatusBarScenarioForRoot(root); + } + BatteryBarGeometry geometry = BatteryBarGeometry.resolve(settings, scenario); + if (isFullscreenScenario(scenario)) { + removeEmbeddedBatteryBarView(root); + applyOverlayBatteryBar(root, settings, geometry, scenario, color); + return; + } + removeOverlayBatteryBar(root); + String signature = buildSignature(root, settings, color, geometry, scenario); BatteryBarView existingView = batteryBarViews.get(root); String lastSignature = lastSignatures.get(root); if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) { existingView.bringToFront(); return; } + applyEmbeddedBatteryBar(rootGroup, settings, geometry, scenario, color); + lastSignatures.put(root, signature); + } + + private void applyEmbeddedBatteryBar( + ViewGroup rootGroup, + SbtSettings settings, + BatteryBarGeometry geometry, + BatteryBarGeometry.Scenario scenario, + int color + ) { BatteryBarView barView = ensureBatteryBarView(rootGroup); layoutBatteryBarView(rootGroup, barView); - barView.update(rootGroup, settings, batteryLevel, plugged, color); + barView.update(rootGroup, settings, geometry, batteryLevel, plugged, color); barView.bringToFront(); barView.invalidate(); - lastSignatures.put(root, signature); + lastSignatures.put(rootGroup, buildSignature(rootGroup, settings, color, geometry, scenario)); + } + + private void applyOverlayBatteryBar( + View root, + SbtSettings settings, + BatteryBarGeometry geometry, + BatteryBarGeometry.Scenario scenario, + int color + ) { + String signature = buildSignature(root, settings, color, geometry, scenario); + OverlayBatteryBar overlay = overlayBatteryBars.get(root); + if (overlay == null) { + overlay = new OverlayBatteryBar(root.getContext()); + overlayBatteryBars.put(root, overlay); + } + OverlayBounds bounds = fullscreenOverlayBounds(root); + boolean attached = overlay.ensureAttached(bounds.width, bounds.height); + if (!attached) { + scheduleOverlayRetry(root); + removeOverlayBatteryBar(root); + return; + } + pendingOverlayRetries.remove(root); + overlay.view.update(overlay.host, settings, geometry, batteryLevel, plugged, color); + if (!signature.equals(lastOverlaySignatures.get(root))) { + overlay.updateLayout(bounds.width, bounds.height); + lastOverlaySignatures.put(root, signature); + } + overlay.view.invalidate(); + } + + private void scheduleOverlayRetry(View root) { + if (root == null || !root.isAttachedToWindow() || !pendingOverlayRetries.add(root)) { + return; + } + root.postDelayed(() -> { + pendingOverlayRetries.remove(root); + if (roots.contains(root) && root.isAttachedToWindow()) { + applyBatteryBar(root); + } + }, 250L); } private BatteryBarView ensureBatteryBarView(ViewGroup root) { @@ -278,9 +477,16 @@ final class BatteryBarController { ); } - private String buildSignature(View root, SbtSettings settings, int color) { + private String buildSignature( + View root, + SbtSettings settings, + int color, + BatteryBarGeometry geometry, + BatteryBarGeometry.Scenario scenario + ) { return root.getWidth() + "|" + root.getHeight() + "|" + + scenario + "|" + batteryLevel + "|" + plugged + "|" + color + "|" @@ -289,6 +495,7 @@ final class BatteryBarController { + settings.batteryBarAlignment + "|" + settings.batteryBarThicknessDp + "|" + settings.batteryBarEdgeOffsetDp + "|" + + (geometry != null ? geometry.signature() : "") + "|" + settings.batteryBarMinLevel + "|" + settings.batteryBarMaxLevel + "|" + settings.batteryBarDefaultDischargeColor + "|" @@ -296,7 +503,228 @@ final class BatteryBarController { + BatteryBarStyle.encodeThresholds(settings.batteryBarThresholds); } + private BatteryBarGeometry.Scenario scenarioForRoot(View root) { + if (isKeyguardRoot(root)) { + return BatteryBarGeometry.Scenario.LOCKSCREEN; + } + boolean fullscreen = isPhoneRoot(root) + && isUnlockedScene() + && (isStatusBarSurfaceHidden(root) || isFullscreenSystemBarState()); + boolean landscape = isLandscapeForRoot(root, fullscreen); + if (fullscreen) { + return landscape + ? BatteryBarGeometry.Scenario.FULLSCREEN_LANDSCAPE + : BatteryBarGeometry.Scenario.FULLSCREEN_PORTRAIT; + } + if (isPhoneRoot(root) && isUnlockedScene() && isTransparentStatusBarActive()) { + return landscape + ? BatteryBarGeometry.Scenario.TRANSPARENT_LANDSCAPE + : BatteryBarGeometry.Scenario.TRANSPARENT_PORTRAIT; + } + return landscape + ? BatteryBarGeometry.Scenario.UNLOCKED_LANDSCAPE + : BatteryBarGeometry.Scenario.UNLOCKED_PORTRAIT; + } + + private BatteryBarGeometry.Scenario visibleStatusBarScenarioForRoot(View root) { + boolean landscape = isLandscapeForRoot(root, isFullscreenSystemBarState()); + if (isPhoneRoot(root) && isUnlockedScene() && isTransparentStatusBarActive()) { + return landscape + ? BatteryBarGeometry.Scenario.TRANSPARENT_LANDSCAPE + : BatteryBarGeometry.Scenario.TRANSPARENT_PORTRAIT; + } + return landscape + ? BatteryBarGeometry.Scenario.UNLOCKED_LANDSCAPE + : BatteryBarGeometry.Scenario.UNLOCKED_PORTRAIT; + } + + private boolean isFullscreenScenario(BatteryBarGeometry.Scenario scenario) { + return scenario == BatteryBarGeometry.Scenario.FULLSCREEN_PORTRAIT + || scenario == BatteryBarGeometry.Scenario.FULLSCREEN_LANDSCAPE; + } + + private boolean shouldRenderOnRoot(View root) { + SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene(); + if (isKeyguardRoot(root)) { + return scene == null || !scene.isUnlocked(); + } + if (isPhoneRoot(root)) { + return scene == null || scene.isUnlocked(); + } + return true; + } + + private boolean isUnlockedScene() { + SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene(); + return scene == null || scene.isUnlocked(); + } + + private boolean isPhoneRoot(View root) { + return root != null && root.getClass().getName().contains("PhoneStatusBarView"); + } + + private boolean isKeyguardRoot(View root) { + return root != null && root.getClass().getName().contains("KeyguardStatusBarView"); + } + + private boolean isLandscapeForRoot(View root, boolean preferDisplayBounds) { + if (root == null) { + return false; + } + if (preferDisplayBounds) { + Rect bounds = currentWindowBounds(root.getContext()); + if (bounds.width() > 0 && bounds.height() > 0 && bounds.width() != bounds.height()) { + return bounds.width() > bounds.height(); + } + } + return root.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; + } + + private boolean isTransparentStatusBarActive() { + return (systemBarAppearance & APPEARANCE_LIGHT_STATUS_BARS) != 0; + } + + private boolean isFullscreenSystemBarState() { + return (systemBarRequestedVisibleTypes & WindowInsets.Type.statusBars()) == 0; + } + + private boolean shouldRenderVisibleFullscreenBar(View root) { + return root != null && isFullscreenSystemBarState() && statusBarTransientShown; + } + + private void updateTransientBarState(java.util.List args, boolean visible) { + Integer displayId = args != null && !args.isEmpty() ? asInteger(args.get(0)) : null; + boolean statusBarsAffected = transientArgsIncludeStatusBars(args); + if (displayId == null || displayId != 0 || !statusBarsAffected) { + return; + } + updateStatusBarTransientShown(visible); + } + + private void updateStatusBarTransientShown(boolean shown) { + if (statusBarTransientShown == shown) { + return; + } + statusBarTransientShown = shown; + postRefreshAllRoots(); + } + + private boolean transientArgsIncludeStatusBars(java.util.List args) { + if (args == null || args.size() < 2) { + return true; + } + Object typesArg = args.get(1); + if (!(typesArg instanceof int[] types)) { + return true; + } + int statusBars = WindowInsets.Type.statusBars(); + for (int type : types) { + if ((type & statusBars) != 0) { + return true; + } + } + return false; + } + + private OverlayBounds fullscreenOverlayBounds(View root) { + int width = root.getWidth(); + 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) { + if (context == null) { + return new Rect(); + } + WindowManager windowManager = context.getSystemService(WindowManager.class); + if (windowManager == null) { + return new Rect(); + } + try { + WindowMetrics metrics = windowManager.getCurrentWindowMetrics(); + return metrics != null ? metrics.getBounds() : new Rect(); + } catch (Throwable ignored) { + return new Rect(); + } + } + + private record OverlayBounds(int width, int height) { + } + + private void updateSystemBarState(java.util.List args) { + if (args == null || args.size() < 2) { + return; + } + Integer displayId = asInteger(args.get(0)); + Integer appearance = asInteger(args.get(1)); + Integer behavior = args.size() > 4 ? asInteger(args.get(4)) : null; + Integer requestedVisibleTypes = args.size() > 5 ? asInteger(args.get(5)) : null; + if (displayId == null || displayId != 0 || appearance == null) { + return; + } + String packageName = args.size() > 6 && args.get(6) instanceof String value ? value : ""; + boolean changed = systemBarAppearance != appearance + || (behavior != null && systemBarBehavior != behavior) + || (requestedVisibleTypes != null + && systemBarRequestedVisibleTypes != requestedVisibleTypes) + || !systemBarPackageName.equals(packageName); + systemBarAppearance = appearance; + if (behavior != null) { + systemBarBehavior = behavior; + } + if (requestedVisibleTypes != null) { + systemBarRequestedVisibleTypes = requestedVisibleTypes; + } + systemBarPackageName = packageName; + if (changed) { + postRefreshAllRoots(); + } + } + + private void postRefreshAllRoots() { + for (View root : new ArrayList<>(roots)) { + if (root != null && root.isAttachedToWindow()) { + root.post(() -> { + if (roots.contains(root) && root.isAttachedToWindow()) { + applyBatteryBar(root); + } + }); + } + } + } + + private static boolean isStatusBarSurfaceHidden(View root) { + if (root == null) { + return false; + } + if (!root.isShown()) { + return true; + } + int[] location = new int[2]; + root.getLocationOnScreen(location); + float left = location[0] + root.getTranslationX(); + float top = location[1] + root.getTranslationY(); + float right = left + root.getWidth(); + float bottom = top + root.getHeight(); + int screenWidth = root.getResources().getDisplayMetrics().widthPixels; + int screenHeight = root.getResources().getDisplayMetrics().heightPixels; + return bottom <= 0f || top >= screenHeight || right <= 0f || left >= screenWidth; + } + + private static Integer asInteger(Object value) { + return value instanceof Integer integer ? integer : null; + } + private void removeBatteryBarView(View root) { + removeEmbeddedBatteryBarView(root); + removeOverlayBatteryBar(root); + } + + private void removeEmbeddedBatteryBarView(View root) { BatteryBarView view = batteryBarViews.remove(root); lastSignatures.remove(root); if (view != null) { @@ -304,6 +732,14 @@ final class BatteryBarController { } } + private void removeOverlayBatteryBar(View root) { + OverlayBatteryBar overlay = overlayBatteryBars.remove(root); + lastOverlaySignatures.remove(root); + if (overlay != null) { + overlay.remove(); + } + } + private int resolveBarColor(View root, SbtSettings settings) { int fallback = resolveClockColor(root); return BatteryBarStyle.resolveColor( @@ -358,11 +794,117 @@ final class BatteryBarController { } } + private static final class OverlayBatteryBar { + private final WindowManager windowManager; + private final FrameLayout host; + private final BatteryBarView view; + private boolean attached; + private int width; + private int height; + + OverlayBatteryBar(Context context) { + Context safeContext = context != null ? context : null; + windowManager = safeContext != null + ? safeContext.getSystemService(WindowManager.class) + : null; + host = new FrameLayout(context); + host.setClickable(false); + host.setFocusable(false); + host.setFocusableInTouchMode(false); + view = new BatteryBarView(context); + view.setClickable(false); + view.setFocusable(false); + view.setFocusableInTouchMode(false); + host.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + + boolean ensureAttached(int width, int height) { + if (windowManager == null || width <= 0 || height <= 0) { + return false; + } + if (attached) { + return updateLayout(width, height); + } + try { + windowManager.addView(host, layoutParams(width, height)); + attached = true; + this.width = width; + this.height = height; + layoutHost(width, height); + return true; + } catch (Throwable ignored) { + attached = false; + return false; + } + } + + boolean updateLayout(int width, int height) { + if (!attached || windowManager == null || width <= 0 || height <= 0) { + return false; + } + if (this.width == width && this.height == height) { + return true; + } + this.width = width; + this.height = height; + try { + windowManager.updateViewLayout(host, layoutParams(width, height)); + layoutHost(width, height); + return true; + } catch (Throwable ignored) { + return false; + } + } + + void remove() { + if (!attached || windowManager == null) { + attached = false; + return; + } + try { + windowManager.removeViewImmediate(host); + } catch (Throwable ignored) { + } finally { + attached = false; + } + } + + private WindowManager.LayoutParams layoutParams(int width, int height) { + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + width, + height, + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, + PixelFormat.TRANSLUCENT); + params.gravity = Gravity.TOP | Gravity.START; + params.x = 0; + params.y = 0; + params.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + params.setTitle("StatusBarTweakBatteryBar"); + return params; + } + + private void layoutHost(int width, int height) { + int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + host.measure(widthSpec, heightSpec); + host.layout(0, 0, width, height); + } + } + private static final class BatteryBarView extends View { private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Rect drawRect = new Rect(); private ViewGroup host; private SbtSettings settings; + private BatteryBarGeometry geometry; private int batteryLevel = -1; private boolean charging; @@ -375,12 +917,14 @@ final class BatteryBarController { void update( ViewGroup host, SbtSettings settings, + BatteryBarGeometry geometry, int batteryLevel, boolean charging, int color ) { this.host = host; this.settings = settings; + this.geometry = geometry; this.batteryLevel = batteryLevel; this.charging = charging; paint.setColor(color); @@ -398,14 +942,17 @@ final class BatteryBarController { if (width <= 0 || height <= 0) { return; } - int thickness = Math.max(1, dpToPx(getContext(), settings.batteryBarThicknessDp)); + BatteryBarGeometry activeGeometry = geometry != null + ? geometry + : BatteryBarGeometry.global(settings); + int thickness = Math.max(1, dpToPx(getContext(), activeGeometry.thicknessDp)); int edgeOffset = Math.max(0, dpToPx(getContext(), settings.batteryBarEdgeOffsetDp)); int left = edgeOffset; int right = width - edgeOffset; if (right <= left) { return; } - int top = resolveTop(settings, thickness, height); + int top = resolveTop(activeGeometry, thickness, height); int bottom = Math.min(height, top + thickness); if (bottom <= top) { return; @@ -416,9 +963,9 @@ final class BatteryBarController { if (barWidth <= 0) { return; } - if ("rtl".equals(settings.batteryBarAlignment)) { + if ("rtl".equals(activeGeometry.alignment)) { drawRect.set(right - barWidth, top, right, bottom); - } else if ("center".equals(settings.batteryBarAlignment)) { + } else if ("center".equals(activeGeometry.alignment)) { int centerX = (left + right) / 2; int half = barWidth / 2; drawRect.set(centerX - half, top, centerX - half + barWidth, bottom); @@ -427,8 +974,8 @@ final class BatteryBarController { } } - private int resolveTop(SbtSettings settings, int thickness, int height) { - if ("bottom".equals(settings.batteryBarPosition)) { + private int resolveTop(BatteryBarGeometry geometry, int thickness, int height) { + if (geometry != null && "bottom".equals(geometry.position)) { return Math.max(0, height - thickness); } return 0; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java index 4af88b7..1e6ac9e 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/SnapshotRenderer.java @@ -444,6 +444,9 @@ final class SnapshotRenderer { if (lowerKey.contains("wifi")) { return "wifi"; } + if (lowerKey.contains("mobile2")) { + return "mobile2"; + } if (lowerKey.contains("mobile")) { return "mobile"; } @@ -452,6 +455,9 @@ final class SnapshotRenderer { if (idName.contains("wifi") || className.contains("wifi")) { return "wifi"; } + if (idName.contains("mobile2") || className.contains("mobile2")) { + return "mobile2"; + } if (idName.contains("mobile") || className.contains("mobile")) { return "mobile"; } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java index fbae374..5aff65a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedIconSnapshotRenderController.java @@ -174,7 +174,7 @@ public final class UnlockedIconSnapshotRenderController { } CollectedIcons collectedIcons = previousIcons; if (forceStockRefresh || sceneChanged || stockSourcesChanged || collectedIcons == null) { - collectedIcons = sourceCollector.collect(root, anchors, scene); + collectedIcons = sourceCollector.collect(root, anchors, scene, settings); lastCollectedIconsByRoot.put(root, collectedIcons); } else if (forceChipRefresh && collectedIcons != null) { collectedIcons = sourceCollector.collectChips(root, anchors, collectedIcons); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java index ebed5a7..1faeaf7 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/render/UnlockedStockSourceCollector.java @@ -23,6 +23,7 @@ import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle; import se.ajpanton.statusbartweak.runtime.mode.SceneKey; import se.ajpanton.statusbartweak.runtime.notifications.NotificationActiveKeyStore; import se.ajpanton.statusbartweak.runtime.notifications.NotificationUnreadTracker; +import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; final class UnlockedStockSourceCollector { @@ -52,7 +53,7 @@ final class UnlockedStockSourceCollector { return anchors; } - CollectedIcons collect(View root, RootAnchors anchors, SceneKey scene) { + CollectedIcons collect(View root, RootAnchors anchors, SceneKey scene, SbtSettings settings) { ArrayList statusIcons = new ArrayList<>(); ArrayList notificationIcons = new ArrayList<>(); ArrayList statusChips = new ArrayList<>(); @@ -73,6 +74,7 @@ final class UnlockedStockSourceCollector { if (anchors.battery != null && !ViewGeometry.isDescendantOf(anchors.battery, anchors.statusRoot)) { collectStatusSnapshotViews(anchors.battery, statusIcons); } + selectDualSimStatusSignals(statusIcons, settings); normalizeStatusIconSourceBounds(root, statusIcons); if (scene != null && scene.isAod()) { if (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS @@ -551,6 +553,290 @@ final class UnlockedStockSourceCollector { Math.max(1, height)); } + private void selectDualSimStatusSignals(ArrayList sources, SbtSettings settings) { + if (sources == null || sources.size() < 2) { + return; + } + String mode = settings != null + ? settings.systemIconDualSimSignalMode + : SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT; + if (SbtSettings.SYSTEM_ICON_DUAL_SIM_SEPARATE.equals(mode)) { + deduplicateSameStatusSignalViews(sources); + return; + } + selectMobileStatusSignal(sources, mode); + deduplicateSameStatusSignalViews(sources); + } + + private void deduplicateSameStatusSignalViews(ArrayList sources) { + LinkedHashMap firstIndexByView = new LinkedHashMap<>(); + for (int i = 0; i < sources.size(); i++) { + SnapshotSource source = sources.get(i); + if (statusSignalKind(source) == null || source.view() == null) { + continue; + } + Integer firstIndex = firstIndexByView.get(source.view()); + if (firstIndex == null) { + firstIndexByView.put(source.view(), i); + continue; + } + SnapshotSource existing = sources.get(firstIndex); + if (statusSignalSourceScore(source) > statusSignalSourceScore(existing)) { + sources.set(firstIndex, source); + } + sources.remove(i); + i--; + } + } + + private void selectMobileStatusSignal(ArrayList sources, String mode) { + ArrayList mobileIndexes = new ArrayList<>(); + for (int i = 0; i < sources.size(); i++) { + String signal = statusSignalKind(sources.get(i)); + if (signal != null + && signal.startsWith("mobile") + && !isMobileRadioTechnologySource(sources.get(i).view())) { + mobileIndexes.add(i); + } + } + if (mobileIndexes.size() < 2) { + return; + } + int keptIndex = selectedMobileStatusSignalIndex(sources, mobileIndexes, mode); + ArrayList preservedRadioTechSources = new ArrayList<>(); + for (int i = mobileIndexes.size() - 1; i >= 0; i--) { + int index = mobileIndexes.get(i); + if (index != keptIndex) { + SnapshotSource radioTechSource = mobileRadioTechnologyChildSource(sources.get(index)); + if (radioTechSource != null) { + preservedRadioTechSources.add(radioTechSource); + } + sources.remove(index); + } + } + sources.addAll(preservedRadioTechSources); + } + + private int selectedMobileStatusSignalIndex( + ArrayList sources, + ArrayList mobileIndexes, + String mode + ) { + if (SbtSettings.SYSTEM_ICON_DUAL_SIM_SECOND.equals(mode)) { + return mobileIndexes.get(1); + } + if (SbtSettings.SYSTEM_ICON_DUAL_SIM_STRONGEST.equals(mode) + || SbtSettings.SYSTEM_ICON_DUAL_SIM_WEAKEST.equals(mode)) { + int selected = selectedBySignalStrength(sources, mobileIndexes, + SbtSettings.SYSTEM_ICON_DUAL_SIM_STRONGEST.equals(mode)); + if (selected >= 0) { + return selected; + } + } + if (SbtSettings.SYSTEM_ICON_DUAL_SIM_DATA.equals(mode)) { + int selected = selectedByRole(sources, mobileIndexes, "data"); + if (selected >= 0) { + return selected; + } + } + if (SbtSettings.SYSTEM_ICON_DUAL_SIM_PHONE.equals(mode)) { + int selected = selectedByRole(sources, mobileIndexes, "phone", "voice", "call"); + if (selected >= 0) { + return selected; + } + } + return mobileIndexes.get(0); + } + + private int selectedBySignalStrength( + ArrayList sources, + ArrayList indexes, + boolean strongest + ) { + int selectedIndex = -1; + int selectedStrength = 0; + for (int index : indexes) { + int strength = signalStrength(sources.get(index)); + if (strength < 0) { + continue; + } + if (selectedIndex < 0 + || (strongest && strength > selectedStrength) + || (!strongest && strength < selectedStrength)) { + selectedIndex = index; + selectedStrength = strength; + } + } + return selectedIndex; + } + + private int selectedByRole( + ArrayList sources, + ArrayList indexes, + String... needles + ) { + for (int index : indexes) { + String text = statusSignalText(sources.get(index)).toLowerCase(Locale.ROOT); + for (String needle : needles) { + if (text.contains(needle)) { + return index; + } + } + } + return -1; + } + + private String statusSignalKind(SnapshotSource source) { + String value = statusSignalText(source).toLowerCase(Locale.ROOT); + if (value.contains("wifi")) { + return "wifi"; + } + if (value.contains("mobile2")) { + return "mobile2"; + } + if (value.contains("mobile")) { + return "mobile"; + } + return null; + } + + private String statusSignalText(SnapshotSource source) { + View view = source != null ? source.view() : null; + if (view == null) { + return ""; + } + StringBuilder text = new StringBuilder(); + text.append(safeText(source.keyHint())).append(' '); + text.append(reflectString(view, "getSlot", "mSlot", "slot")).append(' '); + text.append(ViewIdNames.idName(view)).append(' '); + CharSequence contentDescription = view.getContentDescription(); + if (contentDescription != null) { + text.append(contentDescription).append(' '); + } + text.append(view.getClass().getName()).append(' '); + Object parent = view.getParent(); + int depth = 0; + while (parent instanceof View parentView && depth++ < 3) { + text.append(ViewIdNames.idName(parentView)).append(' '); + CharSequence parentDescription = parentView.getContentDescription(); + if (parentDescription != null) { + text.append(parentDescription).append(' '); + } + text.append(parentView.getClass().getName()).append(' '); + parent = parentView.getParent(); + } + return text.toString(); + } + + private boolean isMobileRadioTechnologySource(View view) { + if (view == null) { + return false; + } + String idName = ViewIdNames.idName(view).toLowerCase(Locale.ROOT); + String className = view.getClass().getName().toLowerCase(Locale.ROOT); + CharSequence contentDescription = view.getContentDescription(); + String content = contentDescription != null + ? contentDescription.toString().toLowerCase(Locale.ROOT) + : ""; + return idName.contains("mobile_type") + || idName.contains("network_type") + || idName.contains("data_type") + || className.contains("mobiletype") + || className.contains("networktype") + || className.contains("datatype") + || isRadioTechnologyText(content); + } + + private boolean isRadioTechnologyText(String text) { + if (text == null || text.isEmpty()) { + return false; + } + String value = text.toLowerCase(Locale.ROOT); + return value.equals("5g") + || value.equals("4g") + || value.equals("4g+") + || value.equals("3g") + || value.equals("2g") + || value.equals("lte") + || value.equals("h+") + || value.equals("nr"); + } + + private SnapshotSource mobileRadioTechnologyChildSource(SnapshotSource source) { + View sourceView = source != null ? source.view() : null; + if (!(sourceView instanceof ViewGroup group)) { + return null; + } + View radioTechView = firstMobileRadioTechnologyChild(group); + if (radioTechView == null) { + return null; + } + return new SnapshotSource(radioTechView, SnapshotMode.VIEW, "mobile_type"); + } + + private View firstMobileRadioTechnologyChild(ViewGroup group) { + for (int i = 0; i < group.getChildCount(); i++) { + View child = group.getChildAt(i); + if (isMobileRadioTechnologySource(child) + && child.getVisibility() == View.VISIBLE + && measuredWidth(child) > 0 + && measuredHeight(child) > 0) { + return child; + } + if (child instanceof ViewGroup childGroup) { + View nested = firstMobileRadioTechnologyChild(childGroup); + if (nested != null) { + return nested; + } + } + } + return null; + } + + private int signalStrength(SnapshotSource source) { + String text = statusSignalText(source).toLowerCase(Locale.ROOT); + int parsed = parseSignalBars(text); + if (parsed >= 0) { + return parsed; + } + View view = source != null ? source.view() : null; + Drawable drawable = view instanceof ImageView imageView ? imageView.getDrawable() : null; + return drawable != null ? drawable.getLevel() : -1; + } + + private int parseSignalBars(String text) { + if (text == null || text.isEmpty()) { + return -1; + } + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (!Character.isDigit(c)) { + continue; + } + int value = Character.digit(c, 10); + String suffix = text.substring(i + 1, Math.min(text.length(), i + 10)); + if (suffix.contains("bar") || suffix.contains("level")) { + return value; + } + } + return -1; + } + + private int statusSignalSourceScore(SnapshotSource source) { + View view = source != null ? source.view() : null; + if (view == null) { + return 0; + } + String className = view.getClass().getName(); + int score = Math.max(0, view.getWidth()) * Math.max(0, view.getHeight()); + if (isModernSignalContainer(view)) { + score += 1_000_000; + } else if (className.contains("StatusBarMobile") || className.contains("StatusBarWifi")) { + score += 100_000; + } + return score; + } + private int syntheticStatusIconWidth(View view, int fallbackSize) { int width = measuredWidth(view); if (width > 0) { @@ -1262,6 +1548,7 @@ final class UnlockedStockSourceCollector { settings.layoutStatusMiddleSide, settings.layoutStatusVerticalOffsetPx, settings.systemIconBlockedModes, + settings.systemIconDualSimSignalMode, settings.appIconBlockedModes, settings.appIconExceptionRules, settings.statusChipsHideAll, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/BatteryBarGeometry.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/BatteryBarGeometry.java new file mode 100644 index 0000000..df8a53b --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/BatteryBarGeometry.java @@ -0,0 +1,118 @@ +package se.ajpanton.statusbartweak.shell.settings; + +public final class BatteryBarGeometry { + + public enum Scenario { + UNLOCKED_PORTRAIT("unlocked_portrait"), + UNLOCKED_LANDSCAPE("unlocked_landscape"), + TRANSPARENT_PORTRAIT("transparent_portrait"), + TRANSPARENT_LANDSCAPE("transparent_landscape"), + FULLSCREEN_PORTRAIT("fullscreen_portrait"), + FULLSCREEN_LANDSCAPE("fullscreen_landscape"), + LOCKSCREEN("lockscreen"); + + public final String id; + + Scenario(String id) { + this.id = id; + } + } + + public final String position; + public final String alignment; + public final int thicknessDp; + + public BatteryBarGeometry( + String position, + String alignment, + int thicknessDp + ) { + this.position = sanitizePosition(position); + this.alignment = sanitizeAlignment(alignment); + this.thicknessDp = clamp( + thicknessDp, + SbtDefaults.BATTERY_BAR_THICKNESS_DP_MIN, + SbtDefaults.BATTERY_BAR_THICKNESS_DP_MAX); + } + + public static BatteryBarGeometry global(SbtSettings settings) { + if (settings == null) { + return new BatteryBarGeometry( + SbtDefaults.BATTERY_BAR_POSITION_DEFAULT, + SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT, + SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT); + } + return new BatteryBarGeometry( + settings.batteryBarPosition, + settings.batteryBarAlignment, + settings.batteryBarThicknessDp); + } + + public static BatteryBarGeometry resolve( + SbtSettings settings, + Scenario scenario + ) { + BatteryBarGeometry fallback = global(settings); + if (settings == null || scenario == null) { + return fallback; + } + if (!settings.batteryBarGeometryOverrideEnabledScenarios.contains(scenario.id)) { + return fallback; + } + return decode(settings.batteryBarGeometryOverrides.get(scenario.id), fallback); + } + + public static BatteryBarGeometry decode(String value, BatteryBarGeometry fallback) { + BatteryBarGeometry safeFallback = fallback != null ? fallback : global(null); + if (value == null || value.isEmpty()) { + return safeFallback; + } + String[] parts = value.split("\\|", -1); + if (parts.length != 3 && parts.length != 4) { + return safeFallback; + } + return new BatteryBarGeometry( + parts[0], + parts[1], + parseInt(parts[2], safeFallback.thicknessDp)); + } + + public String encode() { + return position + "|" + alignment + "|" + thicknessDp; + } + + public String signature() { + return encode(); + } + + public static String enabledKey(Scenario scenario) { + return SbtSettings.batteryBarGeometryOverrideEnabledKey(scenario.id); + } + + public static String valueKey(Scenario scenario) { + return SbtSettings.batteryBarGeometryKey(scenario.id); + } + + private static String sanitizePosition(String position) { + return "bottom".equals(position) ? "bottom" : "top"; + } + + private static String sanitizeAlignment(String alignment) { + if ("rtl".equals(alignment) || "center".equals(alignment)) { + return alignment; + } + return "ltr"; + } + + private static int parseInt(String value, int fallback) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ignored) { + return fallback; + } + } + + private static int clamp(int value, int min, int max) { + return Math.max(min, Math.min(max, value)); + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java index 5637c3e..312907a 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java @@ -134,6 +134,7 @@ public final class SbtDefaults { public static final String LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT = "left"; public static final int LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT = 0; public static final int LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT = 38; + public static final String SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT = "separate"; public static final int LAYOUT_ICON_HEIGHT_STEPS_MIN = 10; public static final int LAYOUT_ICON_HEIGHT_STEPS_MAX = 100; public static final int LAYOUT_ICON_CONTAINER_COUNT_MIN = 0; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index f023f30..541edcf 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -72,6 +72,9 @@ public final class SbtSettings { public static final String KEY_BATTERY_BAR_ALIGNMENT = "battery_bar_alignment"; public static final String KEY_BATTERY_BAR_THICKNESS_DP = "battery_bar_thickness_dp"; public static final String KEY_BATTERY_BAR_EDGE_OFFSET_DP = "battery_bar_edge_offset_dp"; + public static final String KEY_BATTERY_BAR_GEOMETRY_OVERRIDE_ENABLED_PREFIX = + "battery_bar_geometry_override_enabled_"; + public static final String KEY_BATTERY_BAR_GEOMETRY_PREFIX = "battery_bar_geometry_"; public static final String KEY_BATTERY_BAR_MIN_LEVEL = "battery_bar_min_level"; public static final String KEY_BATTERY_BAR_MAX_LEVEL = "battery_bar_max_level"; public static final String KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR = "battery_bar_default_discharge_color"; @@ -156,6 +159,14 @@ public final class SbtSettings { public static final String KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS = "layout_status_icon_height_steps"; public static final String KEY_SYSTEM_ICON_BLOCKED_MODES = SystemIconRules.PREF_KEY_BLOCKED_MODES; public static final String KEY_SYSTEM_ICON_HIDE_SLOTS = "system_icon_hide_slots"; + public static final String KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE = "system_icon_dual_sim_signal_mode"; + public static final String SYSTEM_ICON_DUAL_SIM_SEPARATE = "separate"; + public static final String SYSTEM_ICON_DUAL_SIM_FIRST = "first"; + public static final String SYSTEM_ICON_DUAL_SIM_SECOND = "second"; + public static final String SYSTEM_ICON_DUAL_SIM_STRONGEST = "strongest"; + public static final String SYSTEM_ICON_DUAL_SIM_WEAKEST = "weakest"; + public static final String SYSTEM_ICON_DUAL_SIM_DATA = "data"; + public static final String SYSTEM_ICON_DUAL_SIM_PHONE = "phone"; public static final String KEY_APP_ICON_BLOCKED_MODES = AppIconRules.PREF_KEY_BLOCKED_MODES; public static final String KEY_APP_ICON_EXCEPTION_RULES = AppIconRules.PREF_KEY_EXCEPTION_RULES; @@ -251,6 +262,14 @@ public final class SbtSettings { return base == null || index <= 0 ? base : base + "_" + (index + 1); } + public static String batteryBarGeometryOverrideEnabledKey(String scenarioId) { + return KEY_BATTERY_BAR_GEOMETRY_OVERRIDE_ENABLED_PREFIX + scenarioId; + } + + public static String batteryBarGeometryKey(String scenarioId) { + return KEY_BATTERY_BAR_GEOMETRY_PREFIX + scenarioId; + } + private static String suffixedKey(String base, int index) { return indexedKey(base, index); } @@ -281,6 +300,8 @@ public final class SbtSettings { public final String batteryBarAlignment; public final int batteryBarThicknessDp; public final int batteryBarEdgeOffsetDp; + public final Set batteryBarGeometryOverrideEnabledScenarios; + public final Map batteryBarGeometryOverrides; public final int batteryBarMinLevel; public final int batteryBarMaxLevel; public final String batteryBarDefaultDischargeColor; @@ -362,6 +383,7 @@ public final class SbtSettings { public final int layoutStatusAodIconHeightSteps; public final Map clockCameraTypeOverrides; public final Map systemIconBlockedModes; + public final String systemIconDualSimSignalMode; public final boolean statusChipsHideAll; public final boolean statusChipsHideMediaUnlocked; public final boolean statusChipsHideNavUnlocked; @@ -403,6 +425,8 @@ public final class SbtSettings { String batteryBarAlignment, int batteryBarThicknessDp, int batteryBarEdgeOffsetDp, + Set batteryBarGeometryOverrideEnabledScenarios, + Map batteryBarGeometryOverrides, int batteryBarMinLevel, int batteryBarMaxLevel, String batteryBarDefaultDischargeColor, @@ -484,6 +508,7 @@ public final class SbtSettings { int[] layoutStatusAodVerticalOffsetsPx, Map clockCameraTypeOverrides, Map systemIconBlockedModes, + String systemIconDualSimSignalMode, boolean statusChipsHideAll, boolean statusChipsHideMediaUnlocked, boolean statusChipsHideNavUnlocked, @@ -534,6 +559,9 @@ public final class SbtSettings { batteryBarEdgeOffsetDp, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX); + this.batteryBarGeometryOverrideEnabledScenarios = + sanitizeBatteryBarScenarioSet(batteryBarGeometryOverrideEnabledScenarios); + this.batteryBarGeometryOverrides = sanitizeBatteryBarScenarioMap(batteryBarGeometryOverrides); int clampedMinLevel = clampInt( batteryBarMinLevel, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, @@ -753,6 +781,9 @@ public final class SbtSettings { this.systemIconBlockedModes = systemIconBlockedModes != null ? Collections.unmodifiableMap(new HashMap<>(systemIconBlockedModes)) : Collections.emptyMap(); + this.systemIconDualSimSignalMode = validSystemIconDualSimSignalMode(systemIconDualSimSignalMode) + ? systemIconDualSimSignalMode + : SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT; this.statusChipsHideAll = statusChipsHideAll; this.statusChipsHideMediaUnlocked = statusChipsHideMediaUnlocked; this.statusChipsHideNavUnlocked = statusChipsHideNavUnlocked; @@ -810,6 +841,8 @@ public final class SbtSettings { SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT, SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT, + Collections.emptySet(), + Collections.emptyMap(), SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT, SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT, SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT, @@ -891,6 +924,7 @@ public final class SbtSettings { null, Collections.emptyMap(), Collections.emptyMap(), + SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT, false, false, false, @@ -914,6 +948,22 @@ public final class SbtSettings { return "dot"; } + public static boolean validSystemIconDualSimSignalMode(String mode) { + return SYSTEM_ICON_DUAL_SIM_SEPARATE.equals(mode) + || SYSTEM_ICON_DUAL_SIM_FIRST.equals(mode) + || SYSTEM_ICON_DUAL_SIM_SECOND.equals(mode) + || SYSTEM_ICON_DUAL_SIM_STRONGEST.equals(mode) + || SYSTEM_ICON_DUAL_SIM_WEAKEST.equals(mode) + || SYSTEM_ICON_DUAL_SIM_DATA.equals(mode) + || SYSTEM_ICON_DUAL_SIM_PHONE.equals(mode); + } + + public static String readSystemIconDualSimSignalMode(String mode) { + return validSystemIconDualSimSignalMode(mode) + ? mode + : SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT; + } + public static SbtSettings from(Context context) { if (context == null) { return defaults(); @@ -1057,6 +1107,8 @@ public final class SbtSettings { clampInt(prefs.getInt(KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT), SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX), + readBatteryBarScenarioSetFromPrefs(prefs), + readBatteryBarScenarioMapFromPrefs(prefs), clampInt(prefs.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX), @@ -1262,6 +1314,9 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.loadOverrides(prefs, KEY_CLOCK_CAMERA_TYPE_OVERRIDES), systemIconBlockedModes, + readSystemIconDualSimSignalMode(prefs.getString( + KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, + SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT)), prefs.getBoolean(KEY_STATUS_CHIPS_HIDE_ALL, false), prefs.getBoolean(KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED, false), prefs.getBoolean(KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED, false), @@ -1355,6 +1410,8 @@ public final class SbtSettings { clampInt(bundle.getInt(KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT), SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX), + readBatteryBarScenarioSetFromBundle(bundle), + readBatteryBarScenarioMapFromBundle(bundle), clampInt(bundle.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX), @@ -1560,6 +1617,9 @@ public final class SbtSettings { SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT), ClockCameraTypeSupport.parseOverrides(readStringSetFromBundle(bundle, KEY_CLOCK_CAMERA_TYPE_OVERRIDES)), systemIconBlockedModes, + readSystemIconDualSimSignalMode(bundle.getString( + KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, + SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT)), bundle.getBoolean(KEY_STATUS_CHIPS_HIDE_ALL, false), bundle.getBoolean(KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED, false), bundle.getBoolean(KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED, false), @@ -1728,6 +1788,100 @@ public final class SbtSettings { return new HashSet<>(list); } + private static Set readBatteryBarScenarioSetFromPrefs(SharedPreferences prefs) { + if (prefs == null) { + return Collections.emptySet(); + } + HashSet result = new HashSet<>(); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + if (prefs.getBoolean(batteryBarGeometryOverrideEnabledKey(scenario.id), false)) { + result.add(scenario.id); + } + } + return result; + } + + private static Set readBatteryBarScenarioSetFromBundle(Bundle bundle) { + if (bundle == null) { + return Collections.emptySet(); + } + HashSet result = new HashSet<>(); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + if (bundle.getBoolean(batteryBarGeometryOverrideEnabledKey(scenario.id), false)) { + result.add(scenario.id); + } + } + return result; + } + + private static Map readBatteryBarScenarioMapFromPrefs(SharedPreferences prefs) { + if (prefs == null) { + return Collections.emptyMap(); + } + HashMap result = new HashMap<>(); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + String value = prefs.getString(batteryBarGeometryKey(scenario.id), null); + if (value != null && !value.isEmpty()) { + result.put(scenario.id, value); + } + } + return result; + } + + private static Map readBatteryBarScenarioMapFromBundle(Bundle bundle) { + if (bundle == null) { + return Collections.emptyMap(); + } + HashMap result = new HashMap<>(); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + String value = bundle.getString(batteryBarGeometryKey(scenario.id), null); + if (value != null && !value.isEmpty()) { + result.put(scenario.id, value); + } + } + return result; + } + + private static Set sanitizeBatteryBarScenarioSet(Set source) { + if (source == null || source.isEmpty()) { + return Collections.emptySet(); + } + HashSet result = new HashSet<>(); + for (String id : source) { + if (isBatteryBarScenarioId(id)) { + result.add(id); + } + } + return result.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(result); + } + + private static Map sanitizeBatteryBarScenarioMap(Map source) { + if (source == null || source.isEmpty()) { + return Collections.emptyMap(); + } + HashMap result = new HashMap<>(); + for (Map.Entry entry : source.entrySet()) { + String id = entry.getKey(); + String value = entry.getValue(); + if (isBatteryBarScenarioId(id) && value != null && !value.isEmpty()) { + result.put(id, value); + } + } + return result.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(result); + } + + private static boolean isBatteryBarScenarioId(String id) { + if (id == null) { + return false; + } + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + if (scenario.id.equals(id)) { + return true; + } + } + return false; + } + private static Map readSystemIconBlockedModesFromPrefs(SharedPreferences prefs) { if (prefs == null) { return Collections.emptyMap(); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java index bda1b1c..28d37af 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java @@ -99,6 +99,12 @@ public class SbtSettingsProvider extends ContentProvider { prefs.getInt(SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP, SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT)); out.putInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, prefs.getInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT)); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + String enabledKey = BatteryBarGeometry.enabledKey(scenario); + String valueKey = BatteryBarGeometry.valueKey(scenario); + out.putBoolean(enabledKey, prefs.getBoolean(enabledKey, false)); + out.putString(valueKey, prefs.getString(valueKey, null)); + } out.putInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT)); out.putInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL, @@ -328,6 +334,10 @@ public class SbtSettingsProvider extends ContentProvider { SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtSettings::layoutStatusVerticalOffsetKey, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); + out.putString(SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, + SbtSettings.readSystemIconDualSimSignalMode(prefs.getString( + SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, + SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT))); out.putInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT, prefs.getBoolean( diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java index cde7d93..4a0d095 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/BatteryBarFragment.java @@ -32,6 +32,7 @@ import java.util.LinkedHashSet; import java.util.List; import se.ajpanton.statusbartweak.R; +import se.ajpanton.statusbartweak.shell.settings.BatteryBarGeometry; import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle; import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtSettings; @@ -75,6 +76,8 @@ public final class BatteryBarFragment extends Fragment { MaterialButton addThresholdButton = root.findViewById(R.id.battery_bar_add_threshold_button); LinearLayout thresholdsContainer = root.findViewById(R.id.battery_bar_thresholds_container); View geometrySection = root.findViewById(R.id.battery_bar_geometry_section); + LinearLayout scenarioOverridesContainer = + root.findViewById(R.id.battery_bar_scenario_overrides_container); mappingSlider.setValueFrom(0f); mappingSlider.setValueTo(100f); mappingSlider.setStepSize(1f); @@ -167,6 +170,8 @@ public final class BatteryBarFragment extends Fragment { SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT); + renderScenarioOverrides(scenarioOverridesContainer, prefs); + setSectionEnabled(geometrySection, enabled.isChecked()); mappingSlider.addOnChangeListener((slider, value, fromUser) -> { if (!fromUser) { @@ -347,6 +352,252 @@ public final class BatteryBarFragment extends Fragment { } } + private void renderScenarioOverrides(LinearLayout container, SharedPreferences prefs) { + if (container == null) { + return; + } + container.removeAllViews(); + for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { + addScenarioOverrideRow(container, prefs, scenario, scenarioLabelRes(scenario)); + } + } + + private void addScenarioOverrideRow( + LinearLayout container, + SharedPreferences prefs, + BatteryBarGeometry.Scenario scenario, + int labelRes + ) { + Context context = container.getContext(); + LinearLayout row = new LinearLayout(context); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setOrientation(LinearLayout.HORIZONTAL); + LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + rowParams.topMargin = ViewTreeSupport.dp(context, 8); + row.setLayoutParams(rowParams); + + LinearLayout textColumn = new LinearLayout(context); + textColumn.setOrientation(LinearLayout.VERTICAL); + textColumn.setLayoutParams(new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f)); + + SwitchMaterial enabledSwitch = new SwitchMaterial(context); + enabledSwitch.setText(labelRes); + enabledSwitch.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + textColumn.addView(enabledSwitch); + + TextView summary = new TextView(context); + summary.setTextAppearance(android.R.style.TextAppearance_Material_Body2); + summary.setPadding(ViewTreeSupport.dp(context, 48), 0, 0, 0); + textColumn.addView(summary); + + MaterialButton editButton = new MaterialButton(context, null, com.google.android.material.R.attr.materialButtonOutlinedStyle); + editButton.setText(R.string.battery_bar_edit_override); + + row.addView(textColumn); + row.addView(editButton); + container.addView(row); + bindScenarioOverride(scenario, labelRes, enabledSwitch, editButton, summary, prefs); + } + + private int scenarioLabelRes(BatteryBarGeometry.Scenario scenario) { + if (scenario == BatteryBarGeometry.Scenario.UNLOCKED_LANDSCAPE) { + return R.string.battery_bar_unlocked_landscape_override; + } + if (scenario == BatteryBarGeometry.Scenario.TRANSPARENT_PORTRAIT) { + return R.string.battery_bar_transparent_portrait_override; + } + if (scenario == BatteryBarGeometry.Scenario.TRANSPARENT_LANDSCAPE) { + return R.string.battery_bar_transparent_landscape_override; + } + if (scenario == BatteryBarGeometry.Scenario.FULLSCREEN_PORTRAIT) { + return R.string.battery_bar_fullscreen_portrait_override; + } + if (scenario == BatteryBarGeometry.Scenario.FULLSCREEN_LANDSCAPE) { + return R.string.battery_bar_fullscreen_landscape_override; + } + if (scenario == BatteryBarGeometry.Scenario.LOCKSCREEN) { + return R.string.battery_bar_lockscreen_override; + } + return R.string.battery_bar_unlocked_portrait_override; + } + + private void bindScenarioOverride( + BatteryBarGeometry.Scenario scenario, + int labelRes, + SwitchMaterial enabledSwitch, + MaterialButton editButton, + TextView summary, + SharedPreferences prefs + ) { + if (scenario == null || prefs == null) { + return; + } + String enabledKey = BatteryBarGeometry.enabledKey(scenario); + boolean enabled = prefs.getBoolean(enabledKey, false); + if (enabledSwitch != null) { + enabledSwitch.setChecked(enabled); + enabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences.Editor editor = prefs.edit().putBoolean(enabledKey, isChecked); + if (isChecked && !prefs.contains(BatteryBarGeometry.valueKey(scenario))) { + editor.putString( + BatteryBarGeometry.valueKey(scenario), + globalGeometryFromPrefs(prefs).encode()); + } + editor.apply(); + SbtSettings.ensureReadable(requireContext()); + updateScenarioSummary(scenario, summary, editButton, prefs); + }); + } + if (editButton != null) { + editButton.setOnClickListener(v -> showScenarioGeometryDialog( + scenario, + labelRes, + prefs, + summary, + editButton)); + } + updateScenarioSummary(scenario, summary, editButton, prefs); + } + + private void updateScenarioSummary( + BatteryBarGeometry.Scenario scenario, + TextView summary, + MaterialButton editButton, + SharedPreferences prefs + ) { + if (scenario == null || prefs == null) { + return; + } + boolean enabled = prefs.getBoolean(BatteryBarGeometry.enabledKey(scenario), false); + BatteryBarGeometry geometry = scenarioGeometryFromPrefs(prefs, scenario); + if (summary != null) { + summary.setText(getString( + R.string.battery_bar_override_summary, + geometry.position, + geometry.alignment, + geometry.thicknessDp)); + summary.setAlpha(enabled ? 1f : 0.65f); + } + if (editButton != null) { + editButton.setEnabled(enabled); + } + } + + private BatteryBarGeometry globalGeometryFromPrefs(SharedPreferences prefs) { + return new BatteryBarGeometry( + prefs.getString( + SbtSettings.KEY_BATTERY_BAR_POSITION, + SbtDefaults.BATTERY_BAR_POSITION_DEFAULT), + prefs.getString( + SbtSettings.KEY_BATTERY_BAR_ALIGNMENT, + SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT), + prefs.getInt( + SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP, + SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT)); + } + + private BatteryBarGeometry scenarioGeometryFromPrefs( + SharedPreferences prefs, + BatteryBarGeometry.Scenario scenario + ) { + return BatteryBarGeometry.decode( + prefs.getString(BatteryBarGeometry.valueKey(scenario), null), + globalGeometryFromPrefs(prefs)); + } + + private void showScenarioGeometryDialog( + BatteryBarGeometry.Scenario scenario, + int labelRes, + SharedPreferences prefs, + TextView summary, + MaterialButton editButton + ) { + BatteryBarGeometry current = scenarioGeometryFromPrefs(prefs, scenario); + LinearLayout content = new LinearLayout(requireContext()); + content.setOrientation(LinearLayout.VERTICAL); + int padding = Math.round(20 * getResources().getDisplayMetrics().density); + content.setPadding(padding, 0, padding, 0); + + TextView positionTitle = dialogLabel(R.string.battery_bar_position_title); + content.addView(positionTitle); + RadioGroup positionGroup = new RadioGroup(requireContext()); + RadioButton positionTop = dialogRadio(R.string.battery_bar_position_top); + RadioButton positionBottom = dialogRadio(R.string.battery_bar_position_bottom); + positionGroup.addView(positionTop); + positionGroup.addView(positionBottom); + applyPositionSelection(positionTop, positionBottom, current.position); + content.addView(positionGroup); + + TextView alignmentTitle = dialogLabel(R.string.battery_bar_alignment_title); + alignmentTitle.setPadding(0, padding / 2, 0, 0); + content.addView(alignmentTitle); + RadioGroup alignmentGroup = new RadioGroup(requireContext()); + RadioButton alignmentLtr = dialogRadio(R.string.battery_bar_alignment_ltr); + RadioButton alignmentCenter = dialogRadio(R.string.battery_bar_alignment_center); + RadioButton alignmentRtl = dialogRadio(R.string.battery_bar_alignment_rtl); + alignmentGroup.addView(alignmentLtr); + alignmentGroup.addView(alignmentCenter); + alignmentGroup.addView(alignmentRtl); + applyAlignmentSelection(alignmentLtr, alignmentRtl, alignmentCenter, current.alignment); + content.addView(alignmentGroup); + + EditText thicknessInput = dialogNumberInput(current.thicknessDp); + content.addView(dialogLabel(R.string.battery_bar_thickness_title)); + content.addView(thicknessInput); + + new MaterialAlertDialogBuilder(requireContext()) + .setTitle(getString(R.string.battery_bar_override_dialog_title, getString(labelRes))) + .setView(content) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + BatteryBarGeometry updated = new BatteryBarGeometry( + positionBottom.isChecked() ? "bottom" : "top", + alignmentRtl.isChecked() + ? "rtl" + : alignmentCenter.isChecked() ? "center" : "ltr", + ViewTreeSupport.clamp( + ViewTreeSupport.parseInt(thicknessInput, current.thicknessDp), + SbtDefaults.BATTERY_BAR_THICKNESS_DP_MIN, + SbtDefaults.BATTERY_BAR_THICKNESS_DP_MAX)); + prefs.edit() + .putString(BatteryBarGeometry.valueKey(scenario), updated.encode()) + .apply(); + SbtSettings.ensureReadable(requireContext()); + updateScenarioSummary(scenario, summary, editButton, prefs); + }) + .show(); + } + + private TextView dialogLabel(int resId) { + TextView view = new TextView(requireContext()); + view.setText(resId); + view.setTextAppearance(android.R.style.TextAppearance_Material_Body1); + return view; + } + + private RadioButton dialogRadio(int resId) { + RadioButton radioButton = new RadioButton(requireContext()); + radioButton.setId(View.generateViewId()); + radioButton.setText(resId); + return radioButton; + } + + private EditText dialogNumberInput(int value) { + EditText input = new EditText(requireContext()); + input.setInputType(InputType.TYPE_CLASS_NUMBER); + input.setSingleLine(true); + input.setGravity(Gravity.CENTER); + input.setText(String.valueOf(value)); + return input; + } + private void adjustMapping(SharedPreferences prefs, RangeSlider slider, TextView minLabel, diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java index 11cc229..b523935 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/SystemIconsFragment.java @@ -10,9 +10,11 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.res.ColorStateList; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.NinePatchDrawable; import android.os.Bundle; +import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; @@ -20,6 +22,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.PopupWindow; import android.widget.TextView; import androidx.annotation.NonNull; @@ -40,6 +43,8 @@ public class SystemIconsFragment extends Fragment { private static final String QUICKSTAR_PACKAGE = "com.samsung.android.qstuner"; private static final int TOGGLE_ICON_DP = 24; + private static final int MODE_TOGGLE_COLUMN_DP = 96; + private static final int DROPDOWN_ARROW_DP = 18; private static final List SECTIONS = Arrays.asList( new SystemIconSection( @@ -124,6 +129,7 @@ public class SystemIconsFragment extends Fragment { .getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE); SbtSettings.ensureReadable(requireContext()); Map blockedModes = new HashMap<>(SbtSettings.from(requireContext()).systemIconBlockedModes); + int leadingColumnWidth = leadingColumnWidth(requireContext()); Map sectionContainers = new HashMap<>(); sectionContainers.put(R.string.system_icons_group_connectivity, @@ -139,13 +145,189 @@ public class SystemIconsFragment extends Fragment { continue; } for (SystemIconEntry entry : section.entries) { - sectionContainer.addView(createRow(sectionContainer, prefs, blockedModes, entry)); + sectionContainer.addView(createRow( + sectionContainer, + prefs, + blockedModes, + entry, + leadingColumnWidth)); } } applyLayoutAvailability(root, prefs); return root; } + private int leadingColumnWidth(Context context) { + int toggleColumnWidth = ViewTreeSupport.dp(context, MODE_TOGGLE_COLUMN_DP); + TextView probe = new TextView(context); + probe.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); + float textWidth = 0f; + for (String label : dualSimSignalModeLabels(context)) { + textWidth = Math.max(textWidth, probe.getPaint().measureText(label)); + } + int dropdownWidth = Math.round(textWidth) + + ViewTreeSupport.dp(context, DROPDOWN_ARROW_DP + 4); + return Math.max(toggleColumnWidth, dropdownWidth); + } + + private void bindDualSimSignalMode( + LinearLayout section, + SharedPreferences prefs, + int leadingColumnWidth + ) { + if (section == null || prefs == null) { + return; + } + Context context = section.getContext(); + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + rowParams.topMargin = ViewTreeSupport.dp(context, 4); + section.addView(row, rowParams); + + String[] labels = dualSimSignalModeLabels(context); + String current = SbtSettings.readSystemIconDualSimSignalMode(prefs.getString( + SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, + SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT)); + LinearLayout dropdownCell = new LinearLayout(context); + dropdownCell.setOrientation(LinearLayout.HORIZONTAL); + dropdownCell.setGravity(Gravity.CENTER_VERTICAL); + dropdownCell.setClickable(true); + dropdownCell.setFocusable(true); + LinearLayout.LayoutParams dropdownCellParams = new LinearLayout.LayoutParams( + leadingColumnWidth, + ViewGroup.LayoutParams.WRAP_CONTENT); + row.addView(dropdownCell, dropdownCellParams); + + TextView selected = new TextView(context); + selected.setText(labels[indexForDualSimSignalMode(current)]); + selected.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); + selected.setSingleLine(true); + selected.setEllipsize(TextUtils.TruncateAt.END); + LinearLayout.LayoutParams selectedParams = new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT); + selectedParams.weight = 1f; + dropdownCell.addView(selected, selectedParams); + + TextView arrow = new TextView(context); + arrow.setText("\u25BE"); + arrow.setGravity(Gravity.CENTER); + arrow.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); + dropdownCell.addView(arrow, new LinearLayout.LayoutParams( + ViewTreeSupport.dp(context, DROPDOWN_ARROW_DP), + ViewGroup.LayoutParams.WRAP_CONTENT)); + dropdownCell.setOnClickListener(v -> showDualSimSignalModePopup( + dropdownCell, + selected, + prefs)); + + TextView label = new TextView(context); + label.setText(R.string.system_icons_dual_sim_signal_mode); + label.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); + LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams( + 0, + ViewGroup.LayoutParams.WRAP_CONTENT); + labelParams.weight = 1f; + labelParams.leftMargin = ViewTreeSupport.dp(context, 8); + row.addView(label, labelParams); + } + + private void showDualSimSignalModePopup( + View anchor, + TextView selected, + SharedPreferences prefs + ) { + if (anchor == null || selected == null || prefs == null) { + return; + } + Context context = anchor.getContext(); + LinearLayout content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + int background = MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorSurface, + 0xFF202020); + content.setBackgroundColor(background); + String[] labels = dualSimSignalModeLabels(context); + for (int i = 0; i < labels.length; i++) { + int index = i; + TextView item = new TextView(context); + item.setText(labels[i]); + item.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); + item.setSingleLine(true); + item.setPadding( + 0, + ViewTreeSupport.dp(context, 12), + 0, + ViewTreeSupport.dp(context, 12)); + content.addView(item, new LinearLayout.LayoutParams( + Math.max(anchor.getWidth(), ViewTreeSupport.dp(context, MODE_TOGGLE_COLUMN_DP)), + ViewGroup.LayoutParams.WRAP_CONTENT)); + item.setOnClickListener(v -> { + String mode = dualSimSignalModeForIndex(index); + prefs.edit() + .putString(SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, mode) + .apply(); + SbtSettings.ensureReadable(requireContext()); + selected.setText(labels[index]); + Object popup = content.getTag(); + if (popup instanceof PopupWindow popupWindow) { + popupWindow.dismiss(); + } + }); + } + PopupWindow popupWindow = new PopupWindow( + content, + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + true); + popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); + popupWindow.setOutsideTouchable(true); + popupWindow.setElevation(ViewTreeSupport.dp(context, 8)); + content.setTag(popupWindow); + popupWindow.showAsDropDown(anchor); + } + + private String[] dualSimSignalModeLabels(Context context) { + return new String[] { + context.getString(R.string.system_icons_dual_sim_separate), + context.getString(R.string.system_icons_dual_sim_sim1), + context.getString(R.string.system_icons_dual_sim_sim2), + context.getString(R.string.system_icons_dual_sim_strongest), + context.getString(R.string.system_icons_dual_sim_weakest), + context.getString(R.string.system_icons_dual_sim_data), + context.getString(R.string.system_icons_dual_sim_phone) + }; + } + + private String dualSimSignalModeForIndex(int index) { + return switch (index) { + case 1 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_FIRST; + case 2 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_SECOND; + case 3 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_STRONGEST; + case 4 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_WEAKEST; + case 5 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_DATA; + case 6 -> SbtSettings.SYSTEM_ICON_DUAL_SIM_PHONE; + default -> SbtSettings.SYSTEM_ICON_DUAL_SIM_SEPARATE; + }; + } + + private int indexForDualSimSignalMode(String mode) { + return switch (mode) { + case SbtSettings.SYSTEM_ICON_DUAL_SIM_FIRST -> 1; + case SbtSettings.SYSTEM_ICON_DUAL_SIM_SECOND -> 2; + case SbtSettings.SYSTEM_ICON_DUAL_SIM_STRONGEST -> 3; + case SbtSettings.SYSTEM_ICON_DUAL_SIM_WEAKEST -> 4; + case SbtSettings.SYSTEM_ICON_DUAL_SIM_DATA -> 5; + case SbtSettings.SYSTEM_ICON_DUAL_SIM_PHONE -> 6; + default -> 0; + }; + } + private void applyLayoutAvailability(View root, SharedPreferences prefs) { if (root == null || prefs == null) { return; @@ -168,7 +350,8 @@ public class SystemIconsFragment extends Fragment { private View createRow(ViewGroup parent, SharedPreferences prefs, Map blockedModes, - SystemIconEntry entry) { + SystemIconEntry entry, + int leadingColumnWidth) { Context context = parent.getContext(); LinearLayout row = new LinearLayout(context); row.setOrientation(LinearLayout.HORIZONTAL); @@ -181,7 +364,7 @@ public class SystemIconsFragment extends Fragment { LinearLayout modeStrip = new LinearLayout(context); modeStrip.setOrientation(LinearLayout.HORIZONTAL); LinearLayout.LayoutParams modeStripParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, + leadingColumnWidth, ViewGroup.LayoutParams.WRAP_CONTENT); modeStrip.setLayoutParams(modeStripParams); modeStrip.addView(createModeToggle(context, prefs, blockedModes, entry, @@ -202,6 +385,16 @@ public class SystemIconsFragment extends Fragment { label.setText(context.getString(entry.labelRes)); label.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1); row.addView(label); + if (entry.slots.contains("mobile")) { + LinearLayout container = new LinearLayout(context); + container.setOrientation(LinearLayout.VERTICAL); + container.setLayoutParams(new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + container.addView(row); + bindDualSimSignalMode(container, prefs, leadingColumnWidth); + return container; + } return row; } diff --git a/app/src/main/res/layout/fragment_battery_bar.xml b/app/src/main/res/layout/fragment_battery_bar.xml index 2d2588d..c1ee861 100644 --- a/app/src/main/res/layout/fragment_battery_bar.xml +++ b/app/src/main/res/layout/fragment_battery_bar.xml @@ -299,6 +299,45 @@ + + + + + + + + + + + + Hide status icons Status-icon rules for the custom layout engine. Icons represent, in order:\nAOD, Lockscreen, Unlocked. + Dual-SIM signal icons + Separate + SIM1 + SIM2 + Strongest + Weakest + Data SIM + Phone SIM Connectivity Notifications System @@ -135,6 +143,18 @@ Thickness (dp) Edge offset (dp) + Scenario overrides + Optional geometry overrides. Disabled scenarios use the default geometry above. + Unlocked portrait + Unlocked landscape + Transparent statusbar portrait + Transparent statusbar landscape + Fullscreen portrait + Fullscreen landscape + Lockscreen + Edit + Edit %1$s geometry + %1$s, %2$s, %3$ddp thick Battery mapping ...0 at %1$d%% ...full at %1$d%%