38 Commits
Author SHA1 Message Date
ajp_anton a5deb9ff10 Release 0.8 2026-08-20 23:28:07 +00:00
ajp_anton 07c3444642 Replace Python layout prototype with editable playground 2026-08-20 23:27:42 +00:00
ajp_anton 1d5bcb4e39 Classify call chips by SystemUI structure 2026-08-20 21:20:10 +00:00
ajp_anton 51d86e8ad1 Clear ended call chips from status layout 2026-08-08 04:41:52 +00:00
ajp_anton 394e1faf24 Add configurable statusbar clock outline 2026-07-29 12:56:56 +00:00
ajp_anton fa3341cbec Release 0.7 2026-07-15 23:54:22 +00:00
ajp_anton 988fbcebcd Remove redundant AOD capability wrappers 2026-07-15 23:16:50 +00:00
ajp_anton f20e214076 Remove unused battery bar geometry parameter 2026-07-15 19:21:33 +00:00
ajp_anton 50fc608eae Share default settings decoding 2026-07-15 19:20:16 +00:00
ajp_anton 898e33ccfa Consolidate settings decoding 2026-07-15 19:18:57 +00:00
ajp_anton 38b1602fe8 Streamline battery bar rendering and cleanup 2026-07-15 18:00:18 +00:00
ajp_anton 560f4e19ba Handle app surfaces over lockscreen 2026-07-11 12:59:04 +00:00
ajp_anton 682782c232 Reduce draw-only statusbar work 2026-07-11 12:12:28 +00:00
ajp_anton 25e28c8501 Release 0.6 2026-07-09 02:54:38 +00:00
ajp_anton 624b5ee6b7 Prevent privacy overlay statusbar reveal 2026-07-09 02:54:12 +00:00
ajp_anton 3ebebb5218 Expand emulator compatibility coverage 2026-07-08 14:59:34 +00:00
ajp_anton 18f30fce57 Keep unlock transition in unlocked scene 2026-07-08 03:20:06 +00:00
ajp_anton 352b7ab443 Limit non-Samsung notification style options 2026-07-08 02:38:30 +00:00
ajp_anton 66f3c2e206 Gate Samsung-specific runtime hooks 2026-07-08 01:59:47 +00:00
ajp_anton 321048f787 Centralize SystemUI capability checks 2026-07-08 01:58:03 +00:00
ajp_anton 4f57e9e33c Add experimental generic AOD compatibility 2026-07-08 01:56:11 +00:00
ajp_anton 282e9a9283 Clarify AOSP AOD compatibility 2026-07-07 15:09:02 +00:00
ajp_anton 87de1c45e6 Keep Samsung lockscreen layout on statusbar roots 2026-07-07 14:57:01 +00:00
ajp_anton 9083a3038a Support mixed-case notification packages 2026-07-07 09:41:38 +00:00
ajp_anton 38b937ca3e Mark AOSP 36 dot mode unsupported 2026-07-01 20:36:43 +00:00
ajp_anton bbdbd50d84 Expand verified AOSP 36 coverage 2026-07-01 20:34:41 +00:00
ajp_anton 86eb5fec7b Verify AOSP 36 lockscreen layout 2026-07-01 20:24:49 +00:00
ajp_anton 0ab20825d0 Update AOSP 36 compatibility status 2026-07-01 20:23:44 +00:00
ajp_anton 212dd5110c Improve AOSP stock statusbar suppression 2026-07-01 20:12:38 +00:00
ajp_anton 7be5f64262 Add statusbar test harness app 2026-07-01 19:39:49 +00:00
ajp_anton 5df8bd9e78 Expand AOSP36 compatibility evidence 2026-07-01 14:37:59 +00:00
ajp_anton 4964b39d01 Support AOSP lockscreen layout roots 2026-07-01 13:37:40 +00:00
ajp_anton 42449f8c6a Add initial AOSP statusbar compatibility 2026-07-01 12:52:44 +00:00
ajp_anton 861ffd52d5 Document compatibility test matrix 2026-07-01 11:29:04 +00:00
ajp_anton 75861e2480 Release 0.5 2026-06-30 19:44:25 +00:00
ajp_anton 9dfca417bd Mirror stock statusbar transitions 2026-06-30 18:16:51 +00:00
ajp_anton 18f1c6f20f Use statusbar insets for fullscreen battery curves 2026-06-24 23:31:20 +00:00
ajp_anton 2fe90ce3fc Reduce lockscreen stock surface cleanup work 2026-06-23 18:05:36 +00:00
66 changed files with 3861 additions and 1080 deletions
+1
View File
@@ -8,6 +8,7 @@
/.gradle-tmp/ /.gradle-tmp/
/build/ /build/
/app/build/ /app/build/
/*/build/
/.externalNativeBuild/ /.externalNativeBuild/
/.cxx/ /.cxx/
/captures/ /captures/
+96 -24
View File
@@ -1,15 +1,23 @@
# StatusBarTweak # StatusBarTweak
StatusBarTweak is an Xposed/LSPosed module for Samsung SystemUI. It focuses on StatusBarTweak is an Xposed/LSPosed module originally built for Samsung
making the status bar, lockscreen, and Always On Display more configurable while SystemUI. It focuses on making the status bar, lockscreen, and Always On
still keeping Samsung's own icons and visual style where possible. Display more configurable while still keeping the system's own icons and visual
style where possible.
The project is currently tested only on a Samsung Galaxy Z Fold5 running One UI The project is physically tested only on a Samsung Galaxy Z Fold5 running One UI
8.0 / Android 16. Expect device- and One UI-version-specific behaviour. 8.0 / Android 16. Other SystemUI builds have emulator-tested support as shown
in the compatibility matrix.
## AI Assistance
This project was developed with help from AI-assisted coding tools. The design
decisions, physical device testing, and final behaviour are still manually reviewed.
## What It Does ## What It Does
- Custom status bar layout for the unlocked screen, lockscreen, and AOD. - Custom status bar layout for the unlocked screen, lockscreen, and AOD where
supported by the SystemUI build.
- Independent placement of clock, carrier text, notification icons, status - Independent placement of clock, carrier text, notification icons, status
icons, and Samsung status chips. icons, and Samsung status chips.
- Separate notification display handling for Samsung's cards, icons, and dot - Separate notification display handling for Samsung's cards, icons, and dot
@@ -41,31 +49,64 @@ It also supports lightweight markup:
- Pipe alignment with `|`, useful for aligning split clock rows around a camera - Pipe alignment with `|`, useful for aligning split clock rows around a camera
cutout. cutout.
- Colour variants and reverse Polish notation colour math for light/dark status - Colour variants and reverse Polish notation colour math for light/dark status
bar states, for example `color(#batterybar 1.2 * ; invRGB 0.5 *RGB)`. bar states, for example `color(#batterybar 1.2 *RGB ; #batterybar 0.5 *RGB)`, which uses the batterybar's dynamic colour as its base. For the bright tint, it brightens it up some more, while for the dark tint, it is darkened.
The in-app clock page has the full syntax help and examples. The in-app clock page has the full syntax help and examples.
## Requirements ## Requirements
- A rooted Samsung device. - A rooted Android 14+ device or emulator. Actual feature support depends on
- A modern Xposed implementation such as LSPosed. the SystemUI implementation; see the compatibility matrix.
- A modern Xposed implementation such as LSPosed or another libxposed-compatible runtime.
- Xposed API 101 or newer. - Xposed API 101 or newer.
- A Samsung/One UI version close to One UI 8.0 / Android 16. - SystemUI access for the module scope.
This module hooks Samsung SystemUI internals. It is not expected to work on The module is developed and manually tested on a Samsung Galaxy Z Fold5 running
non-Samsung SystemUI, and updates to One UI may require fixes. One UI 8.0 / Android 16. Non-Samsung builds have emulator-tested support as
shown below, but hardware-dependent behavior remains experimental unless it has
also been verified on physical hardware.
## Device Compatibility ## Device Compatibility
StatusBarTweak is Samsung-specific in its current form. Some lower-level ideas, Some behavior cannot be fully verified on emulators. Emulator testing can still
such as drawing a custom battery bar or parsing custom clock text, could be verify whether hooks load, whether SystemUI stays stable, whether settings apply,
ported elsewhere. The layout engine itself depends on Samsung's SystemUI, whether excessive background work appears in logs, and whether the steady state
lockscreen, AOD service, notification display modes, status icons, and status looks the way it should.
chip implementation.
Plain Android or other OEM skins would need separate hooks and probably a `⚠️ Experimental` means best-effort support exists, but the emulator cannot
different source-collection layer. They should be treated as unsupported rather expose the required hardware or state for real verification.
than untested-compatible.
| Feature | Galaxy Z Fold5<br>One UI 8 / Android 16 | AOSP API 34<br>Android 14 | AOSP API 35<br>Android 15 | AOSP API 36<br>Android 16 | Google APIs API 35<br>Android 15 | Google APIs API 36<br>Android 16 | LineageOS 22<br>Android 15 | LineageOS 23<br>Android 16 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Module loads in SystemUI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Settings app opens and writes settings | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Custom unlocked statusbar layout | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Custom lockscreen statusbar layout | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Custom AOD statusbar layout | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
| Notification icons mode | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
| Notification cards mode | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Notification dot mode and read-state behavior | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Hide notification icons by app | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
| Hide status icons | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
| Dual-SIM status icon handling | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
| Samsung status chips: media/navigation/call | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Samsung charging chip handling | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Privacy indicator stability | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Custom statusbar clock text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Custom drawer clock/date text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Stored clock pattern previews | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Battery bar: base statusbar modes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Battery bar: fullscreen and transparent statusbar modes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Battery bar: curved top-edge geometry | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
| Battery bar threshold colours and colour math | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Camera cutout and under-display-camera handling | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
| Rotation and fold/unfold handling | ✅ | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified |
| Drawer scrolling and background-work performance | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Debug visualizers and debug notifications | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
Current emulator targets use rooted LSPosed snapshots. They are useful for
compatibility work, but hardware-specific entries remain marked as experimental
when the emulator cannot expose the required state.
## Build ## Build
@@ -81,8 +122,39 @@ Release builds require local signing configuration:
./gradlew assembleRelease ./gradlew assembleRelease
``` ```
## Development Note ## Test Harness
This project has been coded with the help of AI as an implementation aid. The The repository also includes a small companion app for compatibility testing.
design decisions, device testing, and final behaviour are still manually It is not required for normal use of the module. It creates repeatable
reviewed. statusbar scenarios such as opaque, transparent, fullscreen, privacy-indicator,
and notification states:
```bash
./gradlew :test-harness:assembleDebug
adb install -r test-harness/build/outputs/apk/debug/test-harness-debug.apk
adb shell am start -n se.ajpanton.statusbartweak.harness/.MainActivity
```
Automation can launch scenarios directly:
```bash
adb shell am start -n se.ajpanton.statusbartweak.harness/.ScenarioActivity --es scenario fullscreen_landscape
adb shell am start -n se.ajpanton.statusbartweak.harness/.MainActivity --es command post_normal
```
Useful scenario values include `opaque_light`, `opaque_dark`, `transparent`,
`fullscreen_portrait`, `fullscreen_landscape`, `camera`, `microphone`, and
`camera_microphone`.
Useful command values include `post_normal`, `post_ongoing`, `post_grouped`,
`update`, `start_chronometer`, `start_churn`, `stop_churn`, `start_media`,
`start_foreground`, `stop_service`, and `clear`.
Battery charging state is controlled by Android system commands rather than by
the harness APK:
```bash
adb shell cmd battery set status 2
adb shell cmd battery set level 80
adb shell cmd battery reset
```
+1 -1
View File
@@ -69,7 +69,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "se.ajpanton.statusbartweak" applicationId = "se.ajpanton.statusbartweak"
minSdk = 35 minSdk = 34
targetSdk = 36 targetSdk = 36
val buildVersionName = project.findProperty("sbtVersionName")?.toString() val buildVersionName = project.findProperty("sbtVersionName")?.toString()
?: fallbackVersionName ?: fallbackVersionName
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.module;
import io.github.libxposed.api.XposedInterface; import io.github.libxposed.api.XposedInterface;
import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.XposedModuleInterface;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.features.StandaloneFeatureRegistry; import se.ajpanton.statusbartweak.runtime.features.StandaloneFeatureRegistry;
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository; import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
@@ -15,11 +16,6 @@ import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
*/ */
public final class ModuleCoordinator { public final class ModuleCoordinator {
private static final String PACKAGE_ANDROID = "android";
private static final String PACKAGE_SYSTEM = "system";
private static final String PACKAGE_SYSTEMUI = "com.android.systemui";
private static final String PACKAGE_AODSERVICE = "com.samsung.android.app.aodservice";
private final RuntimeContext runtimeContext; private final RuntimeContext runtimeContext;
private final StandaloneFeatureRegistry standaloneFeatureRegistry; private final StandaloneFeatureRegistry standaloneFeatureRegistry;
@@ -33,10 +29,7 @@ public final class ModuleCoordinator {
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) { public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
String processName = param.getProcessName(); String processName = param.getProcessName();
if (!PACKAGE_SYSTEMUI.equals(processName) if (!SystemUiCapabilities.isHandledProcessName(processName)) {
&& !PACKAGE_ANDROID.equals(processName)
&& !PACKAGE_SYSTEM.equals(processName)
&& !PACKAGE_AODSERVICE.equals(processName)) {
return; return;
} }
standaloneFeatureRegistry.onModuleLoaded(param); standaloneFeatureRegistry.onModuleLoaded(param);
@@ -54,9 +47,6 @@ public final class ModuleCoordinator {
} }
private static boolean shouldHandlePackage(String packageName) { private static boolean shouldHandlePackage(String packageName) {
return PACKAGE_SYSTEMUI.equals(packageName) return SystemUiCapabilities.isHandledPackageName(packageName);
|| PACKAGE_ANDROID.equals(packageName)
|| PACKAGE_SYSTEM.equals(packageName)
|| PACKAGE_AODSERVICE.equals(packageName);
} }
} }
@@ -0,0 +1,66 @@
package se.ajpanton.statusbartweak.platform;
import android.os.Build;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
public final class SystemUiCapabilities {
public static final String PACKAGE_ANDROID = "android";
public static final String PACKAGE_SYSTEM = "system";
public static final String PACKAGE_SYSTEMUI = "com.android.systemui";
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() {
}
public static boolean isSamsungOneUi() {
return SAMSUNG_ONE_UI;
}
public static boolean supportsLockscreenShadeWindowRoot() {
return !isSamsungOneUi();
}
public static boolean supportsSamsungAodServiceProcess() {
return isSamsungOneUi();
}
public static boolean supportsSamsungAodMiscControllers() {
return isSamsungOneUi();
}
public static boolean supportsSamsungStatusChipHooks() {
return isSamsungOneUi();
}
public static boolean supportsSamsungNotificationDisplayStyles() {
return isSamsungOneUi();
}
public static boolean isSystemUiPackageName(String packageName) {
return PACKAGE_SYSTEMUI.equals(packageName);
}
public static boolean isHandledPackageName(String packageName) {
return PACKAGE_SYSTEMUI.equals(packageName)
|| PACKAGE_ANDROID.equals(packageName)
|| PACKAGE_SYSTEM.equals(packageName)
|| (supportsSamsungAodServiceProcess()
&& PACKAGE_SAMSUNG_AOD_SERVICE.equals(packageName));
}
public static boolean isHandledProcessName(String processName) {
return isHandledPackageName(processName);
}
public static boolean supportsExperimentalGenericAodStatusbarRoot(SbtSettings settings) {
return !isSamsungOneUi()
&& settings != null
&& settings.debugExperimentalAodStatusbarRoot;
}
public static boolean treatsUnknownAodNotificationStyleAsIcons(SbtSettings settings) {
return supportsExperimentalGenericAodStatusbarRoot(settings);
}
}
@@ -80,6 +80,10 @@ public final class NotificationKeyReflection {
return null; return null;
} }
String normalized = key.trim(); String normalized = key.trim();
String pipePackage = packageFromPipeNotificationKey(normalized);
if (pipePackage != null) {
return pipePackage;
}
int suffix = normalized.indexOf('@'); int suffix = normalized.indexOf('@');
if (suffix >= 0) { if (suffix >= 0) {
normalized = normalized.substring(0, suffix); normalized = normalized.substring(0, suffix);
@@ -91,6 +95,19 @@ public final class NotificationKeyReflection {
return AppIconRules.isValidPackageName(normalized) ? normalized : null; return AppIconRules.isValidPackageName(normalized) ? normalized : null;
} }
private static String packageFromPipeNotificationKey(String key) {
int firstPipe = key.indexOf('|');
if (firstPipe < 0) {
return null;
}
int secondPipe = key.indexOf('|', firstPipe + 1);
if (secondPipe <= firstPipe + 1) {
return null;
}
String packageName = key.substring(firstPipe + 1, secondPipe);
return AppIconRules.isValidPackageName(packageName) ? packageName : null;
}
private static String nonEmptyStringFromMethod(Object target, String methodName) { private static String nonEmptyStringFromMethod(Object target, String methodName) {
Object value = ReflectionSupport.invokeMethod(target, methodName); Object value = ReflectionSupport.invokeMethod(target, methodName);
return value instanceof String string && !string.isEmpty() ? string : null; return value instanceof String string && !string.isEmpty() ? string : null;
@@ -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,8 +8,9 @@ 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) { if (view == null || view.getId() == View.NO_ID || (view.getId() >>> 24) == 0) {
return ""; return "";
} }
try { try {
@@ -1,7 +1,7 @@
package se.ajpanton.statusbartweak.runtime.features; package se.ajpanton.statusbartweak.runtime.features;
import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.XposedModuleInterface;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings; import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
/** /**
* Small independent runtime slice. * Small independent runtime slice.
@@ -15,7 +15,8 @@ public interface RuntimeFeature {
String getName(); String getName();
static boolean isSystemUiPackage(XposedModuleInterface.PackageReadyParam param) { static boolean isSystemUiPackage(XposedModuleInterface.PackageReadyParam param) {
return param != null && SbtSettings.PACKAGE_SYSTEMUI.equals(param.getPackageName()); return param != null
&& SystemUiCapabilities.isSystemUiPackageName(param.getPackageName());
} }
default void onModuleLoaded( default void onModuleLoaded(
@@ -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(
@@ -434,10 +422,12 @@ final class BatteryBarController {
) { ) {
BatteryBarView barView = ensureBatteryBarView(rootGroup); BatteryBarView barView = ensureBatteryBarView(rootGroup);
layoutBatteryBarView(rootGroup, barView); layoutBatteryBarView(rootGroup, barView);
barView.update(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());
@@ -461,7 +452,7 @@ final class BatteryBarController {
return; return;
} }
pendingOverlayRetries.remove(root); pendingOverlayRetries.remove(root);
overlay.view.update(overlay.host, settings, geometry, batteryLevel, plugged, color); overlay.view.update(overlay.host, root, settings, geometry, batteryLevel, plugged, color);
if (!signature.equals(lastOverlaySignatures.get(root))) { if (!signature.equals(lastOverlaySignatures.get(root))) {
overlay.updateLayout(bounds.width, bounds.height); overlay.updateLayout(bounds.width, bounds.height);
lastOverlaySignatures.put(root, signature); lastOverlaySignatures.put(root, signature);
@@ -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;
@@ -1039,6 +1058,7 @@ final class BatteryBarController {
private final Path drawPath = new Path(); private final Path drawPath = new Path();
private final PathMeasure pathMeasure = new PathMeasure(); private final PathMeasure pathMeasure = new PathMeasure();
private ViewGroup host; private ViewGroup host;
private View roundedCornerSource;
private SbtSettings settings; private SbtSettings settings;
private BatteryBarGeometry geometry; private BatteryBarGeometry geometry;
private int batteryLevel = -1; private int batteryLevel = -1;
@@ -1053,6 +1073,7 @@ final class BatteryBarController {
void update( void update(
ViewGroup host, ViewGroup host,
View roundedCornerSource,
SbtSettings settings, SbtSettings settings,
BatteryBarGeometry geometry, BatteryBarGeometry geometry,
int batteryLevel, int batteryLevel,
@@ -1060,6 +1081,7 @@ final class BatteryBarController {
int color int color
) { ) {
this.host = host; this.host = host;
this.roundedCornerSource = roundedCornerSource;
this.settings = settings; this.settings = settings;
this.geometry = geometry; this.geometry = geometry;
this.batteryLevel = batteryLevel; this.batteryLevel = batteryLevel;
@@ -1226,7 +1248,12 @@ final class BatteryBarController {
} }
private float resolveTopCornerRadius(float fallback) { private float resolveTopCornerRadius(float fallback) {
WindowInsets insets = getRootWindowInsets(); WindowInsets insets = roundedCornerSource != null
? roundedCornerSource.getRootWindowInsets()
: null;
if (insets == null) {
insets = getRootWindowInsets();
}
if (insets == null) { if (insets == null) {
return fallback; return fallback;
} }
@@ -1,6 +1,7 @@
package se.ajpanton.statusbartweak.runtime.features.chips; package se.ajpanton.statusbartweak.runtime.features.chips;
import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.XposedModuleInterface;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature; import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
@@ -32,6 +33,9 @@ public final class StatusChipsFeature implements RuntimeFeature {
} }
private void installController(RuntimeContext context, ClassLoader classLoader) { private void installController(RuntimeContext context, ClassLoader classLoader) {
if (!SystemUiCapabilities.supportsSamsungStatusChipHooks()) {
return;
}
if (statusChipHider == null) { if (statusChipHider == null) {
statusChipHider = new StatusChipHider(context); statusChipHider = new StatusChipHider(context);
} }
@@ -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;
@@ -38,8 +38,10 @@ import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
final class StockClockController { final class StockClockController {
private static final String CLOCK_CLASS_NAME = private static final String[] CLOCK_CLASS_NAMES = {
"com.android.systemui.statusbar.policy.QSClockIndicatorView"; "com.android.systemui.statusbar.policy.QSClockIndicatorView",
"com.android.systemui.statusbar.policy.Clock"
};
private final RuntimeContext runtimeContext; private final RuntimeContext runtimeContext;
private final Set<TextView> trackedClocks = private final Set<TextView> trackedClocks =
@@ -64,8 +66,25 @@ final class StockClockController {
return; return;
} }
registerLayoutModeListenerIfNeeded(); registerLayoutModeListenerIfNeeded();
Class<?> clockClass = ReflectionSupport.requireClass(CLOCK_CLASS_NAME, classLoader); ArrayList<Class<?>> clockClasses = new ArrayList<>();
for (String className : CLOCK_CLASS_NAMES) {
Class<?> clockClass = ReflectionSupport.findClassIfExists(className, classLoader);
if (clockClass != null) {
clockClasses.add(clockClass);
}
}
if (clockClasses.isEmpty()) {
hooksInstalled = true;
return;
}
XposedInterface framework = runtimeContext.getFramework(); XposedInterface framework = runtimeContext.getFramework();
for (Class<?> clockClass : clockClasses) {
hookClockClass(framework, clockClass);
}
hooksInstalled = true;
}
private void hookClockClass(XposedInterface framework, Class<?> clockClass) {
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> { XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
Object target = chain.getThisObject(); Object target = chain.getThisObject();
@@ -104,7 +123,7 @@ final class StockClockController {
} }
return result; return result;
}); });
XposedHookSupport.hookAllMethods(framework, clockClass, "notifyTimeChanged", chain -> { XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "notifyTimeChanged", chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
Object target = chain.getThisObject(); Object target = chain.getThisObject();
if (!(target instanceof TextView)) { if (!(target instanceof TextView)) {
@@ -123,6 +142,14 @@ final class StockClockController {
trackAndApply(clockView, bellSound); trackAndApply(clockView, bellSound);
return result; return result;
}); });
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "updateClock", chain -> {
Object result = chain.proceed();
Object target = chain.getThisObject();
if (target instanceof TextView clockView) {
trackAndApply(clockView, null);
}
return result;
});
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setTextColor", chain -> { XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setTextColor", chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
if (chain.getThisObject() instanceof TextView clockView) { if (chain.getThisObject() instanceof TextView clockView) {
@@ -137,7 +164,6 @@ final class StockClockController {
} }
return result; return result;
}); });
hooksInstalled = true;
} }
private void reapplyClockIfCustomOrOwned(TextView clockView) { private void reapplyClockIfCustomOrOwned(TextView clockView) {
@@ -162,6 +188,9 @@ final class StockClockController {
} }
private void trackAndApply(TextView clockView, Object bellSound) { private void trackAndApply(TextView clockView, Object bellSound) {
if (isNotificationShadeDescendant(clockView)) {
return;
}
trackedClocks.add(clockView); trackedClocks.add(clockView);
registerReceiverIfNeeded(clockView.getContext()); registerReceiverIfNeeded(clockView.getContext());
if (bellSound != null) { if (bellSound != null) {
@@ -219,6 +248,10 @@ final class StockClockController {
} }
private void applyClockText(TextView clockView) { private void applyClockText(TextView clockView) {
if (isNotificationShadeDescendant(clockView)) {
restoreOwnedClock(clockView);
return;
}
SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext()); SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext());
boolean layoutEnabled = settings.clockEnabled; boolean layoutEnabled = settings.clockEnabled;
if (layoutEnabled) { if (layoutEnabled) {
@@ -330,6 +363,18 @@ final class StockClockController {
|| (settings.clockCustomFormatEnabled && !pattern.isEmpty()); || (settings.clockCustomFormatEnabled && !pattern.isEmpty());
} }
private boolean isNotificationShadeDescendant(View view) {
View current = view;
while (current != null) {
if (current.getClass().getName().contains("NotificationShadeWindowView")) {
return true;
}
Object parent = current.getParent();
current = parent instanceof View ? (View) parent : null;
}
return false;
}
private void applyClockLineMode(TextView clockView) { private void applyClockLineMode(TextView clockView) {
clockView.setSingleLine(true); clockView.setSingleLine(true);
clockView.setHorizontallyScrolling(false); clockView.setHorizontallyScrolling(false);
@@ -1,6 +1,7 @@
package se.ajpanton.statusbartweak.runtime.features.misc; package se.ajpanton.statusbartweak.runtime.features.misc;
import io.github.libxposed.api.XposedModuleInterface; import io.github.libxposed.api.XposedModuleInterface;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature; import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
@@ -31,6 +32,9 @@ public final class MiscFeature implements RuntimeFeature {
} }
private void installControllers(RuntimeContext context, ClassLoader classLoader) { private void installControllers(RuntimeContext context, ClassLoader classLoader) {
if (!SystemUiCapabilities.supportsSamsungAodMiscControllers()) {
return;
}
if (aodCallVisibilityController == null) { if (aodCallVisibilityController == null) {
aodCallVisibilityController = new AodCallVisibilityController(context); aodCallVisibilityController = new AodCallVisibilityController(context);
} }
@@ -43,8 +43,11 @@ final class StockUnlockedNotificationIconsController {
if (hooksInstalled || classLoader == null) { if (hooksInstalled || classLoader == null) {
return; return;
} }
Class<?> containerClass = ReflectionSupport.requireClass(CONTAINER_CLASS_NAME, classLoader); Class<?> containerClass = ReflectionSupport.findClassIfExists(CONTAINER_CLASS_NAME, classLoader);
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> { if (containerClass == null) {
return;
}
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
if (chain.getThisObject() instanceof View view) { if (chain.getThisObject() instanceof View view) {
if (shouldManageContainer(view)) { if (shouldManageContainer(view)) {
@@ -54,7 +57,7 @@ final class StockUnlockedNotificationIconsController {
} }
return result; return result;
}); });
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
if (chain.getThisObject() instanceof View view) { if (chain.getThisObject() instanceof View view) {
if (shouldManageContainer(view)) { if (shouldManageContainer(view)) {
trackContainer(view); trackContainer(view);
@@ -63,7 +66,7 @@ final class StockUnlockedNotificationIconsController {
} }
return chain.proceed(); return chain.proceed();
}); });
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onLayout", chain -> { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
if (chain.getThisObject() instanceof View view) { if (chain.getThisObject() instanceof View view) {
if (shouldManageContainer(view)) { if (shouldManageContainer(view)) {
@@ -75,7 +78,7 @@ final class StockUnlockedNotificationIconsController {
} }
return result; return result;
}); });
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
if (!(chain.getThisObject() instanceof View view)) { if (!(chain.getThisObject() instanceof View view)) {
return chain.proceed(); return chain.proceed();
} }
@@ -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);
}
}
}
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.runtime.hooks;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -34,6 +35,9 @@ public final class XposedHookSupport {
if (!methodName.equals(method.getName())) { if (!methodName.equals(method.getName())) {
continue; continue;
} }
if (Modifier.isAbstract(method.getModifiers())) {
continue;
}
method.setAccessible(true); method.setAccessible(true);
handles.add(framework.hook(method).intercept(hooker)); handles.add(framework.hook(method).intercept(hooker));
} }
@@ -59,6 +63,9 @@ public final class XposedHookSupport {
if (!methodName.equals(method.getName())) { if (!methodName.equals(method.getName())) {
continue; continue;
} }
if (Modifier.isAbstract(method.getModifiers())) {
continue;
}
method.setAccessible(true); method.setAccessible(true);
handles.add(framework.hook(method).intercept(hooker)); handles.add(framework.hook(method).intercept(hooker));
} }
@@ -83,4 +90,20 @@ public final class XposedHookSupport {
} }
return handles; return handles;
} }
public static List<XposedInterface.HookHandle> hookAllConstructorsIfExists(
XposedInterface framework,
Class<?> type,
XposedInterface.Hooker hooker
) {
Objects.requireNonNull(framework, "framework");
Objects.requireNonNull(type, "type");
Objects.requireNonNull(hooker, "hooker");
List<XposedInterface.HookHandle> handles = new ArrayList<>();
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
constructor.setAccessible(true);
handles.add(framework.hook(constructor).intercept(hooker));
}
return handles;
}
} }
@@ -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,15 +26,17 @@ 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;
} }
void install(Class<?> darkIconDispatcherClass) { void install(Class<?> darkIconDispatcherClass) {
XposedHookSupport.hookAllConstructors( XposedHookSupport.hookAllConstructorsIfExists(
runtimeContext.getFramework(), runtimeContext.getFramework(),
darkIconDispatcherClass, darkIconDispatcherClass,
chain -> { chain -> {
@@ -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);
@@ -10,6 +10,7 @@ public final class ModeStateRepository {
private SceneKey activeScene = SceneKey.unlocked(); private SceneKey activeScene = SceneKey.unlocked();
private boolean systemUiStatusBarStateKnown; private boolean systemUiStatusBarStateKnown;
private int systemUiStatusBarState; private int systemUiStatusBarState;
private boolean keyguardGoingAway;
private boolean layoutEnabled; private boolean layoutEnabled;
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>(); private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
private final List<SceneListener> sceneListeners = new ArrayList<>(); private final List<SceneListener> sceneListeners = new ArrayList<>();
@@ -27,6 +28,14 @@ public final class ModeStateRepository {
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2; return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
} }
public synchronized boolean isKeyguardGoingAway() {
return keyguardGoingAway;
}
public synchronized void setKeyguardGoingAway(boolean goingAway) {
keyguardGoingAway = goingAway;
}
public synchronized boolean isLayoutEnabled() { public synchronized boolean isLayoutEnabled() {
return layoutEnabled; return layoutEnabled;
} }
@@ -23,8 +23,10 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
"com.android.systemui.statusbar.StatusBarStateControllerImpl"; "com.android.systemui.statusbar.StatusBarStateControllerImpl";
private static final String CLASS_KEYGUARD_STATE_CONTROLLER = private static final String CLASS_KEYGUARD_STATE_CONTROLLER =
"com.android.systemui.statusbar.policy.KeyguardStateControllerImpl"; "com.android.systemui.statusbar.policy.KeyguardStateControllerImpl";
private static final int STATE_SHADE = 0;
private boolean installed; private boolean installed;
private boolean keyguardGoingAway;
@Override @Override
public String getName() { public String getName() {
@@ -78,8 +80,14 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
"notifyKeyguardGoingAway", "notifyKeyguardGoingAway",
chain -> { chain -> {
Object result = chain.proceed(); Object result = chain.proceed();
if (firstBooleanArg(chain.getArgs())) { Boolean goingAway = firstBooleanArg(chain.getArgs());
if (Boolean.TRUE.equals(goingAway)) {
keyguardGoingAway = true;
context.getModeStateRepository().setKeyguardGoingAway(true);
context.getModeStateRepository().setUnlocked(); context.getModeStateRepository().setUnlocked();
} else if (Boolean.FALSE.equals(goingAway)) {
keyguardGoingAway = false;
context.getModeStateRepository().setKeyguardGoingAway(false);
} }
return result; return result;
}); });
@@ -92,14 +100,24 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
Object rawState = ReflectionSupport.getFieldValue(controller, "mState"); Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
int state = rawState instanceof Integer integer ? integer : 0; int state = rawState instanceof Integer integer ? integer : 0;
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false); boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
if (keyguardGoingAway && !dozing) {
if (state == STATE_SHADE) {
keyguardGoingAway = false;
context.getModeStateRepository().setKeyguardGoingAway(false);
} else {
state = STATE_SHADE;
}
}
context.getModeStateRepository().setSystemUiStatusBarState( context.getModeStateRepository().setSystemUiStatusBarState(
state, state,
dozing, dozing,
SystemNotificationDisplayStyleDetector.read(systemContext(controller))); SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
} }
private boolean firstBooleanArg(List<Object> args) { private Boolean firstBooleanArg(List<Object> args) {
return args != null && !args.isEmpty() && Boolean.TRUE.equals(args.get(0)); return args != null && !args.isEmpty() && args.get(0) instanceof Boolean bool
? bool
: null;
} }
private Context systemContext(Object controller) { private Context systemContext(Object controller) {
@@ -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;
}
}
@@ -119,6 +119,17 @@ public final class OwnedIconHostManager {
} }
} }
public void setUnlockedHostsTransform(
View root,
float alpha,
float translationX,
float translationY
) {
for (String tag : UNLOCKED_HOST_ORDER) {
setHostTransform(root, tag, alpha, translationX, translationY);
}
}
public void bringUnlockedHostsToFront(View root) { public void bringUnlockedHostsToFront(View root) {
if (!(root instanceof ViewGroup parent)) { if (!(root instanceof ViewGroup parent)) {
return; return;
@@ -169,6 +180,31 @@ public final class OwnedIconHostManager {
} }
} }
private void setHostTransform(
View root,
String tag,
float alpha,
float translationX,
float translationY
) {
if (!(root instanceof ViewGroup parent) || tag == null) {
return;
}
FrameLayout host = findDirectHost(parent, tag);
if (host == null) {
return;
}
if (host.getAlpha() != alpha) {
host.setAlpha(alpha);
}
if (host.getTranslationX() != translationX) {
host.setTranslationX(translationX);
}
if (host.getTranslationY() != translationY) {
host.setTranslationY(translationY);
}
}
private ArrayList<View> unlockedHosts(ViewGroup parent) { private ArrayList<View> unlockedHosts(ViewGroup parent) {
ArrayList<View> hosts = new ArrayList<>(); ArrayList<View> hosts = new ArrayList<>();
for (String tag : UNLOCKED_HOST_ORDER) { for (String tag : UNLOCKED_HOST_ORDER) {
@@ -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;
}
}
}
@@ -10,7 +10,9 @@ import java.util.ArrayList;
import java.util.Objects; import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
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;
@@ -28,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();
@@ -64,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
) { ) {
@@ -79,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);
@@ -152,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);
} }
} }
@@ -179,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);
} }
} }
@@ -234,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);
} }
@@ -312,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);
} }
} }
@@ -346,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
@@ -360,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;
} }
@@ -377,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;
} }
@@ -464,7 +470,9 @@ public final class UnlockedIconSnapshotRenderController {
NotificationDisplayStyle style = scene.notificationDisplayStyle(); NotificationDisplayStyle style = scene.notificationDisplayStyle();
return settings.layoutNotifEnabledAod return settings.layoutNotifEnabledAod
&& (style == NotificationDisplayStyle.ICONS && (style == NotificationDisplayStyle.ICONS
|| style == NotificationDisplayStyle.DOT); || style == NotificationDisplayStyle.DOT
|| (style == NotificationDisplayStyle.UNKNOWN
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings)));
} }
return settings.layoutNotifEnabledUnlocked; return settings.layoutNotifEnabledUnlocked;
} }
@@ -592,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)) {
@@ -630,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;
} }
@@ -665,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() {
@@ -11,6 +11,7 @@ import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.R;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
@@ -856,7 +857,9 @@ final class UnlockedLayoutPlanner {
if (style == NotificationDisplayStyle.DOT) { if (style == NotificationDisplayStyle.DOT) {
return settings.layoutNotifEnabledAod ? 1 : 0; return settings.layoutNotifEnabledAod ? 1 : 0;
} }
if (style != NotificationDisplayStyle.ICONS) { if (style != NotificationDisplayStyle.ICONS
&& !(style == NotificationDisplayStyle.UNKNOWN
&& SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings))) {
return 0; return 0;
} }
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0; return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
@@ -18,6 +18,7 @@ import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection; import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
import se.ajpanton.statusbartweak.runtime.ViewIdNames; import se.ajpanton.statusbartweak.runtime.ViewIdNames;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
@@ -63,7 +64,7 @@ final class UnlockedStockSourceCollector {
ArrayList<SnapshotSource> statusChips = new ArrayList<>(); ArrayList<SnapshotSource> statusChips = new ArrayList<>();
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>(); ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
if (scene != null && scene.isAod()) { if (scene != null && scene.isAod()) {
collectAodStatusSnapshotViews(root, statusIcons, scene); collectAodStatusSnapshotViews(root, statusIcons, scene, settings);
} }
if (statusIcons.isEmpty()) { if (statusIcons.isEmpty()) {
if (anchors.statusRoot != null) { if (anchors.statusRoot != null) {
@@ -82,8 +83,7 @@ final class UnlockedStockSourceCollector {
selectDualSimStatusSignals(statusIcons, settings); selectDualSimStatusSignals(statusIcons, settings);
normalizeStatusIconSourceBounds(root, statusIcons); normalizeStatusIconSourceBounds(root, statusIcons);
if (scene != null && scene.isAod()) { if (scene != null && scene.isAod()) {
if (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS if (isAodIconNotificationStyle(scene, settings)) {
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
notificationIcons.addAll( notificationIcons.addAll(
LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root)); LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root));
if (notificationIcons.isEmpty() if (notificationIcons.isEmpty()
@@ -92,6 +92,13 @@ final class UnlockedStockSourceCollector {
LockscreenCardsNotificationIconSourceStore LockscreenCardsNotificationIconSourceStore
.snapshotAodNotificationPackageSources(root)); .snapshotAodNotificationPackageSources(root));
} }
if (notificationIcons.isEmpty()
&& SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings)) {
View notificationRoot = anchors != null && anchors.notificationRoot != null
? anchors.notificationRoot
: root;
collectNotificationSnapshotViews(notificationRoot, notificationIcons);
}
} }
} else if (anchors.notificationRoot != null) { } else if (anchors.notificationRoot != null) {
collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons); collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons);
@@ -111,14 +118,14 @@ final class UnlockedStockSourceCollector {
private void collectAodStatusSnapshotViews( private void collectAodStatusSnapshotViews(
View root, View root,
ArrayList<SnapshotSource> out, ArrayList<SnapshotSource> out,
SceneKey scene SceneKey scene,
SbtSettings settings
) { ) {
if (root == null || out == null) { if (root == null || out == null) {
return; return;
} }
boolean iconMode = scene != null boolean iconMode = scene != null
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS && isAodIconNotificationStyle(scene, settings);
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
View excludedNotificationContainer = iconMode View excludedNotificationContainer = iconMode
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root) ? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); : LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
@@ -129,6 +136,17 @@ final class UnlockedStockSourceCollector {
} }
} }
private boolean isAodIconNotificationStyle(SceneKey scene, SbtSettings settings) {
if (scene == null || !scene.isAod()) {
return false;
}
NotificationDisplayStyle style = scene.notificationDisplayStyle();
return style == NotificationDisplayStyle.ICONS
|| style == NotificationDisplayStyle.DOT
|| (style == NotificationDisplayStyle.UNKNOWN
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(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) {
@@ -193,7 +211,7 @@ final class UnlockedStockSourceCollector {
statusSourceSignature = 31 * statusSourceSignature + batteryChargingSignature(root); statusSourceSignature = 31 * statusSourceSignature + batteryChargingSignature(root);
if (scene != null && scene.isAod()) { if (scene != null && scene.isAod()) {
statusSourceSignature = 31 * statusSourceSignature statusSourceSignature = 31 * statusSourceSignature
+ aodLayoutAnchorSignature(root, anchors, scene); + aodLayoutAnchorSignature(root, anchors, scene, settings);
} }
int notificationSourceSignature = sourceLayoutListSignature( int notificationSourceSignature = sourceLayoutListSignature(
previousIcons != null ? previousIcons.notificationIcons : null); previousIcons != null ? previousIcons.notificationIcons : null);
@@ -238,13 +256,17 @@ final class UnlockedStockSourceCollector {
chipSourceSignature); chipSourceSignature);
} }
private int aodLayoutAnchorSignature(View root, RootAnchors anchors, SceneKey scene) { private int aodLayoutAnchorSignature(
View root,
RootAnchors anchors,
SceneKey scene,
SbtSettings settings
) {
if (root == null) { if (root == null) {
return 0; return 0;
} }
boolean iconMode = scene != null boolean iconMode = scene != null
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS && isAodIconNotificationStyle(scene, settings);
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
View notificationContainer = iconMode View notificationContainer = iconMode
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root) ? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root); : LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
@@ -317,7 +339,7 @@ final class UnlockedStockSourceCollector {
} }
if (clockView == null if (clockView == null
&& view instanceof TextView textView && view instanceof TextView textView
&& view.getClass().getName().contains("QSClockIndicatorView")) { && isStatusbarClockView(textView)) {
clockView = textView; clockView = textView;
} }
if (statusEndContent == null && idName.equals("status_bar_end_side_content")) { if (statusEndContent == null && idName.equals("status_bar_end_side_content")) {
@@ -370,6 +392,12 @@ final class UnlockedStockSourceCollector {
carrierView); carrierView);
} }
private boolean isStatusbarClockView(TextView view) {
String className = view.getClass().getName();
return className.contains("QSClockIndicatorView")
|| className.equals("com.android.systemui.statusbar.policy.Clock");
}
private boolean isCarrierTextView(TextView view) { private boolean isCarrierTextView(TextView view) {
if (view == null) { if (view == null) {
return false; return false;
@@ -1192,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;
@@ -1217,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) {
@@ -1308,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) {
@@ -17,7 +17,7 @@ import java.util.regex.Pattern;
public final class AppIconRules { public final class AppIconRules {
private static final Pattern PACKAGE_NAME_PATTERN = private static final Pattern PACKAGE_NAME_PATTERN =
Pattern.compile("^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$"); Pattern.compile("^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$");
public static final String PREF_KEY_BLOCKED_MODES = "app_icon_blocked_modes"; public static final String PREF_KEY_BLOCKED_MODES = "app_icon_blocked_modes";
public static final String PREF_KEY_EXCEPTION_RULES = "app_icon_exception_rules"; public static final String PREF_KEY_EXCEPTION_RULES = "app_icon_exception_rules";
@@ -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;
@@ -99,6 +104,7 @@ public final class SbtDefaults {
public static final boolean DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT = false; public static final boolean DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT = false;
public static final boolean DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT = false; public static final boolean DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT = false;
public static final boolean DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT = false; public static final boolean DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT = false;
public static final boolean DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT = false;
public static final int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0; public static final int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0;
public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0; public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0;
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,
@@ -195,6 +204,9 @@ public class SbtSettingsProvider extends ContentProvider {
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT)); SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT));
out.putBoolean(SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
prefs.getBoolean(SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT));
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX, out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX, prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT)); SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT));
@@ -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;
@@ -27,6 +27,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.R;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.shell.debug.DebugNotifications; import se.ajpanton.statusbartweak.shell.debug.DebugNotifications;
import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport; import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport;
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults; import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
@@ -64,6 +65,8 @@ public class IconsDebugFragment extends Fragment {
SwitchMaterial visualizeAodStockContainersSwitch = SwitchMaterial visualizeAodStockContainersSwitch =
root.findViewById(R.id.debug_visualize_aod_stock_containers_switch); root.findViewById(R.id.debug_visualize_aod_stock_containers_switch);
MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button); MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button);
SwitchMaterial experimentalAodStatusbarRootSwitch =
root.findViewById(R.id.debug_experimental_aod_statusbar_root_switch);
SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch); SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch);
TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified); TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified);
LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row); LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row);
@@ -137,6 +140,18 @@ public class IconsDebugFragment extends Fragment {
SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT,
true); true);
ViewTreeSupport.bindBooleanPreference(
requireContext(),
prefs,
experimentalAodStatusbarRootSwitch,
SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT,
true);
if (SystemUiCapabilities.isSamsungOneUi()
&& experimentalAodStatusbarRootSwitch != null) {
experimentalAodStatusbarRootSwitch.setChecked(false);
experimentalAodStatusbarRootSwitch.setEnabled(false);
}
bindRestartSystemUiButton(restartSystemUiButton); bindRestartSystemUiButton(restartSystemUiButton);
updateVisualizerSwitchAvailability( updateVisualizerSwitchAvailability(
prefs, prefs,
@@ -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);
@@ -9,6 +9,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import se.ajpanton.statusbartweak.R; import se.ajpanton.statusbartweak.R;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings; import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings;
import se.ajpanton.statusbartweak.shell.settings.SbtSettings; import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
@@ -40,6 +41,10 @@ final class LockedNotificationModeUi {
return; return;
} }
group.setOnCheckedChangeListener(null); group.setOnCheckedChangeListener(null);
if (!SystemUiCapabilities.supportsSamsungNotificationDisplayStyles()) {
bindIconsOnly(context, prefs, group, hint);
return;
}
RadioButton cards = button(context, R.string.debug_notification_style_cards, MODE_CARDS); RadioButton cards = button(context, R.string.debug_notification_style_cards, MODE_CARDS);
RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS); RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
RadioButton dot = button(context, R.string.debug_notification_style_dot, MODE_DOT); RadioButton dot = button(context, R.string.debug_notification_style_dot, MODE_DOT);
@@ -76,7 +81,31 @@ final class LockedNotificationModeUi {
}); });
} }
private static void bindIconsOnly(
Context context,
SharedPreferences prefs,
RadioGroup group,
TextView hint
) {
RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
group.removeAllViews();
group.addView(icons);
group.check(icons.getId());
group.setEnabled(false);
icons.setEnabled(false);
if (hint != null) {
hint.setText(R.string.layout_locked_notifications_hint_icons_only);
}
if (!MODE_ICONS.equals(prefs.getString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_ICONS))) {
prefs.edit().putString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_ICONS).apply();
SbtSettings.ensureReadable(context);
}
}
static String currentMode(Context context) { static String currentMode(Context context) {
if (!SystemUiCapabilities.supportsSamsungNotificationDisplayStyles()) {
return MODE_ICONS;
}
NotificationDisplayStyleSettings.Style style = NotificationDisplayStyleSettings.Style style =
NotificationDisplayStyleSettings.read(context).style(); NotificationDisplayStyleSettings.read(context).style();
return currentMode(style); return currentMode(style);
@@ -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"
@@ -111,6 +111,13 @@
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:text="@string/debug_restart_systemui" /> android:text="@string/debug_restart_systemui" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/debug_experimental_aod_statusbar_root_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/debug_experimental_aod_statusbar_root" />
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
@@ -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>
+6 -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>
@@ -30,6 +29,7 @@
<string name="debug_visualize_aod_stock_containers">AOD stock containers and derived statusbar</string> <string name="debug_visualize_aod_stock_containers">AOD stock containers and derived statusbar</string>
<string name="debug_section_systemui">SystemUI</string> <string name="debug_section_systemui">SystemUI</string>
<string name="debug_restart_systemui">Restart SystemUI</string> <string name="debug_restart_systemui">Restart SystemUI</string>
<string name="debug_experimental_aod_statusbar_root">Experimental non-Samsung AOD statusbar layout</string>
<string name="debug_restart_systemui_started">SystemUI restart requested.</string> <string name="debug_restart_systemui_started">SystemUI restart requested.</string>
<string name="debug_restart_systemui_failed">Could not restart SystemUI. Root shell failed.</string> <string name="debug_restart_systemui_failed">Could not restart SystemUI. Root shell failed.</string>
<string name="debug_notification_style_cards">Cards</string> <string name="debug_notification_style_cards">Cards</string>
@@ -50,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>
@@ -71,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>
@@ -125,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>
@@ -141,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>
@@ -162,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>
@@ -184,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>
@@ -200,6 +174,7 @@
<string name="layout_chip_height_steps">Chip height (% of statusbar)</string> <string name="layout_chip_height_steps">Chip height (% of statusbar)</string>
<string name="layout_locked_notifications_title">Locked notifications mode</string> <string name="layout_locked_notifications_title">Locked notifications mode</string>
<string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string> <string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string>
<string name="layout_locked_notifications_hint_icons_only">This SystemUI exposes notification icons only.</string>
<string name="layout_misc_title">Misc</string> <string name="layout_misc_title">Misc</string>
<string name="label_max_unlocked_icons_per_row">Max unlocked icons per row</string> <string name="label_max_unlocked_icons_per_row">Max unlocked icons per row</string>
<string name="layout_padding_title">Padding</string> <string name="layout_padding_title">Padding</string>
@@ -232,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>
@@ -255,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 ""
+1
View File
@@ -21,3 +21,4 @@ dependencyResolutionManagement {
rootProject.name = "StatusBarTweak" rootProject.name = "StatusBarTweak"
include(":app") include(":app")
include(":test-harness")
+21
View File
@@ -0,0 +1,21 @@
plugins {
id("com.android.application")
}
android {
namespace = "se.ajpanton.statusbartweak.harness"
compileSdk = 36
defaultConfig {
applicationId = "se.ajpanton.statusbartweak.harness"
minSdk = 26
targetSdk = 36
versionCode = 1
versionName = "1.0"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.microphone"
android:required="false" />
<application
android:allowBackup="false"
android:icon="@drawable/ic_harness"
android:label="@string/app_name"
android:theme="@style/Theme.StatusBarTweakHarness">
<service
android:name=".HarnessForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|mediaPlayback" />
<activity
android:name=".ScenarioActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:exported="true"
android:launchMode="singleTop" />
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,140 @@
package se.ajpanton.statusbartweak.harness;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.media.MediaMetadata;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Build;
import android.os.IBinder;
public final class HarnessForegroundService extends Service {
public static final String ACTION_START_MEDIA = "se.ajpanton.statusbartweak.harness.START_MEDIA";
public static final String ACTION_START_FOREGROUND =
"se.ajpanton.statusbartweak.harness.START_FOREGROUND";
public static final String ACTION_STOP = "se.ajpanton.statusbartweak.harness.STOP";
private static final String CHANNEL_ID = "sbt_harness_foreground";
private static final int NOTIFICATION_ID = 42_000;
private MediaSession mediaSession;
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent != null ? intent.getAction() : null;
if (ACTION_STOP.equals(action)) {
stopSelf();
return START_NOT_STICKY;
}
if (ACTION_START_MEDIA.equals(action)) {
startMediaState();
return START_STICKY;
}
startForegroundState();
return START_STICKY;
}
@Override
public void onDestroy() {
if (mediaSession != null) {
mediaSession.setActive(false);
mediaSession.release();
mediaSession = null;
}
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void startMediaState() {
ensureChannel();
if (mediaSession == null) {
mediaSession = new MediaSession(this, "StatusBarTweakHarness");
}
mediaSession.setMetadata(new MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, "Harness media")
.putString(MediaMetadata.METADATA_KEY_ARTIST, "StatusBarTweak")
.build());
mediaSession.setPlaybackState(new PlaybackState.Builder()
.setState(PlaybackState.STATE_PLAYING, 0, 1f)
.setActions(PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_STOP)
.build());
mediaSession.setActive(true);
Notification.Builder builder = baseBuilder("Harness media", "MediaSession playback active")
.setCategory(Notification.CATEGORY_TRANSPORT)
.setOngoing(true)
.setStyle(new Notification.MediaStyle()
.setMediaSession(mediaSession.getSessionToken())
.setShowActionsInCompactView(0))
.addAction(R.drawable.ic_notify, "Stop", stopIntent());
startForegroundCompat(builder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
}
private void startForegroundState() {
ensureChannel();
Notification notification = baseBuilder(
"Harness foreground service",
"Generic foreground service notification active")
.setCategory(Notification.CATEGORY_SERVICE)
.setOngoing(true)
.addAction(R.drawable.ic_notify, "Stop", stopIntent())
.build();
startForegroundCompat(notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
}
private Notification.Builder baseBuilder(String title, String text) {
Intent launch = new Intent(this, MainActivity.class);
PendingIntent launchIntent = PendingIntent.getActivity(
this,
1,
launch,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
return new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle(title)
.setContentText(text)
.setContentIntent(launchIntent)
.setShowWhen(true);
}
private PendingIntent stopIntent() {
Intent stop = new Intent(this, HarnessForegroundService.class);
stop.setAction(ACTION_STOP);
return PendingIntent.getService(
this,
2,
stop,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
}
private void startForegroundCompat(Notification notification, int type) {
if (Build.VERSION.SDK_INT >= 29) {
startForeground(NOTIFICATION_ID, notification, type);
} else {
startForeground(NOTIFICATION_ID, notification);
}
}
private void ensureChannel() {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null) {
return;
}
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"StatusBarTweak harness foreground",
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("Foreground and media scenarios for StatusBarTweak testing");
channel.setShowBadge(false);
nm.createNotificationChannel(channel);
}
}
@@ -0,0 +1,367 @@
package se.ajpanton.statusbartweak.harness;
import android.Manifest;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
public final class MainActivity extends Activity {
public static final String EXTRA_COMMAND = "command";
public static final String COMMAND_POST_NORMAL = "post_normal";
public static final String COMMAND_POST_ONGOING = "post_ongoing";
public static final String COMMAND_POST_GROUPED = "post_grouped";
public static final String COMMAND_UPDATE = "update";
public static final String COMMAND_START_CHRONOMETER = "start_chronometer";
public static final String COMMAND_START_CHURN = "start_churn";
public static final String COMMAND_STOP_CHURN = "stop_churn";
public static final String COMMAND_START_MEDIA = "start_media";
public static final String COMMAND_START_FOREGROUND = "start_foreground";
public static final String COMMAND_STOP_SERVICE = "stop_service";
public static final String COMMAND_CLEAR = "clear";
private static final String CHANNEL_ID = "sbt_harness";
private static final int NOTIFICATION_BASE_ID = 41_000;
private final Handler handler = new Handler(Looper.getMainLooper());
private int updatingCounter = 0;
private int churnCounter = 0;
private Runnable churnRunnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestNotificationPermissionIfNeeded();
setContentView(createContent());
handleCommand(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleCommand(intent);
}
private View createContent() {
ScrollView scroll = new ScrollView(this);
scroll.setBackgroundColor(0xfff7efe5);
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setPadding(dp(20), dp(20), dp(20), dp(28));
scroll.addView(root, new ScrollView.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT));
TextView title = new TextView(this);
title.setText("StatusBarTweak Test Harness");
title.setTextColor(Color.BLACK);
title.setTextSize(24);
title.setGravity(Gravity.START);
root.addView(title, matchWrap());
TextView description = new TextView(this);
description.setText(
"Open a scenario, then observe StatusBarTweak behavior. "
+ "The same APK can be used on emulators and physical devices.");
description.setTextColor(0xff333333);
description.setTextSize(15);
description.setPadding(0, dp(8), 0, dp(12));
root.addView(description, matchWrap());
addSection(root, "Statusbar surfaces");
addScenario(root, "Opaque light statusbar", ScenarioActivity.SCENARIO_OPAQUE_LIGHT);
addScenario(root, "Opaque dark statusbar", ScenarioActivity.SCENARIO_OPAQUE_DARK);
addScenario(root, "Transparent statusbar", ScenarioActivity.SCENARIO_TRANSPARENT);
addScenario(root, "Fullscreen portrait", ScenarioActivity.SCENARIO_FULLSCREEN_PORTRAIT);
addScenario(root, "Fullscreen landscape", ScenarioActivity.SCENARIO_FULLSCREEN_LANDSCAPE);
addSection(root, "Privacy indicators");
addScenario(root, "Camera privacy indicator", ScenarioActivity.SCENARIO_CAMERA);
addScenario(root, "Microphone privacy indicator", ScenarioActivity.SCENARIO_MICROPHONE);
addScenario(root, "Camera + microphone indicators", ScenarioActivity.SCENARIO_CAMERA_MICROPHONE);
addSection(root, "Chip-like public states");
addButton(root, "Start media session", v -> startHarnessService(
HarnessForegroundService.ACTION_START_MEDIA));
addButton(root, "Start foreground service", v -> startHarnessService(
HarnessForegroundService.ACTION_START_FOREGROUND));
addButton(root, "Post stopwatch/chronometer", v -> postChronometerNotification());
addButton(root, "Stop media/foreground service", v -> stopHarnessService());
addSection(root, "Notifications");
addButton(root, "Post normal notification", v -> postNotification(1, false, false, "Normal"));
addButton(root, "Post ongoing notification", v -> postNotification(2, true, false, "Ongoing"));
addButton(root, "Post grouped notifications", v -> postGroupedNotifications());
addButton(root, "Update notification text", v -> {
updatingCounter++;
postNotification(3, false, true, "Updated " + updatingCounter);
});
addButton(root, "Start notification churn", v -> startNotificationChurn());
addButton(root, "Stop notification churn", v -> stopNotificationChurn());
addButton(root, "Clear harness notifications", v -> {
stopNotificationChurn();
clearNotifications();
});
return scroll;
}
private void handleCommand(Intent intent) {
if (intent == null) {
return;
}
String command = intent.getStringExtra(EXTRA_COMMAND);
if (command == null) {
return;
}
switch (command) {
case COMMAND_POST_NORMAL:
postNotification(1, false, false, "Normal");
break;
case COMMAND_POST_ONGOING:
postNotification(2, true, false, "Ongoing");
break;
case COMMAND_POST_GROUPED:
postGroupedNotifications();
break;
case COMMAND_UPDATE:
updatingCounter++;
postNotification(3, false, true, "Updated " + updatingCounter);
break;
case COMMAND_START_CHRONOMETER:
postChronometerNotification();
break;
case COMMAND_START_CHURN:
startNotificationChurn();
break;
case COMMAND_STOP_CHURN:
stopNotificationChurn();
break;
case COMMAND_START_MEDIA:
startHarnessService(HarnessForegroundService.ACTION_START_MEDIA);
break;
case COMMAND_START_FOREGROUND:
startHarnessService(HarnessForegroundService.ACTION_START_FOREGROUND);
break;
case COMMAND_STOP_SERVICE:
stopHarnessService();
break;
case COMMAND_CLEAR:
stopNotificationChurn();
clearNotifications();
break;
default:
break;
}
}
private void addScenario(LinearLayout root, String label, String scenario) {
addButton(root, label, v -> {
Intent intent = new Intent(this, ScenarioActivity.class);
intent.putExtra(ScenarioActivity.EXTRA_SCENARIO, scenario);
startActivity(intent);
});
}
private void addSection(LinearLayout root, String label) {
TextView view = new TextView(this);
view.setText(label);
view.setTextColor(0xff444444);
view.setTextSize(17);
view.setPadding(0, dp(18), 0, dp(6));
root.addView(view, matchWrap());
}
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
Button button = new Button(this);
button.setText(label);
button.setAllCaps(false);
button.setOnClickListener(listener);
LinearLayout.LayoutParams params = matchWrap();
params.setMargins(0, dp(4), 0, dp(4));
root.addView(button, params);
}
private void postNotification(int slot, boolean ongoing, boolean alertOnlyOnce, String label) {
requestNotificationPermissionIfNeeded();
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null || !canPostNotifications()) {
return;
}
ensureChannel(nm);
Intent launch = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(
this,
slot,
launch,
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Harness " + label)
.setContentText("Package: " + getPackageName())
.setContentIntent(pendingIntent)
.setShowWhen(true)
.setOnlyAlertOnce(alertOnlyOnce)
.setCategory(Notification.CATEGORY_STATUS);
if (ongoing) {
builder.setOngoing(true);
}
nm.notify(NOTIFICATION_BASE_ID + slot, builder.build());
}
private void postGroupedNotifications() {
requestNotificationPermissionIfNeeded();
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null || !canPostNotifications()) {
return;
}
ensureChannel(nm);
String group = "sbt_harness_group";
for (int i = 0; i < 3; i++) {
Notification notification = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Harness grouped " + (i + 1))
.setContentText("Grouped notification")
.setGroup(group)
.setShowWhen(false)
.build();
nm.notify(NOTIFICATION_BASE_ID + 10 + i, notification);
}
Notification summary = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Harness group")
.setContentText("Group summary")
.setGroup(group)
.setGroupSummary(true)
.build();
nm.notify(NOTIFICATION_BASE_ID + 20, summary);
}
private void postChronometerNotification() {
requestNotificationPermissionIfNeeded();
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm == null || !canPostNotifications()) {
return;
}
ensureChannel(nm);
Notification notification = new Notification.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notify)
.setContentTitle("Harness stopwatch")
.setContentText("Chronometer notification active")
.setWhen(System.currentTimeMillis())
.setUsesChronometer(true)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setCategory(Notification.CATEGORY_STATUS)
.build();
nm.notify(NOTIFICATION_BASE_ID + 30, notification);
}
private void startNotificationChurn() {
requestNotificationPermissionIfNeeded();
if (!canPostNotifications()) {
return;
}
if (churnRunnable != null) {
return;
}
churnRunnable = new Runnable() {
@Override
public void run() {
churnCounter++;
postNotification(40, false, true, "Churn " + churnCounter);
postNotification(41, true, true, "Ongoing churn " + churnCounter);
if (churnCounter % 2 == 0) {
postNotification(42, false, true, "Intermittent " + churnCounter);
} else {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.cancel(NOTIFICATION_BASE_ID + 42);
}
}
handler.postDelayed(this, 1000);
}
};
churnRunnable.run();
}
private void stopNotificationChurn() {
if (churnRunnable != null) {
handler.removeCallbacks(churnRunnable);
churnRunnable = null;
}
}
private void startHarnessService(String action) {
requestNotificationPermissionIfNeeded();
if (!canPostNotifications()) {
return;
}
Intent intent = new Intent(this, HarnessForegroundService.class);
intent.setAction(action);
startForegroundService(intent);
}
private void stopHarnessService() {
Intent intent = new Intent(this, HarnessForegroundService.class);
intent.setAction(HarnessForegroundService.ACTION_STOP);
startService(intent);
}
private void clearNotifications() {
NotificationManager nm = getSystemService(NotificationManager.class);
if (nm != null) {
nm.cancelAll();
}
stopHarnessService();
}
private void ensureChannel(NotificationManager nm) {
NotificationChannel channel = new NotificationChannel(
CHANNEL_ID,
"StatusBarTweak harness",
NotificationManager.IMPORTANCE_DEFAULT);
channel.setDescription("Scenario notifications for StatusBarTweak compatibility testing");
channel.setShowBadge(false);
nm.createNotificationChannel(channel);
}
private void requestNotificationPermissionIfNeeded() {
if (Build.VERSION.SDK_INT < 33 || canPostNotifications()) {
return;
}
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 200);
}
private boolean canPostNotifications() {
return Build.VERSION.SDK_INT < 33
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED;
}
private LinearLayout.LayoutParams matchWrap() {
return new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
}
@@ -0,0 +1,479 @@
package se.ajpanton.statusbartweak.harness;
import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
public final class ScenarioActivity extends Activity {
public static final String EXTRA_SCENARIO = "scenario";
public static final String SCENARIO_OPAQUE_LIGHT = "opaque_light";
public static final String SCENARIO_OPAQUE_DARK = "opaque_dark";
public static final String SCENARIO_TRANSPARENT = "transparent";
public static final String SCENARIO_FULLSCREEN_PORTRAIT = "fullscreen_portrait";
public static final String SCENARIO_FULLSCREEN_LANDSCAPE = "fullscreen_landscape";
public static final String SCENARIO_CAMERA = "camera";
public static final String SCENARIO_MICROPHONE = "microphone";
public static final String SCENARIO_CAMERA_MICROPHONE = "camera_microphone";
private static final int REQUEST_CAMERA = 301;
private static final int REQUEST_MICROPHONE = 302;
private static final int REQUEST_CAMERA_MICROPHONE = 303;
private HandlerThread cameraThread;
private Handler cameraHandler;
private CameraDevice cameraDevice;
private AudioRecord audioRecord;
private Thread audioThread;
private volatile boolean audioRunning;
private TextView statusText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
applyScenario(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
stopCamera();
stopMicrophone();
applyScenario(intent);
}
private void applyScenario(Intent intent) {
String scenario = intent != null ? intent.getStringExtra(EXTRA_SCENARIO) : null;
if (scenario == null) {
scenario = SCENARIO_OPAQUE_LIGHT;
}
final String activeScenario = scenario;
boolean lightBars = configureWindow(activeScenario);
View content = createScenarioView(activeScenario);
setContentView(content);
content.post(() -> {
setLightBars(lightBars);
if (isFullscreen(activeScenario)) {
hideSystemBars();
}
});
startScenarioIfNeeded(activeScenario);
}
@Override
protected void onDestroy() {
stopCamera();
stopMicrophone();
super.onDestroy();
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
boolean granted = true;
for (int result : grantResults) {
granted &= result == PackageManager.PERMISSION_GRANTED;
}
granted &= grantResults.length > 0;
if (!granted) {
setStatus("Permission denied.");
return;
}
if (requestCode == REQUEST_CAMERA) {
startCamera();
} else if (requestCode == REQUEST_MICROPHONE) {
startMicrophone();
} else if (requestCode == REQUEST_CAMERA_MICROPHONE) {
startCameraAndMicrophone();
}
}
private boolean configureWindow(String scenario) {
Window window = getWindow();
if (Build.VERSION.SDK_INT >= 28) {
window.getAttributes().layoutInDisplayCutoutMode =
android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
}
if (SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else if (SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
window.setStatusBarColor(Color.WHITE);
window.setNavigationBarColor(Color.WHITE);
return true;
} else if (SCENARIO_OPAQUE_DARK.equals(scenario)) {
window.setStatusBarColor(Color.BLACK);
window.setNavigationBarColor(Color.BLACK);
return false;
} else if (SCENARIO_TRANSPARENT.equals(scenario)) {
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.TRANSPARENT);
if (Build.VERSION.SDK_INT >= 30) {
window.setDecorFitsSystemWindows(false);
}
return false;
} else if (isFullscreen(scenario)) {
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(Color.BLACK);
if (Build.VERSION.SDK_INT >= 30) {
window.setDecorFitsSystemWindows(false);
}
return false;
} else {
window.setStatusBarColor(0xff202124);
window.setNavigationBarColor(0xff202124);
return false;
}
}
private View createScenarioView(String scenario) {
LinearLayout root = new LinearLayout(this);
root.setOrientation(LinearLayout.VERTICAL);
root.setGravity(Gravity.CENTER);
root.setPadding(dp(24), dp(60), dp(24), dp(60));
root.setBackgroundColor(backgroundColor(scenario));
TextView title = new TextView(this);
title.setText(titleFor(scenario));
title.setTextColor(textColor(scenario));
title.setTextSize(28);
title.setGravity(Gravity.CENTER);
root.addView(title, matchWrap());
statusText = new TextView(this);
statusText.setText(statusFor(scenario));
statusText.setTextColor(textColor(scenario));
statusText.setAlpha(0.85f);
statusText.setTextSize(16);
statusText.setGravity(Gravity.CENTER);
statusText.setPadding(0, dp(12), 0, dp(20));
root.addView(statusText, matchWrap());
if (isFullscreen(scenario)) {
addButton(root, "Temporarily show system bars", v -> showSystemBarsBriefly());
}
if (SCENARIO_CAMERA.equals(scenario)) {
addButton(root, "Start camera privacy indicator", v -> startCamera());
addButton(root, "Stop camera", v -> stopCamera());
}
if (SCENARIO_MICROPHONE.equals(scenario)) {
addButton(root, "Start microphone privacy indicator", v -> startMicrophone());
addButton(root, "Stop microphone", v -> stopMicrophone());
}
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
addButton(root, "Start both indicators", v -> startCameraAndMicrophone());
addButton(root, "Stop both", v -> {
stopCamera();
stopMicrophone();
});
}
addButton(root, "Finish scenario", v -> finish());
return root;
}
private void startScenarioIfNeeded(String scenario) {
if (SCENARIO_CAMERA.equals(scenario)) {
startCamera();
} else if (SCENARIO_MICROPHONE.equals(scenario)) {
startMicrophone();
} else if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
startCameraAndMicrophone();
}
}
private int backgroundColor(String scenario) {
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
return Color.WHITE;
}
if (SCENARIO_TRANSPARENT.equals(scenario)) {
return 0xff0d47a1;
}
if (isFullscreen(scenario)) {
return 0xff1b5e20;
}
return 0xff101214;
}
private int textColor(String scenario) {
return SCENARIO_OPAQUE_LIGHT.equals(scenario) ? Color.BLACK : Color.WHITE;
}
private String titleFor(String scenario) {
switch (scenario) {
case SCENARIO_OPAQUE_LIGHT:
return "Opaque light statusbar";
case SCENARIO_OPAQUE_DARK:
return "Opaque dark statusbar";
case SCENARIO_TRANSPARENT:
return "Transparent statusbar";
case SCENARIO_FULLSCREEN_PORTRAIT:
return "Fullscreen portrait";
case SCENARIO_FULLSCREEN_LANDSCAPE:
return "Fullscreen landscape";
case SCENARIO_CAMERA:
return "Camera privacy indicator";
case SCENARIO_MICROPHONE:
return "Microphone privacy indicator";
case SCENARIO_CAMERA_MICROPHONE:
return "Camera + microphone indicators";
default:
return "Scenario";
}
}
private String statusFor(String scenario) {
if (isFullscreen(scenario)) {
return "System bars are hidden. Swipe from an edge or use the button below to reveal them.";
}
if (SCENARIO_TRANSPARENT.equals(scenario)) {
return "Content is laid out behind a transparent statusbar.";
}
if (SCENARIO_CAMERA.equals(scenario)) {
return "Camera should stay open until stopped or this screen is closed.";
}
if (SCENARIO_MICROPHONE.equals(scenario)) {
return "AudioRecord should stay active until stopped or this screen is closed.";
}
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
return "Camera and AudioRecord should stay active until stopped or this screen is closed.";
}
return "This screen uses a normal app statusbar.";
}
private void startCamera() {
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
return;
}
if (cameraDevice != null) {
setStatus("Camera already active.");
return;
}
CameraManager manager = getSystemService(CameraManager.class);
if (manager == null) {
setStatus("CameraManager unavailable.");
return;
}
try {
String[] ids = manager.getCameraIdList();
if (ids.length == 0) {
setStatus("No camera reported by this device.");
return;
}
cameraThread = new HandlerThread("SbtHarnessCamera");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
manager.openCamera(ids[0], new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
cameraDevice = camera;
setStatus("Camera active: " + ids[0]);
}
@Override
public void onDisconnected(CameraDevice camera) {
stopCamera();
setStatus("Camera disconnected.");
}
@Override
public void onError(CameraDevice camera, int error) {
stopCamera();
setStatus("Camera error: " + error);
}
}, cameraHandler);
} catch (CameraAccessException | SecurityException | IllegalArgumentException e) {
stopCamera();
setStatus("Camera failed: " + e.getClass().getSimpleName());
}
}
private void startCameraAndMicrophone() {
boolean hasCamera = checkSelfPermission(Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED;
boolean hasMic = checkSelfPermission(Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED;
if (!hasCamera || !hasMic) {
requestPermissions(
new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
REQUEST_CAMERA_MICROPHONE);
return;
}
startCamera();
startMicrophone();
}
private void stopCamera() {
if (cameraDevice != null) {
cameraDevice.close();
cameraDevice = null;
}
if (cameraThread != null) {
cameraThread.quitSafely();
cameraThread = null;
cameraHandler = null;
}
setStatus("Camera stopped.");
}
private void startMicrophone() {
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_MICROPHONE);
return;
}
if (audioRecord != null) {
setStatus("Microphone already active.");
return;
}
int sampleRate = 8000;
int minBuffer = AudioRecord.getMinBufferSize(
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT);
int bufferSize = Math.max(minBuffer, 2048);
try {
audioRecord = new AudioRecord(
MediaRecorder.AudioSource.MIC,
sampleRate,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize);
audioRecord.startRecording();
audioRunning = true;
audioThread = new Thread(() -> {
byte[] buffer = new byte[bufferSize];
while (audioRunning && audioRecord != null) {
audioRecord.read(buffer, 0, buffer.length);
}
}, "SbtHarnessMic");
audioThread.start();
setStatus("Microphone active.");
} catch (SecurityException | IllegalStateException | IllegalArgumentException e) {
stopMicrophone();
setStatus("Microphone failed: " + e.getClass().getSimpleName());
}
}
private void stopMicrophone() {
audioRunning = false;
if (audioRecord != null) {
try {
audioRecord.stop();
} catch (IllegalStateException ignored) {
}
audioRecord.release();
audioRecord = null;
}
audioThread = null;
setStatus("Microphone stopped.");
}
private void setStatus(String status) {
if (statusText != null) {
runOnUiThread(() -> statusText.setText(status));
}
}
private boolean isFullscreen(String scenario) {
return SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)
|| SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario);
}
private void hideSystemBars() {
if (Build.VERSION.SDK_INT >= 30) {
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
controller.hide(WindowInsets.Type.systemBars());
}
} else {
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
}
private void showSystemBarsBriefly() {
if (Build.VERSION.SDK_INT >= 30) {
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
controller.show(WindowInsets.Type.systemBars());
}
} else {
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
}
getWindow().getDecorView().postDelayed(this::hideSystemBars, 2500);
}
private void setLightBars(boolean lightBars) {
if (Build.VERSION.SDK_INT >= 30) {
WindowInsetsController controller = getWindow().getInsetsController();
if (controller != null) {
int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
controller.setSystemBarsAppearance(lightBars ? mask : 0, mask);
}
} else {
int flags = getWindow().getDecorView().getSystemUiVisibility();
if (lightBars) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
} else {
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
getWindow().getDecorView().setSystemUiVisibility(flags);
}
}
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
Button button = new Button(this);
button.setText(label);
button.setAllCaps(false);
button.setOnClickListener(listener);
LinearLayout.LayoutParams params = matchWrap();
params.setMargins(0, dp(8), 0, 0);
root.addView(button, params);
}
private LinearLayout.LayoutParams matchWrap() {
return new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
private int dp(int value) {
return Math.round(value * getResources().getDisplayMetrics().density);
}
}
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:fillColor="#202124"
android:pathData="M4,4h40v40h-40z" />
<path
android:fillColor="#00E676"
android:pathData="M4,4h32v5h-32z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M8,16h32v4h-32zM8,24h24v4h-24zM8,32h16v4h-16z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFF"
android:pathData="M4,5h16v11h-12l-4,4z" />
</vector>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="harness_accent">#00E676</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">SBT Test Harness</string>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.StatusBarTweakHarness" parent="@android:style/Theme.Material.NoActionBar">
<item name="android:fontFamily">sans</item>
<item name="android:windowActionModeOverlay">true</item>
<item name="android:navigationBarColor">#111111</item>
<item name="android:colorAccent">@color/harness_accent</item>
</style>
</resources>
+1 -1
View File
@@ -1 +1 @@
version=0.4 version=0.8