38 Commits
Author SHA1 Message Date
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
ajp_anton 2249212ae3 Release 0.4 2026-06-23 12:44:54 +00:00
ajp_anton d1160a2d2d Avoid stock statusbar flash when enabling layout 2026-06-23 12:37:59 +00:00
ajp_anton 28f08eb2b2 Fix call chip lifecycle tracking 2026-06-22 22:01:48 +00:00
ajp_anton dbdc8e2af8 Fix middle-side radio selection 2026-06-22 20:18:14 +00:00
ajp_anton ea3d8221b5 Stabilize statusbar during privacy overlay 2026-06-22 20:00:14 +00:00
ajp_anton 1fa5fd24b1 Support animated notification icon snapshots 2026-06-22 14:39:17 +00:00
ajp_anton 8e7e825516 Fix call chip rendering and placement stability 2026-06-22 11:29:09 +00:00
ajp_anton de1c03b20f Release 0.3 2026-06-22 03:48:15 +00:00
ajp_anton ee309161fd Prevent drawer overlay in wide settings mode 2026-06-22 03:43:55 +00:00
ajp_anton 9c7707830c Normalize battery bar corner radius for screen zoom 2026-06-22 02:00:05 +00:00
ajp_anton dd215cde82 Restructure battery bar scenario settings 2026-06-22 01:37:08 +00:00
ajp_anton b0270b8ceb Simplify battery bar geometry settings 2026-06-22 01:11:02 +00:00
ajp_anton 3bfcb14079 Preview default threshold battery colours 2026-06-22 00:22:12 +00:00
53 changed files with 3402 additions and 918 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,65 @@
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 SystemUiCapabilities() {
}
public static boolean isSamsungOneUi() {
return "samsung".equalsIgnoreCase(Build.MANUFACTURER);
}
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;
@@ -0,0 +1,27 @@
package se.ajpanton.statusbartweak.runtime;
public final class RuntimeMutationGuard {
private static final ThreadLocal<Integer> SNAPSHOT_RENDER_DEPTH =
ThreadLocal.withInitial(() -> 0);
private RuntimeMutationGuard() {
}
public static void enterSnapshotRenderMutation() {
SNAPSHOT_RENDER_DEPTH.set(SNAPSHOT_RENDER_DEPTH.get() + 1);
}
public static void exitSnapshotRenderMutation() {
int depth = SNAPSHOT_RENDER_DEPTH.get() - 1;
if (depth <= 0) {
SNAPSHOT_RENDER_DEPTH.remove();
} else {
SNAPSHOT_RENDER_DEPTH.set(depth);
}
}
public static boolean isSnapshotRenderMutation() {
Integer depth = SNAPSHOT_RENDER_DEPTH.get();
return depth != null && depth > 0;
}
}
@@ -8,7 +8,7 @@ public final class ViewIdNames {
} }
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(
@@ -12,8 +12,11 @@ import android.graphics.Path;
import android.graphics.PathMeasure; import android.graphics.PathMeasure;
import android.graphics.PixelFormat; import android.graphics.PixelFormat;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.util.DisplayMetrics;
import android.view.Gravity; import android.view.Gravity;
import android.view.RoundedCorner;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.ViewParent; import android.view.ViewParent;
@@ -384,12 +387,7 @@ final class BatteryBarController {
return; return;
} }
SbtSettings settings = RuntimeSettingsCache.get(root.getContext()); SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
if (!settings.batteryBarEnabled || root.getWidth() <= 0 || root.getHeight() <= 0) { if (root.getWidth() <= 0 || root.getHeight() <= 0) {
removeBatteryBarView(root);
return;
}
int color = resolveBarColor(root, settings);
if (!shouldRenderOnRoot(root)) {
removeBatteryBarView(root); removeBatteryBarView(root);
return; return;
} }
@@ -400,6 +398,15 @@ final class BatteryBarController {
if (transientStatusBarReveal) { if (transientStatusBarReveal) {
scenario = visibleStatusBarScenarioForRoot(root); scenario = visibleStatusBarScenarioForRoot(root);
} }
if (!isBatteryBarEnabled(settings, scenario)) {
removeBatteryBarView(root);
return;
}
int color = resolveBarColor(root, settings);
if (!shouldRenderOnRoot(root)) {
removeBatteryBarView(root);
return;
}
BatteryBarGeometry geometry = BatteryBarGeometry.resolve(settings, scenario); BatteryBarGeometry geometry = BatteryBarGeometry.resolve(settings, scenario);
if (isFullscreenScenario(scenario)) { if (isFullscreenScenario(scenario)) {
removeEmbeddedBatteryBarView(root); removeEmbeddedBatteryBarView(root);
@@ -427,7 +434,7 @@ 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, buildSignature(rootGroup, settings, color, geometry, scenario));
@@ -454,7 +461,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);
@@ -541,6 +548,7 @@ final class BatteryBarController {
+ plugged + "|" + plugged + "|"
+ color + "|" + color + "|"
+ settings.batteryBarEnabled + "|" + settings.batteryBarEnabled + "|"
+ settings.batteryBarScenarioEnabled + "|"
+ settings.batteryBarPosition + "|" + settings.batteryBarPosition + "|"
+ settings.batteryBarAlignment + "|" + settings.batteryBarAlignment + "|"
+ settings.batteryBarThicknessDp + "|" + settings.batteryBarThicknessDp + "|"
@@ -577,6 +585,21 @@ final class BatteryBarController {
: BatteryBarGeometry.Scenario.UNLOCKED_PORTRAIT; : BatteryBarGeometry.Scenario.UNLOCKED_PORTRAIT;
} }
private boolean isBatteryBarEnabled(
SbtSettings settings,
BatteryBarGeometry.Scenario scenario
) {
if (settings == null || scenario == null) {
return false;
}
if (scenario == BatteryBarGeometry.Scenario.UNLOCKED_PORTRAIT
|| scenario == BatteryBarGeometry.Scenario.UNLOCKED_LANDSCAPE) {
return settings.batteryBarEnabled;
}
Boolean scenarioEnabled = settings.batteryBarScenarioEnabled.get(scenario.id);
return scenarioEnabled == null || scenarioEnabled;
}
private BatteryBarGeometry.Scenario visibleStatusBarScenarioForRoot(View root) { private BatteryBarGeometry.Scenario visibleStatusBarScenarioForRoot(View root) {
boolean landscape = isLandscapeForRoot(root, isFullscreenSystemBarState()); boolean landscape = isLandscapeForRoot(root, isFullscreenSystemBarState());
if (isPhoneRoot(root) && isUnlockedScene() && isTransparentStatusBarActive()) { if (isPhoneRoot(root) && isUnlockedScene() && isTransparentStatusBarActive()) {
@@ -1015,8 +1038,8 @@ final class BatteryBarController {
private final Path fullCurvePath = new Path(); private final Path fullCurvePath = new Path();
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 final float[] pathPoint = new float[2];
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;
@@ -1031,6 +1054,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,
@@ -1038,6 +1062,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;
@@ -1063,7 +1088,18 @@ final class BatteryBarController {
? geometry ? geometry
: BatteryBarGeometry.global(settings); : BatteryBarGeometry.global(settings);
int thickness = Math.max(1, dpToPx(getContext(), activeGeometry.thicknessDp)); int thickness = Math.max(1, dpToPx(getContext(), activeGeometry.thicknessDp));
int edgeOffset = Math.max(0, dpToPx(getContext(), settings.batteryBarEdgeOffsetDp)); int edgeOffset = Math.max(0, dpToPx(getContext(), activeGeometry.edgeOffsetDp));
float fraction = resolveFraction(batteryLevel, settings);
if (shouldDrawCurvedTop(activeGeometry)) {
buildCurvedPath(
width,
height,
thickness,
edgeOffset,
fraction,
activeGeometry.alignment);
return;
}
int left = edgeOffset; int left = edgeOffset;
int right = width - edgeOffset; int right = width - edgeOffset;
if (right <= left) { if (right <= left) {
@@ -1074,11 +1110,6 @@ final class BatteryBarController {
if (bottom <= top) { if (bottom <= top) {
return; return;
} }
float fraction = resolveFraction(batteryLevel, settings);
if (shouldDrawCurvedTop(activeGeometry)) {
buildCurvedPath(left, right, height, thickness, fraction, activeGeometry.alignment);
return;
}
int availableWidth = right - left; int availableWidth = right - left;
int barWidth = Math.round(availableWidth * fraction); int barWidth = Math.round(availableWidth * fraction);
if (barWidth <= 0) { if (barWidth <= 0) {
@@ -1100,53 +1131,47 @@ final class BatteryBarController {
return false; return false;
} }
return !SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF.equals( return !SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF.equals(
settings.batteryBarCurvedGeometryMode); geometry.curvedGeometryMode);
} }
private void buildCurvedPath( private void buildCurvedPath(
int left, int width,
int right,
int height, int height,
int thickness, int thickness,
int edgeOffset,
float fraction, float fraction,
String alignment String alignment
) { ) {
if (fraction <= 0f || right <= left || height <= 0 || thickness <= 0) { if (fraction <= 0f || width <= 0 || height <= 0 || thickness <= 0) {
return; return;
} }
float halfStroke = thickness / 2f; buildFullCurvePath(width, height, thickness);
float leftCenter = left + halfStroke;
float rightCenter = right - halfStroke;
if (rightCenter <= leftCenter) {
return;
}
buildFullCurvePath(leftCenter, rightCenter, height, thickness);
pathMeasure.setPath(fullCurvePath, false); pathMeasure.setPath(fullCurvePath, false);
float fullLength = pathMeasure.getLength(); float fullLength = pathMeasure.getLength();
if (fullLength <= 0f) { if (fullLength <= 0f) {
return; return;
} }
float targetLength = SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals( float pathOffset = Math.min(Math.max(0f, edgeOffset), fullLength / 2f);
settings.batteryBarCurvedGeometryMode) float usableStart = pathOffset;
? fullLength * fraction float usableEnd = fullLength - pathOffset;
: targetDistanceForHorizontalFraction( float usableLength = usableEnd - usableStart;
leftCenter, if (usableLength <= 0f) {
rightCenter, return;
fullLength, }
fraction); float targetLength = usableLength * fraction;
if (targetLength <= 0f) { if (targetLength <= 0f) {
return; return;
} }
targetLength = Math.min(fullLength, targetLength); targetLength = Math.min(usableLength, targetLength);
float start = 0f; float start = usableStart;
float end = targetLength; float end = usableStart + targetLength;
if ("rtl".equals(alignment)) { if ("rtl".equals(alignment)) {
start = fullLength - targetLength; start = usableEnd - targetLength;
end = fullLength; end = usableEnd;
} else if ("center".equals(alignment)) { } else if ("center".equals(alignment)) {
float center = fullLength / 2f; float center = (usableStart + usableEnd) / 2f;
start = Math.max(0f, center - targetLength / 2f); start = Math.max(usableStart, center - targetLength / 2f);
end = Math.min(fullLength, center + targetLength / 2f); end = Math.min(usableEnd, center + targetLength / 2f);
} }
if (end <= start) { if (end <= start) {
return; return;
@@ -1156,64 +1181,124 @@ final class BatteryBarController {
pathStrokeWidth = thickness; pathStrokeWidth = thickness;
} }
private void buildFullCurvePath(float left, float right, int height, int thickness) { private void buildFullCurvePath(int width, int height, int thickness) {
fullCurvePath.reset(); fullCurvePath.reset();
float halfStroke = thickness / 2f; float halfStroke = thickness / 2f;
if (width <= thickness) {
return;
}
float topY = halfStroke; float topY = halfStroke;
float bottomY = Math.max(topY, height - halfStroke); float bottomY = Math.max(topY, height - halfStroke);
float verticalSpan = bottomY - topY; float verticalSpan = bottomY - topY;
float radius = Math.min(verticalSpan, (right - left) / 2f); float outerRadius = Math.min(
fullCurvePath.moveTo(left, bottomY); resolveTopCornerRadius(verticalSpan + halfStroke),
if (radius <= 0f) { width / 2f);
fullCurvePath.lineTo(right, topY); float curveRadius = outerRadius - halfStroke;
if (curveRadius <= 0f) {
buildSharpTopPath(width, topY, bottomY, halfStroke);
return; return;
} }
fullCurvePath.lineTo(left, topY + radius); float leftTopX = outerRadius;
fullCurvePath.quadTo(left, topY, left + radius, topY); float rightTopX = width - outerRadius;
fullCurvePath.lineTo(right - radius, topY); if (rightTopX <= leftTopX) {
fullCurvePath.quadTo(right, topY, right, topY + radius); buildSharpTopPath(width, topY, bottomY, halfStroke);
fullCurvePath.lineTo(right, bottomY); return;
}
RectF leftCorner = new RectF(
halfStroke,
halfStroke,
outerRadius + curveRadius,
outerRadius + curveRadius);
RectF rightCorner = new RectF(
width - outerRadius - curveRadius,
halfStroke,
width - halfStroke,
outerRadius + curveRadius);
float leftStartAngle = leftCornerStartAngle(bottomY, outerRadius, curveRadius);
float leftStartX = cornerX(leftCorner.centerX(), curveRadius, leftStartAngle);
float leftStartY = cornerY(leftCorner.centerY(), curveRadius, leftStartAngle);
fullCurvePath.moveTo(halfStroke, bottomY);
fullCurvePath.lineTo(leftStartX, leftStartY);
fullCurvePath.arcTo(leftCorner, leftStartAngle, 270f - leftStartAngle);
fullCurvePath.lineTo(rightTopX, topY);
float rightEndAngle = rightCornerEndAngle(bottomY, outerRadius, curveRadius);
fullCurvePath.arcTo(rightCorner, 270f, rightEndAngle - 270f);
fullCurvePath.lineTo(width - halfStroke, bottomY);
} }
private float targetDistanceForHorizontalFraction( private float resolveTopCornerRadius(float fallback) {
float left, WindowInsets insets = roundedCornerSource != null
float right, ? roundedCornerSource.getRootWindowInsets()
float fullLength, : null;
float fraction if (insets == null) {
) { insets = getRootWindowInsets();
if (fraction >= 1f) {
return fullLength;
} }
float horizontalSpan = right - left; if (insets == null) {
if (horizontalSpan <= 0f) { return fallback;
return fullLength * fraction;
} }
float targetHorizontal = horizontalSpan * fraction; int leftRadius = roundedCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT);
if (targetHorizontal <= 0f) { int rightRadius = roundedCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT);
return 0f; if (leftRadius > 0 && rightRadius > 0) {
return Math.min(leftRadius, rightRadius);
} }
int steps = Math.max(32, Math.min(512, Math.round(fullLength / 2f))); if (leftRadius > 0 || rightRadius > 0) {
float previousDistance = 0f; return Math.max(leftRadius, rightRadius);
float previousX = left;
float accumulatedHorizontal = 0f;
for (int step = 1; step <= steps; step++) {
float distance = fullLength * step / steps;
if (!pathMeasure.getPosTan(distance, pathPoint, null)) {
continue;
}
float dx = Math.abs(pathPoint[0] - previousX);
if (accumulatedHorizontal + dx >= targetHorizontal) {
if (dx <= 0f) {
return distance;
}
float ratio = (targetHorizontal - accumulatedHorizontal) / dx;
return previousDistance + (distance - previousDistance) * ratio;
}
accumulatedHorizontal += dx;
previousDistance = distance;
previousX = pathPoint[0];
} }
return fullLength; return 0f;
}
private int roundedCornerRadius(WindowInsets insets, int position) {
RoundedCorner corner = insets.getRoundedCorner(position);
if (corner == null) {
return 0;
}
return stableDensityRadius(Math.max(0, corner.getRadius()));
}
private int stableDensityRadius(int radius) {
if (radius <= 0 || getResources() == null) {
return radius;
}
DisplayMetrics metrics = getResources().getDisplayMetrics();
int currentDpi = metrics != null ? metrics.densityDpi : 0;
int stableDpi = DisplayMetrics.DENSITY_DEVICE_STABLE;
if (currentDpi <= 0 || stableDpi <= 0 || currentDpi == stableDpi) {
return radius;
}
return Math.max(1, Math.round(radius * (stableDpi / (float) currentDpi)));
}
private void buildSharpTopPath(int width, float topY, float bottomY, float halfStroke) {
fullCurvePath.moveTo(halfStroke, bottomY);
fullCurvePath.lineTo(halfStroke, topY);
fullCurvePath.lineTo(width - halfStroke, topY);
fullCurvePath.lineTo(width - halfStroke, bottomY);
}
private float leftCornerStartAngle(float bottomY, float outerRadius, float curveRadius) {
if (bottomY >= outerRadius) {
return 180f;
}
float ratio = Math.max(-1f, Math.min(1f, (outerRadius - bottomY) / curveRadius));
return 180f + (float) Math.toDegrees(Math.asin(ratio));
}
private float rightCornerEndAngle(float bottomY, float outerRadius, float curveRadius) {
if (bottomY >= outerRadius) {
return 360f;
}
float ratio = Math.max(-1f, Math.min(1f, (outerRadius - bottomY) / curveRadius));
return 360f - (float) Math.toDegrees(Math.asin(ratio));
}
private float cornerX(float centerX, float radius, float angleDegrees) {
return centerX + radius * (float) Math.cos(Math.toRadians(angleDegrees));
}
private float cornerY(float centerY, float radius, float angleDegrees) {
return centerY + radius * (float) Math.sin(Math.toRadians(angleDegrees));
} }
private int resolveTop(BatteryBarGeometry geometry, int thickness, int height) { private int resolveTop(BatteryBarGeometry geometry, int thickness, int height) {
@@ -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);
} }
@@ -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) {
@@ -153,7 +179,7 @@ final class StockClockController {
} }
runtimeContext.getModeStateRepository().addLayoutEnabledListener(enabled -> { runtimeContext.getModeStateRepository().addLayoutEnabledListener(enabled -> {
if (enabled) { if (enabled) {
restoreAllOwnedClocks(); suspendOwnedClockTickers();
} else { } else {
refreshAllClocks(); refreshAllClocks();
} }
@@ -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) {
@@ -212,23 +241,26 @@ final class StockClockController {
} }
} }
private void removeAllRowOverlays() { private void suspendOwnedClockTickers() {
for (TextView clockView : new ArrayList<>(trackedClocks)) { for (TextView clockView : new ArrayList<>(ownedClocks)) {
removeRowOverlay(clockView); stopTicker(clockView);
} }
} }
private void restoreAllOwnedClocks() {
for (TextView clockView : new ArrayList<>(trackedClocks)) {
restoreOwnedClock(clockView);
}
removeAllRowOverlays();
}
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 || !hasCustomClockTextEnabled(settings)) { if (layoutEnabled) {
if (ownedClocks.contains(clockView)) {
stopTicker(clockView);
}
return;
}
if (!hasCustomClockTextEnabled(settings)) {
restoreOwnedClock(clockView); restoreOwnedClock(clockView);
return; return;
} }
@@ -331,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();
} }
@@ -237,6 +240,9 @@ final class StockUnlockedNotificationIconsController {
} }
private boolean shouldManageContainer(View view) { private boolean shouldManageContainer(View view) {
if (!isFeatureEnabled(view.getContext())) {
return false;
}
if (!isUnlockedScene()) { if (!isUnlockedScene()) {
return false; return false;
} }
@@ -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;
}
} }
@@ -33,7 +33,7 @@ final class DarkIconDispatcherGuard {
} }
void install(Class<?> darkIconDispatcherClass) { void install(Class<?> darkIconDispatcherClass) {
XposedHookSupport.hookAllConstructors( XposedHookSupport.hookAllConstructorsIfExists(
runtimeContext.getFramework(), runtimeContext.getFramework(),
darkIconDispatcherClass, darkIconDispatcherClass,
chain -> { chain -> {
@@ -10,8 +10,10 @@ import android.graphics.drawable.Drawable;
import android.os.Looper; import android.os.Looper;
import android.os.PowerManager; import android.os.PowerManager;
import android.service.notification.StatusBarNotification; import android.service.notification.StatusBarNotification;
import android.telecom.TelecomManager;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@@ -33,7 +35,9 @@ import java.util.Set;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.function.Function; import java.util.function.Function;
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection; import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
import se.ajpanton.statusbartweak.runtime.RuntimeMutationGuard;
import se.ajpanton.statusbartweak.runtime.ViewIdNames; import se.ajpanton.statusbartweak.runtime.ViewIdNames;
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext; import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport; import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
@@ -77,6 +81,7 @@ final class StockLayoutCanvasController {
"entries" "entries"
}; };
private static final Set<String> TARGET_ID_NAMES = Set.of( private static final Set<String> TARGET_ID_NAMES = Set.of(
"clock",
"left_clock_container", "left_clock_container",
"notification_icon_area", "notification_icon_area",
"notification_icon_area_inner", "notification_icon_area_inner",
@@ -91,10 +96,12 @@ final class StockLayoutCanvasController {
private static final Set<String> TARGET_CLASS_SUBSTRINGS = Set.of( private static final Set<String> TARGET_CLASS_SUBSTRINGS = Set.of(
"QSClockIndicatorView", "QSClockIndicatorView",
"statusbar.policy.Clock",
"SecShelfNotificationIconContainer" "SecShelfNotificationIconContainer"
); );
private static final Set<String> LAYOUT_OFF_UNLOCKED_ONLY_IDS = Set.of( private static final Set<String> LAYOUT_OFF_UNLOCKED_ONLY_IDS = Set.of(
"clock",
"left_clock_container", "left_clock_container",
"notification_icon_area", "notification_icon_area",
"notification_icon_area_inner", "notification_icon_area_inner",
@@ -134,8 +141,14 @@ final class StockLayoutCanvasController {
private final Map<View, Integer> aodBeforeDrawSignatureByRoot = new WeakHashMap<>(); private final Map<View, Integer> aodBeforeDrawSignatureByRoot = new WeakHashMap<>();
private final Set<View> trackedUnlockedStockSurfaces = private final Set<View> trackedUnlockedStockSurfaces =
Collections.newSetFromMap(new WeakHashMap<>()); Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> suppressedHiddenStatusBarRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> privacyOverlayHiddenStatusBarRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> trackedLockedStatusBarIconSurfaces = private final Set<View> trackedLockedStatusBarIconSurfaces =
Collections.newSetFromMap(new WeakHashMap<>()); Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> lockedStatusBarIconSurfaceReadyRoots =
Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingUnlockedAppearanceRefreshRoots = private final Set<View> pendingUnlockedAppearanceRefreshRoots =
Collections.newSetFromMap(new WeakHashMap<>()); Collections.newSetFromMap(new WeakHashMap<>());
private final Set<View> pendingUnlockedClockTextRefreshRoots = private final Set<View> pendingUnlockedClockTextRefreshRoots =
@@ -185,6 +198,8 @@ final class StockLayoutCanvasController {
private int notificationDrawerCloseGeneration; private int notificationDrawerCloseGeneration;
private boolean notificationDrawerClosePending; private boolean notificationDrawerClosePending;
private int aodVisualAlphaGeneration; private int aodVisualAlphaGeneration;
private int systemBarRequestedVisibleTypes = WindowInsets.Type.statusBars();
private boolean statusBarTransientShown;
StockLayoutCanvasController(RuntimeContext runtimeContext) { StockLayoutCanvasController(RuntimeContext runtimeContext) {
this.runtimeContext = runtimeContext; this.runtimeContext = runtimeContext;
@@ -213,10 +228,124 @@ final class StockLayoutCanvasController {
statusIconModelTracker.installHooks(classLoader); statusIconModelTracker.installHooks(classLoader);
installShadeExpansionHooks(classLoader); installShadeExpansionHooks(classLoader);
installShadeHeaderAnimationGuardHook(classLoader); installShadeHeaderAnimationGuardHook(classLoader);
installSystemBarAttributeListener(classLoader);
installTransientBarListener(classLoader);
installViewRootHook(classLoader); installViewRootHook(classLoader);
hooksInstalled = true; hooksInstalled = true;
} }
private void installSystemBarAttributeListener(ClassLoader classLoader) {
Class<?> commandQueueClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.CommandQueue",
classLoader);
if (commandQueueClass == null) {
return;
}
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
commandQueueClass,
"onSystemBarAttributesChanged",
chain -> {
Object result = chain.proceed();
updateSystemBarRequestedVisibleTypes(chain.getArgs());
return result;
});
}
private void updateSystemBarRequestedVisibleTypes(List<Object> args) {
if (args == null || args.size() <= 5) {
return;
}
Integer displayId = asInteger(args.get(0));
Integer requestedVisibleTypes = asInteger(args.get(5));
if (displayId == null || displayId != 0 || requestedVisibleTypes == null) {
return;
}
if (systemBarRequestedVisibleTypes == requestedVisibleTypes) {
return;
}
systemBarRequestedVisibleTypes = requestedVisibleTypes;
if (statusBarsRequestedVisible()) {
releaseSuppressedHiddenStatusBarRoots();
privacyOverlayHiddenStatusBarRoots.clear();
}
}
private void installTransientBarListener(ClassLoader classLoader) {
Class<?> commandQueueClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.CommandQueue",
classLoader);
if (commandQueueClass != null) {
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
commandQueueClass,
"showTransient",
chain -> {
Object result = chain.proceed();
updateTransientBarState(chain.getArgs(), true);
return result;
});
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
commandQueueClass,
"abortTransient",
chain -> {
Object result = chain.proceed();
updateTransientBarState(chain.getArgs(), false);
return result;
});
}
installStatusBarAutoHideElementListener(classLoader);
}
private void installStatusBarAutoHideElementListener(ClassLoader classLoader) {
Class<?> statusBarAutoHideElementClass = ReflectionSupport.findClassIfExists(
"com.android.systemui.statusbar.phone.CentralSurfacesImpl$5",
classLoader);
if (statusBarAutoHideElementClass == null) {
return;
}
XposedHookSupport.hookAllMethodsIfExists(
runtimeContext.getFramework(),
statusBarAutoHideElementClass,
"hide",
chain -> {
Object result = chain.proceed();
updateStatusBarTransientShown(false);
return result;
});
}
private void updateTransientBarState(List<Object> args, boolean visible) {
Integer displayId = args != null && !args.isEmpty() ? asInteger(args.get(0)) : null;
boolean statusBarsAffected = transientArgsIncludeStatusBars(args);
if (displayId == null || displayId != 0 || !statusBarsAffected) {
return;
}
updateStatusBarTransientShown(visible);
}
private void updateStatusBarTransientShown(boolean shown) {
statusBarTransientShown = shown;
}
private boolean transientArgsIncludeStatusBars(List<Object> args) {
if (args == null || args.size() < 2) {
return true;
}
Object typesArg = args.get(1);
if (!(typesArg instanceof int[] types)) {
return true;
}
int statusBars = WindowInsets.Type.statusBars();
for (int type : types) {
if ((type & statusBars) != 0) {
return true;
}
}
return false;
}
private void installNotificationCollectionHooks(ClassLoader classLoader) { private void installNotificationCollectionHooks(ClassLoader classLoader) {
for (String className : List.of( for (String className : List.of(
"com.android.systemui.statusbar.notification.collection.NotifCollection", "com.android.systemui.statusbar.notification.collection.NotifCollection",
@@ -405,6 +534,13 @@ final class StockLayoutCanvasController {
Object target = chain.getThisObject(); Object target = chain.getThisObject();
Object alphaArg = firstArg(chain.getArgs()); Object alphaArg = firstArg(chain.getArgs());
if (target instanceof View view && alphaArg instanceof Float alpha) { if (target instanceof View view && alphaArg instanceof Float alpha) {
rememberPrivacyOverlayHiddenStatusBarRoot(view);
if (shouldSuppressHiddenStatusBarRootAlpha(view, alpha)) {
return null;
}
if (shouldSuppressPrivacyOverlayStatusBarAlpha(view, alpha)) {
return null;
}
if (shouldSuppressConflictingShadeHeaderAlpha(view, alpha)) { if (shouldSuppressConflictingShadeHeaderAlpha(view, alpha)) {
return null; return null;
} }
@@ -421,7 +557,11 @@ final class StockLayoutCanvasController {
rememberAodPluginVisualAlpha(view, alpha); rememberAodPluginVisualAlpha(view, alpha);
} }
} }
return chain.proceed(); Object result = chain.proceed();
if (target instanceof View view) {
syncUnlockedHostsFromStockStatusBar(view);
}
return result;
}); });
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setVisibility", chain -> { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setVisibility", chain -> {
Object target = chain.getThisObject(); Object target = chain.getThisObject();
@@ -437,6 +577,7 @@ final class StockLayoutCanvasController {
Object result = chain.proceed(); Object result = chain.proceed();
if (target instanceof View view && visibilityArg instanceof Integer visibility) { if (target instanceof View view && visibilityArg instanceof Integer visibility) {
scheduleStatusChipRefreshIfContentChanged(view); scheduleStatusChipRefreshIfContentChanged(view);
syncUnlockedHostsFromStockStatusBar(view);
} }
return result; return result;
}); });
@@ -448,7 +589,26 @@ final class StockLayoutCanvasController {
return null; return null;
} }
} }
return chain.proceed(); Object result = chain.proceed();
if (target instanceof View view) {
syncUnlockedHostsFromStockStatusBar(view);
}
return result;
});
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "setTranslationX", chain -> {
Object target = chain.getThisObject();
Object translationArg = firstArg(chain.getArgs());
if (target instanceof View view && translationArg instanceof Float translationX) {
rememberPrivacyOverlayHiddenStatusBarRoot(view);
if (shouldSuppressPrivacyOverlayStatusBarTranslationX(view, translationX)) {
return null;
}
}
Object result = chain.proceed();
if (target instanceof View view) {
syncUnlockedHostsFromStockStatusBar(view);
}
return result;
}); });
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "layout", chain -> { XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "layout", chain -> {
Object target = chain.getThisObject(); Object target = chain.getThisObject();
@@ -465,15 +625,21 @@ final class StockLayoutCanvasController {
right, right,
bottom); bottom);
if (override != null) { if (override != null) {
return chain.proceed(new Object[]{ Object result = chain.proceed(new Object[]{
override.left, override.left,
override.top, override.top,
override.right, override.right,
override.bottom override.bottom
}); });
syncUnlockedHostsFromStockStatusBar(view);
return result;
} }
} }
return chain.proceed(); Object result = chain.proceed();
if (target instanceof View view) {
syncUnlockedHostsFromStockStatusBar(view);
}
return result;
}); });
} }
@@ -566,13 +732,19 @@ final class StockLayoutCanvasController {
}); });
} }
Class<?> clockClass = ReflectionSupport.findClassIfExists( String[] clockClassNames = {
"com.android.systemui.statusbar.policy.QSClockIndicatorView", "com.android.systemui.statusbar.policy.QSClockIndicatorView",
classLoader); "com.android.systemui.statusbar.policy.Clock"
if (clockClass != null) { };
for (String className : clockClassNames) {
Class<?> clockClass = ReflectionSupport.findClassIfExists(className, classLoader);
if (clockClass == null) {
continue;
}
hookViewAppearanceInvalidation(clockClass, "onDarkChanged"); hookViewAppearanceInvalidation(clockClass, "onDarkChanged");
hookViewAppearanceInvalidation(clockClass, "setTextColor"); hookViewAppearanceInvalidation(clockClass, "setTextColor");
hookClockTextInvalidation(clockClass, "notifyTimeChanged"); hookClockTextInvalidation(clockClass, "notifyTimeChanged");
hookClockTextInvalidation(clockClass, "updateClock");
hookClockTextInvalidation(clockClass, "setText"); hookClockTextInvalidation(clockClass, "setText");
hookViewLayoutInvalidation(clockClass, "onLayout"); hookViewLayoutInvalidation(clockClass, "onLayout");
} }
@@ -1352,7 +1524,7 @@ final class StockLayoutCanvasController {
} }
hideLockscreenCardsNotificationHost(root); hideLockscreenCardsNotificationHost(root);
if (activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS if (activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
&& isAodLayoutRoot(root)) { && isAodLayoutRoot(root, activeScene)) {
renderAodCardsNotificationStripIfNeeded(root, settings, activeScene); renderAodCardsNotificationStripIfNeeded(root, settings, activeScene);
} else { } else {
removeAodCardsNotificationHost(root); removeAodCardsNotificationHost(root);
@@ -1491,6 +1663,9 @@ final class StockLayoutCanvasController {
} }
private void markStatusChipLifecycleDirtyIfNeeded(View view, View fallbackRoot) { private void markStatusChipLifecycleDirtyIfNeeded(View view, View fallbackRoot) {
if (RuntimeMutationGuard.isSnapshotRenderMutation()) {
return;
}
if (view == null || !isStatusChipCandidate(view)) { if (view == null || !isStatusChipCandidate(view)) {
View root = view != null ? unlockedStatusBarRootFor(view) : fallbackRoot; View root = view != null ? unlockedStatusBarRootFor(view) : fallbackRoot;
if (deferDrawerStatusChipRefresh(root)) { if (deferDrawerStatusChipRefresh(root)) {
@@ -1919,12 +2094,15 @@ final class StockLayoutCanvasController {
if (current.isUnlocked() && !previous.isUnlocked()) { if (current.isUnlocked() && !previous.isUnlocked()) {
darkIconDispatcherGuard.restoreForcedStates(); darkIconDispatcherGuard.restoreForcedStates();
trackedLockedStatusBarIconSurfaces.clear(); trackedLockedStatusBarIconSurfaces.clear();
lockedStatusBarIconSurfaceReadyRoots.clear();
} else if (previous.isLockscreen() && current.isAod()) { } else if (previous.isLockscreen() && current.isAod()) {
darkIconDispatcherGuard.restoreForcedStates(); darkIconDispatcherGuard.restoreForcedStates();
hideTrackedLockedStatusBarIconSurfaces(null); hideTrackedLockedStatusBarIconSurfaces(null);
lockedStatusBarIconSurfaceReadyRoots.clear();
} else if (previous.isLockscreen() && !current.isLockscreen()) { } else if (previous.isLockscreen() && !current.isLockscreen()) {
darkIconDispatcherGuard.restoreForcedStates(); darkIconDispatcherGuard.restoreForcedStates();
restoreTrackedLockedStatusBarIconSurfaces(null); restoreTrackedLockedStatusBarIconSurfaces(null);
lockedStatusBarIconSurfaceReadyRoots.clear();
} }
boolean lockscreenEntry = current.isLockscreen(); boolean lockscreenEntry = current.isLockscreen();
if (lockscreenEntry) { if (lockscreenEntry) {
@@ -1965,6 +2143,7 @@ final class StockLayoutCanvasController {
} }
unlockedIconRenderController.clearRoot(root); unlockedIconRenderController.clearRoot(root);
dirtyUnlockedLayoutRoots.add(root); dirtyUnlockedLayoutRoots.add(root);
lockedStatusBarIconSurfaceReadyRoots.remove(root);
if (isViewThread(root)) { if (isViewThread(root)) {
applyToRoot(root); applyToRoot(root);
} else { } else {
@@ -2113,6 +2292,87 @@ final class StockLayoutCanvasController {
debugBoundsOverlayController.bringToFront(root); debugBoundsOverlayController.bringToFront(root);
} }
private void syncUnlockedHostsFromStockStatusBar(View view) {
if (!isPhoneStatusBarRoot(view)) {
return;
}
View root = unlockedStatusBarRootWithUnlockedHostsFor(view);
if (root == null) {
return;
}
syncUnlockedHostsToStockStatusBar(
root,
runtimeContext.getModeStateRepository().getActiveScene());
}
private void syncUnlockedHostsToStockStatusBar(View root, SceneKey activeScene) {
if (root == null || !hasDirectUnlockedLayoutHost(root)) {
return;
}
if (!statusBarsRequestedVisible() && !statusBarTransientShown) {
ownedIconHostManager.setUnlockedHostsTransform(root, 0f, 0f, 0f);
return;
}
if (activeScene == null || !activeScene.isUnlocked()) {
ownedIconHostManager.setUnlockedHostsTransform(root, 1f, 0f, 0f);
return;
}
View stockStatusBar = phoneStatusBarRootIn(root);
if (stockStatusBar == null || stockStatusBar == root) {
ownedIconHostManager.setUnlockedHostsTransform(root, 1f, 0f, 0f);
return;
}
float alpha = stockStatusBar.getVisibility() == View.VISIBLE
? clampAlpha(stockStatusBar.getAlpha())
: 0f;
float translationX = stockStatusBar.getLeft() + stockStatusBar.getTranslationX();
float translationY = stockStatusBar.getTop() + stockStatusBar.getTranslationY();
ownedIconHostManager.setUnlockedHostsTransform(
root,
alpha,
translationX,
translationY);
}
private View unlockedStatusBarRootWithUnlockedHostsFor(View view) {
View current = view;
while (current != null) {
if (isUnlockedStatusBarRoot(current) && hasDirectUnlockedLayoutHost(current)) {
return current;
}
Object parent = current.getParent();
current = parent instanceof View parentView ? parentView : null;
}
View root = view.getRootView();
return isUnlockedStatusBarRoot(root) && hasDirectUnlockedLayoutHost(root) ? root : null;
}
private View phoneStatusBarRootIn(View root) {
if (root == null) {
return null;
}
if (isPhoneStatusBarRoot(root)) {
return root;
}
if (!(root instanceof ViewGroup group)) {
return null;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(group);
while (!queue.isEmpty()) {
View current = queue.removeFirst();
if (current != root && isPhoneStatusBarRoot(current)) {
return current;
}
if (current instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
queue.addLast(childGroup.getChildAt(i));
}
}
}
return null;
}
private void applyToRootInternal(View root) { private void applyToRootInternal(View root) {
if (root == null) { if (root == null) {
return; return;
@@ -2120,31 +2380,29 @@ final class StockLayoutCanvasController {
Context context = root.getContext(); Context context = root.getContext();
SbtSettings settings = RuntimeSettingsCache.get(context); SbtSettings settings = RuntimeSettingsCache.get(context);
boolean layoutEnabled = settings.clockEnabled; boolean layoutEnabled = settings.clockEnabled;
boolean statusBarRoot = isUnlockedStatusBarRoot(root); runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
updateActiveScene(root, context);
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
boolean statusBarRoot = isStatusBarLayoutRoot(root, activeScene);
markDirtyIfSettingsChanged(root, settings, statusBarRoot); markDirtyIfSettingsChanged(root, settings, statusBarRoot);
markDirtyIfClockTimeChanged(root, settings, statusBarRoot); markDirtyIfClockTimeChanged(root, settings, statusBarRoot);
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
if (!layoutEnabled) { if (!layoutEnabled) {
updateActiveScene(root, context);
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
statusIconModelTracker.updateActiveSlots(false, activeScene); statusIconModelTracker.updateActiveSlots(false, activeScene);
applyLayoutOffRoot(root, settings, statusBarRoot, activeScene); applyLayoutOffRoot(root, settings, statusBarRoot, activeScene);
return; return;
} }
installPendingPluginClassLoaderHooks(); installPendingPluginClassLoaderHooks();
layoutOffCleanedRoots.remove(root); layoutOffCleanedRoots.remove(root);
updateActiveScene(root, context);
if (statusIconModelTracker.updateActiveSlots( if (statusIconModelTracker.updateActiveSlots(
true, true,
runtimeContext.getModeStateRepository().getActiveScene())) { activeScene)) {
unlockedIconRenderController.clearRoot(root); unlockedIconRenderController.clearRoot(root);
dirtyUnlockedLayoutRoots.add(root); dirtyUnlockedLayoutRoots.add(root);
} }
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
boolean unlockedScene = activeScene != null && activeScene.isUnlocked(); boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
boolean aodScene = activeScene != null && activeScene.isAod(); boolean aodScene = activeScene != null && activeScene.isAod();
boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root); boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root);
boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root); boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root, activeScene);
boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root); boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root);
boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot; boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot;
if (aodScene if (aodScene
@@ -2225,11 +2483,14 @@ final class StockLayoutCanvasController {
activeScene, activeScene,
rootDirty, rootDirty,
lockedCarrierTextSource()); lockedCarrierTextSource());
boolean shouldShowUnlockedHosts = unlockedScene || aodScene || renderResult.shouldOwnLockscreen(); boolean shouldShowUnlockedHosts = unlockedScene
|| aodScene
|| (!unlockedScene && renderResult.shouldOwnLockscreen());
ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts); ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts);
if (shouldShowUnlockedHosts) { if (shouldShowUnlockedHosts) {
bringUnlockedHostsAndDebugOverlayToFront(root); bringUnlockedHostsAndDebugOverlayToFront(root);
} }
syncUnlockedHostsToStockStatusBar(root, activeScene);
syncAodOwnedHostVisualState(root, unlockedHosts); syncAodOwnedHostVisualState(root, unlockedHosts);
} }
} }
@@ -2260,6 +2521,7 @@ final class StockLayoutCanvasController {
renderResult.layoutChanged(), renderResult.layoutChanged(),
false, false,
activeScene); activeScene);
hideTrackedUnlockedStockSurfaces(root);
hideTrackedStatusChipSources(root); hideTrackedStatusChipSources(root);
if (renderResult.chipSourcesChanged() if (renderResult.chipSourcesChanged()
&& !scanTree && !scanTree
@@ -2289,6 +2551,35 @@ final class StockLayoutCanvasController {
}); });
} }
private void hideTrackedUnlockedStockSurfaces(View root) {
if (root == null || trackedUnlockedStockSurfaces.isEmpty()) {
return;
}
ArrayDeque<View> staleViews = new ArrayDeque<>();
for (View view : trackedUnlockedStockSurfaces) {
if (view == null || !view.isAttachedToWindow()) {
staleViews.add(view);
continue;
}
if (!isDescendantOf(view, root)) {
continue;
}
if (ownedIconHostManager.isOwnedHost(view)) {
staleViews.add(view);
continue;
}
if (isTrackableStatusChipSurface(view)) {
hideStatusChipSurface(root, view);
} else if (shouldHideNonChipView(view)) {
hideView(view, true);
} else {
restoreView(view);
staleViews.add(view);
}
}
removeStaleViews(trackedUnlockedStockSurfaces, staleViews);
}
private void applyLayoutOffRoot( private void applyLayoutOffRoot(
View root, View root,
SbtSettings settings, SbtSettings settings,
@@ -2329,6 +2620,17 @@ final class StockLayoutCanvasController {
|| findDirectOwnedHost(group, OwnedIconHostManager.TAG_AOD_CARDS_NOTIFICATION_HOST) != null; || findDirectOwnedHost(group, OwnedIconHostManager.TAG_AOD_CARDS_NOTIFICATION_HOST) != null;
} }
private boolean hasDirectUnlockedLayoutHost(View root) {
if (!(root instanceof ViewGroup group)) {
return false;
}
return findDirectOwnedHost(group, OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST) != null
|| findDirectOwnedHost(group, OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST) != null
|| findDirectOwnedHost(group, OwnedIconHostManager.TAG_UNLOCKED_CLOCK_HOST) != null
|| findDirectOwnedHost(group, OwnedIconHostManager.TAG_UNLOCKED_CARRIER_HOST) != null
|| findDirectOwnedHost(group, OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST) != null;
}
private boolean hasLockscreenCardsNotificationHost(View root) { private boolean hasLockscreenCardsNotificationHost(View root) {
return root instanceof ViewGroup group return root instanceof ViewGroup group
&& findDirectOwnedHost( && findDirectOwnedHost(
@@ -2437,8 +2739,7 @@ final class StockLayoutCanvasController {
} }
if (!isTrackableStatusChipSurface(view) if (!isTrackableStatusChipSurface(view)
&& !isTrackedHiddenStatusChipSource(root, view)) { && !isTrackedHiddenStatusChipSource(root, view)) {
hiddenViewStates.remove(view); forgetTrackedStatusChipState(view);
trackedUnlockedStockSurfaces.remove(view);
staleViews.add(view); staleViews.add(view);
continue; continue;
} }
@@ -2918,9 +3219,16 @@ final class StockLayoutCanvasController {
} }
private void hideOrDiscoverLockedStatusBarIconSurfaces(View root) { private void hideOrDiscoverLockedStatusBarIconSurfaces(View root) {
hideTrackedLockedStatusBarIconSurfaces(root); if (root != null && lockedStatusBarIconSurfaceReadyRoots.contains(root)) {
if (root != null) { return;
trackAndHideLockedStatusBarIconSurfaces(root); }
boolean hasTrackedSurfacesInRoot = hideTrackedLockedStatusBarIconSurfaces(root);
int discovered = 0;
if (!hasTrackedSurfacesInRoot && root != null) {
discovered = trackAndHideLockedStatusBarIconSurfaces(root);
}
if (root != null && (hasTrackedSurfacesInRoot || discovered > 0)) {
lockedStatusBarIconSurfaceReadyRoots.add(root);
} }
} }
@@ -2928,30 +3236,29 @@ final class StockLayoutCanvasController {
if (trackedLockedStatusBarIconSurfaces.isEmpty()) { if (trackedLockedStatusBarIconSurfaces.isEmpty()) {
return false; return false;
} }
boolean foundInRoot = false; boolean foundActiveInRoot = false;
ArrayDeque<View> staleViews = new ArrayDeque<>(); ArrayDeque<View> staleViews = new ArrayDeque<>();
for (View view : trackedLockedStatusBarIconSurfaces) { for (View view : trackedLockedStatusBarIconSurfaces) {
if (view == null || !view.isAttachedToWindow()) { if (view == null || !view.isAttachedToWindow()) {
staleViews.add(view); staleViews.add(view);
continue; continue;
} }
if (root == null || isDescendantOf(view, root)) { boolean inRoot = root == null || isDescendantOf(view, root);
foundInRoot = true;
if (isAlreadySuppressedHiddenView(view)) {
continue;
}
}
if (!isLockedStatusBarIconSurface(view)) { if (!isLockedStatusBarIconSurface(view)) {
restoreView(view); restoreView(view);
staleViews.add(view); staleViews.add(view);
continue; continue;
} }
if (root == null || isDescendantOf(view, root)) { if (inRoot) {
foundActiveInRoot = true;
if (isAlreadySuppressedHiddenView(view)) {
continue;
}
hideView(view, isCarrierView(view)); hideView(view, isCarrierView(view));
} }
} }
removeStaleViews(trackedLockedStatusBarIconSurfaces, staleViews); removeStaleViews(trackedLockedStatusBarIconSurfaces, staleViews);
return foundInRoot; return foundActiveInRoot;
} }
private boolean isAlreadySuppressedHiddenView(View view) { private boolean isAlreadySuppressedHiddenView(View view) {
@@ -2989,6 +3296,7 @@ final class StockLayoutCanvasController {
private boolean isTrackedStatusChipWriteTarget(View view) { private boolean isTrackedStatusChipWriteTarget(View view) {
if (view == null if (view == null
|| ownHiddenViewAlphaWriteDepth > 0 || ownHiddenViewAlphaWriteDepth > 0
|| RuntimeMutationGuard.isSnapshotRenderMutation()
|| !trackedStatusChipSources.contains(view) || !trackedStatusChipSources.contains(view)
|| !hiddenViewStates.containsKey(view) || !hiddenViewStates.containsKey(view)
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
@@ -3008,6 +3316,7 @@ final class StockLayoutCanvasController {
private boolean isStatusChipShellWriteTarget(View view) { private boolean isStatusChipShellWriteTarget(View view) {
if (view == null if (view == null
|| ownHiddenViewAlphaWriteDepth > 0 || ownHiddenViewAlphaWriteDepth > 0
|| RuntimeMutationGuard.isSnapshotRenderMutation()
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
|| !isLayoutEnabled(view.getContext()) || !isLayoutEnabled(view.getContext())
|| !isStatusChipShell(view)) { || !isStatusChipShell(view)) {
@@ -3111,6 +3420,11 @@ final class StockLayoutCanvasController {
} }
} }
removeStaleViews(trackedLockedStatusBarIconSurfaces, restoredViews); removeStaleViews(trackedLockedStatusBarIconSurfaces, restoredViews);
if (root != null) {
lockedStatusBarIconSurfaceReadyRoots.remove(root);
} else {
lockedStatusBarIconSurfaceReadyRoots.clear();
}
} }
private boolean hasTrackedStatusChipSources(View root) { private boolean hasTrackedStatusChipSources(View root) {
@@ -3344,6 +3658,129 @@ final class StockLayoutCanvasController {
return true; return true;
} }
private boolean shouldSuppressHiddenStatusBarRootAlpha(View view, float alpha) {
boolean privacyOverlayRoot = privacyOverlayHiddenStatusBarRoots.contains(view);
boolean suppress = alpha > 0f
&& ownHiddenViewAlphaWriteDepth <= 0
&& isPhoneStatusBarRoot(view)
&& view.isAttachedToWindow()
&& isLayoutEnabled(view.getContext())
&& !statusBarsRequestedVisible()
&& (privacyOverlayRoot || !statusBarTransientShown);
if (suppress) {
suppressedHiddenStatusBarRoots.add(view);
}
return suppress;
}
private void rememberPrivacyOverlayHiddenStatusBarRoot(View view) {
if (view == null
|| !isPhoneStatusBarRoot(view)
|| !view.isAttachedToWindow()
|| statusBarsRequestedVisible()
|| !isLayoutEnabled(view.getContext())
|| ongoingPrivacyChipInRoot(view) == null) {
return;
}
privacyOverlayHiddenStatusBarRoots.add(view);
}
private void releaseSuppressedHiddenStatusBarRoots() {
if (suppressedHiddenStatusBarRoots.isEmpty()) {
return;
}
ArrayDeque<View> staleRoots = new ArrayDeque<>();
for (View root : suppressedHiddenStatusBarRoots) {
if (root == null || !root.isAttachedToWindow()) {
staleRoots.add(root);
continue;
}
if (!isLayoutEnabled(root.getContext())) {
staleRoots.add(root);
continue;
}
Runnable restore = () -> {
if (root.isAttachedToWindow()
&& statusBarsRequestedVisible()
&& root.getAlpha() <= 0f) {
root.setAlpha(1f);
}
};
if (root.getHandler() != null) {
root.post(restore);
} else {
restore.run();
}
staleRoots.add(root);
}
removeStaleViews(suppressedHiddenStatusBarRoots, staleRoots);
}
private boolean shouldSuppressPrivacyOverlayStatusBarAlpha(View view, float alpha) {
if (alpha >= 0.99f
|| ownHiddenViewAlphaWriteDepth > 0
|| !isPhoneStatusBarRoot(view)
|| !isLayoutEnabled(view.getContext())
|| ongoingPrivacyChipInRoot(view) == null) {
return false;
}
if (alpha <= 0f && !statusBarsRequestedVisible()) {
return false;
}
return true;
}
private boolean shouldSuppressPrivacyOverlayStatusBarTranslationX(View view, float translationX) {
if (Math.abs(translationX) < 0.5f
|| ownHiddenViewAlphaWriteDepth > 0
|| !isPhoneStatusBarRoot(view)
|| !isLayoutEnabled(view.getContext())
|| ongoingPrivacyChipInRoot(view) == null) {
return false;
}
return true;
}
private boolean statusBarsRequestedVisible() {
return (systemBarRequestedVisibleTypes & WindowInsets.Type.statusBars()) != 0;
}
private static Integer asInteger(Object value) {
return value instanceof Integer integer ? integer : null;
}
private boolean isPhoneStatusBarRoot(View view) {
return view != null
&& view.getClass().getName().contains("PhoneStatusBarView");
}
private boolean isOngoingPrivacyChip(View view) {
return view != null
&& view.getClass().getName().contains("OngoingPrivacyChip")
&& "privacy_chip".equals(ViewIdNames.idName(view));
}
private View ongoingPrivacyChipInRoot(View view) {
View root = view != null ? view.getRootView() : null;
if (!(root instanceof ViewGroup group)) {
return null;
}
ArrayDeque<View> queue = new ArrayDeque<>();
queue.add(group);
while (!queue.isEmpty()) {
View current = queue.removeFirst();
if (isOngoingPrivacyChip(current)) {
return current;
}
if (current instanceof ViewGroup childGroup) {
for (int i = 0; i < childGroup.getChildCount(); i++) {
queue.addLast(childGroup.getChildAt(i));
}
}
}
return null;
}
private boolean isStatusChipSource(View view) { private boolean isStatusChipSource(View view) {
if (view == null || view.getVisibility() != View.VISIBLE) { if (view == null || view.getVisibility() != View.VISIBLE) {
return false; return false;
@@ -3384,6 +3821,9 @@ final class StockLayoutCanvasController {
if (view == null || ownedIconHostManager.isOwnedHost(view)) { if (view == null || ownedIconHostManager.isOwnedHost(view)) {
return false; return false;
} }
if (view.getVisibility() != View.VISIBLE) {
return false;
}
String idName = ViewIdNames.idName(view); String idName = ViewIdNames.idName(view);
if (!"ongoing_activity_capsule".equals(idName)) { if (!"ongoing_activity_capsule".equals(idName)) {
return false; return false;
@@ -3412,9 +3852,26 @@ final class StockLayoutCanvasController {
|| !isStatusChipCandidate(view)) { || !isStatusChipCandidate(view)) {
return false; return false;
} }
if (isCallStatusChip(view)
&& view.getVisibility() != View.VISIBLE
&& !isPhoneCallActive(view)) {
return false;
}
return hasVisibleStatusChipContent(view); return hasVisibleStatusChipContent(view);
} }
private boolean isPhoneCallActive(View view) {
if (view == null || view.getContext() == null) {
return false;
}
try {
TelecomManager telecomManager = view.getContext().getSystemService(TelecomManager.class);
return telecomManager != null && telecomManager.isInCall();
} catch (RuntimeException ignored) {
return false;
}
}
private boolean hasVisibleStatusChipContent(View source) { private boolean hasVisibleStatusChipContent(View source) {
if (!(source instanceof ViewGroup group)) { if (!(source instanceof ViewGroup group)) {
return false; return false;
@@ -4230,11 +4687,19 @@ final class StockLayoutCanvasController {
|| isStatusChipShell(view)) { || isStatusChipShell(view)) {
return false; return false;
} }
hiddenViewStates.remove(view); forgetTrackedStatusChip(view);
return true;
}
private void forgetTrackedStatusChip(View view) {
forgetTrackedStatusChipState(view);
trackedStatusChipSources.remove(view); trackedStatusChipSources.remove(view);
}
private void forgetTrackedStatusChipState(View view) {
hiddenViewStates.remove(view);
trackedUnlockedStockSurfaces.remove(view); trackedUnlockedStockSurfaces.remove(view);
statusChipLifecycleLayoutSignatureByView.remove(view); statusChipLifecycleLayoutSignatureByView.remove(view);
return true;
} }
private void cleanupLayoutOffRoot(View root, SceneKey activeScene) { private void cleanupLayoutOffRoot(View root, SceneKey activeScene) {
@@ -4244,6 +4709,7 @@ final class StockLayoutCanvasController {
} }
clearTrackedStatusChipState(); clearTrackedStatusChipState();
trackedLockedStatusBarIconSurfaces.clear(); trackedLockedStatusBarIconSurfaces.clear();
lockedStatusBarIconSurfaceReadyRoots.clear();
trackedUnlockedStockSurfaces.clear(); trackedUnlockedStockSurfaces.clear();
confirmedAodPluginViews.clear(); confirmedAodPluginViews.clear();
aodPluginVisualAlphaByView.clear(); aodPluginVisualAlphaByView.clear();
@@ -4681,8 +5147,9 @@ final class StockLayoutCanvasController {
View root, View root,
SceneKey activeScene SceneKey activeScene
) { ) {
if (!isUnlockedStatusBarRoot(root) if (!isStatusBarLayoutRoot(root, activeScene)
&& !(activeScene != null && activeScene.isAod() && isAodLayoutRoot(root))) { && !(activeScene != null && activeScene.isAod()
&& isAodLayoutRoot(root, activeScene))) {
return null; return null;
} }
if (activeScene != null if (activeScene != null
@@ -4738,11 +5205,30 @@ final class StockLayoutCanvasController {
|| className.contains("StatusBarWindowView"); || className.contains("StatusBarWindowView");
} }
private boolean isStatusBarLayoutRoot(View root, SceneKey activeScene) {
return isUnlockedStatusBarRoot(root)
|| (activeScene != null
&& activeScene.isLockscreen()
&& isNotificationShadeWindowRoot(root)
&& SystemUiCapabilities.supportsLockscreenShadeWindowRoot());
}
private void updateActiveScene(View root, Context context) { private void updateActiveScene(View root, Context context) {
if (root == null) { if (root == null) {
return; return;
} }
if (runtimeContext.getModeStateRepository().hasSystemUiStatusBarState()) { if (runtimeContext.getModeStateRepository().hasSystemUiStatusBarState()) {
if (isNotificationShadeWindowRoot(root)
&& isKeyguardLocked(context)
&& !runtimeContext.getModeStateRepository().isKeyguardGoingAway()) {
NotificationDisplayStyle notificationStyle =
SystemNotificationDisplayStyleDetector.read(context);
if (isInteractive(context)) {
runtimeContext.getModeStateRepository().setLockscreen(notificationStyle);
} else {
runtimeContext.getModeStateRepository().setAod(notificationStyle);
}
}
return; return;
} }
NotificationDisplayStyle notificationStyle = NotificationDisplayStyle notificationStyle =
@@ -4765,7 +5251,24 @@ final class StockLayoutCanvasController {
} }
private boolean isAodLayoutRoot(View root) { private boolean isAodLayoutRoot(View root) {
return isAodRoot(root) || hasConfirmedAodPluginDescendant(root); return isAodLayoutRoot(root, null);
}
private boolean isAodLayoutRoot(View root, SceneKey activeScene) {
return isAodRoot(root)
|| hasConfirmedAodPluginDescendant(root)
|| isExperimentalAodStatusBarRoot(root, activeScene);
}
private boolean isExperimentalAodStatusBarRoot(View root, SceneKey activeScene) {
if (root == null
|| activeScene == null
|| !activeScene.isAod()
|| !isUnlockedStatusBarRoot(root)) {
return false;
}
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
} }
private boolean hasConfirmedAodPluginDescendant(View root) { private boolean hasConfirmedAodPluginDescendant(View root) {
@@ -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) {
@@ -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) {
@@ -11,6 +11,7 @@ import android.graphics.Path;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.BatteryManager; import android.os.BatteryManager;
@@ -35,6 +36,7 @@ import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection; import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
import se.ajpanton.statusbartweak.runtime.RuntimeMutationGuard;
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;
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds; import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
@@ -238,35 +240,51 @@ final class SnapshotRenderer {
proxy.setTag(key); proxy.setTag(key);
proxyChanged = true; proxyChanged = true;
} }
BitmapResult bitmapResult = bitmapFor(proxy, bounds);
Bitmap bitmap = bitmapResult.bitmap();
SnapshotRenderState desiredState = renderStateFor(placement); SnapshotRenderState desiredState = renderStateFor(placement);
boolean bitmapChanged = bitmapResult.created() Drawable animatedDrawable = animatedNotificationDrawable(placement);
|| !desiredState.equals(statesByProxy.get(proxy)); if (animatedDrawable != null) {
if (bitmapChanged) { SnapshotRenderState currentState = statesByProxy.get(proxy);
if (placement.overflowDot()) { boolean drawableChanged = !(proxy.getDrawable() instanceof AnimationDrawable)
drawOverflowDot(bitmap, placement.overflowDotColor(), placement.sourceOffsetX()); || !canReuseAnimatedProxy(currentState, desiredState);
} else if (placement.heldSignal() != null) { if (drawableChanged) {
drawHeldSignal(placement.heldSignal(), bitmap); Drawable proxyDrawable = newDrawableInstance(animatedDrawable);
} else if (placement.source() != null proxy.setImageDrawable(proxyDrawable);
&& placement.source().mode() == SnapshotMode.APP_ICON) { proxyChanged = true;
drawAppIcon(host.getContext(), placement.source(), bitmap); }
} else if (placement.source() != null) { startAnimationIfNeeded(proxy.getDrawable());
drawSnapshot(placement, bitmap); if (!desiredState.equals(currentState)) {
applyStatusBarTintTarget(placement.source(), bitmap); statesByProxy.put(proxy, desiredState);
normalizeSemiTransparentDarkIcon(bitmap); }
if (placement.signal()) { } else {
saveHeldSignal(key, bounds, bitmap); BitmapResult bitmapResult = bitmapFor(proxy, bounds);
Bitmap bitmap = bitmapResult.bitmap();
boolean bitmapChanged = bitmapResult.created()
|| !desiredState.equals(statesByProxy.get(proxy));
if (bitmapChanged) {
if (placement.overflowDot()) {
drawOverflowDot(bitmap, placement.overflowDotColor(), placement.sourceOffsetX());
} else if (placement.heldSignal() != null) {
drawHeldSignal(placement.heldSignal(), bitmap);
} else if (placement.source() != null
&& placement.source().mode() == SnapshotMode.APP_ICON) {
drawAppIcon(host.getContext(), placement.source(), bitmap);
} else if (placement.source() != null) {
drawSnapshot(placement, bitmap);
applyStatusBarTintTarget(placement.source(), bitmap);
normalizeSemiTransparentDarkIcon(bitmap);
if (placement.signal()) {
saveHeldSignal(key, bounds, bitmap);
}
} else {
bitmap.eraseColor(Color.TRANSPARENT);
} }
} else { statesByProxy.put(proxy, desiredState);
bitmap.eraseColor(Color.TRANSPARENT); proxyChanged = true;
}
if (proxy.getDrawable() == null) {
proxy.setImageBitmap(bitmap);
proxyChanged = true;
} }
statesByProxy.put(proxy, desiredState);
proxyChanged = true;
}
if (proxy.getDrawable() == null) {
proxy.setImageBitmap(bitmap);
proxyChanged = true;
} }
if (proxy.getAlpha() != 1f) { if (proxy.getAlpha() != 1f) {
proxy.setAlpha(1f); proxy.setAlpha(1f);
@@ -304,6 +322,52 @@ final class SnapshotRenderer {
return renderedBounds; return renderedBounds;
} }
private boolean canReuseAnimatedProxy(
SnapshotRenderState currentState,
SnapshotRenderState desiredState
) {
return currentState != null
&& desiredState != null
&& Objects.equals(currentState.key(), desiredState.key())
&& currentState.width() == desiredState.width()
&& currentState.height() == desiredState.height()
&& currentState.signal() == desiredState.signal()
&& currentState.overflowDot() == desiredState.overflowDot()
&& currentState.overflowDotColor() == desiredState.overflowDotColor()
&& currentState.sourceOffsetX() == desiredState.sourceOffsetX()
&& currentState.sourceRenderBoundsSignature() == desiredState.sourceRenderBoundsSignature()
&& Objects.equals(currentState.heldSignalKey(), desiredState.heldSignalKey())
&& currentState.heldBitmapId() == desiredState.heldBitmapId()
&& currentState.sourceMode() == desiredState.sourceMode()
&& currentState.tintSignature() == desiredState.tintSignature();
}
private Drawable animatedNotificationDrawable(SnapshotPlacement placement) {
if (placement == null
|| placement.source() == null
|| placement.source().mode() != SnapshotMode.NOTIFICATION_DRAWABLE
|| !(placement.source().view() instanceof ImageView imageView)) {
return null;
}
Drawable drawable = imageView.getDrawable();
return drawable instanceof AnimationDrawable ? drawable : null;
}
private Drawable newDrawableInstance(Drawable drawable) {
Drawable.ConstantState constantState = drawable != null ? drawable.getConstantState() : null;
if (constantState != null) {
return constantState.newDrawable().mutate();
}
return drawable != null ? drawable.mutate() : null;
}
private void startAnimationIfNeeded(Drawable drawable) {
if (drawable instanceof AnimationDrawable animationDrawable && !animationDrawable.isRunning()) {
animationDrawable.setVisible(true, true);
animationDrawable.start();
}
}
void clear(ViewGroup host) { void clear(ViewGroup host) {
LinkedHashMap<String, ImageView> proxies = proxiesByHost.remove(host); LinkedHashMap<String, ImageView> proxies = proxiesByHost.remove(host);
if (proxies == null) { if (proxies == null) {
@@ -748,16 +812,26 @@ final class SnapshotRenderer {
bitmap.getHeight())) { bitmap.getHeight())) {
return; return;
} }
sourceView.setVisibility(View.VISIBLE); RuntimeMutationGuard.enterSnapshotRenderMutation();
sourceView.setAlpha(1f); try {
if (source.mode() == SnapshotMode.STATUS_CHIP) { sourceView.setVisibility(View.VISIBLE);
drawTemporarilySizedView(sourceView, canvas, bitmap.getWidth(), bitmap.getHeight()); sourceView.setAlpha(1f);
} else { if (source.mode() == SnapshotMode.STATUS_CHIP) {
sourceView.draw(canvas); drawTemporarilySizedView(sourceView, canvas, bitmap.getWidth(), bitmap.getHeight());
} else {
sourceView.draw(canvas);
}
} finally {
RuntimeMutationGuard.exitSnapshotRenderMutation();
} }
} finally { } finally {
sourceView.setAlpha(oldAlpha); RuntimeMutationGuard.enterSnapshotRenderMutation();
sourceView.setVisibility(oldVisibility); try {
sourceView.setAlpha(oldAlpha);
sourceView.setVisibility(oldVisibility);
} finally {
RuntimeMutationGuard.exitSnapshotRenderMutation();
}
} }
} }
@@ -1183,7 +1257,9 @@ final class SnapshotRenderer {
view.getRight(), view.getRight(),
view.getBottom(), view.getBottom(),
view.getMeasuredWidth(), view.getMeasuredWidth(),
view.getMeasuredHeight())); view.getMeasuredHeight(),
view.getVisibility(),
view.getAlpha()));
if (view instanceof ViewGroup group) { if (view instanceof ViewGroup group) {
for (int i = 0; i < group.getChildCount(); i++) { for (int i = 0; i < group.getChildCount(); i++) {
captureFrameState(group.getChildAt(i), state); captureFrameState(group.getChildAt(i), state);
@@ -1207,6 +1283,12 @@ final class SnapshotRenderer {
View.MeasureSpec.makeMeasureSpec(frame.measuredHeight, View.MeasureSpec.EXACTLY)); View.MeasureSpec.makeMeasureSpec(frame.measuredHeight, View.MeasureSpec.EXACTLY));
} }
view.layout(frame.left, frame.top, frame.right, frame.bottom); view.layout(frame.left, frame.top, frame.right, frame.bottom);
if (view.getAlpha() != frame.alpha) {
view.setAlpha(frame.alpha);
}
if (view.getVisibility() != frame.visibility) {
view.setVisibility(frame.visibility);
}
} }
} }
@@ -1221,7 +1303,9 @@ final class SnapshotRenderer {
int right, int right,
int bottom, int bottom,
int measuredWidth, int measuredWidth,
int measuredHeight int measuredHeight,
int visibility,
float alpha
) { ) {
} }
@@ -21,6 +21,10 @@ final class UnlockedChipPlacementController {
} }
void clear() { void clear() {
resetMissingChipFallback();
}
private void resetMissingChipFallback() {
heldPlacements = null; heldPlacements = null;
lastCollisionBounds = null; lastCollisionBounds = null;
lastRootWidth = 0; lastRootWidth = 0;
@@ -47,10 +51,11 @@ final class UnlockedChipPlacementController {
return rawPlacements; return rawPlacements;
} }
ArrayList<SnapshotPlacement> placements = isAnyChipHidingEnabled(settings) boolean hasMetricSources = metricSources != null && !metricSources.isEmpty();
ArrayList<SnapshotPlacement> placements = isAnyChipHidingEnabled(settings) || !hasMetricSources
? new ArrayList<>() ? new ArrayList<>()
: heldPlacements(root); : heldPlacements(root);
if (placements.isEmpty()) { if (placements.isEmpty() && hasMetricSources) {
placements = emptyPlacement(root, metricSources); placements = emptyPlacement(root, metricSources);
} }
return placements; return placements;
@@ -10,6 +10,7 @@ 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.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;
@@ -464,7 +465,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;
} }
@@ -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;
@@ -88,7 +89,13 @@ final class UnlockedLayoutPlanner {
Bounds aodStatusbarBounds = scene != null && scene.isAod() Bounds aodStatusbarBounds = scene != null && scene.isAod()
? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds) ? aodStatusbarBounds(root, settings, aodNotificationBounds, aodStatusAnchorBounds)
: null; : null;
LayoutEdges edges = layoutEdges(root, settings, anchors, scene, aodStatusAnchorBounds); LayoutEdges edges = layoutEdges(
root,
settings,
anchors,
collectedIcons,
scene,
aodStatusAnchorBounds);
int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds); int statusbarHeight = statusbarHeight(root, anchors, scene, aodStatusContentBounds);
if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) { if (clockEnabledForScene(settings, scene) && clockLayout != null && clockLayout.width() > 0) {
bands.add(UnlockedLayoutBand.clock( bands.add(UnlockedLayoutBand.clock(
@@ -850,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
&& isExperimentalGenericAod(settings))) {
return 0; return 0;
} }
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0; return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
@@ -864,6 +873,10 @@ final class UnlockedLayoutPlanner {
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT; && scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT;
} }
private boolean isExperimentalGenericAod(SbtSettings settings) {
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
}
private ArrayList<SnapshotPlacement> unreadNotificationPlacements( private ArrayList<SnapshotPlacement> unreadNotificationPlacements(
ArrayList<SnapshotPlacement> placements ArrayList<SnapshotPlacement> placements
) { ) {
@@ -1393,6 +1406,7 @@ final class UnlockedLayoutPlanner {
View root, View root,
SbtSettings settings, SbtSettings settings,
RootAnchors anchors, RootAnchors anchors,
CollectedIcons collectedIcons,
SceneKey scene, SceneKey scene,
Bounds aodStatusAnchorBounds Bounds aodStatusAnchorBounds
) { ) {
@@ -1419,11 +1433,29 @@ final class UnlockedLayoutPlanner {
if (settings.layoutPaddingRightAddStock) { if (settings.layoutPaddingRightAddStock) {
right -= stockInsets.right; right -= stockInsets.right;
} }
if (scene != null && scene.isUnlocked() && hasStatusChipSources(collectedIcons)) {
LayoutEdges recentEdges = recentNonAodEdgesByRootWidth.get(rootWidth);
if (isReusableUnlockedEdge(left, recentEdges)) {
left = recentEdges.left();
}
}
LayoutEdges edges = new LayoutEdges(left, right); LayoutEdges edges = new LayoutEdges(left, right);
rememberNonAodEdges(rootWidth, scene, edges); rememberNonAodEdges(rootWidth, scene, edges);
return edges; return edges;
} }
private boolean hasStatusChipSources(CollectedIcons icons) {
return icons != null
&& ((icons.statusChips != null && !icons.statusChips.isEmpty())
|| (icons.statusChipMetrics != null && !icons.statusChipMetrics.isEmpty()));
}
private boolean isReusableUnlockedEdge(int currentLeft, LayoutEdges recentEdges) {
return recentEdges != null
&& recentEdges.right() > recentEdges.left()
&& Math.abs(currentLeft - recentEdges.left()) <= 8;
}
private LayoutEdges stableAodEdges(int rootWidth, LayoutEdges aodEdges) { private LayoutEdges stableAodEdges(int rootWidth, LayoutEdges aodEdges) {
if (rootWidth <= 0) { if (rootWidth <= 0) {
return aodEdges; return aodEdges;
@@ -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,12 @@ final class UnlockedStockSourceCollector {
LockscreenCardsNotificationIconSourceStore LockscreenCardsNotificationIconSourceStore
.snapshotAodNotificationPackageSources(root)); .snapshotAodNotificationPackageSources(root));
} }
if (notificationIcons.isEmpty() && isExperimentalGenericAod(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 +117,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 +135,21 @@ 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 boolean isExperimentalGenericAod(SbtSettings settings) {
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
}
private String reflectString(View view, String methodName, String... fieldNames) { private String reflectString(View view, String methodName, String... fieldNames) {
Object methodValue = ReflectionSupport.invokeMethod(view, methodName); Object methodValue = ReflectionSupport.invokeMethod(view, methodName);
if (methodValue != null) { if (methodValue != null) {
@@ -193,7 +214,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 +259,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 +342,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 +395,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;
@@ -1188,6 +1219,7 @@ final class UnlockedStockSourceCollector {
View view = source.view(); View view = source.view();
if (view == null if (view == null
|| view == root || view == root
|| view.getVisibility() != View.VISIBLE
|| !view.isAttachedToWindow() || !view.isAttachedToWindow()
|| !ViewGeometry.isDescendantOf(view, root) || !ViewGeometry.isDescendantOf(view, root)
|| ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false) || ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)
@@ -1301,7 +1333,7 @@ final class UnlockedStockSourceCollector {
} }
private boolean isStatusChipMetricCandidate(View root, View view, boolean allowHidden) { private boolean isStatusChipMetricCandidate(View root, View view, boolean allowHidden) {
if (view == null || view == root || root == null) { if (view == null || view == root || root == null || view.getVisibility() != View.VISIBLE) {
return false; return false;
} }
if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) { if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) {
@@ -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";
@@ -417,7 +417,7 @@ public final class AppIconRules {
continue; continue;
} }
String[] parts = entry.split("\\|", -1); String[] parts = entry.split("\\|", -1);
if (parts.length != 2 && parts.length != 4) { if (parts.length != 4) {
continue; continue;
} }
String pkg = parts[0]; String pkg = parts[0];
@@ -429,13 +429,8 @@ public final class AppIconRules {
int lastSeenOffsetMinutes; int lastSeenOffsetMinutes;
try { try {
firstSeen = Long.parseLong(parts[1]); firstSeen = Long.parseLong(parts[1]);
if (parts.length == 4) { lastSeen = Long.parseLong(parts[2]);
lastSeen = Long.parseLong(parts[2]); lastSeenOffsetMinutes = Integer.parseInt(parts[3]);
lastSeenOffsetMinutes = Integer.parseInt(parts[3]);
} else {
lastSeen = firstSeen;
lastSeenOffsetMinutes = currentTimezoneOffsetMinutes();
}
} catch (NumberFormatException ignored) { } catch (NumberFormatException ignored) {
continue; continue;
} }
@@ -21,11 +21,28 @@ public final class BatteryBarGeometry {
public final String position; public final String position;
public final String alignment; public final String alignment;
public final int thicknessDp; public final int thicknessDp;
public final int edgeOffsetDp;
public final String curvedGeometryMode;
public BatteryBarGeometry( public BatteryBarGeometry(
String position, String position,
String alignment, String alignment,
int thicknessDp int thicknessDp
) {
this(
position,
alignment,
thicknessDp,
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT,
SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT);
}
public BatteryBarGeometry(
String position,
String alignment,
int thicknessDp,
int edgeOffsetDp,
String curvedGeometryMode
) { ) {
this.position = sanitizePosition(position); this.position = sanitizePosition(position);
this.alignment = sanitizeAlignment(alignment); this.alignment = sanitizeAlignment(alignment);
@@ -33,6 +50,11 @@ public final class BatteryBarGeometry {
thicknessDp, thicknessDp,
SbtDefaults.BATTERY_BAR_THICKNESS_DP_MIN, SbtDefaults.BATTERY_BAR_THICKNESS_DP_MIN,
SbtDefaults.BATTERY_BAR_THICKNESS_DP_MAX); SbtDefaults.BATTERY_BAR_THICKNESS_DP_MAX);
this.edgeOffsetDp = clamp(
edgeOffsetDp,
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN,
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX);
this.curvedGeometryMode = sanitizeCurvedGeometryMode(curvedGeometryMode);
} }
public static BatteryBarGeometry global(SbtSettings settings) { public static BatteryBarGeometry global(SbtSettings settings) {
@@ -45,7 +67,9 @@ public final class BatteryBarGeometry {
return new BatteryBarGeometry( return new BatteryBarGeometry(
settings.batteryBarPosition, settings.batteryBarPosition,
settings.batteryBarAlignment, settings.batteryBarAlignment,
settings.batteryBarThicknessDp); settings.batteryBarThicknessDp,
settings.batteryBarEdgeOffsetDp,
settings.batteryBarCurvedGeometryMode);
} }
public static BatteryBarGeometry resolve( public static BatteryBarGeometry resolve(
@@ -68,17 +92,19 @@ public final class BatteryBarGeometry {
return safeFallback; return safeFallback;
} }
String[] parts = value.split("\\|", -1); String[] parts = value.split("\\|", -1);
if (parts.length != 3 && parts.length != 4) { if (parts.length != 5) {
return safeFallback; return safeFallback;
} }
return new BatteryBarGeometry( return new BatteryBarGeometry(
parts[0], parts[0],
parts[1], parts[1],
parseInt(parts[2], safeFallback.thicknessDp)); parseInt(parts[2], safeFallback.thicknessDp),
parseInt(parts[3], safeFallback.edgeOffsetDp),
parts[4]);
} }
public String encode() { public String encode() {
return position + "|" + alignment + "|" + thicknessDp; return position + "|" + alignment + "|" + thicknessDp + "|" + edgeOffsetDp + "|" + curvedGeometryMode;
} }
public String signature() { public String signature() {
@@ -104,6 +130,13 @@ public final class BatteryBarGeometry {
return "ltr"; return "ltr";
} }
private static String sanitizeCurvedGeometryMode(String mode) {
if ("length".equals(mode)) {
return "length";
}
return "off";
}
private static int parseInt(String value, int fallback) { private static int parseInt(String value, int fallback) {
try { try {
return Integer.parseInt(value); return Integer.parseInt(value);
@@ -51,7 +51,7 @@ public final class SbtDefaults {
public static final boolean AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT = false; public static final boolean AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT = false;
public static final boolean BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT = true; public static final boolean BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT = true;
public static final boolean BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT = true; public static final boolean BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT = true;
public static final boolean BATTERY_BAR_ENABLED_DEFAULT = false; public static final boolean BATTERY_BAR_ENABLED_DEFAULT = true;
public static final String BATTERY_BAR_POSITION_DEFAULT = "top"; public static final String BATTERY_BAR_POSITION_DEFAULT = "top";
public static final String BATTERY_BAR_ALIGNMENT_DEFAULT = "ltr"; public static final String BATTERY_BAR_ALIGNMENT_DEFAULT = "ltr";
public static final int BATTERY_BAR_THICKNESS_DP_DEFAULT = 2; public static final int BATTERY_BAR_THICKNESS_DP_DEFAULT = 2;
@@ -99,6 +99,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;
@@ -39,6 +39,8 @@ public final class SbtSettings {
"debug_visualize_camera_cutout"; "debug_visualize_camera_cutout";
public static final String KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS = public static final String KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS =
"debug_visualize_aod_stock_containers"; "debug_visualize_aod_stock_containers";
public static final String KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT =
"debug_experimental_aod_statusbar_root";
public static final String KEY_LOCKED_NOTIFICATION_MODE = public static final String KEY_LOCKED_NOTIFICATION_MODE =
"locked_notification_mode"; "locked_notification_mode";
public static final String KEY_UNLOCKED_MAX_ICONS_PER_ROW = "unlocked_max_icons_per_row"; public static final String KEY_UNLOCKED_MAX_ICONS_PER_ROW = "unlocked_max_icons_per_row";
@@ -78,13 +80,14 @@ public final class SbtSettings {
public static final String KEY_BATTERY_BAR_GEOMETRY_OVERRIDE_ENABLED_PREFIX = public static final String KEY_BATTERY_BAR_GEOMETRY_OVERRIDE_ENABLED_PREFIX =
"battery_bar_geometry_override_enabled_"; "battery_bar_geometry_override_enabled_";
public static final String KEY_BATTERY_BAR_GEOMETRY_PREFIX = "battery_bar_geometry_"; public static final String KEY_BATTERY_BAR_GEOMETRY_PREFIX = "battery_bar_geometry_";
public static final String KEY_BATTERY_BAR_SCENARIO_ENABLED_PREFIX =
"battery_bar_scenario_enabled_";
public static final String KEY_BATTERY_BAR_MIN_LEVEL = "battery_bar_min_level"; public static final String KEY_BATTERY_BAR_MIN_LEVEL = "battery_bar_min_level";
public static final String KEY_BATTERY_BAR_MAX_LEVEL = "battery_bar_max_level"; public static final String KEY_BATTERY_BAR_MAX_LEVEL = "battery_bar_max_level";
public static final String KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR = "battery_bar_default_discharge_color"; public static final String KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR = "battery_bar_default_discharge_color";
public static final String KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR = "battery_bar_default_charge_color"; public static final String KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR = "battery_bar_default_charge_color";
public static final String KEY_BATTERY_BAR_THRESHOLDS = "battery_bar_thresholds"; public static final String KEY_BATTERY_BAR_THRESHOLDS = "battery_bar_thresholds";
public static final String BATTERY_BAR_CURVED_GEOMETRY_OFF = "off"; public static final String BATTERY_BAR_CURVED_GEOMETRY_OFF = "off";
public static final String BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL = "horizontal";
public static final String BATTERY_BAR_CURVED_GEOMETRY_LENGTH = "length"; public static final String BATTERY_BAR_CURVED_GEOMETRY_LENGTH = "length";
public static final String KEY_CLOCK_ENABLED = "clock_enabled"; public static final String KEY_CLOCK_ENABLED = "clock_enabled";
public static final String KEY_CLOCK_ENABLED_LOCKSCREEN = "clock_enabled_lockscreen"; public static final String KEY_CLOCK_ENABLED_LOCKSCREEN = "clock_enabled_lockscreen";
@@ -173,7 +176,6 @@ public final class SbtSettings {
public static final String KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX = "layout_status_vertical_offset_px"; public static final String KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX = "layout_status_vertical_offset_px";
public static final String KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS = "layout_status_icon_height_steps"; public static final String KEY_LAYOUT_STATUS_ICON_HEIGHT_STEPS = "layout_status_icon_height_steps";
public static final String KEY_SYSTEM_ICON_BLOCKED_MODES = SystemIconRules.PREF_KEY_BLOCKED_MODES; public static final String KEY_SYSTEM_ICON_BLOCKED_MODES = SystemIconRules.PREF_KEY_BLOCKED_MODES;
public static final String KEY_SYSTEM_ICON_HIDE_SLOTS = "system_icon_hide_slots";
public static final String KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE = "system_icon_dual_sim_signal_mode"; public static final String KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE = "system_icon_dual_sim_signal_mode";
public static final String SYSTEM_ICON_DUAL_SIM_SEPARATE = "separate"; public static final String SYSTEM_ICON_DUAL_SIM_SEPARATE = "separate";
public static final String SYSTEM_ICON_DUAL_SIM_FIRST = "first"; public static final String SYSTEM_ICON_DUAL_SIM_FIRST = "first";
@@ -285,6 +287,10 @@ public final class SbtSettings {
return KEY_BATTERY_BAR_GEOMETRY_PREFIX + scenarioId; return KEY_BATTERY_BAR_GEOMETRY_PREFIX + scenarioId;
} }
public static String batteryBarScenarioEnabledKey(String scenarioId) {
return KEY_BATTERY_BAR_SCENARIO_ENABLED_PREFIX + scenarioId;
}
private static String suffixedKey(String base, int index) { private static String suffixedKey(String base, int index) {
return indexedKey(base, index); return indexedKey(base, index);
} }
@@ -318,6 +324,7 @@ public final class SbtSettings {
public final String batteryBarCurvedGeometryMode; public final String batteryBarCurvedGeometryMode;
public final Set<String> batteryBarGeometryOverrideEnabledScenarios; public final Set<String> batteryBarGeometryOverrideEnabledScenarios;
public final Map<String, String> batteryBarGeometryOverrides; public final Map<String, String> batteryBarGeometryOverrides;
public final Map<String, Boolean> batteryBarScenarioEnabled;
public final int batteryBarMinLevel; public final int batteryBarMinLevel;
public final int batteryBarMaxLevel; public final int batteryBarMaxLevel;
public final String batteryBarDefaultDischargeColor; public final String batteryBarDefaultDischargeColor;
@@ -420,6 +427,7 @@ public final class SbtSettings {
public final boolean debugVisualizeChipContainer; public final boolean debugVisualizeChipContainer;
public final boolean debugVisualizeCameraCutout; public final boolean debugVisualizeCameraCutout;
public final boolean debugVisualizeAodStockContainers; public final boolean debugVisualizeAodStockContainers;
public final boolean debugExperimentalAodStatusbarRoot;
private SbtSettings(boolean enableAodCutoutLimit, private SbtSettings(boolean enableAodCutoutLimit,
boolean enableLockCutoutLimit, boolean enableLockCutoutLimit,
@@ -451,6 +459,7 @@ public final class SbtSettings {
String batteryBarCurvedGeometryMode, String batteryBarCurvedGeometryMode,
Set<String> batteryBarGeometryOverrideEnabledScenarios, Set<String> batteryBarGeometryOverrideEnabledScenarios,
Map<String, String> batteryBarGeometryOverrides, Map<String, String> batteryBarGeometryOverrides,
Map<String, Boolean> batteryBarScenarioEnabled,
int batteryBarMinLevel, int batteryBarMinLevel,
int batteryBarMaxLevel, int batteryBarMaxLevel,
String batteryBarDefaultDischargeColor, String batteryBarDefaultDischargeColor,
@@ -552,7 +561,8 @@ public final class SbtSettings {
boolean debugVisualizeClockContainer, boolean debugVisualizeClockContainer,
boolean debugVisualizeChipContainer, boolean debugVisualizeChipContainer,
boolean debugVisualizeCameraCutout, boolean debugVisualizeCameraCutout,
boolean debugVisualizeAodStockContainers) { boolean debugVisualizeAodStockContainers,
boolean debugExperimentalAodStatusbarRoot) {
this.enableAodCutoutLimit = enableAodCutoutLimit; this.enableAodCutoutLimit = enableAodCutoutLimit;
this.enableLockCutoutLimit = enableLockCutoutLimit; this.enableLockCutoutLimit = enableLockCutoutLimit;
this.unlockedMaxIconsPerRow = unlockedMaxIconsPerRow; this.unlockedMaxIconsPerRow = unlockedMaxIconsPerRow;
@@ -595,6 +605,7 @@ public final class SbtSettings {
this.batteryBarGeometryOverrideEnabledScenarios = this.batteryBarGeometryOverrideEnabledScenarios =
sanitizeBatteryBarScenarioSet(batteryBarGeometryOverrideEnabledScenarios); sanitizeBatteryBarScenarioSet(batteryBarGeometryOverrideEnabledScenarios);
this.batteryBarGeometryOverrides = sanitizeBatteryBarScenarioMap(batteryBarGeometryOverrides); this.batteryBarGeometryOverrides = sanitizeBatteryBarScenarioMap(batteryBarGeometryOverrides);
this.batteryBarScenarioEnabled = sanitizeBatteryBarScenarioEnabledMap(batteryBarScenarioEnabled);
int clampedMinLevel = clampInt( int clampedMinLevel = clampInt(
batteryBarMinLevel, batteryBarMinLevel,
SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN,
@@ -854,6 +865,7 @@ public final class SbtSettings {
this.debugVisualizeChipContainer = debugVisualizeChipContainer; this.debugVisualizeChipContainer = debugVisualizeChipContainer;
this.debugVisualizeCameraCutout = debugVisualizeCameraCutout; this.debugVisualizeCameraCutout = debugVisualizeCameraCutout;
this.debugVisualizeAodStockContainers = debugVisualizeAodStockContainers; this.debugVisualizeAodStockContainers = debugVisualizeAodStockContainers;
this.debugExperimentalAodStatusbarRoot = debugExperimentalAodStatusbarRoot;
} }
public static SbtSettings defaults() { public static SbtSettings defaults() {
@@ -888,6 +900,7 @@ public final class SbtSettings {
SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT, SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT,
Collections.emptySet(), Collections.emptySet(),
Collections.emptyMap(), Collections.emptyMap(),
Collections.emptyMap(),
SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT,
SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT, SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT,
SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT, SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT,
@@ -989,7 +1002,8 @@ public final class SbtSettings {
SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT,
SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT,
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT, SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT,
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT,
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT
); );
} }
@@ -1017,8 +1031,7 @@ public final class SbtSettings {
} }
public static String readBatteryBarCurvedGeometryMode(String mode) { public static String readBatteryBarCurvedGeometryMode(String mode) {
if (BATTERY_BAR_CURVED_GEOMETRY_HORIZONTAL.equals(mode) if (BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals(mode)) {
|| BATTERY_BAR_CURVED_GEOMETRY_LENGTH.equals(mode)) {
return mode; return mode;
} }
return BATTERY_BAR_CURVED_GEOMETRY_OFF; return BATTERY_BAR_CURVED_GEOMETRY_OFF;
@@ -1227,6 +1240,7 @@ public final class SbtSettings {
SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT), SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT),
readBatteryBarScenarioSetFromPrefs(prefs), readBatteryBarScenarioSetFromPrefs(prefs),
readBatteryBarScenarioMapFromPrefs(prefs), readBatteryBarScenarioMapFromPrefs(prefs),
readBatteryBarScenarioEnabledMapFromPrefs(prefs),
clampInt(prefs.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), clampInt(prefs.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT),
SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN,
SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX), SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX),
@@ -1316,9 +1330,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutNotifPositionKey, SbtSettings::layoutNotifPositionKey,
@@ -1334,9 +1346,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_NOTIF_LOCK_COUNT, KEY_LAYOUT_NOTIF_LOCK_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutLockNotifPositionKey, SbtSettings::layoutLockNotifPositionKey,
@@ -1352,9 +1362,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_NOTIF_AOD_COUNT, KEY_LAYOUT_NOTIF_AOD_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_AOD, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutAodNotifPositionKey, SbtSettings::layoutAodNotifPositionKey,
@@ -1391,9 +1399,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_STATUS_UNLOCKED_COUNT, KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutStatusPositionKey, SbtSettings::layoutStatusPositionKey,
@@ -1409,9 +1415,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_STATUS_LOCK_COUNT, KEY_LAYOUT_STATUS_LOCK_COUNT,
KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutLockStatusPositionKey, SbtSettings::layoutLockStatusPositionKey,
@@ -1427,9 +1431,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
prefs, prefs,
KEY_LAYOUT_STATUS_AOD_COUNT, KEY_LAYOUT_STATUS_AOD_COUNT,
KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT),
readStringArrayFromPrefs( readStringArrayFromPrefs(
prefs, prefs,
SbtSettings::layoutAodStatusPositionKey, SbtSettings::layoutAodStatusPositionKey,
@@ -1476,7 +1478,10 @@ public final class SbtSettings {
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT), SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT),
prefs.getBoolean( prefs.getBoolean(
KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT) SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT),
prefs.getBoolean(
KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT)
); );
} }
@@ -1546,6 +1551,7 @@ public final class SbtSettings {
SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT), SbtDefaults.BATTERY_BAR_CURVED_GEOMETRY_MODE_DEFAULT),
readBatteryBarScenarioSetFromBundle(bundle), readBatteryBarScenarioSetFromBundle(bundle),
readBatteryBarScenarioMapFromBundle(bundle), readBatteryBarScenarioMapFromBundle(bundle),
readBatteryBarScenarioEnabledMapFromBundle(bundle),
clampInt(bundle.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT), clampInt(bundle.getInt(KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT),
SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN,
SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX), SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX),
@@ -1635,9 +1641,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutNotifPositionKey, SbtSettings::layoutNotifPositionKey,
@@ -1653,9 +1657,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_NOTIF_LOCK_COUNT, KEY_LAYOUT_NOTIF_LOCK_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutLockNotifPositionKey, SbtSettings::layoutLockNotifPositionKey,
@@ -1671,9 +1673,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_NOTIF_AOD_COUNT, KEY_LAYOUT_NOTIF_AOD_COUNT,
KEY_LAYOUT_NOTIF_ENABLED_AOD, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutAodNotifPositionKey, SbtSettings::layoutAodNotifPositionKey,
@@ -1710,9 +1710,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_STATUS_UNLOCKED_COUNT, KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
KEY_LAYOUT_STATUS_ENABLED_UNLOCKED, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutStatusPositionKey, SbtSettings::layoutStatusPositionKey,
@@ -1728,9 +1726,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_STATUS_LOCK_COUNT, KEY_LAYOUT_STATUS_LOCK_COUNT,
KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutLockStatusPositionKey, SbtSettings::layoutLockStatusPositionKey,
@@ -1746,9 +1742,7 @@ public final class SbtSettings {
readIconContainerCount( readIconContainerCount(
bundle, bundle,
KEY_LAYOUT_STATUS_AOD_COUNT, KEY_LAYOUT_STATUS_AOD_COUNT,
KEY_LAYOUT_STATUS_ENABLED_AOD, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT),
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT),
readStringArrayFromBundle( readStringArrayFromBundle(
bundle, bundle,
SbtSettings::layoutAodStatusPositionKey, SbtSettings::layoutAodStatusPositionKey,
@@ -1795,7 +1789,10 @@ public final class SbtSettings {
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT), SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT),
bundle.getBoolean( bundle.getBoolean(
KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS, KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT) SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT),
bundle.getBoolean(
KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT)
); );
} }
@@ -1822,35 +1819,21 @@ public final class SbtSettings {
private static int readIconContainerCount( private static int readIconContainerCount(
SharedPreferences prefs, SharedPreferences prefs,
String countKey, String countKey,
String legacyEnabledKey, int defaultCount
int defaultCount,
boolean legacyEnabledDefault
) { ) {
int fallback = iconContainerCountFallback( int value = prefs.getInt(countKey, defaultCount);
prefs.getBoolean(legacyEnabledKey, legacyEnabledDefault),
defaultCount);
int value = prefs.getInt(countKey, fallback);
return clampIconContainerCount(value); return clampIconContainerCount(value);
} }
private static int readIconContainerCount( private static int readIconContainerCount(
Bundle bundle, Bundle bundle,
String countKey, String countKey,
String legacyEnabledKey, int defaultCount
int defaultCount,
boolean legacyEnabledDefault
) { ) {
int fallback = iconContainerCountFallback( int value = bundle.getInt(countKey, defaultCount);
bundle.getBoolean(legacyEnabledKey, legacyEnabledDefault),
defaultCount);
int value = bundle.getInt(countKey, fallback);
return clampIconContainerCount(value); return clampIconContainerCount(value);
} }
private static int iconContainerCountFallback(boolean legacyEnabled, int defaultCount) {
return legacyEnabled ? defaultCount : 0;
}
private static String[] readStringArrayFromPrefs( private static String[] readStringArrayFromPrefs(
SharedPreferences prefs, SharedPreferences prefs,
IndexedKey key, IndexedKey key,
@@ -1989,6 +1972,34 @@ public final class SbtSettings {
return result; return result;
} }
private static Map<String, Boolean> readBatteryBarScenarioEnabledMapFromPrefs(SharedPreferences prefs) {
if (prefs == null) {
return Collections.emptyMap();
}
HashMap<String, Boolean> result = new HashMap<>();
for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) {
String key = batteryBarScenarioEnabledKey(scenario.id);
if (prefs.contains(key)) {
result.put(scenario.id, prefs.getBoolean(key, true));
}
}
return result;
}
private static Map<String, Boolean> readBatteryBarScenarioEnabledMapFromBundle(Bundle bundle) {
if (bundle == null) {
return Collections.emptyMap();
}
HashMap<String, Boolean> result = new HashMap<>();
for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) {
String key = batteryBarScenarioEnabledKey(scenario.id);
if (bundle.containsKey(key)) {
result.put(scenario.id, bundle.getBoolean(key, true));
}
}
return result;
}
private static Set<String> sanitizeBatteryBarScenarioSet(Set<String> source) { private static Set<String> sanitizeBatteryBarScenarioSet(Set<String> source) {
if (source == null || source.isEmpty()) { if (source == null || source.isEmpty()) {
return Collections.emptySet(); return Collections.emptySet();
@@ -2017,6 +2028,21 @@ public final class SbtSettings {
return result.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(result); return result.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(result);
} }
private static Map<String, Boolean> sanitizeBatteryBarScenarioEnabledMap(Map<String, Boolean> source) {
if (source == null || source.isEmpty()) {
return Collections.emptyMap();
}
HashMap<String, Boolean> result = new HashMap<>();
for (Map.Entry<String, Boolean> entry : source.entrySet()) {
String id = entry.getKey();
Boolean value = entry.getValue();
if (isBatteryBarScenarioId(id) && value != null) {
result.put(id, value);
}
}
return result.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(result);
}
private static boolean isBatteryBarScenarioId(String id) { private static boolean isBatteryBarScenarioId(String id) {
if (id == null) { if (id == null) {
return false; return false;
@@ -2035,9 +2061,7 @@ public final class SbtSettings {
} }
Map<String, Integer> parsed = SystemIconRules.parseBlockedModes( Map<String, Integer> parsed = SystemIconRules.parseBlockedModes(
readStringSetFromPrefs(prefs, KEY_SYSTEM_ICON_BLOCKED_MODES)); readStringSetFromPrefs(prefs, KEY_SYSTEM_ICON_BLOCKED_MODES));
HashMap<String, Integer> merged = new HashMap<>(parsed); return parsed.isEmpty() ? Collections.emptyMap() : parsed;
mergeLegacySystemIconHidden(merged, readStringSetFromPrefs(prefs, KEY_SYSTEM_ICON_HIDE_SLOTS));
return merged.isEmpty() ? Collections.emptyMap() : merged;
} }
private static Map<String, Integer> readSystemIconBlockedModesFromBundle(Bundle bundle) { private static Map<String, Integer> readSystemIconBlockedModesFromBundle(Bundle bundle) {
@@ -2046,20 +2070,7 @@ public final class SbtSettings {
} }
Map<String, Integer> parsed = SystemIconRules.parseBlockedModes( Map<String, Integer> parsed = SystemIconRules.parseBlockedModes(
readStringSetFromBundle(bundle, KEY_SYSTEM_ICON_BLOCKED_MODES)); readStringSetFromBundle(bundle, KEY_SYSTEM_ICON_BLOCKED_MODES));
HashMap<String, Integer> merged = new HashMap<>(parsed); return parsed.isEmpty() ? Collections.emptyMap() : parsed;
mergeLegacySystemIconHidden(merged, readStringSetFromBundle(bundle, KEY_SYSTEM_ICON_HIDE_SLOTS));
return merged.isEmpty() ? Collections.emptyMap() : merged;
}
private static void mergeLegacySystemIconHidden(HashMap<String, Integer> merged, Set<String> legacyHidden) {
if (merged == null || legacyHidden == null || legacyHidden.isEmpty()) {
return;
}
for (String slot : legacyHidden) {
SystemIconRules.setModeBlocked(merged, slot,
SystemIconRules.MODE_AOD | SystemIconRules.MODE_LOCK | SystemIconRules.MODE_UNLOCK,
true);
}
} }
public static void ensureReadable(Context context) { public static void ensureReadable(Context context) {
@@ -107,8 +107,12 @@ public class SbtSettingsProvider extends ContentProvider {
for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) { for (BatteryBarGeometry.Scenario scenario : BatteryBarGeometry.Scenario.values()) {
String enabledKey = BatteryBarGeometry.enabledKey(scenario); String enabledKey = BatteryBarGeometry.enabledKey(scenario);
String valueKey = BatteryBarGeometry.valueKey(scenario); String valueKey = BatteryBarGeometry.valueKey(scenario);
String scenarioEnabledKey = SbtSettings.batteryBarScenarioEnabledKey(scenario.id);
out.putBoolean(enabledKey, prefs.getBoolean(enabledKey, false)); out.putBoolean(enabledKey, prefs.getBoolean(enabledKey, false));
out.putString(valueKey, prefs.getString(valueKey, null)); out.putString(valueKey, prefs.getString(valueKey, null));
if (prefs.contains(scenarioEnabledKey)) {
out.putBoolean(scenarioEnabledKey, prefs.getBoolean(scenarioEnabledKey, true));
}
} }
out.putInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, out.putInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL,
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT)); prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT));
@@ -191,6 +195,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));
@@ -261,11 +268,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT)); SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT));
out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT)
? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT
: 0));
out.putString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE, out.putString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE, prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT)); SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT));
@@ -286,11 +289,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_LOCK_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_LOCK_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_LOCK_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_LOCK_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT)
? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT
: 0));
out.putInt(SbtSettings.layoutLockNotifIconHeightKey(), out.putInt(SbtSettings.layoutLockNotifIconHeightKey(),
prefs.getInt(SbtSettings.layoutLockNotifIconHeightKey(), prefs.getInt(SbtSettings.layoutLockNotifIconHeightKey(),
SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT)); SbtDefaults.LAYOUT_NOTIF_LOCK_ICON_HEIGHT_STEPS_DEFAULT));
@@ -305,11 +304,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT); SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_AOD_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_AOD_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_AOD_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_AOD_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT)
? SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT
: 0));
out.putInt(SbtSettings.layoutAodNotifIconHeightKey(), out.putInt(SbtSettings.layoutAodNotifIconHeightKey(),
prefs.getInt(SbtSettings.layoutAodNotifIconHeightKey(), prefs.getInt(SbtSettings.layoutAodNotifIconHeightKey(),
SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT)); SbtDefaults.LAYOUT_NOTIF_AOD_ICON_HEIGHT_STEPS_DEFAULT));
@@ -336,11 +331,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT)); SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT));
out.putInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT)
? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT
: 0));
out.putString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE, out.putString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE, prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT)); SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT));
@@ -365,11 +356,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT))); SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT)));
out.putInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_LOCK_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT)
? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT
: 0));
out.putInt(SbtSettings.layoutLockStatusIconHeightKey(), out.putInt(SbtSettings.layoutLockStatusIconHeightKey(),
prefs.getInt(SbtSettings.layoutLockStatusIconHeightKey(), prefs.getInt(SbtSettings.layoutLockStatusIconHeightKey(),
SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT)); SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT));
@@ -384,11 +371,7 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
out.putInt(SbtSettings.KEY_LAYOUT_STATUS_AOD_COUNT, out.putInt(SbtSettings.KEY_LAYOUT_STATUS_AOD_COUNT,
prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_AOD_COUNT, prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_AOD_COUNT,
prefs.getBoolean( SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT));
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT)
? SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT
: 0));
out.putInt(SbtSettings.layoutAodStatusIconHeightKey(), out.putInt(SbtSettings.layoutAodStatusIconHeightKey(),
prefs.getInt(SbtSettings.layoutAodStatusIconHeightKey(), prefs.getInt(SbtSettings.layoutAodStatusIconHeightKey(),
SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT)); SbtDefaults.LAYOUT_STATUS_ICON_HEIGHT_STEPS_DEFAULT));
@@ -403,7 +386,6 @@ public class SbtSettingsProvider extends ContentProvider {
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT); SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
putStringSetArrayList(out, prefs, SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES); putStringSetArrayList(out, prefs, SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES);
putStringSetArrayList(out, prefs, SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES); putStringSetArrayList(out, prefs, SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES);
putStringSetArrayList(out, prefs, SbtSettings.KEY_SYSTEM_ICON_HIDE_SLOTS);
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL, out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL,
prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL, false)); prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL, false));
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED, out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED,
File diff suppressed because it is too large Load Diff
@@ -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,
@@ -17,6 +17,7 @@ import android.view.inputmethod.EditorInfo;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import android.widget.TextView; import android.widget.TextView;
@@ -148,8 +149,6 @@ public final class LayoutFragment extends Fragment {
R.string.layout_notification_position_title, R.string.layout_notification_position_title,
SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT,
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT,
SbtSettings::layoutNotifPositionKey, SbtSettings::layoutNotifPositionKey,
SbtSettings::layoutNotifMiddleSideKey, SbtSettings::layoutNotifMiddleSideKey,
SbtSettings::layoutNotifVerticalOffsetKey, SbtSettings::layoutNotifVerticalOffsetKey,
@@ -166,8 +165,6 @@ public final class LayoutFragment extends Fragment {
R.string.layout_status_position_title, R.string.layout_status_position_title,
SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT, SbtSettings.KEY_LAYOUT_STATUS_UNLOCKED_COUNT,
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT, SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT,
SbtSettings::layoutStatusPositionKey, SbtSettings::layoutStatusPositionKey,
SbtSettings::layoutStatusMiddleSideKey, SbtSettings::layoutStatusMiddleSideKey,
SbtSettings::layoutStatusVerticalOffsetKey, SbtSettings::layoutStatusVerticalOffsetKey,
@@ -187,8 +184,6 @@ public final class LayoutFragment extends Fragment {
int sectionTitleId, int sectionTitleId,
String countKey, String countKey,
int countDefault, int countDefault,
String legacyEnabledKey,
boolean legacyEnabledDefault,
SbtSettings.IndexedKey positionKey, SbtSettings.IndexedKey positionKey,
SbtSettings.IndexedKey middleSideKey, SbtSettings.IndexedKey middleSideKey,
SbtSettings.IndexedKey verticalOffsetKey, SbtSettings.IndexedKey verticalOffsetKey,
@@ -223,9 +218,7 @@ public final class LayoutFragment extends Fragment {
int count = ViewTreeSupport.iconContainerCount( int count = ViewTreeSupport.iconContainerCount(
prefs, prefs,
countKey, countKey,
legacyEnabledKey, countDefault);
countDefault,
legacyEnabledDefault);
LayoutXmlCardSupport.setIndexedCardTitle(requireContext(), sourceCard, sectionTitleId, 0, count); LayoutXmlCardSupport.setIndexedCardTitle(requireContext(), sourceCard, sectionTitleId, 0, count);
setCompatPadding(sourceCard, count <= 1); setCompatPadding(sourceCard, count <= 1);
boolean cardsMode = LockedNotificationModeUi.MODE_CARDS.equals( boolean cardsMode = LockedNotificationModeUi.MODE_CARDS.equals(
@@ -242,12 +235,11 @@ public final class LayoutFragment extends Fragment {
countContainer.addView(ViewTreeSupport.iconContainerCountControl( countContainer.addView(ViewTreeSupport.iconContainerCountControl(
requireContext(), requireContext(),
count, count,
value -> ViewTreeSupport.persistIconContainerCount( value -> ViewTreeSupport.persistIconContainerCount(
requireContext(), requireContext(),
prefs, prefs,
countKey, countKey,
legacyEnabledKey, value),
value),
() -> { () -> {
if (rebuild[0] != null) { if (rebuild[0] != null) {
rebuild[0].run(); rebuild[0].run();
@@ -868,15 +860,30 @@ public final class LayoutFragment extends Fragment {
} }
private int resolveLeftSideId(RadioGroup group) { private int resolveLeftSideId(RadioGroup group) {
int id = group.getChildCount() > 0 ? group.getChildAt(0).getId() : View.NO_ID; int id = sideButtonId(group, 0);
return id != View.NO_ID ? id : 0; return id != View.NO_ID ? id : 0;
} }
private int resolveRightSideId(RadioGroup group) { private int resolveRightSideId(RadioGroup group) {
int id = group.getChildCount() > 1 ? group.getChildAt(1).getId() : View.NO_ID; int id = sideButtonId(group, 1);
return id != View.NO_ID ? id : resolveLeftSideId(group); return id != View.NO_ID ? id : resolveLeftSideId(group);
} }
private int sideButtonId(RadioGroup group, int radioIndex) {
int seen = 0;
for (int i = 0; i < group.getChildCount(); i++) {
View child = group.getChildAt(i);
if (!(child instanceof RadioButton)) {
continue;
}
if (seen == radioIndex) {
return child.getId();
}
seen++;
}
return View.NO_ID;
}
private void setNumericText(EditText input, int value) { private void setNumericText(EditText input, int value) {
if (input == null) { if (input == null) {
return; return;
@@ -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);
@@ -10,6 +10,7 @@ import android.view.ViewGroup;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RadioButton;
import android.widget.RadioGroup; import android.widget.RadioGroup;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
@@ -173,8 +174,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
R.id.status_vertical_offset_plus_fast, R.id.status_vertical_offset_plus_fast,
R.id.status_vertical_offset_reset, R.id.status_vertical_offset_reset,
R.string.layout_status_position_title, R.string.layout_status_position_title,
aod ? SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD : SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
aod ? SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT : SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT,
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT, SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT,
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT, SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT,
@@ -205,8 +204,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
R.id.notif_vertical_offset_plus_fast, R.id.notif_vertical_offset_plus_fast,
R.id.notif_vertical_offset_reset, R.id.notif_vertical_offset_reset,
R.string.layout_notification_position_title, R.string.layout_notification_position_title,
aod ? SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD : SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
aod ? SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT : SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT, SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT, SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT,
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT, SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT,
@@ -305,8 +302,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
int verticalPlusFastId, int verticalPlusFastId,
int verticalResetId, int verticalResetId,
int titleId, int titleId,
String legacyEnabledKey,
boolean legacyEnabledDefault,
String positionDefault, String positionDefault,
String middleSideDefault, String middleSideDefault,
int verticalOffsetDefault, int verticalOffsetDefault,
@@ -324,9 +319,7 @@ abstract class LockedSceneLayoutFragment extends Fragment {
int count = ViewTreeSupport.iconContainerCount( int count = ViewTreeSupport.iconContainerCount(
prefs, prefs,
countKey, countKey,
legacyEnabledKey, 1);
1,
legacyEnabledDefault);
View primary = configuredIconContainerCard( View primary = configuredIconContainerCard(
context, context,
prefs, prefs,
@@ -354,7 +347,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
verticalOffsetDefault, verticalOffsetDefault,
iconHeightKey, iconHeightKey,
iconHeightDefault, iconHeightDefault,
legacyEnabledKey,
countKey, countKey,
count, count,
rebuild[0]); rebuild[0]);
@@ -390,7 +382,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
null, null,
0, 0,
null, null,
null,
count, count,
null); null);
setTopMargin(context, copy, 0); setTopMargin(context, copy, 0);
@@ -452,7 +443,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
int verticalOffsetDefault, int verticalOffsetDefault,
@Nullable String iconHeightKey, @Nullable String iconHeightKey,
int iconHeightDefault, int iconHeightDefault,
@Nullable String legacyEnabledKey,
@Nullable String countKey, @Nullable String countKey,
int count, int count,
@Nullable Runnable onCountChanged @Nullable Runnable onCountChanged
@@ -470,7 +460,6 @@ abstract class LockedSceneLayoutFragment extends Fragment {
context, context,
prefs, prefs,
countKey, countKey,
legacyEnabledKey,
value), value),
onCountChanged), onCountChanged),
insertIndex); insertIndex);
@@ -787,7 +776,7 @@ abstract class LockedSceneLayoutFragment extends Fragment {
private int firstSideId(RadioGroup group, int rightId) { private int firstSideId(RadioGroup group, int rightId) {
for (int i = 0; i < group.getChildCount(); i++) { for (int i = 0; i < group.getChildCount(); i++) {
View child = group.getChildAt(i); View child = group.getChildAt(i);
if (child.getId() != rightId) { if (child instanceof RadioButton && child.getId() != rightId) {
return child.getId(); return child.getId();
} }
} }
@@ -13,6 +13,7 @@ import androidx.activity.OnBackPressedCallback;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle; import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.core.graphics.Insets; import androidx.core.graphics.Insets;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
@@ -318,13 +319,19 @@ public class MainActivity extends AppCompatActivity implements NavigationView.On
permanentNavigationView.setVisibility(View.VISIBLE); permanentNavigationView.setVisibility(View.VISIBLE);
drawerLayout.closeDrawer(GravityCompat.START, false); drawerLayout.closeDrawer(GravityCompat.START, false);
} else { } else {
drawerNavigationView.setVisibility(View.VISIBLE);
drawerLayout.closeDrawer(GravityCompat.START, false); drawerLayout.closeDrawer(GravityCompat.START, false);
permanentNavigationView.setVisibility(View.GONE); permanentNavigationView.setVisibility(View.GONE);
drawerNavigationView.requestLayout();
} }
drawerToggle.syncState(); drawerToggle.syncState();
} }
if (permanentSidebar) {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, GravityCompat.START);
drawerNavigationView.setVisibility(View.GONE);
} else {
drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, GravityCompat.START);
drawerNavigationView.setVisibility(View.VISIBLE);
drawerNavigationView.requestLayout();
}
updateNavigationHeaderHeight(drawerNavigationView); updateNavigationHeaderHeight(drawerNavigationView);
updateNavigationHeaderHeight(permanentNavigationView); updateNavigationHeaderHeight(permanentNavigationView);
applyPageTitleVisibility(); applyPageTitleVisibility();
@@ -477,8 +477,6 @@ public class SystemIconsFragment extends Fragment {
Set<String> encoded = SystemIconRules.encodeBlockedModes(blockedModes); Set<String> encoded = SystemIconRules.encodeBlockedModes(blockedModes);
prefs.edit() prefs.edit()
.putStringSet(SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES, encoded) .putStringSet(SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES, encoded)
// Clear legacy key so migration does not keep re-forcing stale global hides.
.putStringSet(SbtSettings.KEY_SYSTEM_ICON_HIDE_SLOTS, Collections.emptySet())
.apply(); .apply();
SbtSettings.ensureReadable(requireContext()); SbtSettings.ensureReadable(requireContext());
} }
@@ -238,25 +238,20 @@ final class ViewTreeSupport {
static int iconContainerCount( static int iconContainerCount(
SharedPreferences prefs, SharedPreferences prefs,
String countKey, String countKey,
String legacyEnabledKey, int defaultCount
int defaultCount,
boolean legacyEnabledDefault
) { ) {
int fallback = prefs.getBoolean(legacyEnabledKey, legacyEnabledDefault) ? defaultCount : 0; return SbtSettings.clampIconContainerCount(prefs.getInt(countKey, defaultCount));
return SbtSettings.clampIconContainerCount(prefs.getInt(countKey, fallback));
} }
static void persistIconContainerCount( static void persistIconContainerCount(
Context context, Context context,
SharedPreferences prefs, SharedPreferences prefs,
String countKey, String countKey,
String legacyEnabledKey,
int count int count
) { ) {
int clamped = SbtSettings.clampIconContainerCount(count); int clamped = SbtSettings.clampIconContainerCount(count);
prefs.edit() prefs.edit()
.putInt(countKey, clamped) .putInt(countKey, clamped)
.putBoolean(legacyEnabledKey, clamped > 0)
.apply(); .apply();
SbtSettings.ensureReadable(context); SbtSettings.ensureReadable(context);
} }
+174 -263
View File
@@ -17,38 +17,6 @@
android:text="@string/battery_bar_title" android:text="@string/battery_bar_title"
android:textAppearance="?attr/textAppearanceHeadline5" /> android:textAppearance="?attr/textAppearanceHeadline5" />
<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:text="@string/battery_bar_master_toggle_title"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textAllCaps="true"
android:letterSpacing="0.03"
android:textStyle="bold" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/battery_bar_enabled_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/battery_bar_enabled" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<LinearLayout <LinearLayout
android:id="@+id/battery_bar_geometry_section" android:id="@+id/battery_bar_geometry_section"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -72,46 +40,172 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/battery_bar_position_title" android:text="@string/battery_bar_base_settings_title"
android:textAppearance="?attr/textAppearanceSubtitle1" android:textAppearance="?attr/textAppearanceSubtitle1"
android:textAllCaps="true" android:textAllCaps="true"
android:letterSpacing="0.03" android:letterSpacing="0.03"
android:textStyle="bold" /> android:textStyle="bold" />
<RadioGroup <com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/battery_bar_position_group" android:id="@+id/battery_bar_enabled_switch"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/battery_bar_position_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_position_top" />
<RadioButton
android:id="@+id/battery_bar_position_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_position_bottom" />
</RadioGroup>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/battery_bar_thickness_title"
android:textAppearance="?attr/textAppearanceBody2" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:gravity="center_vertical" android:text="@string/battery_bar_enabled" />
android:orientation="horizontal">
<LinearLayout
android:id="@+id/battery_bar_base_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/battery_bar_position_title"
android:textAppearance="?attr/textAppearanceBody2" />
<RadioGroup
android:id="@+id/battery_bar_position_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/battery_bar_curved_geometry_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_curved_geometry_length" />
<RadioButton
android:id="@+id/battery_bar_position_top"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_position_top" />
<RadioButton
android:id="@+id/battery_bar_position_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_position_bottom" />
</RadioGroup>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/battery_bar_alignment_title"
android:textAppearance="?attr/textAppearanceBody2" />
<LinearLayout
android:id="@+id/battery_bar_alignment_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_ltr"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_ltr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_ltr" />
</LinearLayout>
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_center" />
</LinearLayout>
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_rtl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_rtl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_rtl" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/battery_bar_thickness_title"
android:textAppearance="?attr/textAppearanceBody2" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/battery_bar_thickness_minus" android:id="@+id/battery_bar_thickness_minus"
@@ -139,21 +233,21 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="+" /> android:text="+" />
</LinearLayout> </LinearLayout>
<TextView <TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/battery_bar_edge_offset_title" android:text="@string/battery_bar_edge_offset_title"
android:textAppearance="?attr/textAppearanceBody2" /> android:textAppearance="?attr/textAppearanceBody2" />
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/battery_bar_edge_offset_minus" android:id="@+id/battery_bar_edge_offset_minus"
@@ -181,200 +275,17 @@
android:layout_width="40dp" android:layout_width="40dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="+" /> android:text="+" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/battery_bar_curved_geometry_title"
android:textAppearance="?attr/textAppearanceBody2" />
<RadioGroup
android:id="@+id/battery_bar_curved_geometry_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp">
<RadioButton
android:id="@+id/battery_bar_curved_geometry_off"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_curved_geometry_off" />
<RadioButton
android:id="@+id/battery_bar_curved_geometry_horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_curved_geometry_horizontal" />
<RadioButton
android:id="@+id/battery_bar_curved_geometry_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="36dp"
android:text="@string/battery_bar_curved_geometry_length" />
</RadioGroup>
</LinearLayout>
</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:text="@string/battery_bar_alignment_title"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textAllCaps="true"
android:letterSpacing="0.03"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/battery_bar_alignment_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_ltr"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_ltr"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_ltr" />
</LinearLayout> </LinearLayout>
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_center"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_center" />
</LinearLayout>
<LinearLayout
android:id="@+id/battery_bar_alignment_cell_rtl"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="vertical"
android:padding="4dp">
<RadioButton
android:id="@+id/battery_bar_alignment_rtl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:minHeight="0dp"
android:minWidth="0dp"
android:padding="0dp" />
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginTop="4dp"
android:contentDescription="@null"
app:srcCompat="@drawable/sbt_align_rtl" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView <LinearLayout
android:id="@+id/battery_bar_scenario_cards_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:orientation="vertical" />
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/battery_bar_scenario_overrides_title"
android:textAllCaps="true"
android:textAppearance="?attr/textAppearanceSubtitle1"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/battery_bar_scenario_overrides_hint"
android:textAppearance="?attr/textAppearanceBody2" />
<LinearLayout
android:id="@+id/battery_bar_scenario_overrides_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical" />
</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_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>
+6 -12
View File
@@ -30,6 +30,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>
@@ -134,10 +135,11 @@
<string name="status_chips_hide_battery_label">Hide charging battery chip (unlocked)</string> <string name="status_chips_hide_battery_label">Hide charging battery chip (unlocked)</string>
<string name="misc_title">Misc</string> <string name="misc_title">Misc</string>
<string name="battery_bar_title">Battery bar</string> <string name="battery_bar_title">Battery bar</string>
<string name="battery_bar_master_toggle_title">Master toggle</string> <string name="battery_bar_base_settings_title">Base settings</string>
<string name="battery_bar_enabled">Enable battery bar</string> <string name="battery_bar_enabled">Enable battery bar</string>
<string name="battery_bar_use_base_settings">Use base settings</string>
<string name="battery_bar_position_title">Position</string> <string name="battery_bar_position_title">Position</string>
<string name="battery_bar_position_top">Top</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_ltr">&#8594;</string>
@@ -145,19 +147,10 @@
<string name="battery_bar_alignment_center">&#8596;</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_title">Curved top geometry</string> <string name="battery_bar_curved_geometry_length">Follow screen edges</string>
<string name="battery_bar_curved_geometry_off">Off</string>
<string name="battery_bar_curved_geometry_horizontal">On, horizontal position only</string>
<string name="battery_bar_curved_geometry_length">On, actual curve length</string>
<string name="battery_bar_scenario_overrides_title">Scenario overrides</string>
<string name="battery_bar_scenario_overrides_hint">Optional geometry overrides. Disabled scenarios use the default geometry above.</string>
<string name="battery_bar_scenario_group_unlocked">Unlocked</string>
<string name="battery_bar_scenario_group_transparent">Transparent statusbar</string> <string name="battery_bar_scenario_group_transparent">Transparent statusbar</string>
<string name="battery_bar_scenario_group_fullscreen">Fullscreen</string> <string name="battery_bar_scenario_group_fullscreen">Fullscreen</string>
<string name="battery_bar_lockscreen_override">Lockscreen</string> <string name="battery_bar_lockscreen_override">Lockscreen</string>
<string name="battery_bar_edit_override">Edit</string>
<string name="battery_bar_override_dialog_title">Edit %1$s geometry</string>
<string name="battery_bar_override_summary">%1$s, %2$s, %3$ddp thick</string>
<string name="battery_bar_mapping_title">Battery mapping</string> <string name="battery_bar_mapping_title">Battery mapping</string>
<string name="battery_bar_mapping_min_label">...0 at %1$d%%</string> <string name="battery_bar_mapping_min_label">...0 at %1$d%%</string>
<string name="battery_bar_mapping_max_label">...full at %1$d%%</string> <string name="battery_bar_mapping_max_label">...full at %1$d%%</string>
@@ -208,6 +201,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>
+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,143 @@
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() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
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,374 @@
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);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent);
} else {
startService(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) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return;
}
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,483 @@
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 if (Build.VERSION.SDK_INT >= 23) {
int flags = getWindow().getDecorView().getSystemUiVisibility();
if (lightBars) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
if (Build.VERSION.SDK_INT >= 26) {
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
} else {
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
if (Build.VERSION.SDK_INT >= 26) {
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.2 version=0.6