Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25e28c8501 | ||
|
|
624b5ee6b7 | ||
|
|
3ebebb5218 | ||
|
|
18f30fce57 | ||
|
|
352b7ab443 | ||
|
|
66f3c2e206 | ||
|
|
321048f787 | ||
|
|
4f57e9e33c | ||
|
|
282e9a9283 | ||
|
|
87de1c45e6 | ||
|
|
9083a3038a | ||
|
|
38b937ca3e | ||
|
|
bbdbd50d84 | ||
|
|
86eb5fec7b | ||
|
|
0ab20825d0 | ||
|
|
212dd5110c | ||
|
|
7be5f64262 | ||
|
|
5df8bd9e78 | ||
|
|
4964b39d01 | ||
|
|
42449f8c6a | ||
|
|
861ffd52d5 | ||
|
|
75861e2480 | ||
|
|
9dfca417bd | ||
|
|
18f1c6f20f | ||
|
|
2fe90ce3fc |
@@ -8,6 +8,7 @@
|
||||
/.gradle-tmp/
|
||||
/build/
|
||||
/app/build/
|
||||
/*/build/
|
||||
/.externalNativeBuild/
|
||||
/.cxx/
|
||||
/captures/
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
# StatusBarTweak
|
||||
|
||||
StatusBarTweak is an Xposed/LSPosed module for Samsung SystemUI. It focuses on
|
||||
making the status bar, lockscreen, and Always On Display more configurable while
|
||||
still keeping Samsung's own icons and visual style where possible.
|
||||
StatusBarTweak is an Xposed/LSPosed module originally built for Samsung
|
||||
SystemUI. It focuses on making the status bar, lockscreen, and Always On
|
||||
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
|
||||
8.0 / Android 16. Expect device- and One UI-version-specific behaviour.
|
||||
The project is physically tested only on a Samsung Galaxy Z Fold5 running One UI
|
||||
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
|
||||
|
||||
- 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
|
||||
icons, and Samsung status chips.
|
||||
- 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
|
||||
cutout.
|
||||
- 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.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A rooted Samsung device.
|
||||
- A modern Xposed implementation such as LSPosed.
|
||||
- A rooted Android 14+ device or emulator. Actual feature support depends on
|
||||
the SystemUI implementation; see the compatibility matrix.
|
||||
- A modern Xposed implementation such as LSPosed or another libxposed-compatible runtime.
|
||||
- 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
|
||||
non-Samsung SystemUI, and updates to One UI may require fixes.
|
||||
The module is developed and manually tested on a Samsung Galaxy Z Fold5 running
|
||||
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
|
||||
|
||||
StatusBarTweak is Samsung-specific in its current form. Some lower-level ideas,
|
||||
such as drawing a custom battery bar or parsing custom clock text, could be
|
||||
ported elsewhere. The layout engine itself depends on Samsung's SystemUI,
|
||||
lockscreen, AOD service, notification display modes, status icons, and status
|
||||
chip implementation.
|
||||
Some behavior cannot be fully verified on emulators. Emulator testing can still
|
||||
verify whether hooks load, whether SystemUI stays stable, whether settings apply,
|
||||
whether excessive background work appears in logs, and whether the steady state
|
||||
looks the way it should.
|
||||
|
||||
Plain Android or other OEM skins would need separate hooks and probably a
|
||||
different source-collection layer. They should be treated as unsupported rather
|
||||
than untested-compatible.
|
||||
`⚠️ Experimental` means best-effort support exists, but the emulator cannot
|
||||
expose the required hardware or state for real verification.
|
||||
|
||||
| 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
|
||||
|
||||
@@ -81,8 +122,39 @@ Release builds require local signing configuration:
|
||||
./gradlew assembleRelease
|
||||
```
|
||||
|
||||
## Development Note
|
||||
## Test Harness
|
||||
|
||||
This project has been coded with the help of AI as an implementation aid. The
|
||||
design decisions, device testing, and final behaviour are still manually
|
||||
reviewed.
|
||||
The repository also includes a small companion app for compatibility testing.
|
||||
It is not required for normal use of the module. It creates repeatable
|
||||
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
|
||||
```
|
||||
|
||||
@@ -69,7 +69,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "se.ajpanton.statusbartweak"
|
||||
minSdk = 35
|
||||
minSdk = 34
|
||||
targetSdk = 36
|
||||
val buildVersionName = project.findProperty("sbtVersionName")?.toString()
|
||||
?: fallbackVersionName
|
||||
|
||||
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.module;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
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.StandaloneFeatureRegistry;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
||||
@@ -15,11 +16,6 @@ import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
||||
*/
|
||||
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 StandaloneFeatureRegistry standaloneFeatureRegistry;
|
||||
|
||||
@@ -33,10 +29,7 @@ public final class ModuleCoordinator {
|
||||
|
||||
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
|
||||
String processName = param.getProcessName();
|
||||
if (!PACKAGE_SYSTEMUI.equals(processName)
|
||||
&& !PACKAGE_ANDROID.equals(processName)
|
||||
&& !PACKAGE_SYSTEM.equals(processName)
|
||||
&& !PACKAGE_AODSERVICE.equals(processName)) {
|
||||
if (!SystemUiCapabilities.isHandledProcessName(processName)) {
|
||||
return;
|
||||
}
|
||||
standaloneFeatureRegistry.onModuleLoaded(param);
|
||||
@@ -54,9 +47,6 @@ public final class ModuleCoordinator {
|
||||
}
|
||||
|
||||
private static boolean shouldHandlePackage(String packageName) {
|
||||
return PACKAGE_SYSTEMUI.equals(packageName)
|
||||
|| PACKAGE_ANDROID.equals(packageName)
|
||||
|| PACKAGE_SYSTEM.equals(packageName)
|
||||
|| PACKAGE_AODSERVICE.equals(packageName);
|
||||
return SystemUiCapabilities.isHandledPackageName(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;
|
||||
}
|
||||
String normalized = key.trim();
|
||||
String pipePackage = packageFromPipeNotificationKey(normalized);
|
||||
if (pipePackage != null) {
|
||||
return pipePackage;
|
||||
}
|
||||
int suffix = normalized.indexOf('@');
|
||||
if (suffix >= 0) {
|
||||
normalized = normalized.substring(0, suffix);
|
||||
@@ -91,6 +95,19 @@ public final class NotificationKeyReflection {
|
||||
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) {
|
||||
Object value = ReflectionSupport.invokeMethod(target, methodName);
|
||||
return value instanceof String string && !string.isEmpty() ? string : null;
|
||||
|
||||
@@ -8,7 +8,7 @@ public final class ViewIdNames {
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
|
||||
/**
|
||||
* Small independent runtime slice.
|
||||
@@ -15,7 +15,8 @@ public interface RuntimeFeature {
|
||||
String getName();
|
||||
|
||||
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(
|
||||
|
||||
+11
-3
@@ -434,7 +434,7 @@ final class BatteryBarController {
|
||||
) {
|
||||
BatteryBarView barView = ensureBatteryBarView(rootGroup);
|
||||
layoutBatteryBarView(rootGroup, barView);
|
||||
barView.update(rootGroup, settings, geometry, batteryLevel, plugged, color);
|
||||
barView.update(rootGroup, rootGroup, settings, geometry, batteryLevel, plugged, color);
|
||||
barView.bringToFront();
|
||||
barView.invalidate();
|
||||
lastSignatures.put(rootGroup, buildSignature(rootGroup, settings, color, geometry, scenario));
|
||||
@@ -461,7 +461,7 @@ final class BatteryBarController {
|
||||
return;
|
||||
}
|
||||
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))) {
|
||||
overlay.updateLayout(bounds.width, bounds.height);
|
||||
lastOverlaySignatures.put(root, signature);
|
||||
@@ -1039,6 +1039,7 @@ final class BatteryBarController {
|
||||
private final Path drawPath = new Path();
|
||||
private final PathMeasure pathMeasure = new PathMeasure();
|
||||
private ViewGroup host;
|
||||
private View roundedCornerSource;
|
||||
private SbtSettings settings;
|
||||
private BatteryBarGeometry geometry;
|
||||
private int batteryLevel = -1;
|
||||
@@ -1053,6 +1054,7 @@ final class BatteryBarController {
|
||||
|
||||
void update(
|
||||
ViewGroup host,
|
||||
View roundedCornerSource,
|
||||
SbtSettings settings,
|
||||
BatteryBarGeometry geometry,
|
||||
int batteryLevel,
|
||||
@@ -1060,6 +1062,7 @@ final class BatteryBarController {
|
||||
int color
|
||||
) {
|
||||
this.host = host;
|
||||
this.roundedCornerSource = roundedCornerSource;
|
||||
this.settings = settings;
|
||||
this.geometry = geometry;
|
||||
this.batteryLevel = batteryLevel;
|
||||
@@ -1226,7 +1229,12 @@ final class BatteryBarController {
|
||||
}
|
||||
|
||||
private float resolveTopCornerRadius(float fallback) {
|
||||
WindowInsets insets = getRootWindowInsets();
|
||||
WindowInsets insets = roundedCornerSource != null
|
||||
? roundedCornerSource.getRootWindowInsets()
|
||||
: null;
|
||||
if (insets == null) {
|
||||
insets = getRootWindowInsets();
|
||||
}
|
||||
if (insets == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
+4
@@ -1,6 +1,7 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.chips;
|
||||
|
||||
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.RuntimeFeature;
|
||||
|
||||
@@ -32,6 +33,9 @@ public final class StatusChipsFeature implements RuntimeFeature {
|
||||
}
|
||||
|
||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (!SystemUiCapabilities.supportsSamsungStatusChipHooks()) {
|
||||
return;
|
||||
}
|
||||
if (statusChipHider == null) {
|
||||
statusChipHider = new StatusChipHider(context);
|
||||
}
|
||||
|
||||
+50
-5
@@ -38,8 +38,10 @@ import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class StockClockController {
|
||||
|
||||
private static final String CLOCK_CLASS_NAME =
|
||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView";
|
||||
private static final String[] CLOCK_CLASS_NAMES = {
|
||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView",
|
||||
"com.android.systemui.statusbar.policy.Clock"
|
||||
};
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final Set<TextView> trackedClocks =
|
||||
@@ -64,8 +66,25 @@ final class StockClockController {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
for (Class<?> clockClass : clockClasses) {
|
||||
hookClockClass(framework, clockClass);
|
||||
}
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void hookClockClass(XposedInterface framework, Class<?> clockClass) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
@@ -104,7 +123,7 @@ final class StockClockController {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(framework, clockClass, "notifyTimeChanged", chain -> {
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "notifyTimeChanged", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (!(target instanceof TextView)) {
|
||||
@@ -123,6 +142,14 @@ final class StockClockController {
|
||||
trackAndApply(clockView, bellSound);
|
||||
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 -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof TextView clockView) {
|
||||
@@ -137,7 +164,6 @@ final class StockClockController {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void reapplyClockIfCustomOrOwned(TextView clockView) {
|
||||
@@ -162,6 +188,9 @@ final class StockClockController {
|
||||
}
|
||||
|
||||
private void trackAndApply(TextView clockView, Object bellSound) {
|
||||
if (isNotificationShadeDescendant(clockView)) {
|
||||
return;
|
||||
}
|
||||
trackedClocks.add(clockView);
|
||||
registerReceiverIfNeeded(clockView.getContext());
|
||||
if (bellSound != null) {
|
||||
@@ -219,6 +248,10 @@ final class StockClockController {
|
||||
}
|
||||
|
||||
private void applyClockText(TextView clockView) {
|
||||
if (isNotificationShadeDescendant(clockView)) {
|
||||
restoreOwnedClock(clockView);
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext());
|
||||
boolean layoutEnabled = settings.clockEnabled;
|
||||
if (layoutEnabled) {
|
||||
@@ -330,6 +363,18 @@ final class StockClockController {
|
||||
|| (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) {
|
||||
clockView.setSingleLine(true);
|
||||
clockView.setHorizontallyScrolling(false);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
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.RuntimeFeature;
|
||||
|
||||
@@ -31,6 +32,9 @@ public final class MiscFeature implements RuntimeFeature {
|
||||
}
|
||||
|
||||
private void installControllers(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (!SystemUiCapabilities.supportsSamsungAodMiscControllers()) {
|
||||
return;
|
||||
}
|
||||
if (aodCallVisibilityController == null) {
|
||||
aodCallVisibilityController = new AodCallVisibilityController(context);
|
||||
}
|
||||
|
||||
+8
-5
@@ -43,8 +43,11 @@ final class StockUnlockedNotificationIconsController {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
Class<?> containerClass = ReflectionSupport.requireClass(CONTAINER_CLASS_NAME, classLoader);
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
|
||||
Class<?> containerClass = ReflectionSupport.findClassIfExists(CONTAINER_CLASS_NAME, classLoader);
|
||||
if (containerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
if (shouldManageContainer(view)) {
|
||||
@@ -54,7 +57,7 @@ final class StockUnlockedNotificationIconsController {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
if (shouldManageContainer(view)) {
|
||||
trackContainer(view);
|
||||
@@ -63,7 +66,7 @@ final class StockUnlockedNotificationIconsController {
|
||||
}
|
||||
return chain.proceed();
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
if (shouldManageContainer(view)) {
|
||||
@@ -75,7 +78,7 @@ final class StockUnlockedNotificationIconsController {
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
|
||||
if (!(chain.getThisObject() instanceof View view)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.runtime.hooks;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -34,6 +35,9 @@ public final class XposedHookSupport {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (Modifier.isAbstract(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
handles.add(framework.hook(method).intercept(hooker));
|
||||
}
|
||||
@@ -59,6 +63,9 @@ public final class XposedHookSupport {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (Modifier.isAbstract(method.getModifiers())) {
|
||||
continue;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
handles.add(framework.hook(method).intercept(hooker));
|
||||
}
|
||||
@@ -83,4 +90,20 @@ public final class XposedHookSupport {
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ final class DarkIconDispatcherGuard {
|
||||
}
|
||||
|
||||
void install(Class<?> darkIconDispatcherClass) {
|
||||
XposedHookSupport.hookAllConstructors(
|
||||
XposedHookSupport.hookAllConstructorsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
darkIconDispatcherClass,
|
||||
chain -> {
|
||||
|
||||
+435
-33
@@ -13,6 +13,7 @@ import android.service.notification.StatusBarNotification;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -34,6 +35,7 @@ import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Function;
|
||||
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
|
||||
import se.ajpanton.statusbartweak.runtime.RuntimeMutationGuard;
|
||||
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
||||
@@ -79,6 +81,7 @@ final class StockLayoutCanvasController {
|
||||
"entries"
|
||||
};
|
||||
private static final Set<String> TARGET_ID_NAMES = Set.of(
|
||||
"clock",
|
||||
"left_clock_container",
|
||||
"notification_icon_area",
|
||||
"notification_icon_area_inner",
|
||||
@@ -93,10 +96,12 @@ final class StockLayoutCanvasController {
|
||||
|
||||
private static final Set<String> TARGET_CLASS_SUBSTRINGS = Set.of(
|
||||
"QSClockIndicatorView",
|
||||
"statusbar.policy.Clock",
|
||||
"SecShelfNotificationIconContainer"
|
||||
);
|
||||
|
||||
private static final Set<String> LAYOUT_OFF_UNLOCKED_ONLY_IDS = Set.of(
|
||||
"clock",
|
||||
"left_clock_container",
|
||||
"notification_icon_area",
|
||||
"notification_icon_area_inner",
|
||||
@@ -136,8 +141,14 @@ final class StockLayoutCanvasController {
|
||||
private final Map<View, Integer> aodBeforeDrawSignatureByRoot = new WeakHashMap<>();
|
||||
private final Set<View> trackedUnlockedStockSurfaces =
|
||||
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 =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> lockedStatusBarIconSurfaceReadyRoots =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> pendingUnlockedAppearanceRefreshRoots =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> pendingUnlockedClockTextRefreshRoots =
|
||||
@@ -187,6 +198,8 @@ final class StockLayoutCanvasController {
|
||||
private int notificationDrawerCloseGeneration;
|
||||
private boolean notificationDrawerClosePending;
|
||||
private int aodVisualAlphaGeneration;
|
||||
private int systemBarRequestedVisibleTypes = WindowInsets.Type.statusBars();
|
||||
private boolean statusBarTransientShown;
|
||||
|
||||
StockLayoutCanvasController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
@@ -215,10 +228,124 @@ final class StockLayoutCanvasController {
|
||||
statusIconModelTracker.installHooks(classLoader);
|
||||
installShadeExpansionHooks(classLoader);
|
||||
installShadeHeaderAnimationGuardHook(classLoader);
|
||||
installSystemBarAttributeListener(classLoader);
|
||||
installTransientBarListener(classLoader);
|
||||
installViewRootHook(classLoader);
|
||||
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) {
|
||||
for (String className : List.of(
|
||||
"com.android.systemui.statusbar.notification.collection.NotifCollection",
|
||||
@@ -407,6 +534,10 @@ final class StockLayoutCanvasController {
|
||||
Object target = chain.getThisObject();
|
||||
Object alphaArg = firstArg(chain.getArgs());
|
||||
if (target instanceof View view && alphaArg instanceof Float alpha) {
|
||||
rememberPrivacyOverlayHiddenStatusBarRoot(view);
|
||||
if (shouldSuppressHiddenStatusBarRootAlpha(view, alpha)) {
|
||||
return null;
|
||||
}
|
||||
if (shouldSuppressPrivacyOverlayStatusBarAlpha(view, alpha)) {
|
||||
return null;
|
||||
}
|
||||
@@ -426,7 +557,11 @@ final class StockLayoutCanvasController {
|
||||
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 -> {
|
||||
Object target = chain.getThisObject();
|
||||
@@ -442,6 +577,7 @@ final class StockLayoutCanvasController {
|
||||
Object result = chain.proceed();
|
||||
if (target instanceof View view && visibilityArg instanceof Integer visibility) {
|
||||
scheduleStatusChipRefreshIfContentChanged(view);
|
||||
syncUnlockedHostsFromStockStatusBar(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
@@ -453,17 +589,26 @@ final class StockLayoutCanvasController {
|
||||
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;
|
||||
}
|
||||
}
|
||||
return chain.proceed();
|
||||
Object result = chain.proceed();
|
||||
if (target instanceof View view) {
|
||||
syncUnlockedHostsFromStockStatusBar(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), viewClass, "layout", chain -> {
|
||||
Object target = chain.getThisObject();
|
||||
@@ -480,15 +625,21 @@ final class StockLayoutCanvasController {
|
||||
right,
|
||||
bottom);
|
||||
if (override != null) {
|
||||
return chain.proceed(new Object[]{
|
||||
Object result = chain.proceed(new Object[]{
|
||||
override.left,
|
||||
override.top,
|
||||
override.right,
|
||||
override.bottom
|
||||
});
|
||||
syncUnlockedHostsFromStockStatusBar(view);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return chain.proceed();
|
||||
Object result = chain.proceed();
|
||||
if (target instanceof View view) {
|
||||
syncUnlockedHostsFromStockStatusBar(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -581,13 +732,19 @@ final class StockLayoutCanvasController {
|
||||
});
|
||||
}
|
||||
|
||||
Class<?> clockClass = ReflectionSupport.findClassIfExists(
|
||||
String[] clockClassNames = {
|
||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView",
|
||||
classLoader);
|
||||
if (clockClass != null) {
|
||||
"com.android.systemui.statusbar.policy.Clock"
|
||||
};
|
||||
for (String className : clockClassNames) {
|
||||
Class<?> clockClass = ReflectionSupport.findClassIfExists(className, classLoader);
|
||||
if (clockClass == null) {
|
||||
continue;
|
||||
}
|
||||
hookViewAppearanceInvalidation(clockClass, "onDarkChanged");
|
||||
hookViewAppearanceInvalidation(clockClass, "setTextColor");
|
||||
hookClockTextInvalidation(clockClass, "notifyTimeChanged");
|
||||
hookClockTextInvalidation(clockClass, "updateClock");
|
||||
hookClockTextInvalidation(clockClass, "setText");
|
||||
hookViewLayoutInvalidation(clockClass, "onLayout");
|
||||
}
|
||||
@@ -1367,7 +1524,7 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
hideLockscreenCardsNotificationHost(root);
|
||||
if (activeScene.notificationDisplayStyle() == NotificationDisplayStyle.CARDS
|
||||
&& isAodLayoutRoot(root)) {
|
||||
&& isAodLayoutRoot(root, activeScene)) {
|
||||
renderAodCardsNotificationStripIfNeeded(root, settings, activeScene);
|
||||
} else {
|
||||
removeAodCardsNotificationHost(root);
|
||||
@@ -1937,12 +2094,15 @@ final class StockLayoutCanvasController {
|
||||
if (current.isUnlocked() && !previous.isUnlocked()) {
|
||||
darkIconDispatcherGuard.restoreForcedStates();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
lockedStatusBarIconSurfaceReadyRoots.clear();
|
||||
} else if (previous.isLockscreen() && current.isAod()) {
|
||||
darkIconDispatcherGuard.restoreForcedStates();
|
||||
hideTrackedLockedStatusBarIconSurfaces(null);
|
||||
lockedStatusBarIconSurfaceReadyRoots.clear();
|
||||
} else if (previous.isLockscreen() && !current.isLockscreen()) {
|
||||
darkIconDispatcherGuard.restoreForcedStates();
|
||||
restoreTrackedLockedStatusBarIconSurfaces(null);
|
||||
lockedStatusBarIconSurfaceReadyRoots.clear();
|
||||
}
|
||||
boolean lockscreenEntry = current.isLockscreen();
|
||||
if (lockscreenEntry) {
|
||||
@@ -1983,6 +2143,7 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
unlockedIconRenderController.clearRoot(root);
|
||||
dirtyUnlockedLayoutRoots.add(root);
|
||||
lockedStatusBarIconSurfaceReadyRoots.remove(root);
|
||||
if (isViewThread(root)) {
|
||||
applyToRoot(root);
|
||||
} else {
|
||||
@@ -2131,6 +2292,87 @@ final class StockLayoutCanvasController {
|
||||
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) {
|
||||
if (root == null) {
|
||||
return;
|
||||
@@ -2138,31 +2380,29 @@ final class StockLayoutCanvasController {
|
||||
Context context = root.getContext();
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
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);
|
||||
markDirtyIfClockTimeChanged(root, settings, statusBarRoot);
|
||||
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
|
||||
if (!layoutEnabled) {
|
||||
updateActiveScene(root, context);
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
statusIconModelTracker.updateActiveSlots(false, activeScene);
|
||||
applyLayoutOffRoot(root, settings, statusBarRoot, activeScene);
|
||||
return;
|
||||
}
|
||||
installPendingPluginClassLoaderHooks();
|
||||
layoutOffCleanedRoots.remove(root);
|
||||
updateActiveScene(root, context);
|
||||
if (statusIconModelTracker.updateActiveSlots(
|
||||
true,
|
||||
runtimeContext.getModeStateRepository().getActiveScene())) {
|
||||
activeScene)) {
|
||||
unlockedIconRenderController.clearRoot(root);
|
||||
dirtyUnlockedLayoutRoots.add(root);
|
||||
}
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
|
||||
boolean aodScene = activeScene != null && activeScene.isAod();
|
||||
boolean rootDirty = dirtyUnlockedLayoutRoots.remove(root);
|
||||
boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root);
|
||||
boolean aodLayoutRoot = aodScene && isAodLayoutRoot(root, activeScene);
|
||||
boolean hasAodLayoutPlan = aodScene && unlockedIconRenderController.hasLayoutPlan(root);
|
||||
boolean layoutRoot = aodScene ? aodLayoutRoot : statusBarRoot;
|
||||
if (aodScene
|
||||
@@ -2243,11 +2483,14 @@ final class StockLayoutCanvasController {
|
||||
activeScene,
|
||||
rootDirty,
|
||||
lockedCarrierTextSource());
|
||||
boolean shouldShowUnlockedHosts = unlockedScene || aodScene || renderResult.shouldOwnLockscreen();
|
||||
boolean shouldShowUnlockedHosts = unlockedScene
|
||||
|| aodScene
|
||||
|| (!unlockedScene && renderResult.shouldOwnLockscreen());
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, shouldShowUnlockedHosts);
|
||||
if (shouldShowUnlockedHosts) {
|
||||
bringUnlockedHostsAndDebugOverlayToFront(root);
|
||||
}
|
||||
syncUnlockedHostsToStockStatusBar(root, activeScene);
|
||||
syncAodOwnedHostVisualState(root, unlockedHosts);
|
||||
}
|
||||
}
|
||||
@@ -2278,6 +2521,7 @@ final class StockLayoutCanvasController {
|
||||
renderResult.layoutChanged(),
|
||||
false,
|
||||
activeScene);
|
||||
hideTrackedUnlockedStockSurfaces(root);
|
||||
hideTrackedStatusChipSources(root);
|
||||
if (renderResult.chipSourcesChanged()
|
||||
&& !scanTree
|
||||
@@ -2307,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(
|
||||
View root,
|
||||
SbtSettings settings,
|
||||
@@ -2347,6 +2620,17 @@ final class StockLayoutCanvasController {
|
||||
|| 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) {
|
||||
return root instanceof ViewGroup group
|
||||
&& findDirectOwnedHost(
|
||||
@@ -2935,9 +3219,16 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
|
||||
private void hideOrDiscoverLockedStatusBarIconSurfaces(View root) {
|
||||
hideTrackedLockedStatusBarIconSurfaces(root);
|
||||
if (root != null) {
|
||||
trackAndHideLockedStatusBarIconSurfaces(root);
|
||||
if (root != null && lockedStatusBarIconSurfaceReadyRoots.contains(root)) {
|
||||
return;
|
||||
}
|
||||
boolean hasTrackedSurfacesInRoot = hideTrackedLockedStatusBarIconSurfaces(root);
|
||||
int discovered = 0;
|
||||
if (!hasTrackedSurfacesInRoot && root != null) {
|
||||
discovered = trackAndHideLockedStatusBarIconSurfaces(root);
|
||||
}
|
||||
if (root != null && (hasTrackedSurfacesInRoot || discovered > 0)) {
|
||||
lockedStatusBarIconSurfaceReadyRoots.add(root);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2945,30 +3236,29 @@ final class StockLayoutCanvasController {
|
||||
if (trackedLockedStatusBarIconSurfaces.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
boolean foundInRoot = false;
|
||||
boolean foundActiveInRoot = false;
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : trackedLockedStatusBarIconSurfaces) {
|
||||
if (view == null || !view.isAttachedToWindow()) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
if (root == null || isDescendantOf(view, root)) {
|
||||
foundInRoot = true;
|
||||
if (isAlreadySuppressedHiddenView(view)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
boolean inRoot = root == null || isDescendantOf(view, root);
|
||||
if (!isLockedStatusBarIconSurface(view)) {
|
||||
restoreView(view);
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
if (root == null || isDescendantOf(view, root)) {
|
||||
if (inRoot) {
|
||||
foundActiveInRoot = true;
|
||||
if (isAlreadySuppressedHiddenView(view)) {
|
||||
continue;
|
||||
}
|
||||
hideView(view, isCarrierView(view));
|
||||
}
|
||||
}
|
||||
removeStaleViews(trackedLockedStatusBarIconSurfaces, staleViews);
|
||||
return foundInRoot;
|
||||
return foundActiveInRoot;
|
||||
}
|
||||
|
||||
private boolean isAlreadySuppressedHiddenView(View view) {
|
||||
@@ -3130,6 +3420,11 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
}
|
||||
removeStaleViews(trackedLockedStatusBarIconSurfaces, restoredViews);
|
||||
if (root != null) {
|
||||
lockedStatusBarIconSurfaceReadyRoots.remove(root);
|
||||
} else {
|
||||
lockedStatusBarIconSurfaceReadyRoots.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTrackedStatusChipSources(View root) {
|
||||
@@ -3363,6 +3658,64 @@ final class StockLayoutCanvasController {
|
||||
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
|
||||
@@ -3371,6 +3724,9 @@ final class StockLayoutCanvasController {
|
||||
|| ongoingPrivacyChipInRoot(view) == null) {
|
||||
return false;
|
||||
}
|
||||
if (alpha <= 0f && !statusBarsRequestedVisible()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3385,6 +3741,14 @@ final class StockLayoutCanvasController {
|
||||
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");
|
||||
@@ -4345,6 +4709,7 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
clearTrackedStatusChipState();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
lockedStatusBarIconSurfaceReadyRoots.clear();
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
confirmedAodPluginViews.clear();
|
||||
aodPluginVisualAlphaByView.clear();
|
||||
@@ -4782,8 +5147,9 @@ final class StockLayoutCanvasController {
|
||||
View root,
|
||||
SceneKey activeScene
|
||||
) {
|
||||
if (!isUnlockedStatusBarRoot(root)
|
||||
&& !(activeScene != null && activeScene.isAod() && isAodLayoutRoot(root))) {
|
||||
if (!isStatusBarLayoutRoot(root, activeScene)
|
||||
&& !(activeScene != null && activeScene.isAod()
|
||||
&& isAodLayoutRoot(root, activeScene))) {
|
||||
return null;
|
||||
}
|
||||
if (activeScene != null
|
||||
@@ -4839,11 +5205,30 @@ final class StockLayoutCanvasController {
|
||||
|| 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) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
NotificationDisplayStyle notificationStyle =
|
||||
@@ -4866,7 +5251,24 @@ final class StockLayoutCanvasController {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -10,6 +10,7 @@ public final class ModeStateRepository {
|
||||
private SceneKey activeScene = SceneKey.unlocked();
|
||||
private boolean systemUiStatusBarStateKnown;
|
||||
private int systemUiStatusBarState;
|
||||
private boolean keyguardGoingAway;
|
||||
private boolean layoutEnabled;
|
||||
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
|
||||
private final List<SceneListener> sceneListeners = new ArrayList<>();
|
||||
@@ -27,6 +28,14 @@ public final class ModeStateRepository {
|
||||
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
|
||||
}
|
||||
|
||||
public synchronized boolean isKeyguardGoingAway() {
|
||||
return keyguardGoingAway;
|
||||
}
|
||||
|
||||
public synchronized void setKeyguardGoingAway(boolean goingAway) {
|
||||
keyguardGoingAway = goingAway;
|
||||
}
|
||||
|
||||
public synchronized boolean isLayoutEnabled() {
|
||||
return layoutEnabled;
|
||||
}
|
||||
|
||||
+21
-3
@@ -23,8 +23,10 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
||||
"com.android.systemui.statusbar.StatusBarStateControllerImpl";
|
||||
private static final String CLASS_KEYGUARD_STATE_CONTROLLER =
|
||||
"com.android.systemui.statusbar.policy.KeyguardStateControllerImpl";
|
||||
private static final int STATE_SHADE = 0;
|
||||
|
||||
private boolean installed;
|
||||
private boolean keyguardGoingAway;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -78,8 +80,14 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
||||
"notifyKeyguardGoingAway",
|
||||
chain -> {
|
||||
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();
|
||||
} else if (Boolean.FALSE.equals(goingAway)) {
|
||||
keyguardGoingAway = false;
|
||||
context.getModeStateRepository().setKeyguardGoingAway(false);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
@@ -92,14 +100,24 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
||||
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
|
||||
int state = rawState instanceof Integer integer ? integer : 0;
|
||||
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(
|
||||
state,
|
||||
dozing,
|
||||
SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
|
||||
}
|
||||
|
||||
private boolean firstBooleanArg(List<Object> args) {
|
||||
return args != null && !args.isEmpty() && Boolean.TRUE.equals(args.get(0));
|
||||
private Boolean firstBooleanArg(List<Object> args) {
|
||||
return args != null && !args.isEmpty() && args.get(0) instanceof Boolean bool
|
||||
? bool
|
||||
: null;
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!(root instanceof ViewGroup parent)) {
|
||||
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) {
|
||||
ArrayList<View> hosts = new ArrayList<>();
|
||||
for (String tag : UNLOCKED_HOST_ORDER) {
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
||||
@@ -464,7 +465,9 @@ public final class UnlockedIconSnapshotRenderController {
|
||||
NotificationDisplayStyle style = scene.notificationDisplayStyle();
|
||||
return settings.layoutNotifEnabledAod
|
||||
&& (style == NotificationDisplayStyle.ICONS
|
||||
|| style == NotificationDisplayStyle.DOT);
|
||||
|| style == NotificationDisplayStyle.DOT
|
||||
|| (style == NotificationDisplayStyle.UNKNOWN
|
||||
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings)));
|
||||
}
|
||||
return settings.layoutNotifEnabledUnlocked;
|
||||
}
|
||||
|
||||
+8
-1
@@ -11,6 +11,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
@@ -856,7 +857,9 @@ final class UnlockedLayoutPlanner {
|
||||
if (style == NotificationDisplayStyle.DOT) {
|
||||
return settings.layoutNotifEnabledAod ? 1 : 0;
|
||||
}
|
||||
if (style != NotificationDisplayStyle.ICONS) {
|
||||
if (style != NotificationDisplayStyle.ICONS
|
||||
&& !(style == NotificationDisplayStyle.UNKNOWN
|
||||
&& isExperimentalGenericAod(settings))) {
|
||||
return 0;
|
||||
}
|
||||
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
|
||||
@@ -870,6 +873,10 @@ final class UnlockedLayoutPlanner {
|
||||
&& scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT;
|
||||
}
|
||||
|
||||
private boolean isExperimentalGenericAod(SbtSettings settings) {
|
||||
return SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings);
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> unreadNotificationPlacements(
|
||||
ArrayList<SnapshotPlacement> placements
|
||||
) {
|
||||
|
||||
+42
-11
@@ -18,6 +18,7 @@ import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
|
||||
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
@@ -63,7 +64,7 @@ final class UnlockedStockSourceCollector {
|
||||
ArrayList<SnapshotSource> statusChips = new ArrayList<>();
|
||||
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
|
||||
if (scene != null && scene.isAod()) {
|
||||
collectAodStatusSnapshotViews(root, statusIcons, scene);
|
||||
collectAodStatusSnapshotViews(root, statusIcons, scene, settings);
|
||||
}
|
||||
if (statusIcons.isEmpty()) {
|
||||
if (anchors.statusRoot != null) {
|
||||
@@ -82,8 +83,7 @@ final class UnlockedStockSourceCollector {
|
||||
selectDualSimStatusSignals(statusIcons, settings);
|
||||
normalizeStatusIconSourceBounds(root, statusIcons);
|
||||
if (scene != null && scene.isAod()) {
|
||||
if (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
|
||||
if (isAodIconNotificationStyle(scene, settings)) {
|
||||
notificationIcons.addAll(
|
||||
LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root));
|
||||
if (notificationIcons.isEmpty()
|
||||
@@ -92,6 +92,12 @@ final class UnlockedStockSourceCollector {
|
||||
LockscreenCardsNotificationIconSourceStore
|
||||
.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) {
|
||||
collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons);
|
||||
@@ -111,14 +117,14 @@ final class UnlockedStockSourceCollector {
|
||||
private void collectAodStatusSnapshotViews(
|
||||
View root,
|
||||
ArrayList<SnapshotSource> out,
|
||||
SceneKey scene
|
||||
SceneKey scene,
|
||||
SbtSettings settings
|
||||
) {
|
||||
if (root == null || out == null) {
|
||||
return;
|
||||
}
|
||||
boolean iconMode = scene != null
|
||||
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
|
||||
&& isAodIconNotificationStyle(scene, settings);
|
||||
View excludedNotificationContainer = iconMode
|
||||
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(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) {
|
||||
Object methodValue = ReflectionSupport.invokeMethod(view, methodName);
|
||||
if (methodValue != null) {
|
||||
@@ -193,7 +214,7 @@ final class UnlockedStockSourceCollector {
|
||||
statusSourceSignature = 31 * statusSourceSignature + batteryChargingSignature(root);
|
||||
if (scene != null && scene.isAod()) {
|
||||
statusSourceSignature = 31 * statusSourceSignature
|
||||
+ aodLayoutAnchorSignature(root, anchors, scene);
|
||||
+ aodLayoutAnchorSignature(root, anchors, scene, settings);
|
||||
}
|
||||
int notificationSourceSignature = sourceLayoutListSignature(
|
||||
previousIcons != null ? previousIcons.notificationIcons : null);
|
||||
@@ -238,13 +259,17 @@ final class UnlockedStockSourceCollector {
|
||||
chipSourceSignature);
|
||||
}
|
||||
|
||||
private int aodLayoutAnchorSignature(View root, RootAnchors anchors, SceneKey scene) {
|
||||
private int aodLayoutAnchorSignature(
|
||||
View root,
|
||||
RootAnchors anchors,
|
||||
SceneKey scene,
|
||||
SbtSettings settings
|
||||
) {
|
||||
if (root == null) {
|
||||
return 0;
|
||||
}
|
||||
boolean iconMode = scene != null
|
||||
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
|
||||
&& isAodIconNotificationStyle(scene, settings);
|
||||
View notificationContainer = iconMode
|
||||
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
|
||||
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
|
||||
@@ -317,7 +342,7 @@ final class UnlockedStockSourceCollector {
|
||||
}
|
||||
if (clockView == null
|
||||
&& view instanceof TextView textView
|
||||
&& view.getClass().getName().contains("QSClockIndicatorView")) {
|
||||
&& isStatusbarClockView(textView)) {
|
||||
clockView = textView;
|
||||
}
|
||||
if (statusEndContent == null && idName.equals("status_bar_end_side_content")) {
|
||||
@@ -370,6 +395,12 @@ final class UnlockedStockSourceCollector {
|
||||
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) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
|
||||
@@ -17,7 +17,7 @@ import java.util.regex.Pattern;
|
||||
|
||||
public final class AppIconRules {
|
||||
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_EXCEPTION_RULES = "app_icon_exception_rules";
|
||||
|
||||
@@ -99,6 +99,7 @@ public final class SbtDefaults {
|
||||
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_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_LEFT_PX_DEFAULT = 0;
|
||||
|
||||
@@ -39,6 +39,8 @@ public final class SbtSettings {
|
||||
"debug_visualize_camera_cutout";
|
||||
public static final String KEY_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 =
|
||||
"locked_notification_mode";
|
||||
public static final String KEY_UNLOCKED_MAX_ICONS_PER_ROW = "unlocked_max_icons_per_row";
|
||||
@@ -425,6 +427,7 @@ public final class SbtSettings {
|
||||
public final boolean debugVisualizeChipContainer;
|
||||
public final boolean debugVisualizeCameraCutout;
|
||||
public final boolean debugVisualizeAodStockContainers;
|
||||
public final boolean debugExperimentalAodStatusbarRoot;
|
||||
|
||||
private SbtSettings(boolean enableAodCutoutLimit,
|
||||
boolean enableLockCutoutLimit,
|
||||
@@ -558,7 +561,8 @@ public final class SbtSettings {
|
||||
boolean debugVisualizeClockContainer,
|
||||
boolean debugVisualizeChipContainer,
|
||||
boolean debugVisualizeCameraCutout,
|
||||
boolean debugVisualizeAodStockContainers) {
|
||||
boolean debugVisualizeAodStockContainers,
|
||||
boolean debugExperimentalAodStatusbarRoot) {
|
||||
this.enableAodCutoutLimit = enableAodCutoutLimit;
|
||||
this.enableLockCutoutLimit = enableLockCutoutLimit;
|
||||
this.unlockedMaxIconsPerRow = unlockedMaxIconsPerRow;
|
||||
@@ -861,6 +865,7 @@ public final class SbtSettings {
|
||||
this.debugVisualizeChipContainer = debugVisualizeChipContainer;
|
||||
this.debugVisualizeCameraCutout = debugVisualizeCameraCutout;
|
||||
this.debugVisualizeAodStockContainers = debugVisualizeAodStockContainers;
|
||||
this.debugExperimentalAodStatusbarRoot = debugExperimentalAodStatusbarRoot;
|
||||
}
|
||||
|
||||
public static SbtSettings defaults() {
|
||||
@@ -997,7 +1002,8 @@ public final class SbtSettings {
|
||||
SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_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
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1472,7 +1478,10 @@ public final class SbtSettings {
|
||||
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT),
|
||||
prefs.getBoolean(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1780,7 +1789,10 @@ public final class SbtSettings {
|
||||
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT),
|
||||
bundle.getBoolean(
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -195,6 +195,9 @@ public class SbtSettingsProvider extends ContentProvider {
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
||||
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,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT));
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.shell.debug.DebugNotifications;
|
||||
import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
@@ -64,6 +65,8 @@ public class IconsDebugFragment extends Fragment {
|
||||
SwitchMaterial visualizeAodStockContainersSwitch =
|
||||
root.findViewById(R.id.debug_visualize_aod_stock_containers_switch);
|
||||
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);
|
||||
TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified);
|
||||
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,
|
||||
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT,
|
||||
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);
|
||||
updateVisualizerSwitchAvailability(
|
||||
prefs,
|
||||
|
||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||
import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
@@ -40,6 +41,10 @@ final class LockedNotificationModeUi {
|
||||
return;
|
||||
}
|
||||
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 icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
|
||||
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) {
|
||||
if (!SystemUiCapabilities.supportsSamsungNotificationDisplayStyles()) {
|
||||
return MODE_ICONS;
|
||||
}
|
||||
NotificationDisplayStyleSettings.Style style =
|
||||
NotificationDisplayStyleSettings.read(context).style();
|
||||
return currentMode(style);
|
||||
|
||||
@@ -111,6 +111,13 @@
|
||||
android:layout_marginTop="12dp"
|
||||
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>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
<string name="debug_visualize_aod_stock_containers">AOD stock containers and derived statusbar</string>
|
||||
<string name="debug_section_systemui">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_failed">Could not restart SystemUI. Root shell failed.</string>
|
||||
<string name="debug_notification_style_cards">Cards</string>
|
||||
@@ -200,6 +201,7 @@
|
||||
<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_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="label_max_unlocked_icons_per_row">Max unlocked icons per row</string>
|
||||
<string name="layout_padding_title">Padding</string>
|
||||
|
||||
@@ -21,3 +21,4 @@ dependencyResolutionManagement {
|
||||
|
||||
rootProject.name = "StatusBarTweak"
|
||||
include(":app")
|
||||
include(":test-harness")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
+143
@@ -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
@@ -1 +1 @@
|
||||
version=0.4
|
||||
version=0.6
|
||||
|
||||
Reference in New Issue
Block a user