Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5deb9ff10 | ||
|
|
07c3444642 | ||
|
|
1d5bcb4e39 | ||
|
|
51d86e8ad1 | ||
|
|
394e1faf24 | ||
|
|
fa3341cbec | ||
|
|
988fbcebcd | ||
|
|
f20e214076 | ||
|
|
50fc608eae | ||
|
|
898e33ccfa | ||
|
|
38b1602fe8 | ||
|
|
560f4e19ba | ||
|
|
682782c232 | ||
|
|
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/
|
/.gradle-tmp/
|
||||||
/build/
|
/build/
|
||||||
/app/build/
|
/app/build/
|
||||||
|
/*/build/
|
||||||
/.externalNativeBuild/
|
/.externalNativeBuild/
|
||||||
/.cxx/
|
/.cxx/
|
||||||
/captures/
|
/captures/
|
||||||
|
|||||||
@@ -1,15 +1,23 @@
|
|||||||
# StatusBarTweak
|
# StatusBarTweak
|
||||||
|
|
||||||
StatusBarTweak is an Xposed/LSPosed module for Samsung SystemUI. It focuses on
|
StatusBarTweak is an Xposed/LSPosed module originally built for Samsung
|
||||||
making the status bar, lockscreen, and Always On Display more configurable while
|
SystemUI. It focuses on making the status bar, lockscreen, and Always On
|
||||||
still keeping Samsung's own icons and visual style where possible.
|
Display more configurable while still keeping the system's own icons and visual
|
||||||
|
style where possible.
|
||||||
|
|
||||||
The project is currently tested only on a Samsung Galaxy Z Fold5 running One UI
|
The project is physically tested only on a Samsung Galaxy Z Fold5 running One UI
|
||||||
8.0 / Android 16. Expect device- and One UI-version-specific behaviour.
|
8.0 / Android 16. Other SystemUI builds have emulator-tested support as shown
|
||||||
|
in the compatibility matrix.
|
||||||
|
|
||||||
|
## AI Assistance
|
||||||
|
|
||||||
|
This project was developed with help from AI-assisted coding tools. The design
|
||||||
|
decisions, physical device testing, and final behaviour are still manually reviewed.
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
- Custom status bar layout for the unlocked screen, lockscreen, and AOD.
|
- Custom status bar layout for the unlocked screen, lockscreen, and AOD where
|
||||||
|
supported by the SystemUI build.
|
||||||
- Independent placement of clock, carrier text, notification icons, status
|
- Independent placement of clock, carrier text, notification icons, status
|
||||||
icons, and Samsung status chips.
|
icons, and Samsung status chips.
|
||||||
- Separate notification display handling for Samsung's cards, icons, and dot
|
- Separate notification display handling for Samsung's cards, icons, and dot
|
||||||
@@ -41,31 +49,64 @@ It also supports lightweight markup:
|
|||||||
- Pipe alignment with `|`, useful for aligning split clock rows around a camera
|
- Pipe alignment with `|`, useful for aligning split clock rows around a camera
|
||||||
cutout.
|
cutout.
|
||||||
- Colour variants and reverse Polish notation colour math for light/dark status
|
- Colour variants and reverse Polish notation colour math for light/dark status
|
||||||
bar states, for example `color(#batterybar 1.2 * ; invRGB 0.5 *RGB)`.
|
bar states, for example `color(#batterybar 1.2 *RGB ; #batterybar 0.5 *RGB)`, which uses the batterybar's dynamic colour as its base. For the bright tint, it brightens it up some more, while for the dark tint, it is darkened.
|
||||||
|
|
||||||
The in-app clock page has the full syntax help and examples.
|
The in-app clock page has the full syntax help and examples.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- A rooted Samsung device.
|
- A rooted Android 14+ device or emulator. Actual feature support depends on
|
||||||
- A modern Xposed implementation such as LSPosed.
|
the SystemUI implementation; see the compatibility matrix.
|
||||||
|
- A modern Xposed implementation such as LSPosed or another libxposed-compatible runtime.
|
||||||
- Xposed API 101 or newer.
|
- Xposed API 101 or newer.
|
||||||
- A Samsung/One UI version close to One UI 8.0 / Android 16.
|
- SystemUI access for the module scope.
|
||||||
|
|
||||||
This module hooks Samsung SystemUI internals. It is not expected to work on
|
The module is developed and manually tested on a Samsung Galaxy Z Fold5 running
|
||||||
non-Samsung SystemUI, and updates to One UI may require fixes.
|
One UI 8.0 / Android 16. Non-Samsung builds have emulator-tested support as
|
||||||
|
shown below, but hardware-dependent behavior remains experimental unless it has
|
||||||
|
also been verified on physical hardware.
|
||||||
|
|
||||||
## Device Compatibility
|
## Device Compatibility
|
||||||
|
|
||||||
StatusBarTweak is Samsung-specific in its current form. Some lower-level ideas,
|
Some behavior cannot be fully verified on emulators. Emulator testing can still
|
||||||
such as drawing a custom battery bar or parsing custom clock text, could be
|
verify whether hooks load, whether SystemUI stays stable, whether settings apply,
|
||||||
ported elsewhere. The layout engine itself depends on Samsung's SystemUI,
|
whether excessive background work appears in logs, and whether the steady state
|
||||||
lockscreen, AOD service, notification display modes, status icons, and status
|
looks the way it should.
|
||||||
chip implementation.
|
|
||||||
|
|
||||||
Plain Android or other OEM skins would need separate hooks and probably a
|
`⚠️ Experimental` means best-effort support exists, but the emulator cannot
|
||||||
different source-collection layer. They should be treated as unsupported rather
|
expose the required hardware or state for real verification.
|
||||||
than untested-compatible.
|
|
||||||
|
| Feature | Galaxy Z Fold5<br>One UI 8 / Android 16 | AOSP API 34<br>Android 14 | AOSP API 35<br>Android 15 | AOSP API 36<br>Android 16 | Google APIs API 35<br>Android 15 | Google APIs API 36<br>Android 16 | LineageOS 22<br>Android 15 | LineageOS 23<br>Android 16 |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| Module loads in SystemUI | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Settings app opens and writes settings | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Custom unlocked statusbar layout | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Custom lockscreen statusbar layout | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Custom AOD statusbar layout | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
|
||||||
|
| Notification icons mode | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
|
||||||
|
| Notification cards mode | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Notification dot mode and read-state behavior | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Hide notification icons by app | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
|
||||||
|
| Hide status icons | ✅ | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental | ✅ AOD experimental |
|
||||||
|
| Dual-SIM status icon handling | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
|
||||||
|
| Samsung status chips: media/navigation/call | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Samsung charging chip handling | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
|
||||||
|
| Privacy indicator stability | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Custom statusbar clock text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Custom drawer clock/date text | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Stored clock pattern previews | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Battery bar: base statusbar modes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Battery bar: fullscreen and transparent statusbar modes | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Battery bar: curved top-edge geometry | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
|
||||||
|
| Battery bar threshold colours and colour math | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Camera cutout and under-display-camera handling | ✅ | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental | ⚠️ Experimental |
|
||||||
|
| Rotation and fold/unfold handling | ✅ | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified | ✅ rotation verified |
|
||||||
|
| Drawer scrolling and background-work performance | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
| Debug visualizers and debug notifications | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||||
|
|
||||||
|
Current emulator targets use rooted LSPosed snapshots. They are useful for
|
||||||
|
compatibility work, but hardware-specific entries remain marked as experimental
|
||||||
|
when the emulator cannot expose the required state.
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@@ -81,8 +122,39 @@ Release builds require local signing configuration:
|
|||||||
./gradlew assembleRelease
|
./gradlew assembleRelease
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development Note
|
## Test Harness
|
||||||
|
|
||||||
This project has been coded with the help of AI as an implementation aid. The
|
The repository also includes a small companion app for compatibility testing.
|
||||||
design decisions, device testing, and final behaviour are still manually
|
It is not required for normal use of the module. It creates repeatable
|
||||||
reviewed.
|
statusbar scenarios such as opaque, transparent, fullscreen, privacy-indicator,
|
||||||
|
and notification states:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :test-harness:assembleDebug
|
||||||
|
adb install -r test-harness/build/outputs/apk/debug/test-harness-debug.apk
|
||||||
|
adb shell am start -n se.ajpanton.statusbartweak.harness/.MainActivity
|
||||||
|
```
|
||||||
|
|
||||||
|
Automation can launch scenarios directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell am start -n se.ajpanton.statusbartweak.harness/.ScenarioActivity --es scenario fullscreen_landscape
|
||||||
|
adb shell am start -n se.ajpanton.statusbartweak.harness/.MainActivity --es command post_normal
|
||||||
|
```
|
||||||
|
|
||||||
|
Useful scenario values include `opaque_light`, `opaque_dark`, `transparent`,
|
||||||
|
`fullscreen_portrait`, `fullscreen_landscape`, `camera`, `microphone`, and
|
||||||
|
`camera_microphone`.
|
||||||
|
|
||||||
|
Useful command values include `post_normal`, `post_ongoing`, `post_grouped`,
|
||||||
|
`update`, `start_chronometer`, `start_churn`, `stop_churn`, `start_media`,
|
||||||
|
`start_foreground`, `stop_service`, and `clear`.
|
||||||
|
|
||||||
|
Battery charging state is controlled by Android system commands rather than by
|
||||||
|
the harness APK:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb shell cmd battery set status 2
|
||||||
|
adb shell cmd battery set level 80
|
||||||
|
adb shell cmd battery reset
|
||||||
|
```
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "se.ajpanton.statusbartweak"
|
applicationId = "se.ajpanton.statusbartweak"
|
||||||
minSdk = 35
|
minSdk = 34
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
val buildVersionName = project.findProperty("sbtVersionName")?.toString()
|
val buildVersionName = project.findProperty("sbtVersionName")?.toString()
|
||||||
?: fallbackVersionName
|
?: fallbackVersionName
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.module;
|
|||||||
|
|
||||||
import io.github.libxposed.api.XposedInterface;
|
import io.github.libxposed.api.XposedInterface;
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.StandaloneFeatureRegistry;
|
import se.ajpanton.statusbartweak.runtime.features.StandaloneFeatureRegistry;
|
||||||
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
||||||
@@ -15,11 +16,6 @@ import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
|||||||
*/
|
*/
|
||||||
public final class ModuleCoordinator {
|
public final class ModuleCoordinator {
|
||||||
|
|
||||||
private static final String PACKAGE_ANDROID = "android";
|
|
||||||
private static final String PACKAGE_SYSTEM = "system";
|
|
||||||
private static final String PACKAGE_SYSTEMUI = "com.android.systemui";
|
|
||||||
private static final String PACKAGE_AODSERVICE = "com.samsung.android.app.aodservice";
|
|
||||||
|
|
||||||
private final RuntimeContext runtimeContext;
|
private final RuntimeContext runtimeContext;
|
||||||
private final StandaloneFeatureRegistry standaloneFeatureRegistry;
|
private final StandaloneFeatureRegistry standaloneFeatureRegistry;
|
||||||
|
|
||||||
@@ -33,10 +29,7 @@ public final class ModuleCoordinator {
|
|||||||
|
|
||||||
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
|
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
|
||||||
String processName = param.getProcessName();
|
String processName = param.getProcessName();
|
||||||
if (!PACKAGE_SYSTEMUI.equals(processName)
|
if (!SystemUiCapabilities.isHandledProcessName(processName)) {
|
||||||
&& !PACKAGE_ANDROID.equals(processName)
|
|
||||||
&& !PACKAGE_SYSTEM.equals(processName)
|
|
||||||
&& !PACKAGE_AODSERVICE.equals(processName)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
standaloneFeatureRegistry.onModuleLoaded(param);
|
standaloneFeatureRegistry.onModuleLoaded(param);
|
||||||
@@ -54,9 +47,6 @@ public final class ModuleCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldHandlePackage(String packageName) {
|
private static boolean shouldHandlePackage(String packageName) {
|
||||||
return PACKAGE_SYSTEMUI.equals(packageName)
|
return SystemUiCapabilities.isHandledPackageName(packageName);
|
||||||
|| PACKAGE_ANDROID.equals(packageName)
|
|
||||||
|| PACKAGE_SYSTEM.equals(packageName)
|
|
||||||
|| PACKAGE_AODSERVICE.equals(packageName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package se.ajpanton.statusbartweak.platform;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||||
|
|
||||||
|
public final class SystemUiCapabilities {
|
||||||
|
public static final String PACKAGE_ANDROID = "android";
|
||||||
|
public static final String PACKAGE_SYSTEM = "system";
|
||||||
|
public static final String PACKAGE_SYSTEMUI = "com.android.systemui";
|
||||||
|
public static final String PACKAGE_SAMSUNG_AOD_SERVICE = "com.samsung.android.app.aodservice";
|
||||||
|
private static final boolean SAMSUNG_ONE_UI = "samsung".equalsIgnoreCase(Build.MANUFACTURER);
|
||||||
|
|
||||||
|
private SystemUiCapabilities() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSamsungOneUi() {
|
||||||
|
return SAMSUNG_ONE_UI;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsLockscreenShadeWindowRoot() {
|
||||||
|
return !isSamsungOneUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsSamsungAodServiceProcess() {
|
||||||
|
return isSamsungOneUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsSamsungAodMiscControllers() {
|
||||||
|
return isSamsungOneUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsSamsungStatusChipHooks() {
|
||||||
|
return isSamsungOneUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsSamsungNotificationDisplayStyles() {
|
||||||
|
return isSamsungOneUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isSystemUiPackageName(String packageName) {
|
||||||
|
return PACKAGE_SYSTEMUI.equals(packageName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHandledPackageName(String packageName) {
|
||||||
|
return PACKAGE_SYSTEMUI.equals(packageName)
|
||||||
|
|| PACKAGE_ANDROID.equals(packageName)
|
||||||
|
|| PACKAGE_SYSTEM.equals(packageName)
|
||||||
|
|| (supportsSamsungAodServiceProcess()
|
||||||
|
&& PACKAGE_SAMSUNG_AOD_SERVICE.equals(packageName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isHandledProcessName(String processName) {
|
||||||
|
return isHandledPackageName(processName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean supportsExperimentalGenericAodStatusbarRoot(SbtSettings settings) {
|
||||||
|
return !isSamsungOneUi()
|
||||||
|
&& settings != null
|
||||||
|
&& settings.debugExperimentalAodStatusbarRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean treatsUnknownAodNotificationStyleAsIcons(SbtSettings settings) {
|
||||||
|
return supportsExperimentalGenericAodStatusbarRoot(settings);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -80,6 +80,10 @@ public final class NotificationKeyReflection {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
String normalized = key.trim();
|
String normalized = key.trim();
|
||||||
|
String pipePackage = packageFromPipeNotificationKey(normalized);
|
||||||
|
if (pipePackage != null) {
|
||||||
|
return pipePackage;
|
||||||
|
}
|
||||||
int suffix = normalized.indexOf('@');
|
int suffix = normalized.indexOf('@');
|
||||||
if (suffix >= 0) {
|
if (suffix >= 0) {
|
||||||
normalized = normalized.substring(0, suffix);
|
normalized = normalized.substring(0, suffix);
|
||||||
@@ -91,6 +95,19 @@ public final class NotificationKeyReflection {
|
|||||||
return AppIconRules.isValidPackageName(normalized) ? normalized : null;
|
return AppIconRules.isValidPackageName(normalized) ? normalized : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String packageFromPipeNotificationKey(String key) {
|
||||||
|
int firstPipe = key.indexOf('|');
|
||||||
|
if (firstPipe < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int secondPipe = key.indexOf('|', firstPipe + 1);
|
||||||
|
if (secondPipe <= firstPipe + 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String packageName = key.substring(firstPipe + 1, secondPipe);
|
||||||
|
return AppIconRules.isValidPackageName(packageName) ? packageName : null;
|
||||||
|
}
|
||||||
|
|
||||||
private static String nonEmptyStringFromMethod(Object target, String methodName) {
|
private static String nonEmptyStringFromMethod(Object target, String methodName) {
|
||||||
Object value = ReflectionSupport.invokeMethod(target, methodName);
|
Object value = ReflectionSupport.invokeMethod(target, methodName);
|
||||||
return value instanceof String string && !string.isEmpty() ? string : null;
|
return value instanceof String string && !string.isEmpty() ? string : null;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime;
|
package se.ajpanton.statusbartweak.runtime;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@@ -7,8 +8,9 @@ public final class ViewIdNames {
|
|||||||
private ViewIdNames() {
|
private ViewIdNames() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType") // Foreign SystemUI ids are opaque integers, not this app's R.id values.
|
||||||
public static String idName(View view) {
|
public static String idName(View view) {
|
||||||
if (view == null || view.getId() == View.NO_ID) {
|
if (view == null || view.getId() == View.NO_ID || (view.getId() >>> 24) == 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime.features;
|
package se.ajpanton.statusbartweak.runtime.features;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Small independent runtime slice.
|
* Small independent runtime slice.
|
||||||
@@ -15,7 +15,8 @@ public interface RuntimeFeature {
|
|||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
static boolean isSystemUiPackage(XposedModuleInterface.PackageReadyParam param) {
|
static boolean isSystemUiPackage(XposedModuleInterface.PackageReadyParam param) {
|
||||||
return param != null && SbtSettings.PACKAGE_SYSTEMUI.equals(param.getPackageName());
|
return param != null
|
||||||
|
&& SystemUiCapabilities.isSystemUiPackageName(param.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
default void onModuleLoaded(
|
default void onModuleLoaded(
|
||||||
|
|||||||
+89
-62
@@ -59,8 +59,8 @@ final class BatteryBarController {
|
|||||||
private final Map<View, OverlayBatteryBar> overlayBatteryBars = new WeakHashMap<>();
|
private final Map<View, OverlayBatteryBar> overlayBatteryBars = new WeakHashMap<>();
|
||||||
private final Map<View, View.OnLayoutChangeListener> rootLayoutListeners = new WeakHashMap<>();
|
private final Map<View, View.OnLayoutChangeListener> rootLayoutListeners = new WeakHashMap<>();
|
||||||
private final Set<View> pendingOverlayRetries = Collections.newSetFromMap(new WeakHashMap<>());
|
private final Set<View> pendingOverlayRetries = Collections.newSetFromMap(new WeakHashMap<>());
|
||||||
private final Map<View, String> lastSignatures = new WeakHashMap<>();
|
private final Map<View, RenderSignature> lastSignatures = new WeakHashMap<>();
|
||||||
private final Map<View, String> lastOverlaySignatures = new WeakHashMap<>();
|
private final Map<View, RenderSignature> lastOverlaySignatures = new WeakHashMap<>();
|
||||||
private final Map<View, Integer> lastRootTransformSignatures = new WeakHashMap<>();
|
private final Map<View, Integer> lastRootTransformSignatures = new WeakHashMap<>();
|
||||||
private int systemBarAppearance;
|
private int systemBarAppearance;
|
||||||
private int systemBarBehavior;
|
private int systemBarBehavior;
|
||||||
@@ -87,7 +87,7 @@ final class BatteryBarController {
|
|||||||
installRootTransformHooks();
|
installRootTransformHooks();
|
||||||
installSystemBarAttributeListener(classLoader);
|
installSystemBarAttributeListener(classLoader);
|
||||||
installTransientBarListener(classLoader);
|
installTransientBarListener(classLoader);
|
||||||
runtimeContext.getModeStateRepository().addSceneListener((previous, current) -> refreshAllRoots());
|
runtimeContext.getModeStateRepository().addSceneListener((previous, current) -> postRefreshAllRoots());
|
||||||
hooksInstalled = true;
|
hooksInstalled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,14 +149,6 @@ final class BatteryBarController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setY", chain -> {
|
|
||||||
Object result = chain.proceed();
|
|
||||||
Object target = chain.getThisObject();
|
|
||||||
if (target instanceof View root && roots.contains(root)) {
|
|
||||||
applyBatteryBarAfterRootTransform(root);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setAlpha", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(framework, View.class, "setAlpha", chain -> {
|
||||||
Object target = chain.getThisObject();
|
Object target = chain.getThisObject();
|
||||||
Object alphaArg = chain.getArgs() != null && !chain.getArgs().isEmpty()
|
Object alphaArg = chain.getArgs() != null && !chain.getArgs().isEmpty()
|
||||||
@@ -374,10 +366,6 @@ final class BatteryBarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyBatteryBar(View root) {
|
private void applyBatteryBar(View root) {
|
||||||
applyBatteryBarInternal(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void applyBatteryBarInternal(View root) {
|
|
||||||
if (!(root instanceof ViewGroup rootGroup)) {
|
if (!(root instanceof ViewGroup rootGroup)) {
|
||||||
removeBatteryBarView(root);
|
removeBatteryBarView(root);
|
||||||
return;
|
return;
|
||||||
@@ -414,15 +402,15 @@ final class BatteryBarController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
removeOverlayBatteryBar(root);
|
removeOverlayBatteryBar(root);
|
||||||
String signature = buildSignature(root, settings, color, geometry, scenario);
|
RenderSignature signature = RenderSignature.of(
|
||||||
|
root, scenario, batteryLevel, plugged, color, geometry, settings);
|
||||||
BatteryBarView existingView = batteryBarViews.get(root);
|
BatteryBarView existingView = batteryBarViews.get(root);
|
||||||
String lastSignature = lastSignatures.get(root);
|
RenderSignature lastSignature = lastSignatures.get(root);
|
||||||
if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) {
|
if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) {
|
||||||
existingView.bringToFront();
|
existingView.bringToFront();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyEmbeddedBatteryBar(rootGroup, settings, geometry, scenario, color);
|
applyEmbeddedBatteryBar(rootGroup, settings, geometry, scenario, color);
|
||||||
lastSignatures.put(root, signature);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyEmbeddedBatteryBar(
|
private void applyEmbeddedBatteryBar(
|
||||||
@@ -434,10 +422,12 @@ final class BatteryBarController {
|
|||||||
) {
|
) {
|
||||||
BatteryBarView barView = ensureBatteryBarView(rootGroup);
|
BatteryBarView barView = ensureBatteryBarView(rootGroup);
|
||||||
layoutBatteryBarView(rootGroup, barView);
|
layoutBatteryBarView(rootGroup, barView);
|
||||||
barView.update(rootGroup, settings, geometry, batteryLevel, plugged, color);
|
barView.update(rootGroup, rootGroup, settings, geometry, batteryLevel, plugged, color);
|
||||||
barView.bringToFront();
|
barView.bringToFront();
|
||||||
barView.invalidate();
|
barView.invalidate();
|
||||||
lastSignatures.put(rootGroup, buildSignature(rootGroup, settings, color, geometry, scenario));
|
lastSignatures.put(
|
||||||
|
rootGroup,
|
||||||
|
RenderSignature.of(rootGroup, scenario, batteryLevel, plugged, color, geometry, settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyOverlayBatteryBar(
|
private void applyOverlayBatteryBar(
|
||||||
@@ -447,7 +437,8 @@ final class BatteryBarController {
|
|||||||
BatteryBarGeometry.Scenario scenario,
|
BatteryBarGeometry.Scenario scenario,
|
||||||
int color
|
int color
|
||||||
) {
|
) {
|
||||||
String signature = buildSignature(root, settings, color, geometry, scenario);
|
RenderSignature signature = RenderSignature.of(
|
||||||
|
root, scenario, batteryLevel, plugged, color, geometry, settings);
|
||||||
OverlayBatteryBar overlay = overlayBatteryBars.get(root);
|
OverlayBatteryBar overlay = overlayBatteryBars.get(root);
|
||||||
if (overlay == null) {
|
if (overlay == null) {
|
||||||
overlay = new OverlayBatteryBar(root.getContext());
|
overlay = new OverlayBatteryBar(root.getContext());
|
||||||
@@ -461,7 +452,7 @@ final class BatteryBarController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
pendingOverlayRetries.remove(root);
|
pendingOverlayRetries.remove(root);
|
||||||
overlay.view.update(overlay.host, settings, geometry, batteryLevel, plugged, color);
|
overlay.view.update(overlay.host, root, settings, geometry, batteryLevel, plugged, color);
|
||||||
if (!signature.equals(lastOverlaySignatures.get(root))) {
|
if (!signature.equals(lastOverlaySignatures.get(root))) {
|
||||||
overlay.updateLayout(bounds.width, bounds.height);
|
overlay.updateLayout(bounds.width, bounds.height);
|
||||||
lastOverlaySignatures.put(root, signature);
|
lastOverlaySignatures.put(root, signature);
|
||||||
@@ -534,38 +525,19 @@ final class BatteryBarController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String buildSignature(
|
|
||||||
View root,
|
|
||||||
SbtSettings settings,
|
|
||||||
int color,
|
|
||||||
BatteryBarGeometry geometry,
|
|
||||||
BatteryBarGeometry.Scenario scenario
|
|
||||||
) {
|
|
||||||
return root.getWidth() + "|"
|
|
||||||
+ root.getHeight() + "|"
|
|
||||||
+ scenario + "|"
|
|
||||||
+ batteryLevel + "|"
|
|
||||||
+ plugged + "|"
|
|
||||||
+ color + "|"
|
|
||||||
+ settings.batteryBarEnabled + "|"
|
|
||||||
+ settings.batteryBarScenarioEnabled + "|"
|
|
||||||
+ settings.batteryBarPosition + "|"
|
|
||||||
+ settings.batteryBarAlignment + "|"
|
|
||||||
+ settings.batteryBarThicknessDp + "|"
|
|
||||||
+ settings.batteryBarEdgeOffsetDp + "|"
|
|
||||||
+ settings.batteryBarCurvedGeometryMode + "|"
|
|
||||||
+ (geometry != null ? geometry.signature() : "") + "|"
|
|
||||||
+ settings.batteryBarMinLevel + "|"
|
|
||||||
+ settings.batteryBarMaxLevel + "|"
|
|
||||||
+ settings.batteryBarDefaultDischargeColor + "|"
|
|
||||||
+ settings.batteryBarDefaultChargeColor + "|"
|
|
||||||
+ BatteryBarStyle.encodeThresholds(settings.batteryBarThresholds);
|
|
||||||
}
|
|
||||||
|
|
||||||
private BatteryBarGeometry.Scenario scenarioForRoot(View root) {
|
private BatteryBarGeometry.Scenario scenarioForRoot(View root) {
|
||||||
if (isKeyguardRoot(root)) {
|
if (isKeyguardRoot(root)) {
|
||||||
return BatteryBarGeometry.Scenario.LOCKSCREEN;
|
return BatteryBarGeometry.Scenario.LOCKSCREEN;
|
||||||
}
|
}
|
||||||
|
SceneKey scene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||||
|
if (isPhoneRoot(root)
|
||||||
|
&& scene != null
|
||||||
|
&& scene.isLockscreen()
|
||||||
|
&& isKeyguardLocked(root.getContext())
|
||||||
|
&& !isStatusBarSurfaceHidden(root)
|
||||||
|
&& !isFullscreenSystemBarState()) {
|
||||||
|
return BatteryBarGeometry.Scenario.LOCKSCREEN;
|
||||||
|
}
|
||||||
boolean fullscreen = isPhoneRoot(root)
|
boolean fullscreen = isPhoneRoot(root)
|
||||||
&& (isUnlockedScene() || isLockedPhoneFullscreenRoot(root))
|
&& (isUnlockedScene() || isLockedPhoneFullscreenRoot(root))
|
||||||
&& (isStatusBarSurfaceHidden(root) || isFullscreenSystemBarState());
|
&& (isStatusBarSurfaceHidden(root) || isFullscreenSystemBarState());
|
||||||
@@ -623,10 +595,14 @@ final class BatteryBarController {
|
|||||||
return scene == null || !scene.isUnlocked();
|
return scene == null || !scene.isUnlocked();
|
||||||
}
|
}
|
||||||
if (isPhoneRoot(root)) {
|
if (isPhoneRoot(root)) {
|
||||||
if (isLockedPhoneStatusBarRoot(root)) {
|
if (scene == null || scene.isUnlocked()) {
|
||||||
return isLockedPhoneFullscreenRoot(root);
|
return true;
|
||||||
}
|
}
|
||||||
return scene == null || scene.isUnlocked() || isLockedPhoneFullscreenRoot(root);
|
if (isLockedPhoneStatusBarRoot(root)) {
|
||||||
|
return isLockedPhoneFullscreenRoot(root)
|
||||||
|
|| (!isStatusBarSurfaceHidden(root) && !isFullscreenSystemBarState());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -695,7 +671,15 @@ final class BatteryBarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldRenderVisibleFullscreenBar(View root) {
|
private boolean shouldRenderVisibleFullscreenBar(View root) {
|
||||||
return root != null && isFullscreenSystemBarState() && statusBarTransientShown;
|
return root != null
|
||||||
|
&& isFullscreenSystemBarState()
|
||||||
|
&& statusBarTransientShown
|
||||||
|
&& isStatusBarWindowVisible(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStatusBarWindowVisible(View root) {
|
||||||
|
WindowInsets insets = root.getRootWindowInsets();
|
||||||
|
return insets == null || insets.isVisible(WindowInsets.Type.statusBars());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTransientBarState(java.util.List<Object> args, boolean visible) {
|
private void updateTransientBarState(java.util.List<Object> args, boolean visible) {
|
||||||
@@ -733,13 +717,7 @@ final class BatteryBarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private OverlayBounds fullscreenOverlayBounds(View root) {
|
private OverlayBounds fullscreenOverlayBounds(View root) {
|
||||||
int width = root.getWidth();
|
return new OverlayBounds(root.getWidth(), root.getHeight());
|
||||||
int height = root.getHeight();
|
|
||||||
Rect bounds = currentWindowBounds(root.getContext());
|
|
||||||
if (bounds.width() > 0) {
|
|
||||||
width = bounds.width();
|
|
||||||
}
|
|
||||||
return new OverlayBounds(width, height);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Rect currentWindowBounds(Context context) {
|
private static Rect currentWindowBounds(Context context) {
|
||||||
@@ -761,6 +739,47 @@ final class BatteryBarController {
|
|||||||
private record OverlayBounds(int width, int height) {
|
private record OverlayBounds(int width, int height) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record RenderSignature(
|
||||||
|
int width,
|
||||||
|
int height,
|
||||||
|
BatteryBarGeometry.Scenario scenario,
|
||||||
|
int batteryLevel,
|
||||||
|
boolean plugged,
|
||||||
|
int color,
|
||||||
|
String position,
|
||||||
|
String alignment,
|
||||||
|
int thicknessDp,
|
||||||
|
int edgeOffsetDp,
|
||||||
|
String curvedGeometryMode,
|
||||||
|
int minLevel,
|
||||||
|
int maxLevel
|
||||||
|
) {
|
||||||
|
static RenderSignature of(
|
||||||
|
View root,
|
||||||
|
BatteryBarGeometry.Scenario scenario,
|
||||||
|
int batteryLevel,
|
||||||
|
boolean plugged,
|
||||||
|
int color,
|
||||||
|
BatteryBarGeometry geometry,
|
||||||
|
SbtSettings settings
|
||||||
|
) {
|
||||||
|
return new RenderSignature(
|
||||||
|
root.getWidth(),
|
||||||
|
root.getHeight(),
|
||||||
|
scenario,
|
||||||
|
batteryLevel,
|
||||||
|
plugged,
|
||||||
|
color,
|
||||||
|
geometry.position,
|
||||||
|
geometry.alignment,
|
||||||
|
geometry.thicknessDp,
|
||||||
|
geometry.edgeOffsetDp,
|
||||||
|
geometry.curvedGeometryMode,
|
||||||
|
settings.batteryBarMinLevel,
|
||||||
|
settings.batteryBarMaxLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSystemBarState(java.util.List<Object> args) {
|
private void updateSystemBarState(java.util.List<Object> args) {
|
||||||
if (args == null || args.size() < 2) {
|
if (args == null || args.size() < 2) {
|
||||||
return;
|
return;
|
||||||
@@ -1039,6 +1058,7 @@ final class BatteryBarController {
|
|||||||
private final Path drawPath = new Path();
|
private final Path drawPath = new Path();
|
||||||
private final PathMeasure pathMeasure = new PathMeasure();
|
private final PathMeasure pathMeasure = new PathMeasure();
|
||||||
private ViewGroup host;
|
private ViewGroup host;
|
||||||
|
private View roundedCornerSource;
|
||||||
private SbtSettings settings;
|
private SbtSettings settings;
|
||||||
private BatteryBarGeometry geometry;
|
private BatteryBarGeometry geometry;
|
||||||
private int batteryLevel = -1;
|
private int batteryLevel = -1;
|
||||||
@@ -1053,6 +1073,7 @@ final class BatteryBarController {
|
|||||||
|
|
||||||
void update(
|
void update(
|
||||||
ViewGroup host,
|
ViewGroup host,
|
||||||
|
View roundedCornerSource,
|
||||||
SbtSettings settings,
|
SbtSettings settings,
|
||||||
BatteryBarGeometry geometry,
|
BatteryBarGeometry geometry,
|
||||||
int batteryLevel,
|
int batteryLevel,
|
||||||
@@ -1060,6 +1081,7 @@ final class BatteryBarController {
|
|||||||
int color
|
int color
|
||||||
) {
|
) {
|
||||||
this.host = host;
|
this.host = host;
|
||||||
|
this.roundedCornerSource = roundedCornerSource;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.geometry = geometry;
|
this.geometry = geometry;
|
||||||
this.batteryLevel = batteryLevel;
|
this.batteryLevel = batteryLevel;
|
||||||
@@ -1226,7 +1248,12 @@ final class BatteryBarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private float resolveTopCornerRadius(float fallback) {
|
private float resolveTopCornerRadius(float fallback) {
|
||||||
WindowInsets insets = getRootWindowInsets();
|
WindowInsets insets = roundedCornerSource != null
|
||||||
|
? roundedCornerSource.getRootWindowInsets()
|
||||||
|
: null;
|
||||||
|
if (insets == null) {
|
||||||
|
insets = getRootWindowInsets();
|
||||||
|
}
|
||||||
if (insets == null) {
|
if (insets == null) {
|
||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -1,6 +1,7 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime.features.chips;
|
package se.ajpanton.statusbartweak.runtime.features.chips;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||||
|
|
||||||
@@ -32,6 +33,9 @@ public final class StatusChipsFeature implements RuntimeFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||||
|
if (!SystemUiCapabilities.supportsSamsungStatusChipHooks()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (statusChipHider == null) {
|
if (statusChipHider == null) {
|
||||||
statusChipHider = new StatusChipHider(context);
|
statusChipHider = new StatusChipHider(context);
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -104,6 +104,10 @@ final class DrawerClockTextController {
|
|||||||
untrackDisabledRoles(clockEnabled, dateEnabled);
|
untrackDisabledRoles(clockEnabled, dateEnabled);
|
||||||
|
|
||||||
SurfaceMode surfaceMode = scene != null ? scene.surfaceMode() : null;
|
SurfaceMode surfaceMode = scene != null ? scene.surfaceMode() : null;
|
||||||
|
if (!root.isLayoutRequested()
|
||||||
|
&& hasAttachedCachedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (hasValidTrackedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) {
|
if (hasValidTrackedCandidates(root, clockEnabled, dateEnabled, surfaceMode)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -200,6 +204,28 @@ final class DrawerClockTextController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasAttachedCachedCandidates(
|
||||||
|
View root,
|
||||||
|
boolean clockEnabled,
|
||||||
|
boolean dateEnabled,
|
||||||
|
SurfaceMode surfaceMode
|
||||||
|
) {
|
||||||
|
CandidateCache cache = candidateCacheByRoot.get(root);
|
||||||
|
if (cache == null || cache.surfaceMode != surfaceMode) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (!clockEnabled || isAttachedCachedCandidate(cache.clockView, Role.CLOCK))
|
||||||
|
&& (!dateEnabled || isAttachedCachedCandidate(cache.dateView, Role.DATE));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAttachedCachedCandidate(WeakReference<TextView> reference, Role role) {
|
||||||
|
TextView textView = reference != null ? reference.get() : null;
|
||||||
|
return textView != null
|
||||||
|
&& textView.isAttachedToWindow()
|
||||||
|
&& trackedRoles.get(textView) == role
|
||||||
|
&& ownedTexts.contains(textView);
|
||||||
|
}
|
||||||
|
|
||||||
private void track(TextView textView, Role role) {
|
private void track(TextView textView, Role role) {
|
||||||
if (textView == null || role == null) {
|
if (textView == null || role == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
+50
-5
@@ -38,8 +38,10 @@ import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
|||||||
|
|
||||||
final class StockClockController {
|
final class StockClockController {
|
||||||
|
|
||||||
private static final String CLOCK_CLASS_NAME =
|
private static final String[] CLOCK_CLASS_NAMES = {
|
||||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView";
|
"com.android.systemui.statusbar.policy.QSClockIndicatorView",
|
||||||
|
"com.android.systemui.statusbar.policy.Clock"
|
||||||
|
};
|
||||||
|
|
||||||
private final RuntimeContext runtimeContext;
|
private final RuntimeContext runtimeContext;
|
||||||
private final Set<TextView> trackedClocks =
|
private final Set<TextView> trackedClocks =
|
||||||
@@ -64,8 +66,25 @@ final class StockClockController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
registerLayoutModeListenerIfNeeded();
|
registerLayoutModeListenerIfNeeded();
|
||||||
Class<?> clockClass = ReflectionSupport.requireClass(CLOCK_CLASS_NAME, classLoader);
|
ArrayList<Class<?>> clockClasses = new ArrayList<>();
|
||||||
|
for (String className : CLOCK_CLASS_NAMES) {
|
||||||
|
Class<?> clockClass = ReflectionSupport.findClassIfExists(className, classLoader);
|
||||||
|
if (clockClass != null) {
|
||||||
|
clockClasses.add(clockClass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (clockClasses.isEmpty()) {
|
||||||
|
hooksInstalled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
XposedInterface framework = runtimeContext.getFramework();
|
XposedInterface framework = runtimeContext.getFramework();
|
||||||
|
for (Class<?> clockClass : clockClasses) {
|
||||||
|
hookClockClass(framework, clockClass);
|
||||||
|
}
|
||||||
|
hooksInstalled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hookClockClass(XposedInterface framework, Class<?> clockClass) {
|
||||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
Object target = chain.getThisObject();
|
Object target = chain.getThisObject();
|
||||||
@@ -104,7 +123,7 @@ final class StockClockController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
XposedHookSupport.hookAllMethods(framework, clockClass, "notifyTimeChanged", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "notifyTimeChanged", chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
Object target = chain.getThisObject();
|
Object target = chain.getThisObject();
|
||||||
if (!(target instanceof TextView)) {
|
if (!(target instanceof TextView)) {
|
||||||
@@ -123,6 +142,14 @@ final class StockClockController {
|
|||||||
trackAndApply(clockView, bellSound);
|
trackAndApply(clockView, bellSound);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "updateClock", chain -> {
|
||||||
|
Object result = chain.proceed();
|
||||||
|
Object target = chain.getThisObject();
|
||||||
|
if (target instanceof TextView clockView) {
|
||||||
|
trackAndApply(clockView, null);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
});
|
||||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setTextColor", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setTextColor", chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
if (chain.getThisObject() instanceof TextView clockView) {
|
if (chain.getThisObject() instanceof TextView clockView) {
|
||||||
@@ -137,7 +164,6 @@ final class StockClockController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
hooksInstalled = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reapplyClockIfCustomOrOwned(TextView clockView) {
|
private void reapplyClockIfCustomOrOwned(TextView clockView) {
|
||||||
@@ -162,6 +188,9 @@ final class StockClockController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void trackAndApply(TextView clockView, Object bellSound) {
|
private void trackAndApply(TextView clockView, Object bellSound) {
|
||||||
|
if (isNotificationShadeDescendant(clockView)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
trackedClocks.add(clockView);
|
trackedClocks.add(clockView);
|
||||||
registerReceiverIfNeeded(clockView.getContext());
|
registerReceiverIfNeeded(clockView.getContext());
|
||||||
if (bellSound != null) {
|
if (bellSound != null) {
|
||||||
@@ -219,6 +248,10 @@ final class StockClockController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyClockText(TextView clockView) {
|
private void applyClockText(TextView clockView) {
|
||||||
|
if (isNotificationShadeDescendant(clockView)) {
|
||||||
|
restoreOwnedClock(clockView);
|
||||||
|
return;
|
||||||
|
}
|
||||||
SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext());
|
SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext());
|
||||||
boolean layoutEnabled = settings.clockEnabled;
|
boolean layoutEnabled = settings.clockEnabled;
|
||||||
if (layoutEnabled) {
|
if (layoutEnabled) {
|
||||||
@@ -330,6 +363,18 @@ final class StockClockController {
|
|||||||
|| (settings.clockCustomFormatEnabled && !pattern.isEmpty());
|
|| (settings.clockCustomFormatEnabled && !pattern.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isNotificationShadeDescendant(View view) {
|
||||||
|
View current = view;
|
||||||
|
while (current != null) {
|
||||||
|
if (current.getClass().getName().contains("NotificationShadeWindowView")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Object parent = current.getParent();
|
||||||
|
current = parent instanceof View ? (View) parent : null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void applyClockLineMode(TextView clockView) {
|
private void applyClockLineMode(TextView clockView) {
|
||||||
clockView.setSingleLine(true);
|
clockView.setSingleLine(true);
|
||||||
clockView.setHorizontallyScrolling(false);
|
clockView.setHorizontallyScrolling(false);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface;
|
import io.github.libxposed.api.XposedModuleInterface;
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ public final class MiscFeature implements RuntimeFeature {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void installControllers(RuntimeContext context, ClassLoader classLoader) {
|
private void installControllers(RuntimeContext context, ClassLoader classLoader) {
|
||||||
|
if (!SystemUiCapabilities.supportsSamsungAodMiscControllers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (aodCallVisibilityController == null) {
|
if (aodCallVisibilityController == null) {
|
||||||
aodCallVisibilityController = new AodCallVisibilityController(context);
|
aodCallVisibilityController = new AodCallVisibilityController(context);
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-5
@@ -43,8 +43,11 @@ final class StockUnlockedNotificationIconsController {
|
|||||||
if (hooksInstalled || classLoader == null) {
|
if (hooksInstalled || classLoader == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Class<?> containerClass = ReflectionSupport.requireClass(CONTAINER_CLASS_NAME, classLoader);
|
Class<?> containerClass = ReflectionSupport.findClassIfExists(CONTAINER_CLASS_NAME, classLoader);
|
||||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
|
if (containerClass == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
if (chain.getThisObject() instanceof View view) {
|
if (chain.getThisObject() instanceof View view) {
|
||||||
if (shouldManageContainer(view)) {
|
if (shouldManageContainer(view)) {
|
||||||
@@ -54,7 +57,7 @@ final class StockUnlockedNotificationIconsController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
|
||||||
if (chain.getThisObject() instanceof View view) {
|
if (chain.getThisObject() instanceof View view) {
|
||||||
if (shouldManageContainer(view)) {
|
if (shouldManageContainer(view)) {
|
||||||
trackContainer(view);
|
trackContainer(view);
|
||||||
@@ -63,7 +66,7 @@ final class StockUnlockedNotificationIconsController {
|
|||||||
}
|
}
|
||||||
return chain.proceed();
|
return chain.proceed();
|
||||||
});
|
});
|
||||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
if (chain.getThisObject() instanceof View view) {
|
if (chain.getThisObject() instanceof View view) {
|
||||||
if (shouldManageContainer(view)) {
|
if (shouldManageContainer(view)) {
|
||||||
@@ -75,7 +78,7 @@ final class StockUnlockedNotificationIconsController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
|
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
|
||||||
if (!(chain.getThisObject() instanceof View view)) {
|
if (!(chain.getThisObject() instanceof View view)) {
|
||||||
return chain.proceed();
|
return chain.proceed();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime.hooks;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.WeakHashMap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replacement for legacy Xposed additional-instance-field helpers.
|
|
||||||
*/
|
|
||||||
public final class InstanceStateStore {
|
|
||||||
|
|
||||||
private final WeakHashMap<Object, Map<String, Object>> state = new WeakHashMap<>();
|
|
||||||
|
|
||||||
public synchronized Object get(Object target, String key) {
|
|
||||||
if (target == null || key == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Map<String, Object> fields = state.get(target);
|
|
||||||
return fields != null ? fields.get(key) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void put(Object target, String key, Object value) {
|
|
||||||
if (target == null || key == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<String, Object> fields = state.computeIfAbsent(target, ignored -> new HashMap<>());
|
|
||||||
fields.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void remove(Object target, String key) {
|
|
||||||
if (target == null || key == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Map<String, Object> fields = state.get(target);
|
|
||||||
if (fields == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fields.remove(key);
|
|
||||||
if (fields.isEmpty()) {
|
|
||||||
state.remove(target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ package se.ajpanton.statusbartweak.runtime.hooks;
|
|||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.Modifier;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -34,6 +35,9 @@ public final class XposedHookSupport {
|
|||||||
if (!methodName.equals(method.getName())) {
|
if (!methodName.equals(method.getName())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (Modifier.isAbstract(method.getModifiers())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
handles.add(framework.hook(method).intercept(hooker));
|
handles.add(framework.hook(method).intercept(hooker));
|
||||||
}
|
}
|
||||||
@@ -59,6 +63,9 @@ public final class XposedHookSupport {
|
|||||||
if (!methodName.equals(method.getName())) {
|
if (!methodName.equals(method.getName())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (Modifier.isAbstract(method.getModifiers())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
handles.add(framework.hook(method).intercept(hooker));
|
handles.add(framework.hook(method).intercept(hooker));
|
||||||
}
|
}
|
||||||
@@ -83,4 +90,20 @@ public final class XposedHookSupport {
|
|||||||
}
|
}
|
||||||
return handles;
|
return handles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<XposedInterface.HookHandle> hookAllConstructorsIfExists(
|
||||||
|
XposedInterface framework,
|
||||||
|
Class<?> type,
|
||||||
|
XposedInterface.Hooker hooker
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(framework, "framework");
|
||||||
|
Objects.requireNonNull(type, "type");
|
||||||
|
Objects.requireNonNull(hooker, "hooker");
|
||||||
|
List<XposedInterface.HookHandle> handles = new ArrayList<>();
|
||||||
|
for (Constructor<?> constructor : type.getDeclaredConstructors()) {
|
||||||
|
constructor.setAccessible(true);
|
||||||
|
handles.add(framework.hook(constructor).intercept(hooker));
|
||||||
|
}
|
||||||
|
return handles;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+18
-3
@@ -17,6 +17,7 @@ import se.ajpanton.statusbartweak.runtime.render.StatusBarTintTarget;
|
|||||||
final class DarkIconDispatcherGuard {
|
final class DarkIconDispatcherGuard {
|
||||||
private final RuntimeContext runtimeContext;
|
private final RuntimeContext runtimeContext;
|
||||||
private final BooleanSupplier lockscreenSceneSupplier;
|
private final BooleanSupplier lockscreenSceneSupplier;
|
||||||
|
private final BooleanSupplier lockscreenDarkIconsSupplier;
|
||||||
private final Runnable refreshAllRoots;
|
private final Runnable refreshAllRoots;
|
||||||
private final Set<Object> dispatchers =
|
private final Set<Object> dispatchers =
|
||||||
Collections.newSetFromMap(new WeakHashMap<>());
|
Collections.newSetFromMap(new WeakHashMap<>());
|
||||||
@@ -25,15 +26,17 @@ final class DarkIconDispatcherGuard {
|
|||||||
DarkIconDispatcherGuard(
|
DarkIconDispatcherGuard(
|
||||||
RuntimeContext runtimeContext,
|
RuntimeContext runtimeContext,
|
||||||
BooleanSupplier lockscreenSceneSupplier,
|
BooleanSupplier lockscreenSceneSupplier,
|
||||||
|
BooleanSupplier lockscreenDarkIconsSupplier,
|
||||||
Runnable refreshAllRoots
|
Runnable refreshAllRoots
|
||||||
) {
|
) {
|
||||||
this.runtimeContext = runtimeContext;
|
this.runtimeContext = runtimeContext;
|
||||||
this.lockscreenSceneSupplier = lockscreenSceneSupplier;
|
this.lockscreenSceneSupplier = lockscreenSceneSupplier;
|
||||||
|
this.lockscreenDarkIconsSupplier = lockscreenDarkIconsSupplier;
|
||||||
this.refreshAllRoots = refreshAllRoots;
|
this.refreshAllRoots = refreshAllRoots;
|
||||||
}
|
}
|
||||||
|
|
||||||
void install(Class<?> darkIconDispatcherClass) {
|
void install(Class<?> darkIconDispatcherClass) {
|
||||||
XposedHookSupport.hookAllConstructors(
|
XposedHookSupport.hookAllConstructorsIfExists(
|
||||||
runtimeContext.getFramework(),
|
runtimeContext.getFramework(),
|
||||||
darkIconDispatcherClass,
|
darkIconDispatcherClass,
|
||||||
chain -> {
|
chain -> {
|
||||||
@@ -48,7 +51,7 @@ final class DarkIconDispatcherGuard {
|
|||||||
chain -> {
|
chain -> {
|
||||||
Object dispatcher = chain.getThisObject();
|
Object dispatcher = chain.getThisObject();
|
||||||
track(dispatcher);
|
track(dispatcher);
|
||||||
if (isLockscreenScene()) {
|
if (isLockscreenScene() && !shouldUseDarkLockscreenIcons()) {
|
||||||
forceLightState(dispatcher);
|
forceLightState(dispatcher);
|
||||||
}
|
}
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
@@ -63,7 +66,11 @@ final class DarkIconDispatcherGuard {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (Object dispatcher : new ArrayList<>(dispatchers)) {
|
for (Object dispatcher : new ArrayList<>(dispatchers)) {
|
||||||
forceLightState(dispatcher);
|
if (shouldUseDarkLockscreenIcons()) {
|
||||||
|
restoreState(dispatcher, forcedStates.remove(dispatcher));
|
||||||
|
} else {
|
||||||
|
forceLightState(dispatcher);
|
||||||
|
}
|
||||||
ReflectionSupport.invokeMethod(dispatcher, "applyIconTint");
|
ReflectionSupport.invokeMethod(dispatcher, "applyIconTint");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,6 +92,10 @@ final class DarkIconDispatcherGuard {
|
|||||||
return lockscreenSceneSupplier.getAsBoolean();
|
return lockscreenSceneSupplier.getAsBoolean();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldUseDarkLockscreenIcons() {
|
||||||
|
return lockscreenDarkIconsSupplier.getAsBoolean();
|
||||||
|
}
|
||||||
|
|
||||||
private void track(Object dispatcher) {
|
private void track(Object dispatcher) {
|
||||||
if (dispatcher != null) {
|
if (dispatcher != null) {
|
||||||
dispatchers.add(dispatcher);
|
dispatchers.add(dispatcher);
|
||||||
@@ -92,6 +103,10 @@ final class DarkIconDispatcherGuard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateTintTarget(Object dispatcher) {
|
private void updateTintTarget(Object dispatcher) {
|
||||||
|
if (isLockscreenScene() && shouldUseDarkLockscreenIcons()) {
|
||||||
|
StatusBarTintTarget.setDarkIcons(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
Object intensity = ReflectionSupport.getFieldValue(dispatcher, "mDarkIntensity");
|
Object intensity = ReflectionSupport.getFieldValue(dispatcher, "mDarkIntensity");
|
||||||
if (intensity instanceof Number number) {
|
if (intensity instanceof Number number) {
|
||||||
StatusBarTintTarget.setDarkIcons(number.floatValue() > 0.5f);
|
StatusBarTintTarget.setDarkIcons(number.floatValue() > 0.5f);
|
||||||
|
|||||||
+551
-92
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ public final class ModeStateRepository {
|
|||||||
private SceneKey activeScene = SceneKey.unlocked();
|
private SceneKey activeScene = SceneKey.unlocked();
|
||||||
private boolean systemUiStatusBarStateKnown;
|
private boolean systemUiStatusBarStateKnown;
|
||||||
private int systemUiStatusBarState;
|
private int systemUiStatusBarState;
|
||||||
|
private boolean keyguardGoingAway;
|
||||||
private boolean layoutEnabled;
|
private boolean layoutEnabled;
|
||||||
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
|
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
|
||||||
private final List<SceneListener> sceneListeners = new ArrayList<>();
|
private final List<SceneListener> sceneListeners = new ArrayList<>();
|
||||||
@@ -27,6 +28,14 @@ public final class ModeStateRepository {
|
|||||||
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
|
return systemUiStatusBarStateKnown && systemUiStatusBarState == 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized boolean isKeyguardGoingAway() {
|
||||||
|
return keyguardGoingAway;
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void setKeyguardGoingAway(boolean goingAway) {
|
||||||
|
keyguardGoingAway = goingAway;
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized boolean isLayoutEnabled() {
|
public synchronized boolean isLayoutEnabled() {
|
||||||
return layoutEnabled;
|
return layoutEnabled;
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -23,8 +23,10 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
|||||||
"com.android.systemui.statusbar.StatusBarStateControllerImpl";
|
"com.android.systemui.statusbar.StatusBarStateControllerImpl";
|
||||||
private static final String CLASS_KEYGUARD_STATE_CONTROLLER =
|
private static final String CLASS_KEYGUARD_STATE_CONTROLLER =
|
||||||
"com.android.systemui.statusbar.policy.KeyguardStateControllerImpl";
|
"com.android.systemui.statusbar.policy.KeyguardStateControllerImpl";
|
||||||
|
private static final int STATE_SHADE = 0;
|
||||||
|
|
||||||
private boolean installed;
|
private boolean installed;
|
||||||
|
private boolean keyguardGoingAway;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -78,8 +80,14 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
|||||||
"notifyKeyguardGoingAway",
|
"notifyKeyguardGoingAway",
|
||||||
chain -> {
|
chain -> {
|
||||||
Object result = chain.proceed();
|
Object result = chain.proceed();
|
||||||
if (firstBooleanArg(chain.getArgs())) {
|
Boolean goingAway = firstBooleanArg(chain.getArgs());
|
||||||
|
if (Boolean.TRUE.equals(goingAway)) {
|
||||||
|
keyguardGoingAway = true;
|
||||||
|
context.getModeStateRepository().setKeyguardGoingAway(true);
|
||||||
context.getModeStateRepository().setUnlocked();
|
context.getModeStateRepository().setUnlocked();
|
||||||
|
} else if (Boolean.FALSE.equals(goingAway)) {
|
||||||
|
keyguardGoingAway = false;
|
||||||
|
context.getModeStateRepository().setKeyguardGoingAway(false);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
@@ -92,14 +100,24 @@ public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
|||||||
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
|
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
|
||||||
int state = rawState instanceof Integer integer ? integer : 0;
|
int state = rawState instanceof Integer integer ? integer : 0;
|
||||||
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
|
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
|
||||||
|
if (keyguardGoingAway && !dozing) {
|
||||||
|
if (state == STATE_SHADE) {
|
||||||
|
keyguardGoingAway = false;
|
||||||
|
context.getModeStateRepository().setKeyguardGoingAway(false);
|
||||||
|
} else {
|
||||||
|
state = STATE_SHADE;
|
||||||
|
}
|
||||||
|
}
|
||||||
context.getModeStateRepository().setSystemUiStatusBarState(
|
context.getModeStateRepository().setSystemUiStatusBarState(
|
||||||
state,
|
state,
|
||||||
dozing,
|
dozing,
|
||||||
SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
|
SystemNotificationDisplayStyleDetector.read(systemContext(controller)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean firstBooleanArg(List<Object> args) {
|
private Boolean firstBooleanArg(List<Object> args) {
|
||||||
return args != null && !args.isEmpty() && Boolean.TRUE.equals(args.get(0));
|
return args != null && !args.isEmpty() && args.get(0) instanceof Boolean bool
|
||||||
|
? bool
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Context systemContext(Object controller) {
|
private Context systemContext(Object controller) {
|
||||||
|
|||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
package se.ajpanton.statusbartweak.runtime.render;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import androidx.appcompat.widget.AppCompatTextView;
|
||||||
|
|
||||||
|
/** Draws the outline with one colour, then restores the original span colours for the text fill. */
|
||||||
|
final class ClockOutlineTextView extends AppCompatTextView {
|
||||||
|
private int sourcePaddingLeft;
|
||||||
|
private int sourcePaddingTop;
|
||||||
|
private int sourcePaddingRight;
|
||||||
|
private int sourcePaddingBottom;
|
||||||
|
private int outlineColor;
|
||||||
|
private float outlineThicknessPx;
|
||||||
|
private ForegroundColorSpan outlineSpan;
|
||||||
|
|
||||||
|
ClockOutlineTextView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setSourcePadding(int left, int top, int right, int bottom) {
|
||||||
|
if (sourcePaddingLeft == left
|
||||||
|
&& sourcePaddingTop == top
|
||||||
|
&& sourcePaddingRight == right
|
||||||
|
&& sourcePaddingBottom == bottom) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
sourcePaddingLeft = left;
|
||||||
|
sourcePaddingTop = top;
|
||||||
|
sourcePaddingRight = right;
|
||||||
|
sourcePaddingBottom = bottom;
|
||||||
|
applyPaddedInsets();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean setOutline(ClockOutlineSpec outline) {
|
||||||
|
int color = outline != null ? outline.color : 0;
|
||||||
|
float thickness = outline != null ? outline.thicknessPx : 0f;
|
||||||
|
if (outlineColor == color && outlineThicknessPx == thickness) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
outlineColor = color;
|
||||||
|
outlineThicknessPx = thickness;
|
||||||
|
outlineSpan = new ForegroundColorSpan(color);
|
||||||
|
applyPaddedInsets();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int outlineInsetPx() {
|
||||||
|
return enabled() ? (int) Math.ceil(outlineThicknessPx) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (!enabled()) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Paint paint = getPaint();
|
||||||
|
Paint.Style style = paint.getStyle();
|
||||||
|
float strokeWidth = paint.getStrokeWidth();
|
||||||
|
int color = paint.getColor();
|
||||||
|
ArrayList<SpanState> foregrounds = replaceForegroundColors();
|
||||||
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
|
paint.setStrokeWidth(outlineThicknessPx * 2f);
|
||||||
|
paint.setColor(outlineColor);
|
||||||
|
try {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
} finally {
|
||||||
|
restoreForegroundColors(foregrounds);
|
||||||
|
paint.setStyle(style);
|
||||||
|
paint.setStrokeWidth(strokeWidth);
|
||||||
|
paint.setColor(color);
|
||||||
|
}
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<SpanState> replaceForegroundColors() {
|
||||||
|
if (!(getText() instanceof Spannable text) || text.length() == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ForegroundColorSpan[] spans = text.getSpans(0, text.length(), ForegroundColorSpan.class);
|
||||||
|
ArrayList<SpanState> states = new ArrayList<>(spans.length);
|
||||||
|
for (ForegroundColorSpan span : spans) {
|
||||||
|
states.add(new SpanState(
|
||||||
|
span,
|
||||||
|
text.getSpanStart(span),
|
||||||
|
text.getSpanEnd(span),
|
||||||
|
text.getSpanFlags(span)));
|
||||||
|
text.removeSpan(span);
|
||||||
|
}
|
||||||
|
text.setSpan(outlineSpan, 0, text.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return states;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void restoreForegroundColors(ArrayList<SpanState> states) {
|
||||||
|
if (!(getText() instanceof Spannable text) || outlineSpan == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
text.removeSpan(outlineSpan);
|
||||||
|
if (states == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (SpanState state : states) {
|
||||||
|
text.setSpan(state.span, state.start, state.end, state.flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enabled() {
|
||||||
|
return outlineThicknessPx > 0f && ((outlineColor >>> 24) & 0xff) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPaddedInsets() {
|
||||||
|
int inset = outlineInsetPx();
|
||||||
|
setPadding(
|
||||||
|
sourcePaddingLeft + inset,
|
||||||
|
sourcePaddingTop + inset,
|
||||||
|
sourcePaddingRight + inset,
|
||||||
|
sourcePaddingBottom + inset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record SpanState(ForegroundColorSpan span, int start, int end, int flags) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+40
-20
@@ -1,7 +1,7 @@
|
|||||||
package se.ajpanton.statusbartweak.runtime.render;
|
package se.ajpanton.statusbartweak.runtime.render;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -19,6 +19,10 @@ final class ClockProxyRenderer {
|
|||||||
private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>();
|
private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>();
|
||||||
|
|
||||||
void render(ViewGroup host, ClockPlacement placement) {
|
void render(ViewGroup host, ClockPlacement placement) {
|
||||||
|
render(host, placement, ClockOutlineSpec.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void render(ViewGroup host, ClockPlacement placement, ClockOutlineSpec outline) {
|
||||||
if (host == null || placement == null || placement.rows.isEmpty()) {
|
if (host == null || placement == null || placement.rows.isEmpty()) {
|
||||||
clear(host);
|
clear(host);
|
||||||
return;
|
return;
|
||||||
@@ -36,13 +40,8 @@ final class ClockProxyRenderer {
|
|||||||
views.clear();
|
views.clear();
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
while (views.size() > placement.rows.size()) {
|
|
||||||
TextView view = views.remove(views.size() - 1);
|
|
||||||
detachFromParent(view);
|
|
||||||
hostChanged = true;
|
|
||||||
}
|
|
||||||
while (views.size() < placement.rows.size()) {
|
while (views.size() < placement.rows.size()) {
|
||||||
TextView view = new TextView(host.getContext());
|
TextView view = new ClockOutlineTextView(host.getContext());
|
||||||
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||||
views.add(view);
|
views.add(view);
|
||||||
host.addView(view);
|
host.addView(view);
|
||||||
@@ -50,10 +49,13 @@ final class ClockProxyRenderer {
|
|||||||
}
|
}
|
||||||
for (int i = 0; i < placement.rows.size(); i++) {
|
for (int i = 0; i < placement.rows.size(); i++) {
|
||||||
ClockRow row = placement.rows.get(i);
|
ClockRow row = placement.rows.get(i);
|
||||||
TextView view = views.get(i);
|
ClockOutlineTextView view = (ClockOutlineTextView) views.get(i);
|
||||||
if (copyTextStyleIfChanged(placement.source, view, placement.textSizePx)) {
|
if (copyTextStyleIfChanged(placement.source, view, placement.textSizePx)) {
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
|
if (view.setOutline(outline)) {
|
||||||
|
hostChanged = true;
|
||||||
|
}
|
||||||
if (applyTextColorOverrideIfNeeded(view, placement.textColorOverride)) {
|
if (applyTextColorOverrideIfNeeded(view, placement.textColorOverride)) {
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
@@ -61,7 +63,7 @@ final class ClockProxyRenderer {
|
|||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
if (!sameTextAndColorSpans(view.getText(), row.text)) {
|
if (!sameTextAndColorSpans(view.getText(), row.text)) {
|
||||||
view.setText(row.text);
|
view.setText(row.text, TextView.BufferType.SPANNABLE);
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
if (!Objects.equals(view.getContentDescription(), placement.contentDescription)) {
|
if (!Objects.equals(view.getContentDescription(), placement.contentDescription)) {
|
||||||
@@ -76,21 +78,24 @@ final class ClockProxyRenderer {
|
|||||||
view.setAlpha(1f);
|
view.setAlpha(1f);
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
int left = placement.bounds.left + row.x;
|
int inset = view.outlineInsetPx();
|
||||||
int top = placement.bounds.top + row.y;
|
int left = placement.bounds.left + row.x - inset;
|
||||||
|
int top = placement.bounds.top + row.y - inset;
|
||||||
|
int width = row.width + 2 * inset;
|
||||||
|
int height = row.height + 2 * inset;
|
||||||
int scrollX = Math.max(0, row.clipLeft);
|
int scrollX = Math.max(0, row.clipLeft);
|
||||||
if (view.getScrollX() != scrollX) {
|
if (view.getScrollX() != scrollX) {
|
||||||
view.setScrollX(scrollX);
|
view.setScrollX(scrollX);
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
if (applyTextLayoutParams(host, view, left, top, row.width, row.height)) {
|
if (applyTextLayoutParams(host, view, left, top, width, height)) {
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
if (view.getLeft() != left
|
if (view.getLeft() != left
|
||||||
|| view.getTop() != top
|
|| view.getTop() != top
|
||||||
|| view.getRight() != left + row.width
|
|| view.getRight() != left + width
|
||||||
|| view.getBottom() != top + row.height) {
|
|| view.getBottom() != top + height) {
|
||||||
view.layout(left, top, left + row.width, top + row.height);
|
view.layout(left, top, left + width, top + height);
|
||||||
hostChanged = true;
|
hostChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,7 +216,13 @@ final class ClockProxyRenderer {
|
|||||||
target.setHorizontallyScrolling(true);
|
target.setHorizontallyScrolling(true);
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (target.getPaddingLeft() != source.getPaddingLeft()
|
if (target instanceof ClockOutlineTextView outlineTarget) {
|
||||||
|
changed |= outlineTarget.setSourcePadding(
|
||||||
|
source.getPaddingLeft(),
|
||||||
|
source.getPaddingTop(),
|
||||||
|
source.getPaddingRight(),
|
||||||
|
source.getPaddingBottom());
|
||||||
|
} else if (target.getPaddingLeft() != source.getPaddingLeft()
|
||||||
|| target.getPaddingTop() != source.getPaddingTop()
|
|| target.getPaddingTop() != source.getPaddingTop()
|
||||||
|| target.getPaddingRight() != source.getPaddingRight()
|
|| target.getPaddingRight() != source.getPaddingRight()
|
||||||
|| target.getPaddingBottom() != source.getPaddingBottom()) {
|
|| target.getPaddingBottom() != source.getPaddingBottom()) {
|
||||||
@@ -234,10 +245,7 @@ final class ClockProxyRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean applyTextColorOverrideIfNeeded(TextView view, int color) {
|
private static boolean applyTextColorOverrideIfNeeded(TextView view, int color) {
|
||||||
if (view == null || (color >>> 24) == 0) {
|
if (view == null || (color >>> 24) == 0 || view.getCurrentTextColor() == color) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (view.getCurrentTextColor() == color) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
view.setTextColor(color);
|
view.setTextColor(color);
|
||||||
@@ -276,3 +284,15 @@ final class ClockProxyRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class ClockOutlineSpec {
|
||||||
|
static final ClockOutlineSpec NONE = new ClockOutlineSpec(0, 0f);
|
||||||
|
|
||||||
|
final int color;
|
||||||
|
final float thicknessPx;
|
||||||
|
|
||||||
|
ClockOutlineSpec(int color, float thicknessPx) {
|
||||||
|
this.color = color;
|
||||||
|
this.thicknessPx = thicknessPx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -119,6 +119,17 @@ public final class OwnedIconHostManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUnlockedHostsTransform(
|
||||||
|
View root,
|
||||||
|
float alpha,
|
||||||
|
float translationX,
|
||||||
|
float translationY
|
||||||
|
) {
|
||||||
|
for (String tag : UNLOCKED_HOST_ORDER) {
|
||||||
|
setHostTransform(root, tag, alpha, translationX, translationY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void bringUnlockedHostsToFront(View root) {
|
public void bringUnlockedHostsToFront(View root) {
|
||||||
if (!(root instanceof ViewGroup parent)) {
|
if (!(root instanceof ViewGroup parent)) {
|
||||||
return;
|
return;
|
||||||
@@ -169,6 +180,31 @@ public final class OwnedIconHostManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setHostTransform(
|
||||||
|
View root,
|
||||||
|
String tag,
|
||||||
|
float alpha,
|
||||||
|
float translationX,
|
||||||
|
float translationY
|
||||||
|
) {
|
||||||
|
if (!(root instanceof ViewGroup parent) || tag == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
FrameLayout host = findDirectHost(parent, tag);
|
||||||
|
if (host == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (host.getAlpha() != alpha) {
|
||||||
|
host.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
if (host.getTranslationX() != translationX) {
|
||||||
|
host.setTranslationX(translationX);
|
||||||
|
}
|
||||||
|
if (host.getTranslationY() != translationY) {
|
||||||
|
host.setTranslationY(translationY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ArrayList<View> unlockedHosts(ViewGroup parent) {
|
private ArrayList<View> unlockedHosts(ViewGroup parent) {
|
||||||
ArrayList<View> hosts = new ArrayList<>();
|
ArrayList<View> hosts = new ArrayList<>();
|
||||||
for (String tag : UNLOCKED_HOST_ORDER) {
|
for (String tag : UNLOCKED_HOST_ORDER) {
|
||||||
|
|||||||
@@ -484,4 +484,13 @@ final class LayoutPlan {
|
|||||||
notificationPlacements,
|
notificationPlacements,
|
||||||
carrierPlacement);
|
carrierPlacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LayoutPlan withCarrierPlacement(ClockPlacement placement) {
|
||||||
|
return new LayoutPlan(
|
||||||
|
clockPlacement,
|
||||||
|
chipPlacements,
|
||||||
|
statusPlacements,
|
||||||
|
notificationPlacements,
|
||||||
|
placement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package se.ajpanton.statusbartweak.runtime.render;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.telecom.TelecomManager;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
||||||
|
|
||||||
|
/** Shared call-chip detection for source suppression and snapshot collection. */
|
||||||
|
public final class StatusChipCallSupport {
|
||||||
|
private StatusChipCallSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isInactiveCallChip(View source) {
|
||||||
|
return isCallChip(source) && !isPhoneCallActive(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isCallChip(View source) {
|
||||||
|
if (source == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||||
|
queue.add(source);
|
||||||
|
int scanned = 0;
|
||||||
|
while (!queue.isEmpty() && scanned++ < 48) {
|
||||||
|
View view = queue.removeFirst();
|
||||||
|
String className = view.getClass().getName().toLowerCase(Locale.ROOT);
|
||||||
|
String idName = ViewIdNames.idName(view).toLowerCase(Locale.ROOT);
|
||||||
|
if (className.contains("ongoingcall")
|
||||||
|
|| className.contains("ongoing_call")
|
||||||
|
|| idName.contains("ongoing_call")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (view instanceof ViewGroup childGroup) {
|
||||||
|
for (int i = 0; i < childGroup.getChildCount(); i++) {
|
||||||
|
queue.addLast(childGroup.getChildAt(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
public static boolean isPhoneCallActive(View view) {
|
||||||
|
if (view == null || view.getContext() == null
|
||||||
|
|| view.getContext().checkSelfPermission(Manifest.permission.READ_PHONE_STATE)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
TelecomManager telecomManager = view.getContext().getSystemService(TelecomManager.class);
|
||||||
|
return telecomManager != null && telecomManager.isInCall();
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+84
-10
@@ -10,7 +10,9 @@ import java.util.ArrayList;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
|
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
|
||||||
|
import se.ajpanton.statusbartweak.runtime.features.clock.ClockColorResolver;
|
||||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||||
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
||||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||||
@@ -28,6 +30,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
private final SnapshotRenderer notificationRenderer;
|
private final SnapshotRenderer notificationRenderer;
|
||||||
private final SnapshotRenderer chipRenderer;
|
private final SnapshotRenderer chipRenderer;
|
||||||
private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer();
|
private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer();
|
||||||
|
private final ClockColorResolver clockColorResolver = new ClockColorResolver();
|
||||||
private final ClockProxyRenderer carrierRenderer = new ClockProxyRenderer();
|
private final ClockProxyRenderer carrierRenderer = new ClockProxyRenderer();
|
||||||
private final UnlockedStockSourceCollector sourceCollector;
|
private final UnlockedStockSourceCollector sourceCollector;
|
||||||
private final TextVerticalOffsetScaler textOffsetScaler = new TextVerticalOffsetScaler();
|
private final TextVerticalOffsetScaler textOffsetScaler = new TextVerticalOffsetScaler();
|
||||||
@@ -64,6 +67,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
ViewGroup statusHost,
|
ViewGroup statusHost,
|
||||||
ViewGroup notificationHost,
|
ViewGroup notificationHost,
|
||||||
SceneKey scene,
|
SceneKey scene,
|
||||||
|
boolean statusBarTintTargetEnabled,
|
||||||
boolean forceStockRefresh,
|
boolean forceStockRefresh,
|
||||||
TextView externalCarrierView
|
TextView externalCarrierView
|
||||||
) {
|
) {
|
||||||
@@ -79,7 +83,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
return new RenderResult(true, true, true);
|
return new RenderResult(true, true, true);
|
||||||
}
|
}
|
||||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||||
setStatusBarTintTargetEnabled(scene.isUnlocked());
|
setStatusBarTintTargetEnabled(statusBarTintTargetEnabled);
|
||||||
layoutHostToRoot(root, clockHost);
|
layoutHostToRoot(root, clockHost);
|
||||||
layoutHostToRoot(root, carrierHost);
|
layoutHostToRoot(root, carrierHost);
|
||||||
layoutHostToRoot(root, chipHost);
|
layoutHostToRoot(root, chipHost);
|
||||||
@@ -152,7 +156,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
||||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||||
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
||||||
clockRenderer.render(clockHost, updatedClock);
|
renderClock(clockHost, updatedClock, settings);
|
||||||
return new RenderResult(false, false, false);
|
return new RenderResult(false, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +183,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
updatedClock)) {
|
updatedClock)) {
|
||||||
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
||||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||||
clockRenderer.render(clockHost, updatedClock);
|
renderClock(clockHost, updatedClock, settings);
|
||||||
return new RenderResult(false, false, false);
|
return new RenderResult(false, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,9 +238,10 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
||||||
layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons);
|
layoutPlan = withCurrentDotColors(layoutPlan, collectedIcons);
|
||||||
layoutPlan = withCurrentClockAppearance(root, layoutPlan, collectedIcons, settings);
|
layoutPlan = withCurrentClockAppearance(root, layoutPlan, collectedIcons, settings);
|
||||||
|
layoutPlan = withCurrentCarrierAppearance(layoutPlan);
|
||||||
lastLayoutPlanByRoot.put(root, layoutPlan);
|
lastLayoutPlanByRoot.put(root, layoutPlan);
|
||||||
if (clockEnabledForScene(settings, scene) && layoutPlan.clockPlacement != null) {
|
if (clockEnabledForScene(settings, scene) && layoutPlan.clockPlacement != null) {
|
||||||
clockRenderer.render(clockHost, layoutPlan.clockPlacement);
|
renderClock(clockHost, layoutPlan.clockPlacement, settings);
|
||||||
} else {
|
} else {
|
||||||
clockRenderer.clear(clockHost);
|
clockRenderer.clear(clockHost);
|
||||||
}
|
}
|
||||||
@@ -312,7 +317,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
layoutPlan = updatedPlan;
|
layoutPlan = updatedPlan;
|
||||||
}
|
}
|
||||||
if (settings.clockEnabledUnlocked && layoutPlan.clockPlacement != null) {
|
if (settings.clockEnabledUnlocked && layoutPlan.clockPlacement != null) {
|
||||||
clockRenderer.render(clockHost, layoutPlan.clockPlacement);
|
renderClock(clockHost, layoutPlan.clockPlacement, settings);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +351,8 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
public boolean refreshUnlockedClockText(
|
public boolean refreshUnlockedClockText(
|
||||||
View root,
|
View root,
|
||||||
ViewGroup clockHost,
|
ViewGroup clockHost,
|
||||||
SceneKey scene
|
SceneKey scene,
|
||||||
|
boolean statusBarTintTargetEnabled
|
||||||
) {
|
) {
|
||||||
if (root == null
|
if (root == null
|
||||||
|| clockHost == null
|
|| clockHost == null
|
||||||
@@ -360,7 +366,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||||
setStatusBarTintTargetEnabled(scene.isUnlocked());
|
setStatusBarTintTargetEnabled(statusBarTintTargetEnabled);
|
||||||
if (!clockEnabledForScene(settings, scene)) {
|
if (!clockEnabledForScene(settings, scene)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -377,7 +383,7 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock);
|
LayoutPlan updatedPlan = layoutPlan.withClockPlacement(updatedClock);
|
||||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||||
lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings));
|
lastClockAppearanceByRoot.put(root, clockAppearanceSignature(updatedClock, referenceColor, settings));
|
||||||
clockRenderer.render(clockHost, updatedClock);
|
renderClock(clockHost, updatedClock, settings);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,7 +470,9 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
NotificationDisplayStyle style = scene.notificationDisplayStyle();
|
NotificationDisplayStyle style = scene.notificationDisplayStyle();
|
||||||
return settings.layoutNotifEnabledAod
|
return settings.layoutNotifEnabledAod
|
||||||
&& (style == NotificationDisplayStyle.ICONS
|
&& (style == NotificationDisplayStyle.ICONS
|
||||||
|| style == NotificationDisplayStyle.DOT);
|
|| style == NotificationDisplayStyle.DOT
|
||||||
|
|| (style == NotificationDisplayStyle.UNKNOWN
|
||||||
|
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings)));
|
||||||
}
|
}
|
||||||
return settings.layoutNotifEnabledUnlocked;
|
return settings.layoutNotifEnabledUnlocked;
|
||||||
}
|
}
|
||||||
@@ -592,6 +600,24 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
return plan.withClockPlacement(updatedClock);
|
return plan.withClockPlacement(updatedClock);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LayoutPlan withCurrentCarrierAppearance(LayoutPlan plan) {
|
||||||
|
int targetColor = statusBarTintTargetColor();
|
||||||
|
if (plan == null
|
||||||
|
|| plan.carrierPlacement == null
|
||||||
|
|| !hasAlpha(targetColor)
|
||||||
|
|| plan.carrierPlacement.textColorOverride == targetColor) {
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
|
ClockPlacement previous = plan.carrierPlacement;
|
||||||
|
return plan.withCarrierPlacement(new ClockPlacement(
|
||||||
|
previous.source,
|
||||||
|
previous.contentDescription,
|
||||||
|
previous.rows,
|
||||||
|
previous.bounds,
|
||||||
|
targetColor,
|
||||||
|
previous.textSizePx));
|
||||||
|
}
|
||||||
|
|
||||||
private int clockReferenceColor(ClockPlacement placement, CollectedIcons icons) {
|
private int clockReferenceColor(ClockPlacement placement, CollectedIcons icons) {
|
||||||
int targetColor = statusBarTintTargetColor();
|
int targetColor = statusBarTintTargetColor();
|
||||||
if (hasAlpha(targetColor)) {
|
if (hasAlpha(targetColor)) {
|
||||||
@@ -630,6 +656,51 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : android.graphics.Color.TRANSPARENT;
|
return statusBarTintTargetEnabled ? StatusBarTintTarget.color() : android.graphics.Color.TRANSPARENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderClock(
|
||||||
|
ViewGroup clockHost,
|
||||||
|
ClockPlacement placement,
|
||||||
|
SbtSettings settings
|
||||||
|
) {
|
||||||
|
clockRenderer.render(clockHost, placement, clockOutlineSpec(clockHost, placement, settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ClockOutlineSpec clockOutlineSpec(
|
||||||
|
ViewGroup host,
|
||||||
|
ClockPlacement placement,
|
||||||
|
SbtSettings settings
|
||||||
|
) {
|
||||||
|
if (host == null
|
||||||
|
|| placement == null
|
||||||
|
|| settings == null
|
||||||
|
|| !settings.clockOutlineEnabled
|
||||||
|
|| settings.clockOutlineThicknessDp <= 0
|
||||||
|
|| settings.clockOutlineColor == null
|
||||||
|
|| settings.clockOutlineColor.trim().isEmpty()) {
|
||||||
|
return ClockOutlineSpec.NONE;
|
||||||
|
}
|
||||||
|
int referenceColor = hasAlpha(placement.textColorOverride)
|
||||||
|
? placement.textColorOverride
|
||||||
|
: placement.source != null
|
||||||
|
? placement.source.getCurrentTextColor()
|
||||||
|
: android.graphics.Color.WHITE;
|
||||||
|
try {
|
||||||
|
int color = clockColorResolver.resolveColor(
|
||||||
|
host.getContext(),
|
||||||
|
settings.clockOutlineColor,
|
||||||
|
referenceColor,
|
||||||
|
android.graphics.Color.TRANSPARENT,
|
||||||
|
ClockColorResolver.Options.clock());
|
||||||
|
if (!hasAlpha(color)) {
|
||||||
|
return ClockOutlineSpec.NONE;
|
||||||
|
}
|
||||||
|
return new ClockOutlineSpec(
|
||||||
|
color,
|
||||||
|
settings.clockOutlineThicknessDp * host.getResources().getDisplayMetrics().density);
|
||||||
|
} catch (IllegalArgumentException ignored) {
|
||||||
|
return ClockOutlineSpec.NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean hasAlpha(int color) {
|
private boolean hasAlpha(int color) {
|
||||||
return (color >>> 24) != 0;
|
return (color >>> 24) != 0;
|
||||||
}
|
}
|
||||||
@@ -665,7 +736,10 @@ public final class UnlockedIconSnapshotRenderController {
|
|||||||
settings.clockShowSeconds,
|
settings.clockShowSeconds,
|
||||||
settings.clockShowDatePrefix,
|
settings.clockShowDatePrefix,
|
||||||
settings.clockCustomFormatEnabled,
|
settings.clockCustomFormatEnabled,
|
||||||
settings.clockCustomFormat);
|
settings.clockCustomFormat,
|
||||||
|
settings.clockOutlineEnabled,
|
||||||
|
settings.clockOutlineThicknessDp,
|
||||||
|
settings.clockOutlineColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clearAll() {
|
public void clearAll() {
|
||||||
|
|||||||
+4
-1
@@ -11,6 +11,7 @@ import java.util.Locale;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.R;
|
import se.ajpanton.statusbartweak.R;
|
||||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
||||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||||
@@ -856,7 +857,9 @@ final class UnlockedLayoutPlanner {
|
|||||||
if (style == NotificationDisplayStyle.DOT) {
|
if (style == NotificationDisplayStyle.DOT) {
|
||||||
return settings.layoutNotifEnabledAod ? 1 : 0;
|
return settings.layoutNotifEnabledAod ? 1 : 0;
|
||||||
}
|
}
|
||||||
if (style != NotificationDisplayStyle.ICONS) {
|
if (style != NotificationDisplayStyle.ICONS
|
||||||
|
&& !(style == NotificationDisplayStyle.UNKNOWN
|
||||||
|
&& SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
|
return settings.layoutNotifEnabledAod ? settings.layoutNotifAodCount : 0;
|
||||||
|
|||||||
+46
-11
@@ -18,6 +18,7 @@ import java.util.Locale;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.WeakHashMap;
|
import java.util.WeakHashMap;
|
||||||
|
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
|
import se.ajpanton.statusbartweak.runtime.NotificationKeyReflection;
|
||||||
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
||||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||||
@@ -63,7 +64,7 @@ final class UnlockedStockSourceCollector {
|
|||||||
ArrayList<SnapshotSource> statusChips = new ArrayList<>();
|
ArrayList<SnapshotSource> statusChips = new ArrayList<>();
|
||||||
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
|
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
|
||||||
if (scene != null && scene.isAod()) {
|
if (scene != null && scene.isAod()) {
|
||||||
collectAodStatusSnapshotViews(root, statusIcons, scene);
|
collectAodStatusSnapshotViews(root, statusIcons, scene, settings);
|
||||||
}
|
}
|
||||||
if (statusIcons.isEmpty()) {
|
if (statusIcons.isEmpty()) {
|
||||||
if (anchors.statusRoot != null) {
|
if (anchors.statusRoot != null) {
|
||||||
@@ -82,8 +83,7 @@ final class UnlockedStockSourceCollector {
|
|||||||
selectDualSimStatusSignals(statusIcons, settings);
|
selectDualSimStatusSignals(statusIcons, settings);
|
||||||
normalizeStatusIconSourceBounds(root, statusIcons);
|
normalizeStatusIconSourceBounds(root, statusIcons);
|
||||||
if (scene != null && scene.isAod()) {
|
if (scene != null && scene.isAod()) {
|
||||||
if (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
if (isAodIconNotificationStyle(scene, settings)) {
|
||||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT) {
|
|
||||||
notificationIcons.addAll(
|
notificationIcons.addAll(
|
||||||
LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root));
|
LockscreenCardsNotificationIconSourceStore.snapshotSourcesForAodIconsRoot(root));
|
||||||
if (notificationIcons.isEmpty()
|
if (notificationIcons.isEmpty()
|
||||||
@@ -92,6 +92,13 @@ final class UnlockedStockSourceCollector {
|
|||||||
LockscreenCardsNotificationIconSourceStore
|
LockscreenCardsNotificationIconSourceStore
|
||||||
.snapshotAodNotificationPackageSources(root));
|
.snapshotAodNotificationPackageSources(root));
|
||||||
}
|
}
|
||||||
|
if (notificationIcons.isEmpty()
|
||||||
|
&& SystemUiCapabilities.supportsExperimentalGenericAodStatusbarRoot(settings)) {
|
||||||
|
View notificationRoot = anchors != null && anchors.notificationRoot != null
|
||||||
|
? anchors.notificationRoot
|
||||||
|
: root;
|
||||||
|
collectNotificationSnapshotViews(notificationRoot, notificationIcons);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (anchors.notificationRoot != null) {
|
} else if (anchors.notificationRoot != null) {
|
||||||
collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons);
|
collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons);
|
||||||
@@ -111,14 +118,14 @@ final class UnlockedStockSourceCollector {
|
|||||||
private void collectAodStatusSnapshotViews(
|
private void collectAodStatusSnapshotViews(
|
||||||
View root,
|
View root,
|
||||||
ArrayList<SnapshotSource> out,
|
ArrayList<SnapshotSource> out,
|
||||||
SceneKey scene
|
SceneKey scene,
|
||||||
|
SbtSettings settings
|
||||||
) {
|
) {
|
||||||
if (root == null || out == null) {
|
if (root == null || out == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
boolean iconMode = scene != null
|
boolean iconMode = scene != null
|
||||||
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
&& isAodIconNotificationStyle(scene, settings);
|
||||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
|
|
||||||
View excludedNotificationContainer = iconMode
|
View excludedNotificationContainer = iconMode
|
||||||
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
|
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
|
||||||
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
|
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
|
||||||
@@ -129,6 +136,17 @@ final class UnlockedStockSourceCollector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isAodIconNotificationStyle(SceneKey scene, SbtSettings settings) {
|
||||||
|
if (scene == null || !scene.isAod()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
NotificationDisplayStyle style = scene.notificationDisplayStyle();
|
||||||
|
return style == NotificationDisplayStyle.ICONS
|
||||||
|
|| style == NotificationDisplayStyle.DOT
|
||||||
|
|| (style == NotificationDisplayStyle.UNKNOWN
|
||||||
|
&& SystemUiCapabilities.treatsUnknownAodNotificationStyleAsIcons(settings));
|
||||||
|
}
|
||||||
|
|
||||||
private String reflectString(View view, String methodName, String... fieldNames) {
|
private String reflectString(View view, String methodName, String... fieldNames) {
|
||||||
Object methodValue = ReflectionSupport.invokeMethod(view, methodName);
|
Object methodValue = ReflectionSupport.invokeMethod(view, methodName);
|
||||||
if (methodValue != null) {
|
if (methodValue != null) {
|
||||||
@@ -193,7 +211,7 @@ final class UnlockedStockSourceCollector {
|
|||||||
statusSourceSignature = 31 * statusSourceSignature + batteryChargingSignature(root);
|
statusSourceSignature = 31 * statusSourceSignature + batteryChargingSignature(root);
|
||||||
if (scene != null && scene.isAod()) {
|
if (scene != null && scene.isAod()) {
|
||||||
statusSourceSignature = 31 * statusSourceSignature
|
statusSourceSignature = 31 * statusSourceSignature
|
||||||
+ aodLayoutAnchorSignature(root, anchors, scene);
|
+ aodLayoutAnchorSignature(root, anchors, scene, settings);
|
||||||
}
|
}
|
||||||
int notificationSourceSignature = sourceLayoutListSignature(
|
int notificationSourceSignature = sourceLayoutListSignature(
|
||||||
previousIcons != null ? previousIcons.notificationIcons : null);
|
previousIcons != null ? previousIcons.notificationIcons : null);
|
||||||
@@ -238,13 +256,17 @@ final class UnlockedStockSourceCollector {
|
|||||||
chipSourceSignature);
|
chipSourceSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private int aodLayoutAnchorSignature(View root, RootAnchors anchors, SceneKey scene) {
|
private int aodLayoutAnchorSignature(
|
||||||
|
View root,
|
||||||
|
RootAnchors anchors,
|
||||||
|
SceneKey scene,
|
||||||
|
SbtSettings settings
|
||||||
|
) {
|
||||||
if (root == null) {
|
if (root == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
boolean iconMode = scene != null
|
boolean iconMode = scene != null
|
||||||
&& (scene.notificationDisplayStyle() == NotificationDisplayStyle.ICONS
|
&& isAodIconNotificationStyle(scene, settings);
|
||||||
|| scene.notificationDisplayStyle() == NotificationDisplayStyle.DOT);
|
|
||||||
View notificationContainer = iconMode
|
View notificationContainer = iconMode
|
||||||
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
|
? LockscreenCardsNotificationIconSourceStore.sourceContainerForAodIconsRoot(root)
|
||||||
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
|
: LockscreenCardsNotificationIconSourceStore.sourceContainerForRoot(root);
|
||||||
@@ -317,7 +339,7 @@ final class UnlockedStockSourceCollector {
|
|||||||
}
|
}
|
||||||
if (clockView == null
|
if (clockView == null
|
||||||
&& view instanceof TextView textView
|
&& view instanceof TextView textView
|
||||||
&& view.getClass().getName().contains("QSClockIndicatorView")) {
|
&& isStatusbarClockView(textView)) {
|
||||||
clockView = textView;
|
clockView = textView;
|
||||||
}
|
}
|
||||||
if (statusEndContent == null && idName.equals("status_bar_end_side_content")) {
|
if (statusEndContent == null && idName.equals("status_bar_end_side_content")) {
|
||||||
@@ -370,6 +392,12 @@ final class UnlockedStockSourceCollector {
|
|||||||
carrierView);
|
carrierView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isStatusbarClockView(TextView view) {
|
||||||
|
String className = view.getClass().getName();
|
||||||
|
return className.contains("QSClockIndicatorView")
|
||||||
|
|| className.equals("com.android.systemui.statusbar.policy.Clock");
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isCarrierTextView(TextView view) {
|
private boolean isCarrierTextView(TextView view) {
|
||||||
if (view == null) {
|
if (view == null) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1192,6 +1220,7 @@ final class UnlockedStockSourceCollector {
|
|||||||
|| !view.isAttachedToWindow()
|
|| !view.isAttachedToWindow()
|
||||||
|| !ViewGeometry.isDescendantOf(view, root)
|
|| !ViewGeometry.isDescendantOf(view, root)
|
||||||
|| ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)
|
|| ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)
|
||||||
|
|| StatusChipCallSupport.isInactiveCallChip(view)
|
||||||
|| !isStatusChipCandidate(view)
|
|| !isStatusChipCandidate(view)
|
||||||
|| !hasRenderableStatusChipContent(view)) {
|
|| !hasRenderableStatusChipContent(view)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1217,6 +1246,9 @@ final class UnlockedStockSourceCollector {
|
|||||||
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
|
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (StatusChipCallSupport.isInactiveCallChip(view)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||||
if (width <= 0 || height <= 0 || root == null || width >= root.getWidth() / 2) {
|
if (width <= 0 || height <= 0 || root == null || width >= root.getWidth() / 2) {
|
||||||
@@ -1308,6 +1340,9 @@ final class UnlockedStockSourceCollector {
|
|||||||
if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) {
|
if (!allowHidden && view.getAlpha() <= 0f && !hasRenderableStatusChipContent(view)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (StatusChipCallSupport.isInactiveCallChip(view)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||||
if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) {
|
if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import java.util.regex.Pattern;
|
|||||||
|
|
||||||
public final class AppIconRules {
|
public final class AppIconRules {
|
||||||
private static final Pattern PACKAGE_NAME_PATTERN =
|
private static final Pattern PACKAGE_NAME_PATTERN =
|
||||||
Pattern.compile("^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$");
|
Pattern.compile("^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$");
|
||||||
|
|
||||||
public static final String PREF_KEY_BLOCKED_MODES = "app_icon_blocked_modes";
|
public static final String PREF_KEY_BLOCKED_MODES = "app_icon_blocked_modes";
|
||||||
public static final String PREF_KEY_EXCEPTION_RULES = "app_icon_exception_rules";
|
public static final String PREF_KEY_EXCEPTION_RULES = "app_icon_exception_rules";
|
||||||
|
|||||||
@@ -107,10 +107,6 @@ public final class BatteryBarGeometry {
|
|||||||
return position + "|" + alignment + "|" + thicknessDp + "|" + edgeOffsetDp + "|" + curvedGeometryMode;
|
return position + "|" + alignment + "|" + thicknessDp + "|" + edgeOffsetDp + "|" + curvedGeometryMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String signature() {
|
|
||||||
return encode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String enabledKey(Scenario scenario) {
|
public static String enabledKey(Scenario scenario) {
|
||||||
return SbtSettings.batteryBarGeometryOverrideEnabledKey(scenario.id);
|
return SbtSettings.batteryBarGeometryOverrideEnabledKey(scenario.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ public final class SbtDefaults {
|
|||||||
public static final boolean CLOCK_SHOW_DATE_PREFIX_DEFAULT = false;
|
public static final boolean CLOCK_SHOW_DATE_PREFIX_DEFAULT = false;
|
||||||
public static final boolean CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
public static final boolean CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
||||||
public static final String CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
public static final String CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
||||||
|
public static final boolean CLOCK_OUTLINE_ENABLED_DEFAULT = false;
|
||||||
|
public static final int CLOCK_OUTLINE_THICKNESS_DP_DEFAULT = 1;
|
||||||
|
public static final int CLOCK_OUTLINE_THICKNESS_DP_MIN = 1;
|
||||||
|
public static final int CLOCK_OUTLINE_THICKNESS_DP_MAX = 6;
|
||||||
|
public static final String CLOCK_OUTLINE_COLOR_DEFAULT = "#000 ; #FFF";
|
||||||
public static final boolean DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
public static final boolean DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
||||||
public static final String DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
public static final String DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
||||||
public static final boolean DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
public static final boolean DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
||||||
@@ -99,6 +104,7 @@ public final class SbtDefaults {
|
|||||||
public static final boolean DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT = false;
|
public static final boolean DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT = false;
|
||||||
public static final boolean DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT = false;
|
public static final boolean DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT = false;
|
||||||
public static final boolean DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT = false;
|
public static final boolean DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT = false;
|
||||||
|
public static final boolean DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT = false;
|
||||||
|
|
||||||
public static final int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0;
|
public static final int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0;
|
||||||
public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0;
|
public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -155,6 +155,15 @@ public class SbtSettingsProvider extends ContentProvider {
|
|||||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT));
|
prefs.getBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT));
|
||||||
out.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
out.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||||
prefs.getString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT));
|
prefs.getString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT));
|
||||||
|
out.putBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
|
||||||
|
prefs.getBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_ENABLED_DEFAULT));
|
||||||
|
out.putInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
|
||||||
|
prefs.getInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_DEFAULT));
|
||||||
|
out.putString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
|
||||||
|
prefs.getString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_COLOR_DEFAULT));
|
||||||
out.putBoolean(SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
out.putBoolean(SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||||
prefs.getBoolean(
|
prefs.getBoolean(
|
||||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||||
@@ -195,6 +204,9 @@ public class SbtSettingsProvider extends ContentProvider {
|
|||||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
||||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
||||||
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT));
|
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT));
|
||||||
|
out.putBoolean(SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
|
||||||
|
prefs.getBoolean(SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
|
||||||
|
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT));
|
||||||
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||||
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||||
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT));
|
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT));
|
||||||
|
|||||||
@@ -839,14 +839,14 @@ public final class BatteryBarFragment extends Fragment {
|
|||||||
final boolean[] updating = new boolean[]{false};
|
final boolean[] updating = new boolean[]{false};
|
||||||
if (minus != null) {
|
if (minus != null) {
|
||||||
minus.setOnClickListener(v -> {
|
minus.setOnClickListener(v -> {
|
||||||
int current = scenarioNumberValue(input, controls, prefs, edgeOffset);
|
int current = scenarioNumberValue(input, prefs, edgeOffset);
|
||||||
setScenarioNumberValue(input, current - 1, edgeOffset, updating);
|
setScenarioNumberValue(input, current - 1, edgeOffset, updating);
|
||||||
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (plus != null) {
|
if (plus != null) {
|
||||||
plus.setOnClickListener(v -> {
|
plus.setOnClickListener(v -> {
|
||||||
int current = scenarioNumberValue(input, controls, prefs, edgeOffset);
|
int current = scenarioNumberValue(input, prefs, edgeOffset);
|
||||||
setScenarioNumberValue(input, current + 1, edgeOffset, updating);
|
setScenarioNumberValue(input, current + 1, edgeOffset, updating);
|
||||||
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
||||||
});
|
});
|
||||||
@@ -870,7 +870,7 @@ public final class BatteryBarFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
setScenarioNumberValue(
|
setScenarioNumberValue(
|
||||||
input,
|
input,
|
||||||
scenarioNumberValue(input, controls, prefs, edgeOffset),
|
scenarioNumberValue(input, prefs, edgeOffset),
|
||||||
edgeOffset,
|
edgeOffset,
|
||||||
updating);
|
updating);
|
||||||
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
||||||
@@ -882,7 +882,7 @@ public final class BatteryBarFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
setScenarioNumberValue(
|
setScenarioNumberValue(
|
||||||
input,
|
input,
|
||||||
scenarioNumberValue(input, controls, prefs, edgeOffset),
|
scenarioNumberValue(input, prefs, edgeOffset),
|
||||||
edgeOffset,
|
edgeOffset,
|
||||||
updating);
|
updating);
|
||||||
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
persistScenarioGeometry(prefs, scenarioGroup, geometryFromControls(controls, prefs));
|
||||||
@@ -891,7 +891,6 @@ public final class BatteryBarFragment extends Fragment {
|
|||||||
|
|
||||||
private int scenarioNumberValue(
|
private int scenarioNumberValue(
|
||||||
EditText input,
|
EditText input,
|
||||||
GeometryControls controls,
|
|
||||||
SharedPreferences prefs,
|
SharedPreferences prefs,
|
||||||
boolean edgeOffset
|
boolean edgeOffset
|
||||||
) {
|
) {
|
||||||
@@ -950,8 +949,8 @@ public final class BatteryBarFragment extends Fragment {
|
|||||||
return new BatteryBarGeometry(
|
return new BatteryBarGeometry(
|
||||||
positionValue(checkedPosition),
|
positionValue(checkedPosition),
|
||||||
alignment,
|
alignment,
|
||||||
scenarioNumberValue(controls.thicknessInput, controls, prefs, false),
|
scenarioNumberValue(controls.thicknessInput, prefs, false),
|
||||||
scenarioNumberValue(controls.edgeOffsetInput, controls, prefs, true),
|
scenarioNumberValue(controls.edgeOffsetInput, prefs, true),
|
||||||
curvedGeometryValue(checkedPosition));
|
curvedGeometryValue(checkedPosition));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import com.google.android.material.button.MaterialButton;
|
import com.google.android.material.button.MaterialButton;
|
||||||
import com.google.android.material.card.MaterialCardView;
|
import com.google.android.material.card.MaterialCardView;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import com.google.android.material.slider.Slider;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -93,6 +94,11 @@ public final class ClockFragment extends Fragment {
|
|||||||
EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input);
|
EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input);
|
||||||
ClockPatternPreviewView customFormatPreview =
|
ClockPatternPreviewView customFormatPreview =
|
||||||
root.findViewById(R.id.clock_custom_format_preview);
|
root.findViewById(R.id.clock_custom_format_preview);
|
||||||
|
CheckBox clockOutlineEnabled = root.findViewById(R.id.clock_outline_enabled);
|
||||||
|
View clockOutlineContainer = root.findViewById(R.id.clock_outline_container);
|
||||||
|
Slider clockOutlineThickness = root.findViewById(R.id.clock_outline_thickness);
|
||||||
|
TextView clockOutlineThicknessLabel = root.findViewById(R.id.clock_outline_thickness_value);
|
||||||
|
EditText clockOutlineColor = root.findViewById(R.id.clock_outline_color);
|
||||||
MaterialCardView drawerClockCustomFormatCard =
|
MaterialCardView drawerClockCustomFormatCard =
|
||||||
root.findViewById(R.id.drawer_clock_custom_format_card);
|
root.findViewById(R.id.drawer_clock_custom_format_card);
|
||||||
CheckBox drawerClockCustomFormatEnabled =
|
CheckBox drawerClockCustomFormatEnabled =
|
||||||
@@ -129,6 +135,12 @@ public final class ClockFragment extends Fragment {
|
|||||||
String customFormatValue = prefs.getString(
|
String customFormatValue = prefs.getString(
|
||||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT);
|
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT);
|
||||||
|
int clockOutlineThicknessDp = prefs.getInt(
|
||||||
|
SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_DEFAULT);
|
||||||
|
String clockOutlineColorValue = prefs.getString(
|
||||||
|
SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_COLOR_DEFAULT);
|
||||||
boolean drawerClockCustomFormatEnabledValue = prefs.getBoolean(
|
boolean drawerClockCustomFormatEnabledValue = prefs.getBoolean(
|
||||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT);
|
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT);
|
||||||
@@ -160,6 +172,15 @@ public final class ClockFragment extends Fragment {
|
|||||||
customFormatValue,
|
customFormatValue,
|
||||||
18f,
|
18f,
|
||||||
true);
|
true);
|
||||||
|
bindClockOutline(
|
||||||
|
prefs,
|
||||||
|
clockOutlineEnabled,
|
||||||
|
clockOutlineContainer,
|
||||||
|
clockOutlineThickness,
|
||||||
|
clockOutlineThicknessLabel,
|
||||||
|
clockOutlineThicknessDp,
|
||||||
|
clockOutlineColor,
|
||||||
|
clockOutlineColorValue);
|
||||||
bindCustomFormat(
|
bindCustomFormat(
|
||||||
prefs,
|
prefs,
|
||||||
drawerClockCustomFormatCard,
|
drawerClockCustomFormatCard,
|
||||||
@@ -218,6 +239,69 @@ public final class ClockFragment extends Fragment {
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bindClockOutline(
|
||||||
|
SharedPreferences prefs,
|
||||||
|
CheckBox enabledView,
|
||||||
|
View container,
|
||||||
|
Slider thicknessView,
|
||||||
|
TextView thicknessValueView,
|
||||||
|
int thicknessDp,
|
||||||
|
EditText colorView,
|
||||||
|
String colorExpression
|
||||||
|
) {
|
||||||
|
if (enabledView == null || thicknessView == null || colorView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int clampedThickness = Math.max(
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_MIN,
|
||||||
|
Math.min(thicknessDp, SbtDefaults.CLOCK_OUTLINE_THICKNESS_DP_MAX));
|
||||||
|
enabledView.setChecked(prefs.getBoolean(
|
||||||
|
SbtSettings.KEY_CLOCK_OUTLINE_ENABLED,
|
||||||
|
SbtDefaults.CLOCK_OUTLINE_ENABLED_DEFAULT));
|
||||||
|
thicknessView.setValue(clampedThickness);
|
||||||
|
updateClockOutlineThicknessLabel(thicknessValueView, clampedThickness);
|
||||||
|
colorView.setText(colorExpression != null ? colorExpression : "");
|
||||||
|
updateCustomFormatVisibility(container, enabledView.isChecked());
|
||||||
|
enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||||
|
updateCustomFormatVisibility(container, isChecked);
|
||||||
|
prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_OUTLINE_ENABLED, isChecked).apply();
|
||||||
|
SbtSettings.ensureReadable(requireContext());
|
||||||
|
});
|
||||||
|
thicknessView.addOnChangeListener((slider, value, fromUser) -> {
|
||||||
|
int thickness = Math.round(value);
|
||||||
|
updateClockOutlineThicknessLabel(thicknessValueView, thickness);
|
||||||
|
if (!fromUser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefs.edit().putInt(SbtSettings.KEY_CLOCK_OUTLINE_THICKNESS_DP, thickness).apply();
|
||||||
|
SbtSettings.ensureReadable(requireContext());
|
||||||
|
});
|
||||||
|
colorView.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
prefs.edit()
|
||||||
|
.putString(SbtSettings.KEY_CLOCK_OUTLINE_COLOR,
|
||||||
|
s != null ? s.toString() : "")
|
||||||
|
.apply();
|
||||||
|
SbtSettings.ensureReadable(requireContext());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClockOutlineThicknessLabel(TextView view, int thicknessDp) {
|
||||||
|
if (view != null) {
|
||||||
|
view.setText(getString(R.string.clock_outline_thickness_value, thicknessDp));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateCustomFormatVisibility(View customFormatContainer, boolean enabled) {
|
private void updateCustomFormatVisibility(View customFormatContainer, boolean enabled) {
|
||||||
if (customFormatContainer == null) {
|
if (customFormatContainer == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import se.ajpanton.statusbartweak.R;
|
import se.ajpanton.statusbartweak.R;
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.shell.debug.DebugNotifications;
|
import se.ajpanton.statusbartweak.shell.debug.DebugNotifications;
|
||||||
import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport;
|
import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport;
|
||||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||||
@@ -64,6 +65,8 @@ public class IconsDebugFragment extends Fragment {
|
|||||||
SwitchMaterial visualizeAodStockContainersSwitch =
|
SwitchMaterial visualizeAodStockContainersSwitch =
|
||||||
root.findViewById(R.id.debug_visualize_aod_stock_containers_switch);
|
root.findViewById(R.id.debug_visualize_aod_stock_containers_switch);
|
||||||
MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button);
|
MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button);
|
||||||
|
SwitchMaterial experimentalAodStatusbarRootSwitch =
|
||||||
|
root.findViewById(R.id.debug_experimental_aod_statusbar_root_switch);
|
||||||
SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch);
|
SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch);
|
||||||
TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified);
|
TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified);
|
||||||
LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row);
|
LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row);
|
||||||
@@ -137,6 +140,18 @@ public class IconsDebugFragment extends Fragment {
|
|||||||
SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
SbtSettings.KEY_DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS,
|
||||||
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT,
|
SbtDefaults.DEBUG_VISUALIZE_AOD_STOCK_CONTAINERS_DEFAULT,
|
||||||
true);
|
true);
|
||||||
|
ViewTreeSupport.bindBooleanPreference(
|
||||||
|
requireContext(),
|
||||||
|
prefs,
|
||||||
|
experimentalAodStatusbarRootSwitch,
|
||||||
|
SbtSettings.KEY_DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT,
|
||||||
|
SbtDefaults.DEBUG_EXPERIMENTAL_AOD_STATUSBAR_ROOT_DEFAULT,
|
||||||
|
true);
|
||||||
|
if (SystemUiCapabilities.isSamsungOneUi()
|
||||||
|
&& experimentalAodStatusbarRootSwitch != null) {
|
||||||
|
experimentalAodStatusbarRootSwitch.setChecked(false);
|
||||||
|
experimentalAodStatusbarRootSwitch.setEnabled(false);
|
||||||
|
}
|
||||||
bindRestartSystemUiButton(restartSystemUiButton);
|
bindRestartSystemUiButton(restartSystemUiButton);
|
||||||
updateVisualizerSwitchAvailability(
|
updateVisualizerSwitchAvailability(
|
||||||
prefs,
|
prefs,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public final class LayoutFragment extends Fragment {
|
|||||||
View root = inflater.inflate(R.layout.fragment_layout, container, false);
|
View root = inflater.inflate(R.layout.fragment_layout, container, false);
|
||||||
SharedPreferences prefs = requireContext()
|
SharedPreferences prefs = requireContext()
|
||||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||||
hideUnlockedSubpageObsoleteControls(root);
|
configureUnlockedOnlyModeControls(root);
|
||||||
reorderItemCards(root, prefs);
|
reorderItemCards(root, prefs);
|
||||||
|
|
||||||
setupPositionSection(root,
|
setupPositionSection(root,
|
||||||
@@ -508,7 +508,7 @@ public final class LayoutFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void hideUnlockedSubpageObsoleteControls(View root) {
|
private void configureUnlockedOnlyModeControls(View root) {
|
||||||
hide(root, R.id.clock_position_mode_lockscreen);
|
hide(root, R.id.clock_position_mode_lockscreen);
|
||||||
hide(root, R.id.notif_position_mode_aod);
|
hide(root, R.id.notif_position_mode_aod);
|
||||||
hide(root, R.id.notif_position_mode_lockscreen);
|
hide(root, R.id.notif_position_mode_lockscreen);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import android.widget.TextView;
|
|||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import se.ajpanton.statusbartweak.R;
|
import se.ajpanton.statusbartweak.R;
|
||||||
|
import se.ajpanton.statusbartweak.platform.SystemUiCapabilities;
|
||||||
import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings;
|
import se.ajpanton.statusbartweak.shell.debug.NotificationDisplayStyleSettings;
|
||||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||||
|
|
||||||
@@ -40,6 +41,10 @@ final class LockedNotificationModeUi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
group.setOnCheckedChangeListener(null);
|
group.setOnCheckedChangeListener(null);
|
||||||
|
if (!SystemUiCapabilities.supportsSamsungNotificationDisplayStyles()) {
|
||||||
|
bindIconsOnly(context, prefs, group, hint);
|
||||||
|
return;
|
||||||
|
}
|
||||||
RadioButton cards = button(context, R.string.debug_notification_style_cards, MODE_CARDS);
|
RadioButton cards = button(context, R.string.debug_notification_style_cards, MODE_CARDS);
|
||||||
RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
|
RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
|
||||||
RadioButton dot = button(context, R.string.debug_notification_style_dot, MODE_DOT);
|
RadioButton dot = button(context, R.string.debug_notification_style_dot, MODE_DOT);
|
||||||
@@ -76,7 +81,31 @@ final class LockedNotificationModeUi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void bindIconsOnly(
|
||||||
|
Context context,
|
||||||
|
SharedPreferences prefs,
|
||||||
|
RadioGroup group,
|
||||||
|
TextView hint
|
||||||
|
) {
|
||||||
|
RadioButton icons = button(context, R.string.debug_notification_style_icons, MODE_ICONS);
|
||||||
|
group.removeAllViews();
|
||||||
|
group.addView(icons);
|
||||||
|
group.check(icons.getId());
|
||||||
|
group.setEnabled(false);
|
||||||
|
icons.setEnabled(false);
|
||||||
|
if (hint != null) {
|
||||||
|
hint.setText(R.string.layout_locked_notifications_hint_icons_only);
|
||||||
|
}
|
||||||
|
if (!MODE_ICONS.equals(prefs.getString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_ICONS))) {
|
||||||
|
prefs.edit().putString(SbtSettings.KEY_LOCKED_NOTIFICATION_MODE, MODE_ICONS).apply();
|
||||||
|
SbtSettings.ensureReadable(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static String currentMode(Context context) {
|
static String currentMode(Context context) {
|
||||||
|
if (!SystemUiCapabilities.supportsSamsungNotificationDisplayStyles()) {
|
||||||
|
return MODE_ICONS;
|
||||||
|
}
|
||||||
NotificationDisplayStyleSettings.Style style =
|
NotificationDisplayStyleSettings.Style style =
|
||||||
NotificationDisplayStyleSettings.read(context).style();
|
NotificationDisplayStyleSettings.read(context).style();
|
||||||
return currentMode(style);
|
return currentMode(style);
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
|
android:baselineAligned="false"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -79,6 +79,71 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:cardUseCompatPadding="true"
|
||||||
|
app:strokeColor="@color/sbt_card_outline"
|
||||||
|
app:strokeWidth="1dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:letterSpacing="0.03"
|
||||||
|
android:text="@string/clock_outline_title"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/clock_outline_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/clock_outline_enabled" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/clock_outline_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/clock_outline_thickness_value"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:textAppearance="?attr/textAppearanceBody1" />
|
||||||
|
|
||||||
|
<com.google.android.material.slider.Slider
|
||||||
|
android:id="@+id/clock_outline_thickness"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:valueFrom="1"
|
||||||
|
android:valueTo="6"
|
||||||
|
android:stepSize="1" />
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/clock_outline_color"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:hint="@string/clock_outline_color_hint"
|
||||||
|
android:importantForAutofill="no"
|
||||||
|
android:inputType="textNoSuggestions"
|
||||||
|
android:singleLine="true" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:id="@+id/drawer_clock_custom_format_card"
|
android:id="@+id/drawer_clock_custom_format_card"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -111,6 +111,13 @@
|
|||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:text="@string/debug_restart_systemui" />
|
android:text="@string/debug_restart_systemui" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/debug_experimental_aod_statusbar_root_switch"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:text="@string/debug_experimental_aod_statusbar_root" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
android:id="@+id/hidden_item_root"
|
android:id="@+id/hidden_item_root"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:baselineAligned="false"
|
||||||
android:foreground="?attr/selectableItemBackground"
|
android:foreground="?attr/selectableItemBackground"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:minHeight="56dp"
|
android:minHeight="56dp"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
<color name="sbt_background_top">#FF110B07</color>
|
<color name="sbt_background_top">#FF110B07</color>
|
||||||
<color name="sbt_background_bottom">#FF1B0F08</color>
|
<color name="sbt_background_bottom">#FF1B0F08</color>
|
||||||
<color name="sbt_surface">#FF20140D</color>
|
<color name="sbt_surface">#FF20140D</color>
|
||||||
<color name="sbt_surface_variant">#FF302012</color>
|
|
||||||
<color name="sbt_on_primary">#FFFFFFFF</color>
|
<color name="sbt_on_primary">#FFFFFFFF</color>
|
||||||
<color name="sbt_on_surface">#FFEAF0F3</color>
|
<color name="sbt_on_surface">#FFEAF0F3</color>
|
||||||
<color name="sbt_on_surface_variant">#FFE3D3B6</color>
|
<color name="sbt_on_surface_variant">#FFE3D3B6</color>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<style name="Theme.StatusBarTweak" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<item name="colorPrimary">@color/sbt_primary</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
|
|
||||||
<item name="colorOnPrimary">@color/sbt_on_primary</item>
|
|
||||||
<item name="colorSecondary">@color/sbt_accent</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/sbt_accent_dark</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<item name="colorSurface">@color/sbt_surface</item>
|
|
||||||
<item name="colorOnSurface">@color/sbt_on_surface</item>
|
|
||||||
<item name="android:colorAccent">@color/sbt_accent</item>
|
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
|
||||||
<item name="android:windowBackground">@drawable/sbt_window_background</item>
|
|
||||||
<item name="android:navigationBarColor">@color/sbt_primary_dark</item>
|
|
||||||
<item name="android:statusBarColor">@color/sbt_primary_dark</item>
|
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
|
||||||
<item name="materialCardViewStyle">@style/Widget.StatusBarTweak.Card</item>
|
|
||||||
<item name="materialButtonOutlinedStyle">@style/Widget.StatusBarTweak.OutlinedButton</item>
|
|
||||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.StatusBarTweak.MaterialAlertDialog</item>
|
|
||||||
<item name="sliderStyle">@style/Widget.StatusBarTweak.Slider</item>
|
|
||||||
<item name="switchStyle">@style/Widget.StatusBarTweak.Switch</item>
|
|
||||||
</style>
|
|
||||||
</resources>
|
|
||||||
@@ -8,12 +8,10 @@
|
|||||||
<color name="sbt_background_top">#FFFFF4E8</color>
|
<color name="sbt_background_top">#FFFFF4E8</color>
|
||||||
<color name="sbt_background_bottom">#FFF5E5D2</color>
|
<color name="sbt_background_bottom">#FFF5E5D2</color>
|
||||||
<color name="sbt_surface">#FFFFFFFF</color>
|
<color name="sbt_surface">#FFFFFFFF</color>
|
||||||
<color name="sbt_surface_variant">#FFFFF1E0</color>
|
|
||||||
<color name="sbt_on_primary">#FFFFFFFF</color>
|
<color name="sbt_on_primary">#FFFFFFFF</color>
|
||||||
<color name="sbt_on_surface">#FF111820</color>
|
<color name="sbt_on_surface">#FF111820</color>
|
||||||
<color name="sbt_on_surface_variant">#FF62553F</color>
|
<color name="sbt_on_surface_variant">#FF62553F</color>
|
||||||
<color name="black">#FF000000</color>
|
<color name="black">#FF000000</color>
|
||||||
<color name="white">#FFFFFFFF</color>
|
|
||||||
<color name="sbt_card_outline">#24000000</color>
|
<color name="sbt_card_outline">#24000000</color>
|
||||||
<color name="sbt_panel_outline">#4D000000</color>
|
<color name="sbt_panel_outline">#4D000000</color>
|
||||||
<color name="sbt_hidden_item_active_bg">#1AE65100</color>
|
<color name="sbt_hidden_item_active_bg">#1AE65100</color>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
<string name="nav_layout_lockscreen">    Lockscreen</string>
|
<string name="nav_layout_lockscreen">    Lockscreen</string>
|
||||||
<string name="nav_layout_aod">    AOD</string>
|
<string name="nav_layout_aod">    AOD</string>
|
||||||
<string name="nav_battery_bar">Battery bar</string>
|
<string name="nav_battery_bar">Battery bar</string>
|
||||||
<string name="nav_block_app_notification_icons">Hide notification icons</string>
|
|
||||||
<string name="nav_status_chips">Status chips</string>
|
<string name="nav_status_chips">Status chips</string>
|
||||||
<string name="nav_misc">Misc</string>
|
<string name="nav_misc">Misc</string>
|
||||||
<string name="debug_title">Debugging</string>
|
<string name="debug_title">Debugging</string>
|
||||||
@@ -30,6 +29,7 @@
|
|||||||
<string name="debug_visualize_aod_stock_containers">AOD stock containers and derived statusbar</string>
|
<string name="debug_visualize_aod_stock_containers">AOD stock containers and derived statusbar</string>
|
||||||
<string name="debug_section_systemui">SystemUI</string>
|
<string name="debug_section_systemui">SystemUI</string>
|
||||||
<string name="debug_restart_systemui">Restart SystemUI</string>
|
<string name="debug_restart_systemui">Restart SystemUI</string>
|
||||||
|
<string name="debug_experimental_aod_statusbar_root">Experimental non-Samsung AOD statusbar layout</string>
|
||||||
<string name="debug_restart_systemui_started">SystemUI restart requested.</string>
|
<string name="debug_restart_systemui_started">SystemUI restart requested.</string>
|
||||||
<string name="debug_restart_systemui_failed">Could not restart SystemUI. Root shell failed.</string>
|
<string name="debug_restart_systemui_failed">Could not restart SystemUI. Root shell failed.</string>
|
||||||
<string name="debug_notification_style_cards">Cards</string>
|
<string name="debug_notification_style_cards">Cards</string>
|
||||||
@@ -50,7 +50,6 @@
|
|||||||
<string name="debug_camera_action_undo">↶</string>
|
<string name="debug_camera_action_undo">↶</string>
|
||||||
<string name="debug_camera_other_item">%1$s • %2$s</string>
|
<string name="debug_camera_other_item">%1$s • %2$s</string>
|
||||||
<string name="debug_camera_unknown">unknown</string>
|
<string name="debug_camera_unknown">unknown</string>
|
||||||
<string name="notification_icons_title">Notification icons</string>
|
|
||||||
<string name="system_icons_title">Hide status icons</string>
|
<string name="system_icons_title">Hide status icons</string>
|
||||||
<string name="system_icons_hint">Status-icon rules for the custom layout engine.</string>
|
<string name="system_icons_hint">Status-icon rules for the custom layout engine.</string>
|
||||||
<string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
|
<string name="system_icons_hide_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked.</string>
|
||||||
@@ -71,43 +70,27 @@
|
|||||||
<string name="system_icon_bluetooth">Bluetooth</string>
|
<string name="system_icon_bluetooth">Bluetooth</string>
|
||||||
<string name="system_icon_data_saver">Data saver</string>
|
<string name="system_icon_data_saver">Data saver</string>
|
||||||
<string name="system_icon_do_not_disturb">Do not disturb</string>
|
<string name="system_icon_do_not_disturb">Do not disturb</string>
|
||||||
<string name="system_icon_enhanced_processing">Enhanced processing</string>
|
|
||||||
<string name="system_icon_home_carrier">Homescreen carrier</string>
|
|
||||||
<string name="system_icon_hotspot">Mobile hotspot</string>
|
<string name="system_icon_hotspot">Mobile hotspot</string>
|
||||||
<string name="system_icon_ims">IMS network icons</string>
|
|
||||||
<string name="system_icon_location">Location</string>
|
<string name="system_icon_location">Location</string>
|
||||||
<string name="system_icon_lock_carrier">Lockscreen carrier</string>
|
|
||||||
<string name="system_icon_lockscreen_mum">Two phone user icon</string>
|
|
||||||
<string name="system_icon_managed_profile">Managed profile</string>
|
|
||||||
<string name="system_icon_mobile">Mobile data signal</string>
|
<string name="system_icon_mobile">Mobile data signal</string>
|
||||||
<string name="system_icon_modes">Modes</string>
|
|
||||||
<string name="system_icon_nfc">NFC</string>
|
<string name="system_icon_nfc">NFC</string>
|
||||||
<string name="system_icon_panel_carrier">Expanded panel carrier</string>
|
|
||||||
<string name="system_icon_power_saver">Power saver</string>
|
|
||||||
<string name="system_icon_rcs">RCS</string>
|
|
||||||
<string name="system_icon_two_phone_mode">Two phone mode icon</string>
|
|
||||||
<string name="system_icon_volume">Volume</string>
|
<string name="system_icon_volume">Volume</string>
|
||||||
<string name="system_icon_vpn">VPN</string>
|
<string name="system_icon_vpn">VPN</string>
|
||||||
<string name="system_icon_wifi">Wi-Fi</string>
|
<string name="system_icon_wifi">Wi-Fi</string>
|
||||||
<string name="section_unlocked">Unlocked</string>
|
<string name="section_unlocked">Unlocked</string>
|
||||||
<string name="section_icons_mode">Icons mode</string>
|
|
||||||
<string name="section_cards_mode">Cards mode</string>
|
|
||||||
<string name="section_aod">AOD</string>
|
<string name="section_aod">AOD</string>
|
||||||
<string name="section_lockscreen">Lockscreen</string>
|
<string name="section_lockscreen">Lockscreen</string>
|
||||||
<string name="label_max_icons_per_row">Max icons per row</string>
|
<string name="label_max_icons_per_row">Max icons per row</string>
|
||||||
<string name="label_max_rows">Max rows</string>
|
<string name="label_max_rows">Max rows</string>
|
||||||
<string name="label_even_distribution">Even distribution</string>
|
<string name="label_even_distribution">Even distribution</string>
|
||||||
<string name="label_limit_screen_width">Limit to screen width</string>
|
<string name="label_limit_screen_width">Limit to screen width</string>
|
||||||
<string name="label_cutout_aware">Cutout aware</string>
|
|
||||||
<string name="label_aod_keep_visible_during_call">Keep AOD visible during calls</string>
|
<string name="label_aod_keep_visible_during_call">Keep AOD visible during calls</string>
|
||||||
<string name="label_aod_keep_visible_during_call_hint">Prevents SystemUI from suppressing AOD while an active call is in progress.</string>
|
<string name="label_aod_keep_visible_during_call_hint">Prevents SystemUI from suppressing AOD while an active call is in progress.</string>
|
||||||
<string name="hidden_apps_dialog_title">Notification icon app blocks</string>
|
|
||||||
<string name="hidden_apps_page_title">Hide notification icons</string>
|
<string name="hidden_apps_page_title">Hide notification icons</string>
|
||||||
<string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string>
|
<string name="hidden_apps_dialog_subtitle">Pull down to refresh session apps. List contents are fixed until refresh.</string>
|
||||||
<string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
|
<string name="hidden_apps_page_hint">Icons represent, in order:\nAOD, Lockscreen, Unlocked</string>
|
||||||
<string name="hidden_apps_clear_session">Clear session list</string>
|
<string name="hidden_apps_clear_session">Clear session list</string>
|
||||||
<string name="hidden_apps_empty">No apps yet. Generate a notification first.</string>
|
<string name="hidden_apps_empty">No apps yet. Generate a notification first.</string>
|
||||||
<string name="hidden_apps_close">Close</string>
|
|
||||||
<string name="hidden_apps_exception_regex_input_hint">Regex</string>
|
<string name="hidden_apps_exception_regex_input_hint">Regex</string>
|
||||||
<plurals name="hidden_apps_package_with_exceptions">
|
<plurals name="hidden_apps_package_with_exceptions">
|
||||||
<item quantity="one">%1$s \u2022 %2$d exception</item>
|
<item quantity="one">%1$s \u2022 %2$d exception</item>
|
||||||
@@ -125,7 +108,6 @@
|
|||||||
<string name="hidden_apps_mode_unlock">Unlocked</string>
|
<string name="hidden_apps_mode_unlock">Unlocked</string>
|
||||||
<string name="hidden_apps_package_unknown">(unknown package)</string>
|
<string name="hidden_apps_package_unknown">(unknown package)</string>
|
||||||
<string name="status_chips_title">Status chips</string>
|
<string name="status_chips_title">Status chips</string>
|
||||||
<string name="status_chips_hint">Hide Samsung/SystemUI status chips (music, maps/navigation, call) in the top status area.</string>
|
|
||||||
<string name="status_chips_hide_all_label">Hide other status chips (unlocked)</string>
|
<string name="status_chips_hide_all_label">Hide other status chips (unlocked)</string>
|
||||||
<string name="status_chips_hide_all_hint">Hides detected status chips that are not media, navigation or call. Privacy indicators are excluded.</string>
|
<string name="status_chips_hide_all_hint">Hides detected status chips that are not media, navigation or call. Privacy indicators are excluded.</string>
|
||||||
<string name="status_chips_hide_media_unlocked_label">Hide media chip (unlocked)</string>
|
<string name="status_chips_hide_media_unlocked_label">Hide media chip (unlocked)</string>
|
||||||
@@ -141,9 +123,6 @@
|
|||||||
<string name="battery_bar_position_top">Top edge of screen</string>
|
<string name="battery_bar_position_top">Top edge of screen</string>
|
||||||
<string name="battery_bar_position_bottom">Bottom of status bar</string>
|
<string name="battery_bar_position_bottom">Bottom of status bar</string>
|
||||||
<string name="battery_bar_alignment_title">Alignment</string>
|
<string name="battery_bar_alignment_title">Alignment</string>
|
||||||
<string name="battery_bar_alignment_ltr">→</string>
|
|
||||||
<string name="battery_bar_alignment_rtl">←</string>
|
|
||||||
<string name="battery_bar_alignment_center">↔</string>
|
|
||||||
<string name="battery_bar_thickness_title">Thickness (dp)</string>
|
<string name="battery_bar_thickness_title">Thickness (dp)</string>
|
||||||
<string name="battery_bar_edge_offset_title">Edge offset (dp)</string>
|
<string name="battery_bar_edge_offset_title">Edge offset (dp)</string>
|
||||||
<string name="battery_bar_curved_geometry_length">Follow screen edges</string>
|
<string name="battery_bar_curved_geometry_length">Follow screen edges</string>
|
||||||
@@ -162,15 +141,12 @@
|
|||||||
<string name="battery_bar_thresholds_title">Threshold colours</string>
|
<string name="battery_bar_thresholds_title">Threshold colours</string>
|
||||||
<string name="battery_bar_thresholds_hint">Below a threshold, the first matching colour overrides the default.</string>
|
<string name="battery_bar_thresholds_hint">Below a threshold, the first matching colour overrides the default.</string>
|
||||||
<string name="battery_bar_add_threshold">Add threshold</string>
|
<string name="battery_bar_add_threshold">Add threshold</string>
|
||||||
<string name="battery_bar_colour_dialog_title">Set colour</string>
|
|
||||||
<string name="battery_bar_colour_dialog_hint">Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB</string>
|
<string name="battery_bar_colour_dialog_hint">Examples: #FA0, FA0, or #FA0 ; invRGB 0.5 *RGB</string>
|
||||||
<string name="battery_bar_threshold_percent_title">Threshold percent</string>
|
<string name="battery_bar_threshold_percent_title">Threshold percent</string>
|
||||||
<string name="battery_bar_threshold_percent_hint">Applies when battery is at or below this percent.</string>
|
<string name="battery_bar_threshold_percent_hint">Applies when battery is at or below this percent.</string>
|
||||||
<string name="battery_bar_threshold_change_percent">Change threshold %</string>
|
|
||||||
<string name="battery_bar_threshold_label">Below %1$d%%</string>
|
<string name="battery_bar_threshold_label">Below %1$d%%</string>
|
||||||
<string name="battery_bar_threshold_discharge_colour">Discharge colour</string>
|
<string name="battery_bar_threshold_discharge_colour">Discharge colour</string>
|
||||||
<string name="battery_bar_threshold_charge_colour">Charge colour</string>
|
<string name="battery_bar_threshold_charge_colour">Charge colour</string>
|
||||||
<string name="battery_bar_threshold_remove">Remove threshold</string>
|
|
||||||
<string name="battery_bar_colour_auto">Auto</string>
|
<string name="battery_bar_colour_auto">Auto</string>
|
||||||
<string name="battery_bar_colour_same_as_discharge">Same as discharge</string>
|
<string name="battery_bar_colour_same_as_discharge">Same as discharge</string>
|
||||||
<string name="battery_bar_colour_same_as_default">Default</string>
|
<string name="battery_bar_colour_same_as_default">Default</string>
|
||||||
@@ -184,9 +160,7 @@
|
|||||||
<string name="layout_home_title">Layout</string>
|
<string name="layout_home_title">Layout</string>
|
||||||
<string name="layout_title">Unlocked</string>
|
<string name="layout_title">Unlocked</string>
|
||||||
<string name="layout_master_toggle_title">Master toggle</string>
|
<string name="layout_master_toggle_title">Master toggle</string>
|
||||||
<string name="clock_enabled">Use custom clock</string>
|
|
||||||
<string name="layout_enabled">Use custom layout</string>
|
<string name="layout_enabled">Use custom layout</string>
|
||||||
<string name="clock_position_title">Clock position</string>
|
|
||||||
<string name="layout_clock_position_title">Clock</string>
|
<string name="layout_clock_position_title">Clock</string>
|
||||||
<string name="layout_carrier_position_title">Carrier</string>
|
<string name="layout_carrier_position_title">Carrier</string>
|
||||||
<string name="layout_chip_position_title">Status chip</string>
|
<string name="layout_chip_position_title">Status chip</string>
|
||||||
@@ -200,6 +174,7 @@
|
|||||||
<string name="layout_chip_height_steps">Chip height (% of statusbar)</string>
|
<string name="layout_chip_height_steps">Chip height (% of statusbar)</string>
|
||||||
<string name="layout_locked_notifications_title">Locked notifications mode</string>
|
<string name="layout_locked_notifications_title">Locked notifications mode</string>
|
||||||
<string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string>
|
<string name="layout_locked_notifications_hint">Mirrors the Samsung notification display style for AOD and lockscreen.</string>
|
||||||
|
<string name="layout_locked_notifications_hint_icons_only">This SystemUI exposes notification icons only.</string>
|
||||||
<string name="layout_misc_title">Misc</string>
|
<string name="layout_misc_title">Misc</string>
|
||||||
<string name="label_max_unlocked_icons_per_row">Max unlocked icons per row</string>
|
<string name="label_max_unlocked_icons_per_row">Max unlocked icons per row</string>
|
||||||
<string name="layout_padding_title">Padding</string>
|
<string name="layout_padding_title">Padding</string>
|
||||||
@@ -232,12 +207,15 @@
|
|||||||
<string name="clock_vertical_offset_label">Vertical position</string>
|
<string name="clock_vertical_offset_label">Vertical position</string>
|
||||||
<string name="clock_middle_side_left">Left side</string>
|
<string name="clock_middle_side_left">Left side</string>
|
||||||
<string name="clock_middle_side_right">Right side</string>
|
<string name="clock_middle_side_right">Right side</string>
|
||||||
<string name="clock_custom_format_title">Custom format</string>
|
|
||||||
<string name="clock_custom_format_statusbar_title">Statusbar clock</string>
|
<string name="clock_custom_format_statusbar_title">Statusbar clock</string>
|
||||||
<string name="clock_custom_format_drawer_clock_title">Drawer clock</string>
|
<string name="clock_custom_format_drawer_clock_title">Drawer clock</string>
|
||||||
<string name="clock_custom_format_drawer_date_title">Drawer date</string>
|
<string name="clock_custom_format_drawer_date_title">Drawer date</string>
|
||||||
<string name="clock_custom_format_tools_title">Tools and instructions</string>
|
<string name="clock_custom_format_tools_title">Tools and instructions</string>
|
||||||
<string name="clock_custom_format_enabled">Enable custom date/time pattern</string>
|
<string name="clock_custom_format_enabled">Enable custom date/time pattern</string>
|
||||||
|
<string name="clock_outline_title">Statusbar clock outline</string>
|
||||||
|
<string name="clock_outline_enabled">Add outline</string>
|
||||||
|
<string name="clock_outline_thickness_value">Thickness: %1$d dp</string>
|
||||||
|
<string name="clock_outline_color_hint">Outline colour, for example #000 ; #FFF</string>
|
||||||
<string name="drawer_clock_custom_format_enabled">Enable custom date/time pattern</string>
|
<string name="drawer_clock_custom_format_enabled">Enable custom date/time pattern</string>
|
||||||
<string name="drawer_date_custom_format_enabled">Enable custom date/time pattern</string>
|
<string name="drawer_date_custom_format_enabled">Enable custom date/time pattern</string>
|
||||||
<string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: <i>, <u>, <small>, <big>, <font color=\'...\'>, <font face=\'...\'>\n\u2022 <b> is not supported\n\u2022 Custom font tags: <sans>, <serif>, <mono>, <condensed>\n\u2022 {/font} closes any open <font ...> tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string>
|
<string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: <i>, <u>, <small>, <big>, <font color=\'...\'>, <font face=\'...\'>\n\u2022 <b> is not supported\n\u2022 Custom font tags: <sans>, <serif>, <mono>, <condensed>\n\u2022 {/font} closes any open <font ...> tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string>
|
||||||
@@ -255,7 +233,6 @@
|
|||||||
<string name="clock_font_picker_hint">Search font family</string>
|
<string name="clock_font_picker_hint">Search font family</string>
|
||||||
<string name="clock_font_picker_copied">Copied: %1$s</string>
|
<string name="clock_font_picker_copied">Copied: %1$s</string>
|
||||||
<string name="clock_custom_format_help_link">Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html</string>
|
<string name="clock_custom_format_help_link">Date/time pattern reference only: https://docs.oracle.com/javase/8/docs/api/java/text/SimpleDateFormat.html</string>
|
||||||
<string name="clock_custom_format_invalid">Invalid pattern</string>
|
|
||||||
<string name="label_battery_lockscreen_unplugged_percent">Unplugged battery % on lockscreen</string>
|
<string name="label_battery_lockscreen_unplugged_percent">Unplugged battery % on lockscreen</string>
|
||||||
<string name="label_battery_lockscreen_unplugged_percent_hint">Shows the plain battery percentage on the lockscreen when unplugged.</string>
|
<string name="label_battery_lockscreen_unplugged_percent_hint">Shows the plain battery percentage on the lockscreen when unplugged.</string>
|
||||||
<string name="label_battery_aod_unplugged_percent">Unplugged battery % on AOD</string>
|
<string name="label_battery_aod_unplugged_percent">Unplugged battery % on AOD</string>
|
||||||
|
|||||||
@@ -1,26 +1,4 @@
|
|||||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<style name="Theme.StatusBarTweak" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
|
||||||
<item name="colorPrimary">@color/sbt_primary</item>
|
|
||||||
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
|
|
||||||
<item name="colorOnPrimary">@color/sbt_on_primary</item>
|
|
||||||
<item name="colorSecondary">@color/sbt_accent</item>
|
|
||||||
<item name="colorSecondaryVariant">@color/sbt_accent_dark</item>
|
|
||||||
<item name="colorOnSecondary">@color/black</item>
|
|
||||||
<item name="colorSurface">@color/sbt_surface</item>
|
|
||||||
<item name="colorOnSurface">@color/sbt_on_surface</item>
|
|
||||||
<item name="android:colorAccent">@color/sbt_accent</item>
|
|
||||||
<item name="android:fontFamily">sans-serif</item>
|
|
||||||
<item name="android:windowBackground">@drawable/sbt_window_background</item>
|
|
||||||
<item name="android:navigationBarColor">@color/sbt_primary_dark</item>
|
|
||||||
<item name="android:statusBarColor">@color/sbt_primary_dark</item>
|
|
||||||
<item name="android:windowLightStatusBar">false</item>
|
|
||||||
<item name="materialCardViewStyle">@style/Widget.StatusBarTweak.Card</item>
|
|
||||||
<item name="materialButtonOutlinedStyle">@style/Widget.StatusBarTweak.OutlinedButton</item>
|
|
||||||
<item name="materialAlertDialogTheme">@style/ThemeOverlay.StatusBarTweak.MaterialAlertDialog</item>
|
|
||||||
<item name="sliderStyle">@style/Widget.StatusBarTweak.Slider</item>
|
|
||||||
<item name="switchStyle">@style/Widget.StatusBarTweak.Switch</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Theme.StatusBarTweak.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
<style name="Theme.StatusBarTweak.NoActionBar" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
<item name="colorPrimary">@color/sbt_primary</item>
|
<item name="colorPrimary">@color/sbt_primary</item>
|
||||||
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
|
<item name="colorPrimaryVariant">@color/sbt_primary_dark</item>
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package se.ajpanton.statusbartweak.shell.settings;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class SbtSettingsTest {
|
||||||
|
@Test
|
||||||
|
public void emptySourceMatchesRepresentativeDefaults() {
|
||||||
|
SbtSettings defaults = SbtSettings.defaults();
|
||||||
|
SbtSettings decoded = SbtSettings.fromSource(new MapSource());
|
||||||
|
|
||||||
|
assertEquals(SbtDefaults.CLOCK_ENABLED_DEFAULT, defaults.clockEnabled);
|
||||||
|
assertEquals(SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT, defaults.clockCustomFormat);
|
||||||
|
assertEquals(SbtDefaults.BATTERY_BAR_POSITION_DEFAULT, defaults.batteryBarPosition);
|
||||||
|
assertEquals(SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT, defaults.batteryBarThicknessDp);
|
||||||
|
assertEquals(
|
||||||
|
SbtDefaults.LAYOUT_NOTIF_UNLOCKED_COUNT_DEFAULT,
|
||||||
|
defaults.layoutNotifUnlockedCount);
|
||||||
|
assertEquals(
|
||||||
|
SbtDefaults.LAYOUT_STATUS_UNLOCKED_COUNT_DEFAULT,
|
||||||
|
defaults.layoutStatusUnlockedCount);
|
||||||
|
assertEquals(defaults.clockEnabled, decoded.clockEnabled);
|
||||||
|
assertEquals(defaults.clockCustomFormat, decoded.clockCustomFormat);
|
||||||
|
assertEquals(defaults.batteryBarPosition, decoded.batteryBarPosition);
|
||||||
|
assertEquals(defaults.batteryBarThicknessDp, decoded.batteryBarThicknessDp);
|
||||||
|
assertEquals(defaults.layoutNotifUnlockedCount, decoded.layoutNotifUnlockedCount);
|
||||||
|
assertEquals(defaults.layoutStatusUnlockedCount, decoded.layoutStatusUnlockedCount);
|
||||||
|
assertEquals(defaults.systemIconBlockedModes, decoded.systemIconBlockedModes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sourceDecodingPreservesSettingsNormalization() {
|
||||||
|
MapSource source = new MapSource();
|
||||||
|
source.put(SbtSettings.KEY_CLOCK_ENABLED, false);
|
||||||
|
source.put(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, "H:mm:ss");
|
||||||
|
source.put(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW, 0);
|
||||||
|
source.put(SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX, 500);
|
||||||
|
source.put(SbtSettings.KEY_BATTERY_BAR_CURVED_GEOMETRY_MODE, "unsupported");
|
||||||
|
source.put(SbtSettings.KEY_LAYOUT_NOTIF_UNLOCKED_COUNT, 100);
|
||||||
|
source.put(SbtSettings.KEY_SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE, "unsupported");
|
||||||
|
source.put(
|
||||||
|
SystemIconRules.PREF_KEY_BLOCKED_MODES,
|
||||||
|
setOf("wifi|5", "invalid", "wifi|not-a-number"));
|
||||||
|
source.put(
|
||||||
|
SbtSettings.KEY_APP_ICON_BLOCKED_MODES,
|
||||||
|
setOf("app.example|1|0|1", "bad-entry"));
|
||||||
|
|
||||||
|
SbtSettings decoded = SbtSettings.fromSource(source);
|
||||||
|
|
||||||
|
assertFalse(decoded.clockEnabled);
|
||||||
|
assertEquals("H:mm:ss", decoded.clockCustomFormat);
|
||||||
|
assertEquals(SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN, decoded.unlockedMaxIconsPerRow);
|
||||||
|
assertEquals(500, decoded.clockVerticalOffsetPx);
|
||||||
|
assertEquals(SbtSettings.BATTERY_BAR_CURVED_GEOMETRY_OFF, decoded.batteryBarCurvedGeometryMode);
|
||||||
|
assertEquals(SbtDefaults.LAYOUT_ICON_CONTAINER_COUNT_MAX, decoded.layoutNotifUnlockedCount);
|
||||||
|
assertEquals(
|
||||||
|
SbtDefaults.SYSTEM_ICON_DUAL_SIM_SIGNAL_MODE_DEFAULT,
|
||||||
|
decoded.systemIconDualSimSignalMode);
|
||||||
|
assertEquals(Integer.valueOf(5), decoded.systemIconBlockedModes.get("wifi"));
|
||||||
|
assertEquals(Integer.valueOf(AppIconRules.MODE_AOD | AppIconRules.MODE_UNLOCK),
|
||||||
|
decoded.appIconBlockedModes.get("app.example"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> setOf(String... values) {
|
||||||
|
HashSet<String> result = new HashSet<>();
|
||||||
|
Collections.addAll(result, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MapSource implements SbtSettings.SettingsSource {
|
||||||
|
private final Map<String, Object> values = new HashMap<>();
|
||||||
|
|
||||||
|
void put(String key, Object value) {
|
||||||
|
values.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean getBoolean(String key, boolean defaultValue) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
return value instanceof Boolean ? (Boolean) value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getInt(String key, int defaultValue) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
return value instanceof Integer ? (Integer) value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getString(String key, String defaultValue) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
return value instanceof String ? (String) value : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public Set<String> getStringSet(String key) {
|
||||||
|
Object value = values.get(key);
|
||||||
|
return value instanceof Set ? (Set<String>) value : Collections.emptySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean contains(String key) {
|
||||||
|
return values.containsKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
"""Editable status-bar layout playground.
|
||||||
|
|
||||||
|
Run from this directory with ``python3 playground.py``. Everything below is
|
||||||
|
intended to be edited freely; the model and renderer live in separate files.
|
||||||
|
|
||||||
|
Coordinates use the same convention as the module's layout solver:
|
||||||
|
horizontal zero is the status bar's left edge and vertical zero is its bottom.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from statusbar_lab import StatusBarLab, containers_overlap, vertically_overlaps
|
||||||
|
from statusbar_renderer import render
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Virtual status-bar settings
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
STATUSBAR_WIDTH = 1200
|
||||||
|
STATUSBAR_HEIGHT = 100
|
||||||
|
|
||||||
|
# These are inputs for the algorithm you will write. The playground itself
|
||||||
|
# does not apply them automatically.
|
||||||
|
EDGE_PADDING = 0
|
||||||
|
CONTAINER_PADDING = 8
|
||||||
|
CAMERA_PADDING = 0
|
||||||
|
TEXT_HORIZONTAL_PADDING = 6
|
||||||
|
ICON_DOT_GAP = 0
|
||||||
|
DOT_WIDTH_FACTOR = 0.75
|
||||||
|
|
||||||
|
# Keep these in the same terms as the module settings. They are intentionally
|
||||||
|
# passive until the positioning/truncation algorithm below uses them.
|
||||||
|
CONTAINER_ORDER = ["clock", "chip", "carrier", "notification", "status"]
|
||||||
|
SHRINK_ORDER = ["notification", "status", "chip", "carrier"]
|
||||||
|
|
||||||
|
# Rendering scale only affects the visualizer, never the simulated geometry.
|
||||||
|
RENDER_SCALE = 1.0
|
||||||
|
VERTICAL_RENDER_SCALE = 2.0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Scenario API
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
LAB = StatusBarLab()
|
||||||
|
|
||||||
|
# These lists remain valid when items are added. Clock text lines belong to
|
||||||
|
# one CLOCK group because the group moves them by their relative x offsets.
|
||||||
|
CLOCK = LAB.clock
|
||||||
|
CLOCK_LINES = CLOCK.lines
|
||||||
|
NOTIFICATIONS = LAB.notifications
|
||||||
|
STATUSES = LAB.statuses
|
||||||
|
CHIPS = LAB.chips
|
||||||
|
CARRIERS = LAB.carriers
|
||||||
|
CAMERAS = LAB.cameras
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_settings() -> None:
|
||||||
|
LAB.text_horizontal_padding = TEXT_HORIZONTAL_PADDING
|
||||||
|
LAB.icon_dot_gap = ICON_DOT_GAP
|
||||||
|
LAB.dot_width_factor = DOT_WIDTH_FACTOR
|
||||||
|
|
||||||
|
|
||||||
|
def AddClock(
|
||||||
|
text: str,
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
horizontal_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
):
|
||||||
|
"""Add one text line to the single movable CLOCK group.
|
||||||
|
|
||||||
|
Horizontal offsets are normalized relative to all clock lines. Therefore
|
||||||
|
offsets ``5, 10, -20`` produce the same clock geometry as ``105, 110, 80``.
|
||||||
|
``position`` is ``left``, ``middle`` or ``right``; ``cutout_side`` is the
|
||||||
|
fallback side for a middle clock when a camera splits the status bar.
|
||||||
|
"""
|
||||||
|
_sync_settings()
|
||||||
|
return CLOCK.add_line(text, height, vertical_offset, horizontal_offset, position, cutout_side)
|
||||||
|
|
||||||
|
|
||||||
|
def AddNotification(
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
direction: str = "right",
|
||||||
|
):
|
||||||
|
_sync_settings()
|
||||||
|
return LAB.add_notification(height, vertical_offset, position, cutout_side, direction)
|
||||||
|
|
||||||
|
|
||||||
|
def SetNotificationIcons(number_of_notifications: int) -> None:
|
||||||
|
"""Replace the notification-icon pool and empty every notification container."""
|
||||||
|
LAB.configure_notification_icons(number_of_notifications)
|
||||||
|
|
||||||
|
|
||||||
|
def AddStatus(
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
position: str = "right",
|
||||||
|
cutout_side: str = "right",
|
||||||
|
direction: str = "left",
|
||||||
|
):
|
||||||
|
_sync_settings()
|
||||||
|
return LAB.add_status(height, vertical_offset, position, cutout_side, direction)
|
||||||
|
|
||||||
|
|
||||||
|
def SetStatusIcons(number_of_icons: int, widths: list[float] | None = None) -> None:
|
||||||
|
"""Replace the status-icon pool and empty every status container.
|
||||||
|
|
||||||
|
Missing widths are padded using the first status container's height. Add
|
||||||
|
status containers before calling this function when relying on that default.
|
||||||
|
"""
|
||||||
|
LAB.configure_status_icons(number_of_icons, widths or [])
|
||||||
|
|
||||||
|
|
||||||
|
def AddChip(
|
||||||
|
text: str,
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
):
|
||||||
|
_sync_settings()
|
||||||
|
return LAB.add_chip(text, height, vertical_offset, position, cutout_side)
|
||||||
|
|
||||||
|
|
||||||
|
def AddCarrier(
|
||||||
|
text: str,
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
):
|
||||||
|
_sync_settings()
|
||||||
|
return LAB.add_carrier(text, height, vertical_offset, position, cutout_side)
|
||||||
|
|
||||||
|
|
||||||
|
def AddCamera(width: float, height: float, horizontal_position: float, vertical_position: float):
|
||||||
|
return LAB.add_camera(width, height, horizontal_position, vertical_position)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Simulated app settings: add or remove as many containers as you need.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AddClock("12:34:56", height=40, vertical_offset=0, horizontal_offset=0)
|
||||||
|
AddClock("Mon, 1 Jan", height=24, vertical_offset=42, horizontal_offset=10)
|
||||||
|
|
||||||
|
AddNotification(height=32, vertical_offset=0)
|
||||||
|
AddNotification(height=32, vertical_offset=40)
|
||||||
|
SetNotificationIcons(8)
|
||||||
|
|
||||||
|
AddStatus(height=30, vertical_offset=0)
|
||||||
|
AddStatus(height=30, vertical_offset=38)
|
||||||
|
SetStatusIcons(6, [20, 30, 30, 45])
|
||||||
|
|
||||||
|
AddChip("Navigation 12:34", height=38, vertical_offset=0)
|
||||||
|
AddCarrier("Example carrier", height=30, vertical_offset=45)
|
||||||
|
|
||||||
|
AddCamera(width=90, height=100, horizontal_position=555, vertical_position=0)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Positioning and truncation algorithm
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# All containers currently start at horizontal position zero. Add your layout
|
||||||
|
# algorithm here. Useful operations include:
|
||||||
|
#
|
||||||
|
# CLOCK.set_left(EDGE_PADDING)
|
||||||
|
# NOTIFICATIONS[0].set_right(STATUSBAR_WIDTH - EDGE_PADDING)
|
||||||
|
# CHIPS[0].set_width(120) # clock widths cannot be overridden
|
||||||
|
# NOTIFICATIONS[0].add_icons(3) # takes IDs from the shared pool
|
||||||
|
# NOTIFICATIONS[0].return_icons(1) # returns the newest assigned ID
|
||||||
|
# NOTIFICATIONS[0].set_dot(True)
|
||||||
|
# CLOCK.right_edge_between(10, 50) # ignores clock lines outside this band
|
||||||
|
# containers_overlap(CLOCK, CHIPS[0])
|
||||||
|
# vertically_overlaps(NOTIFICATIONS[0].bounds, CHIPS[0].bounds)
|
||||||
|
|
||||||
|
|
||||||
|
# Keep the renderer call at the end so the editable scenario and algorithm stay
|
||||||
|
# together above it.
|
||||||
|
_sync_settings()
|
||||||
|
render(LAB, STATUSBAR_WIDTH, STATUSBAR_HEIGHT, RENDER_SCALE, VERTICAL_RENDER_SCALE)
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
"""Container model for the status-bar layout playground.
|
||||||
|
|
||||||
|
This module deliberately contains no placement policy. ``playground.py`` owns
|
||||||
|
the scenario and is the only file intended for interactive editing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Iterable, Optional
|
||||||
|
|
||||||
|
|
||||||
|
def text_width(text: str, height: float, horizontal_padding: float) -> float:
|
||||||
|
"""Use stable approximate glyph widths so geometry is available before Tk starts."""
|
||||||
|
narrow = set(" ilI.,:;!|'`")
|
||||||
|
wide = set("MW@#%&")
|
||||||
|
units = sum(0.35 if char in narrow else 0.9 if char in wide else 0.62 for char in text)
|
||||||
|
return max(0.0, units * height + horizontal_padding * 2)
|
||||||
|
|
||||||
|
|
||||||
|
def _bounds_share_vertical_space(first: "Bounds", second: "Bounds") -> bool:
|
||||||
|
return first.bottom < second.top and second.bottom < first.top
|
||||||
|
|
||||||
|
|
||||||
|
def rectangles_overlap(first: "Bounds", second: "Bounds") -> bool:
|
||||||
|
return (
|
||||||
|
_bounds_share_vertical_space(first, second)
|
||||||
|
and first.left < second.right
|
||||||
|
and second.left < first.right
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Bounds:
|
||||||
|
left: float
|
||||||
|
bottom: float
|
||||||
|
right: float
|
||||||
|
top: float
|
||||||
|
|
||||||
|
|
||||||
|
def vertically_overlaps(
|
||||||
|
first: "Bounds | Container | ClockGroup",
|
||||||
|
second: "Bounds | Container | ClockGroup",
|
||||||
|
) -> bool:
|
||||||
|
"""Return whether two containers can collide through horizontal movement.
|
||||||
|
|
||||||
|
A composite clock is expanded to its individual lines, so protruding clock
|
||||||
|
text outside the tested vertical band cannot produce a false collision.
|
||||||
|
"""
|
||||||
|
first_items = _collision_items(first)
|
||||||
|
second_items = _collision_items(second)
|
||||||
|
return any(
|
||||||
|
_bounds_share_vertical_space(first_item.bounds, second_item.bounds)
|
||||||
|
for first_item in first_items
|
||||||
|
for second_item in second_items
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def shares_vertical_space(
|
||||||
|
first: "Bounds | Container | ClockGroup",
|
||||||
|
second: "Bounds | Container | ClockGroup",
|
||||||
|
) -> bool:
|
||||||
|
return vertically_overlaps(first, second)
|
||||||
|
|
||||||
|
|
||||||
|
class Container:
|
||||||
|
"""A mutable, bottom-origin status-bar rectangle."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
lab: "StatusBarLab",
|
||||||
|
kind: str,
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
) -> None:
|
||||||
|
self.lab = lab
|
||||||
|
self.kind = kind
|
||||||
|
self.height = float(height)
|
||||||
|
self.y = float(vertical_offset)
|
||||||
|
self.position = position
|
||||||
|
self.cutout_side = cutout_side
|
||||||
|
self.x = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left(self) -> float:
|
||||||
|
return self.x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right(self) -> float:
|
||||||
|
return self.x + self.width
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bottom(self) -> float:
|
||||||
|
return self.y
|
||||||
|
|
||||||
|
@property
|
||||||
|
def top(self) -> float:
|
||||||
|
return self.y + self.height
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bounds(self) -> Bounds:
|
||||||
|
return Bounds(self.left, self.bottom, self.right, self.top)
|
||||||
|
|
||||||
|
def set_left(self, left: float) -> None:
|
||||||
|
self.x = float(left)
|
||||||
|
|
||||||
|
def set_right(self, right: float) -> None:
|
||||||
|
self.x = float(right) - self.width
|
||||||
|
|
||||||
|
def shift_x(self, amount: float) -> None:
|
||||||
|
self.x += float(amount)
|
||||||
|
|
||||||
|
def vertically_overlaps(self, other: "Bounds") -> bool:
|
||||||
|
return vertically_overlaps(self.bounds, other)
|
||||||
|
|
||||||
|
def overlaps(self, other: "Container | ClockGroup") -> bool:
|
||||||
|
return containers_overlap(self, other)
|
||||||
|
|
||||||
|
|
||||||
|
class TextContainer(Container):
|
||||||
|
def __init__(self, text: str, *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.text = text
|
||||||
|
self._width_override: Optional[float] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def natural_width(self) -> float:
|
||||||
|
return text_width(self.text, self.height, self.lab.text_horizontal_padding)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
return self.natural_width if self._width_override is None else self._width_override
|
||||||
|
|
||||||
|
def set_width(self, width: float) -> None:
|
||||||
|
self._width_override = max(0.0, float(width))
|
||||||
|
|
||||||
|
def restore_natural_width(self) -> None:
|
||||||
|
self._width_override = None
|
||||||
|
|
||||||
|
|
||||||
|
class ClockLine(TextContainer):
|
||||||
|
"""A clock line whose width always follows its text."""
|
||||||
|
|
||||||
|
def __init__(self, horizontal_offset: float, text: str, *args, **kwargs) -> None:
|
||||||
|
super().__init__(text, *args, **kwargs)
|
||||||
|
self.raw_horizontal_offset = float(horizontal_offset)
|
||||||
|
self.relative_x = 0.0
|
||||||
|
|
||||||
|
def set_width(self, width: float) -> None:
|
||||||
|
raise ValueError("Clock line widths are derived from their text and cannot be changed.")
|
||||||
|
|
||||||
|
|
||||||
|
class ClockGroup:
|
||||||
|
"""One movable clock made from one or more independently-sized text lines."""
|
||||||
|
|
||||||
|
def __init__(self, lab: "StatusBarLab") -> None:
|
||||||
|
self.lab = lab
|
||||||
|
self.lines: list[ClockLine] = []
|
||||||
|
self.x = 0.0
|
||||||
|
|
||||||
|
def add_line(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
height: float,
|
||||||
|
vertical_offset: float = 0,
|
||||||
|
horizontal_offset: float = 0,
|
||||||
|
position: str = "left",
|
||||||
|
cutout_side: str = "left",
|
||||||
|
) -> ClockLine:
|
||||||
|
line = ClockLine(
|
||||||
|
horizontal_offset,
|
||||||
|
text,
|
||||||
|
self.lab,
|
||||||
|
"clock",
|
||||||
|
height,
|
||||||
|
vertical_offset,
|
||||||
|
position,
|
||||||
|
cutout_side,
|
||||||
|
)
|
||||||
|
self.lines.append(line)
|
||||||
|
self._apply_relative_offsets()
|
||||||
|
return line
|
||||||
|
|
||||||
|
def _apply_relative_offsets(self) -> None:
|
||||||
|
if not self.lines:
|
||||||
|
return
|
||||||
|
origin = min(line.raw_horizontal_offset for line in self.lines)
|
||||||
|
for line in self.lines:
|
||||||
|
line.relative_x = line.raw_horizontal_offset - origin
|
||||||
|
line.x = self.x + line.relative_x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def left(self) -> float:
|
||||||
|
return min((line.left for line in self.lines), default=self.x)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def right(self) -> float:
|
||||||
|
return max((line.right for line in self.lines), default=self.x)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
return self.right - self.left
|
||||||
|
|
||||||
|
def set_left(self, left: float) -> None:
|
||||||
|
self.x += float(left) - self.left
|
||||||
|
self._apply_relative_offsets()
|
||||||
|
|
||||||
|
def set_right(self, right: float) -> None:
|
||||||
|
self.x += float(right) - self.right
|
||||||
|
self._apply_relative_offsets()
|
||||||
|
|
||||||
|
def shift_x(self, amount: float) -> None:
|
||||||
|
self.x += float(amount)
|
||||||
|
self._apply_relative_offsets()
|
||||||
|
|
||||||
|
def bounds_between(self, bottom: float, top: float) -> Optional[Bounds]:
|
||||||
|
"""Return clock bounds limited to lines that occupy ``bottom..top``."""
|
||||||
|
band = Bounds(float("-inf"), bottom, float("inf"), top)
|
||||||
|
lines = [line for line in self.lines if vertically_overlaps(line.bounds, band)]
|
||||||
|
if not lines:
|
||||||
|
return None
|
||||||
|
return Bounds(
|
||||||
|
min(line.left for line in lines),
|
||||||
|
min(line.bottom for line in lines),
|
||||||
|
max(line.right for line in lines),
|
||||||
|
max(line.top for line in lines),
|
||||||
|
)
|
||||||
|
|
||||||
|
def left_edge_between(self, bottom: float, top: float) -> Optional[float]:
|
||||||
|
bounds = self.bounds_between(bottom, top)
|
||||||
|
return bounds.left if bounds is not None else None
|
||||||
|
|
||||||
|
def right_edge_between(self, bottom: float, top: float) -> Optional[float]:
|
||||||
|
bounds = self.bounds_between(bottom, top)
|
||||||
|
return bounds.right if bounds is not None else None
|
||||||
|
|
||||||
|
def overlaps(self, other: Container | "ClockGroup") -> bool:
|
||||||
|
return containers_overlap(self, other)
|
||||||
|
|
||||||
|
|
||||||
|
class IconPool:
|
||||||
|
def __init__(self, kind: str) -> None:
|
||||||
|
self.kind = kind
|
||||||
|
self.widths: list[Optional[float]] = []
|
||||||
|
self.available_ids: list[int] = []
|
||||||
|
|
||||||
|
def configure(self, count: int, widths: Iterable[float]) -> None:
|
||||||
|
count = max(0, int(count))
|
||||||
|
supplied: list[Optional[float]] = [float(width) for width in widths][:count]
|
||||||
|
supplied.extend([None] * (count - len(supplied)))
|
||||||
|
self.widths = supplied
|
||||||
|
self.available_ids = list(range(count))
|
||||||
|
|
||||||
|
def take(self, count: int) -> list[int]:
|
||||||
|
count = max(0, int(count))
|
||||||
|
assigned = self.available_ids[:count]
|
||||||
|
del self.available_ids[:len(assigned)]
|
||||||
|
return assigned
|
||||||
|
|
||||||
|
def release(self, ids: Iterable[int]) -> None:
|
||||||
|
self.available_ids = sorted(set(self.available_ids).union(ids))
|
||||||
|
|
||||||
|
def width_for(self, icon_id: int, fallback_width: float) -> float:
|
||||||
|
width = self.widths[icon_id]
|
||||||
|
return fallback_width if width is None else width
|
||||||
|
|
||||||
|
|
||||||
|
class IconContainer(Container):
|
||||||
|
def __init__(self, pool: IconPool, direction: str = "right", *args, **kwargs) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if direction not in ("left", "right"):
|
||||||
|
raise ValueError("Icon direction must be 'left' or 'right'.")
|
||||||
|
self.pool = pool
|
||||||
|
self.direction = direction
|
||||||
|
self.icon_ids: list[int] = []
|
||||||
|
self.dot = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon_count(self) -> int:
|
||||||
|
return len(self.icon_ids)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def start_number(self) -> Optional[int]:
|
||||||
|
return self.icon_ids[0] if self.icon_ids else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dot_width(self) -> float:
|
||||||
|
return self.height * self.lab.dot_width_factor
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
icon_width = sum(self.icon_width(icon_id) for icon_id in self.icon_ids)
|
||||||
|
if not self.dot:
|
||||||
|
return icon_width
|
||||||
|
gap = self.lab.icon_dot_gap if self.icon_ids else 0.0
|
||||||
|
return icon_width + gap + self.dot_width
|
||||||
|
|
||||||
|
def add_icons(self, count: int) -> int:
|
||||||
|
assigned = self.pool.take(count)
|
||||||
|
self.icon_ids.extend(assigned)
|
||||||
|
return len(assigned)
|
||||||
|
|
||||||
|
def return_icons(self, count: int) -> int:
|
||||||
|
count = min(max(0, int(count)), len(self.icon_ids))
|
||||||
|
returned = self.icon_ids[-count:] if count else []
|
||||||
|
if count:
|
||||||
|
del self.icon_ids[-count:]
|
||||||
|
self.pool.release(returned)
|
||||||
|
return len(returned)
|
||||||
|
|
||||||
|
def clear_icons(self) -> None:
|
||||||
|
self.return_icons(len(self.icon_ids))
|
||||||
|
|
||||||
|
def set_dot(self, enabled: bool) -> None:
|
||||||
|
self.dot = bool(enabled)
|
||||||
|
|
||||||
|
def toggle_dot(self) -> bool:
|
||||||
|
self.dot = not self.dot
|
||||||
|
return self.dot
|
||||||
|
|
||||||
|
def segments(self) -> list[tuple[str, Optional[int], float]]:
|
||||||
|
icons = [("icon", icon_id, self.icon_width(icon_id)) for icon_id in self.icon_ids]
|
||||||
|
dot = [("dot", None, self.dot_width)] if self.dot else []
|
||||||
|
if self.dot and self.icon_ids and self.lab.icon_dot_gap > 0:
|
||||||
|
dot.insert(0, ("gap", None, self.lab.icon_dot_gap))
|
||||||
|
return icons + dot if self.direction == "right" else dot + list(reversed(icons))
|
||||||
|
|
||||||
|
def icon_width(self, icon_id: int) -> float:
|
||||||
|
return self.pool.width_for(icon_id, self.height)
|
||||||
|
|
||||||
|
|
||||||
|
class CameraCutout(Container):
|
||||||
|
def __init__(self, lab: "StatusBarLab", width: float, height: float, horizontal_position: float, vertical_position: float) -> None:
|
||||||
|
super().__init__(lab, "camera", height, vertical_position)
|
||||||
|
self._width = float(width)
|
||||||
|
self.x = float(horizontal_position)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> float:
|
||||||
|
return self._width
|
||||||
|
|
||||||
|
def set_width(self, width: float) -> None:
|
||||||
|
self._width = max(0.0, float(width))
|
||||||
|
|
||||||
|
|
||||||
|
class StatusBarLab:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.text_horizontal_padding = 6.0
|
||||||
|
self.icon_dot_gap = 0.0
|
||||||
|
self.dot_width_factor = 0.75
|
||||||
|
self.clock = ClockGroup(self)
|
||||||
|
self.notifications: list[IconContainer] = []
|
||||||
|
self.statuses: list[IconContainer] = []
|
||||||
|
self.chips: list[TextContainer] = []
|
||||||
|
self.carriers: list[TextContainer] = []
|
||||||
|
self.cameras: list[CameraCutout] = []
|
||||||
|
self.notification_pool = IconPool("notification")
|
||||||
|
self.status_pool = IconPool("status")
|
||||||
|
|
||||||
|
def add_notification(self, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left", direction: str = "right") -> IconContainer:
|
||||||
|
item = IconContainer(self.notification_pool, direction, self, "notification", height, vertical_offset, position, cutout_side)
|
||||||
|
self.notifications.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def add_status(self, height: float, vertical_offset: float = 0, position: str = "right", cutout_side: str = "right", direction: str = "left") -> IconContainer:
|
||||||
|
item = IconContainer(self.status_pool, direction, self, "status", height, vertical_offset, position, cutout_side)
|
||||||
|
self.statuses.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def add_chip(self, text: str, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left") -> TextContainer:
|
||||||
|
item = TextContainer(text, self, "chip", height, vertical_offset, position, cutout_side)
|
||||||
|
self.chips.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def add_carrier(self, text: str, height: float, vertical_offset: float = 0, position: str = "left", cutout_side: str = "left") -> TextContainer:
|
||||||
|
item = TextContainer(text, self, "carrier", height, vertical_offset, position, cutout_side)
|
||||||
|
self.carriers.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def add_camera(self, width: float, height: float, horizontal_position: float, vertical_position: float) -> CameraCutout:
|
||||||
|
item = CameraCutout(self, width, height, horizontal_position, vertical_position)
|
||||||
|
self.cameras.append(item)
|
||||||
|
return item
|
||||||
|
|
||||||
|
def configure_notification_icons(self, count: int) -> None:
|
||||||
|
self.notification_pool.configure(count, [])
|
||||||
|
for item in self.notifications:
|
||||||
|
item.icon_ids.clear()
|
||||||
|
|
||||||
|
def configure_status_icons(self, count: int, widths: Iterable[float]) -> None:
|
||||||
|
self.status_pool.configure(count, widths)
|
||||||
|
for item in self.statuses:
|
||||||
|
item.icon_ids.clear()
|
||||||
|
|
||||||
|
def render_containers(self) -> list[Container]:
|
||||||
|
return [*self.clock.lines, *self.notifications, *self.statuses, *self.chips, *self.carriers]
|
||||||
|
|
||||||
|
|
||||||
|
def containers_overlap(first: Container | ClockGroup, second: Container | ClockGroup) -> bool:
|
||||||
|
"""Check horizontal and vertical overlap, expanding a clock into its lines."""
|
||||||
|
first_items = _collision_items(first)
|
||||||
|
second_items = _collision_items(second)
|
||||||
|
for first_item in first_items:
|
||||||
|
for second_item in second_items:
|
||||||
|
if first_item is second_item:
|
||||||
|
continue
|
||||||
|
if rectangles_overlap(first_item.bounds, second_item.bounds):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _collision_items(item: Bounds | Container | ClockGroup) -> list[Container]:
|
||||||
|
if isinstance(item, Bounds):
|
||||||
|
return [_BoundsContainer(item)]
|
||||||
|
return item.lines if isinstance(item, ClockGroup) else [item]
|
||||||
|
|
||||||
|
|
||||||
|
class _BoundsContainer:
|
||||||
|
"""Adapter so the public helpers can accept a raw Bounds instance too."""
|
||||||
|
|
||||||
|
def __init__(self, bounds: Bounds) -> None:
|
||||||
|
self.bounds = bounds
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Tk renderer for the status-bar layout playground."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import font as tkfont
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
from statusbar_lab import CameraCutout, Container, IconContainer, StatusBarLab, rectangles_overlap
|
||||||
|
|
||||||
|
|
||||||
|
TYPE_COLOURS = {
|
||||||
|
"clock": "#9fd7ff",
|
||||||
|
"notification": "#a7efaa",
|
||||||
|
"status": "#ffd596",
|
||||||
|
"chip": "#e2b0ff",
|
||||||
|
"carrier": "#ffb4c4",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def render(
|
||||||
|
lab: StatusBarLab,
|
||||||
|
statusbar_width: float,
|
||||||
|
statusbar_height: float,
|
||||||
|
render_scale: float = 1.0,
|
||||||
|
vertical_scale: float = 2.0,
|
||||||
|
margin: int = 40,
|
||||||
|
) -> None:
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("StatusBarTweak layout playground")
|
||||||
|
width = int(statusbar_width * render_scale + margin * 2)
|
||||||
|
height = int(statusbar_height * vertical_scale + margin * 2)
|
||||||
|
canvas = tk.Canvas(root, width=width, height=height, bg="#242424", highlightthickness=0)
|
||||||
|
canvas.pack()
|
||||||
|
|
||||||
|
def point(x: float, y: float) -> tuple[float, float]:
|
||||||
|
return margin + x * render_scale, margin + (statusbar_height - y) * vertical_scale
|
||||||
|
|
||||||
|
left, bottom = point(0, 0)
|
||||||
|
right, top = point(statusbar_width, statusbar_height)
|
||||||
|
canvas.create_rectangle(left, top, right, bottom, fill="white", outline="#4a4a4a", width=5)
|
||||||
|
|
||||||
|
containers = lab.render_containers()
|
||||||
|
for container in containers:
|
||||||
|
_draw_container(canvas, point, container, render_scale, vertical_scale)
|
||||||
|
|
||||||
|
_draw_overlaps(canvas, point, containers)
|
||||||
|
for camera in lab.cameras:
|
||||||
|
_draw_camera(canvas, point, camera)
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_container(canvas: tk.Canvas, point, container: Container, render_scale: float, vertical_scale: float) -> None:
|
||||||
|
if isinstance(container, IconContainer):
|
||||||
|
_draw_icons(canvas, point, container)
|
||||||
|
return
|
||||||
|
|
||||||
|
x1, y1 = point(container.left, container.bottom)
|
||||||
|
x2, y2 = point(container.right, container.top)
|
||||||
|
canvas.create_rectangle(x1, y2, x2, y1, fill=TYPE_COLOURS[container.kind], outline="black", width=1)
|
||||||
|
_draw_clipped_text(canvas, container.text, x1 + 3, (y1 + y2) / 2, max(0, x2 - x1 - 6), container.height * vertical_scale)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_icons(canvas: tk.Canvas, point, container: IconContainer) -> None:
|
||||||
|
colour = TYPE_COLOURS[container.kind]
|
||||||
|
if not container.segments():
|
||||||
|
x, y1 = point(container.left, container.bottom)
|
||||||
|
_, y2 = point(container.left, container.top)
|
||||||
|
canvas.create_line(x, y1, x, y2, fill="black", width=1)
|
||||||
|
return
|
||||||
|
|
||||||
|
cursor = container.left
|
||||||
|
for segment_kind, icon_id, segment_width in container.segments():
|
||||||
|
x1, y1 = point(cursor, container.bottom)
|
||||||
|
x2, y2 = point(cursor + segment_width, container.top)
|
||||||
|
if segment_kind == "gap":
|
||||||
|
cursor += segment_width
|
||||||
|
continue
|
||||||
|
if segment_kind == "dot":
|
||||||
|
canvas.create_rectangle(x1, y2, x2, y1, fill=colour, outline="black", width=1)
|
||||||
|
radius = min(abs(x2 - x1), abs(y1 - y2)) * 0.24
|
||||||
|
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
|
||||||
|
canvas.create_oval(cx - radius, cy - radius, cx + radius, cy + radius, fill="black", outline="black")
|
||||||
|
else:
|
||||||
|
canvas.create_rectangle(x1, y2, x2, y1, fill=colour, outline="black", width=1)
|
||||||
|
_draw_clipped_text(canvas, str(icon_id), (x1 + x2) / 2, (y1 + y2) / 2, abs(x2 - x1) - 2, abs(y1 - y2), anchor="center")
|
||||||
|
cursor += segment_width
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_camera(canvas: tk.Canvas, point, camera: CameraCutout) -> None:
|
||||||
|
x1, y1 = point(camera.left, camera.bottom)
|
||||||
|
x2, y2 = point(camera.right, camera.top)
|
||||||
|
canvas.create_rectangle(x1, y2, x2, y1, fill="black", outline="black")
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_overlaps(canvas: tk.Canvas, point, containers: Iterable[Container]) -> None:
|
||||||
|
items = list(containers)
|
||||||
|
for index, first in enumerate(items):
|
||||||
|
for second in items[index + 1:]:
|
||||||
|
if first.kind == "clock" and second.kind == "clock":
|
||||||
|
continue
|
||||||
|
if not rectangles_overlap(first.bounds, second.bounds):
|
||||||
|
continue
|
||||||
|
left = max(first.left, second.left)
|
||||||
|
right = min(first.right, second.right)
|
||||||
|
bottom = max(first.bottom, second.bottom)
|
||||||
|
top = min(first.top, second.top)
|
||||||
|
x1, y1 = point(left, bottom)
|
||||||
|
x2, y2 = point(right, top)
|
||||||
|
canvas.create_rectangle(x1, y2, x2, y1, fill="red", outline="red")
|
||||||
|
radius = max(abs(x2 - x1), abs(y1 - y2)) / 2 + 12
|
||||||
|
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
|
||||||
|
canvas.create_oval(cx - radius, cy - radius, cx + radius, cy + radius, outline="red", width=3)
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_clipped_text(
|
||||||
|
canvas: tk.Canvas,
|
||||||
|
text: str,
|
||||||
|
x: float,
|
||||||
|
y: float,
|
||||||
|
width: float,
|
||||||
|
height: float,
|
||||||
|
anchor: str = "w",
|
||||||
|
) -> None:
|
||||||
|
if width <= 0:
|
||||||
|
return
|
||||||
|
size = max(7, int(height * 0.42))
|
||||||
|
text_font = tkfont.Font(family="TkDefaultFont", size=size)
|
||||||
|
clipped = _clip_text(text_font, text, width)
|
||||||
|
canvas.create_text(x, y, text=clipped, font=text_font, fill="black", anchor=anchor)
|
||||||
|
|
||||||
|
|
||||||
|
def _clip_text(text_font: tkfont.Font, text: str, width: float) -> str:
|
||||||
|
if text_font.measure(text) <= width:
|
||||||
|
return text
|
||||||
|
ellipsis = "..."
|
||||||
|
while text and text_font.measure(text + ellipsis) > width:
|
||||||
|
text = text[:-1]
|
||||||
|
return text + ellipsis if text else ""
|
||||||
@@ -21,3 +21,4 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
rootProject.name = "StatusBarTweak"
|
rootProject.name = "StatusBarTweak"
|
||||||
include(":app")
|
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>
|
||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
package se.ajpanton.statusbartweak.harness;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ServiceInfo;
|
||||||
|
import android.media.MediaMetadata;
|
||||||
|
import android.media.session.MediaSession;
|
||||||
|
import android.media.session.PlaybackState;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
public final class HarnessForegroundService extends Service {
|
||||||
|
public static final String ACTION_START_MEDIA = "se.ajpanton.statusbartweak.harness.START_MEDIA";
|
||||||
|
public static final String ACTION_START_FOREGROUND =
|
||||||
|
"se.ajpanton.statusbartweak.harness.START_FOREGROUND";
|
||||||
|
public static final String ACTION_STOP = "se.ajpanton.statusbartweak.harness.STOP";
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "sbt_harness_foreground";
|
||||||
|
private static final int NOTIFICATION_ID = 42_000;
|
||||||
|
|
||||||
|
private MediaSession mediaSession;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
String action = intent != null ? intent.getAction() : null;
|
||||||
|
if (ACTION_STOP.equals(action)) {
|
||||||
|
stopSelf();
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
if (ACTION_START_MEDIA.equals(action)) {
|
||||||
|
startMediaState();
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
startForegroundState();
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (mediaSession != null) {
|
||||||
|
mediaSession.setActive(false);
|
||||||
|
mediaSession.release();
|
||||||
|
mediaSession = null;
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startMediaState() {
|
||||||
|
ensureChannel();
|
||||||
|
if (mediaSession == null) {
|
||||||
|
mediaSession = new MediaSession(this, "StatusBarTweakHarness");
|
||||||
|
}
|
||||||
|
mediaSession.setMetadata(new MediaMetadata.Builder()
|
||||||
|
.putString(MediaMetadata.METADATA_KEY_TITLE, "Harness media")
|
||||||
|
.putString(MediaMetadata.METADATA_KEY_ARTIST, "StatusBarTweak")
|
||||||
|
.build());
|
||||||
|
mediaSession.setPlaybackState(new PlaybackState.Builder()
|
||||||
|
.setState(PlaybackState.STATE_PLAYING, 0, 1f)
|
||||||
|
.setActions(PlaybackState.ACTION_PLAY_PAUSE | PlaybackState.ACTION_STOP)
|
||||||
|
.build());
|
||||||
|
mediaSession.setActive(true);
|
||||||
|
|
||||||
|
Notification.Builder builder = baseBuilder("Harness media", "MediaSession playback active")
|
||||||
|
.setCategory(Notification.CATEGORY_TRANSPORT)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setStyle(new Notification.MediaStyle()
|
||||||
|
.setMediaSession(mediaSession.getSessionToken())
|
||||||
|
.setShowActionsInCompactView(0))
|
||||||
|
.addAction(R.drawable.ic_notify, "Stop", stopIntent());
|
||||||
|
startForegroundCompat(builder.build(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startForegroundState() {
|
||||||
|
ensureChannel();
|
||||||
|
Notification notification = baseBuilder(
|
||||||
|
"Harness foreground service",
|
||||||
|
"Generic foreground service notification active")
|
||||||
|
.setCategory(Notification.CATEGORY_SERVICE)
|
||||||
|
.setOngoing(true)
|
||||||
|
.addAction(R.drawable.ic_notify, "Stop", stopIntent())
|
||||||
|
.build();
|
||||||
|
startForegroundCompat(notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification.Builder baseBuilder(String title, String text) {
|
||||||
|
Intent launch = new Intent(this, MainActivity.class);
|
||||||
|
PendingIntent launchIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
1,
|
||||||
|
launch,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
return new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setContentIntent(launchIntent)
|
||||||
|
.setShowWhen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PendingIntent stopIntent() {
|
||||||
|
Intent stop = new Intent(this, HarnessForegroundService.class);
|
||||||
|
stop.setAction(ACTION_STOP);
|
||||||
|
return PendingIntent.getService(
|
||||||
|
this,
|
||||||
|
2,
|
||||||
|
stop,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startForegroundCompat(Notification notification, int type) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 29) {
|
||||||
|
startForeground(NOTIFICATION_ID, notification, type);
|
||||||
|
} else {
|
||||||
|
startForeground(NOTIFICATION_ID, notification);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureChannel() {
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"StatusBarTweak harness foreground",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
channel.setDescription("Foreground and media scenarios for StatusBarTweak testing");
|
||||||
|
channel.setShowBadge(false);
|
||||||
|
nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
package se.ajpanton.statusbartweak.harness;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
public final class MainActivity extends Activity {
|
||||||
|
public static final String EXTRA_COMMAND = "command";
|
||||||
|
public static final String COMMAND_POST_NORMAL = "post_normal";
|
||||||
|
public static final String COMMAND_POST_ONGOING = "post_ongoing";
|
||||||
|
public static final String COMMAND_POST_GROUPED = "post_grouped";
|
||||||
|
public static final String COMMAND_UPDATE = "update";
|
||||||
|
public static final String COMMAND_START_CHRONOMETER = "start_chronometer";
|
||||||
|
public static final String COMMAND_START_CHURN = "start_churn";
|
||||||
|
public static final String COMMAND_STOP_CHURN = "stop_churn";
|
||||||
|
public static final String COMMAND_START_MEDIA = "start_media";
|
||||||
|
public static final String COMMAND_START_FOREGROUND = "start_foreground";
|
||||||
|
public static final String COMMAND_STOP_SERVICE = "stop_service";
|
||||||
|
public static final String COMMAND_CLEAR = "clear";
|
||||||
|
|
||||||
|
private static final String CHANNEL_ID = "sbt_harness";
|
||||||
|
private static final int NOTIFICATION_BASE_ID = 41_000;
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private int updatingCounter = 0;
|
||||||
|
private int churnCounter = 0;
|
||||||
|
private Runnable churnRunnable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
setContentView(createContent());
|
||||||
|
handleCommand(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
handleCommand(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createContent() {
|
||||||
|
ScrollView scroll = new ScrollView(this);
|
||||||
|
scroll.setBackgroundColor(0xfff7efe5);
|
||||||
|
LinearLayout root = new LinearLayout(this);
|
||||||
|
root.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
root.setPadding(dp(20), dp(20), dp(20), dp(28));
|
||||||
|
scroll.addView(root, new ScrollView.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText("StatusBarTweak Test Harness");
|
||||||
|
title.setTextColor(Color.BLACK);
|
||||||
|
title.setTextSize(24);
|
||||||
|
title.setGravity(Gravity.START);
|
||||||
|
root.addView(title, matchWrap());
|
||||||
|
|
||||||
|
TextView description = new TextView(this);
|
||||||
|
description.setText(
|
||||||
|
"Open a scenario, then observe StatusBarTweak behavior. "
|
||||||
|
+ "The same APK can be used on emulators and physical devices.");
|
||||||
|
description.setTextColor(0xff333333);
|
||||||
|
description.setTextSize(15);
|
||||||
|
description.setPadding(0, dp(8), 0, dp(12));
|
||||||
|
root.addView(description, matchWrap());
|
||||||
|
|
||||||
|
addSection(root, "Statusbar surfaces");
|
||||||
|
addScenario(root, "Opaque light statusbar", ScenarioActivity.SCENARIO_OPAQUE_LIGHT);
|
||||||
|
addScenario(root, "Opaque dark statusbar", ScenarioActivity.SCENARIO_OPAQUE_DARK);
|
||||||
|
addScenario(root, "Transparent statusbar", ScenarioActivity.SCENARIO_TRANSPARENT);
|
||||||
|
addScenario(root, "Fullscreen portrait", ScenarioActivity.SCENARIO_FULLSCREEN_PORTRAIT);
|
||||||
|
addScenario(root, "Fullscreen landscape", ScenarioActivity.SCENARIO_FULLSCREEN_LANDSCAPE);
|
||||||
|
|
||||||
|
addSection(root, "Privacy indicators");
|
||||||
|
addScenario(root, "Camera privacy indicator", ScenarioActivity.SCENARIO_CAMERA);
|
||||||
|
addScenario(root, "Microphone privacy indicator", ScenarioActivity.SCENARIO_MICROPHONE);
|
||||||
|
addScenario(root, "Camera + microphone indicators", ScenarioActivity.SCENARIO_CAMERA_MICROPHONE);
|
||||||
|
|
||||||
|
addSection(root, "Chip-like public states");
|
||||||
|
addButton(root, "Start media session", v -> startHarnessService(
|
||||||
|
HarnessForegroundService.ACTION_START_MEDIA));
|
||||||
|
addButton(root, "Start foreground service", v -> startHarnessService(
|
||||||
|
HarnessForegroundService.ACTION_START_FOREGROUND));
|
||||||
|
addButton(root, "Post stopwatch/chronometer", v -> postChronometerNotification());
|
||||||
|
addButton(root, "Stop media/foreground service", v -> stopHarnessService());
|
||||||
|
|
||||||
|
addSection(root, "Notifications");
|
||||||
|
addButton(root, "Post normal notification", v -> postNotification(1, false, false, "Normal"));
|
||||||
|
addButton(root, "Post ongoing notification", v -> postNotification(2, true, false, "Ongoing"));
|
||||||
|
addButton(root, "Post grouped notifications", v -> postGroupedNotifications());
|
||||||
|
addButton(root, "Update notification text", v -> {
|
||||||
|
updatingCounter++;
|
||||||
|
postNotification(3, false, true, "Updated " + updatingCounter);
|
||||||
|
});
|
||||||
|
addButton(root, "Start notification churn", v -> startNotificationChurn());
|
||||||
|
addButton(root, "Stop notification churn", v -> stopNotificationChurn());
|
||||||
|
addButton(root, "Clear harness notifications", v -> {
|
||||||
|
stopNotificationChurn();
|
||||||
|
clearNotifications();
|
||||||
|
});
|
||||||
|
|
||||||
|
return scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCommand(Intent intent) {
|
||||||
|
if (intent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String command = intent.getStringExtra(EXTRA_COMMAND);
|
||||||
|
if (command == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (command) {
|
||||||
|
case COMMAND_POST_NORMAL:
|
||||||
|
postNotification(1, false, false, "Normal");
|
||||||
|
break;
|
||||||
|
case COMMAND_POST_ONGOING:
|
||||||
|
postNotification(2, true, false, "Ongoing");
|
||||||
|
break;
|
||||||
|
case COMMAND_POST_GROUPED:
|
||||||
|
postGroupedNotifications();
|
||||||
|
break;
|
||||||
|
case COMMAND_UPDATE:
|
||||||
|
updatingCounter++;
|
||||||
|
postNotification(3, false, true, "Updated " + updatingCounter);
|
||||||
|
break;
|
||||||
|
case COMMAND_START_CHRONOMETER:
|
||||||
|
postChronometerNotification();
|
||||||
|
break;
|
||||||
|
case COMMAND_START_CHURN:
|
||||||
|
startNotificationChurn();
|
||||||
|
break;
|
||||||
|
case COMMAND_STOP_CHURN:
|
||||||
|
stopNotificationChurn();
|
||||||
|
break;
|
||||||
|
case COMMAND_START_MEDIA:
|
||||||
|
startHarnessService(HarnessForegroundService.ACTION_START_MEDIA);
|
||||||
|
break;
|
||||||
|
case COMMAND_START_FOREGROUND:
|
||||||
|
startHarnessService(HarnessForegroundService.ACTION_START_FOREGROUND);
|
||||||
|
break;
|
||||||
|
case COMMAND_STOP_SERVICE:
|
||||||
|
stopHarnessService();
|
||||||
|
break;
|
||||||
|
case COMMAND_CLEAR:
|
||||||
|
stopNotificationChurn();
|
||||||
|
clearNotifications();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addScenario(LinearLayout root, String label, String scenario) {
|
||||||
|
addButton(root, label, v -> {
|
||||||
|
Intent intent = new Intent(this, ScenarioActivity.class);
|
||||||
|
intent.putExtra(ScenarioActivity.EXTRA_SCENARIO, scenario);
|
||||||
|
startActivity(intent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSection(LinearLayout root, String label) {
|
||||||
|
TextView view = new TextView(this);
|
||||||
|
view.setText(label);
|
||||||
|
view.setTextColor(0xff444444);
|
||||||
|
view.setTextSize(17);
|
||||||
|
view.setPadding(0, dp(18), 0, dp(6));
|
||||||
|
root.addView(view, matchWrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
|
||||||
|
Button button = new Button(this);
|
||||||
|
button.setText(label);
|
||||||
|
button.setAllCaps(false);
|
||||||
|
button.setOnClickListener(listener);
|
||||||
|
LinearLayout.LayoutParams params = matchWrap();
|
||||||
|
params.setMargins(0, dp(4), 0, dp(4));
|
||||||
|
root.addView(button, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postNotification(int slot, boolean ongoing, boolean alertOnlyOnce, String label) {
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null || !canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureChannel(nm);
|
||||||
|
Intent launch = new Intent(this, MainActivity.class);
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
slot,
|
||||||
|
launch,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
|
||||||
|
Notification.Builder builder = new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle("Harness " + label)
|
||||||
|
.setContentText("Package: " + getPackageName())
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setShowWhen(true)
|
||||||
|
.setOnlyAlertOnce(alertOnlyOnce)
|
||||||
|
.setCategory(Notification.CATEGORY_STATUS);
|
||||||
|
if (ongoing) {
|
||||||
|
builder.setOngoing(true);
|
||||||
|
}
|
||||||
|
nm.notify(NOTIFICATION_BASE_ID + slot, builder.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postGroupedNotifications() {
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null || !canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureChannel(nm);
|
||||||
|
String group = "sbt_harness_group";
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
Notification notification = new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle("Harness grouped " + (i + 1))
|
||||||
|
.setContentText("Grouped notification")
|
||||||
|
.setGroup(group)
|
||||||
|
.setShowWhen(false)
|
||||||
|
.build();
|
||||||
|
nm.notify(NOTIFICATION_BASE_ID + 10 + i, notification);
|
||||||
|
}
|
||||||
|
Notification summary = new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle("Harness group")
|
||||||
|
.setContentText("Group summary")
|
||||||
|
.setGroup(group)
|
||||||
|
.setGroupSummary(true)
|
||||||
|
.build();
|
||||||
|
nm.notify(NOTIFICATION_BASE_ID + 20, summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void postChronometerNotification() {
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm == null || !canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureChannel(nm);
|
||||||
|
Notification notification = new Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle("Harness stopwatch")
|
||||||
|
.setContentText("Chronometer notification active")
|
||||||
|
.setWhen(System.currentTimeMillis())
|
||||||
|
.setUsesChronometer(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setOnlyAlertOnce(true)
|
||||||
|
.setCategory(Notification.CATEGORY_STATUS)
|
||||||
|
.build();
|
||||||
|
nm.notify(NOTIFICATION_BASE_ID + 30, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startNotificationChurn() {
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
if (!canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (churnRunnable != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
churnRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
churnCounter++;
|
||||||
|
postNotification(40, false, true, "Churn " + churnCounter);
|
||||||
|
postNotification(41, true, true, "Ongoing churn " + churnCounter);
|
||||||
|
if (churnCounter % 2 == 0) {
|
||||||
|
postNotification(42, false, true, "Intermittent " + churnCounter);
|
||||||
|
} else {
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.cancel(NOTIFICATION_BASE_ID + 42);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
churnRunnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopNotificationChurn() {
|
||||||
|
if (churnRunnable != null) {
|
||||||
|
handler.removeCallbacks(churnRunnable);
|
||||||
|
churnRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startHarnessService(String action) {
|
||||||
|
requestNotificationPermissionIfNeeded();
|
||||||
|
if (!canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(this, HarnessForegroundService.class);
|
||||||
|
intent.setAction(action);
|
||||||
|
startForegroundService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopHarnessService() {
|
||||||
|
Intent intent = new Intent(this, HarnessForegroundService.class);
|
||||||
|
intent.setAction(HarnessForegroundService.ACTION_STOP);
|
||||||
|
startService(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearNotifications() {
|
||||||
|
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||||
|
if (nm != null) {
|
||||||
|
nm.cancelAll();
|
||||||
|
}
|
||||||
|
stopHarnessService();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureChannel(NotificationManager nm) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"StatusBarTweak harness",
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT);
|
||||||
|
channel.setDescription("Scenario notifications for StatusBarTweak compatibility testing");
|
||||||
|
channel.setShowBadge(false);
|
||||||
|
nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requestNotificationPermissionIfNeeded() {
|
||||||
|
if (Build.VERSION.SDK_INT < 33 || canPostNotifications()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canPostNotifications() {
|
||||||
|
return Build.VERSION.SDK_INT < 33
|
||||||
|
|| checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout.LayoutParams matchWrap() {
|
||||||
|
return new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
package se.ajpanton.statusbartweak.harness;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.hardware.camera2.CameraAccessException;
|
||||||
|
import android.hardware.camera2.CameraDevice;
|
||||||
|
import android.hardware.camera2.CameraManager;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.HandlerThread;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowInsets;
|
||||||
|
import android.view.WindowInsetsController;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
public final class ScenarioActivity extends Activity {
|
||||||
|
public static final String EXTRA_SCENARIO = "scenario";
|
||||||
|
public static final String SCENARIO_OPAQUE_LIGHT = "opaque_light";
|
||||||
|
public static final String SCENARIO_OPAQUE_DARK = "opaque_dark";
|
||||||
|
public static final String SCENARIO_TRANSPARENT = "transparent";
|
||||||
|
public static final String SCENARIO_FULLSCREEN_PORTRAIT = "fullscreen_portrait";
|
||||||
|
public static final String SCENARIO_FULLSCREEN_LANDSCAPE = "fullscreen_landscape";
|
||||||
|
public static final String SCENARIO_CAMERA = "camera";
|
||||||
|
public static final String SCENARIO_MICROPHONE = "microphone";
|
||||||
|
public static final String SCENARIO_CAMERA_MICROPHONE = "camera_microphone";
|
||||||
|
|
||||||
|
private static final int REQUEST_CAMERA = 301;
|
||||||
|
private static final int REQUEST_MICROPHONE = 302;
|
||||||
|
private static final int REQUEST_CAMERA_MICROPHONE = 303;
|
||||||
|
|
||||||
|
private HandlerThread cameraThread;
|
||||||
|
private Handler cameraHandler;
|
||||||
|
private CameraDevice cameraDevice;
|
||||||
|
private AudioRecord audioRecord;
|
||||||
|
private Thread audioThread;
|
||||||
|
private volatile boolean audioRunning;
|
||||||
|
private TextView statusText;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
applyScenario(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
stopCamera();
|
||||||
|
stopMicrophone();
|
||||||
|
applyScenario(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyScenario(Intent intent) {
|
||||||
|
String scenario = intent != null ? intent.getStringExtra(EXTRA_SCENARIO) : null;
|
||||||
|
if (scenario == null) {
|
||||||
|
scenario = SCENARIO_OPAQUE_LIGHT;
|
||||||
|
}
|
||||||
|
final String activeScenario = scenario;
|
||||||
|
boolean lightBars = configureWindow(activeScenario);
|
||||||
|
View content = createScenarioView(activeScenario);
|
||||||
|
setContentView(content);
|
||||||
|
content.post(() -> {
|
||||||
|
setLightBars(lightBars);
|
||||||
|
if (isFullscreen(activeScenario)) {
|
||||||
|
hideSystemBars();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
startScenarioIfNeeded(activeScenario);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
stopCamera();
|
||||||
|
stopMicrophone();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||||
|
boolean granted = true;
|
||||||
|
for (int result : grantResults) {
|
||||||
|
granted &= result == PackageManager.PERMISSION_GRANTED;
|
||||||
|
}
|
||||||
|
granted &= grantResults.length > 0;
|
||||||
|
if (!granted) {
|
||||||
|
setStatus("Permission denied.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (requestCode == REQUEST_CAMERA) {
|
||||||
|
startCamera();
|
||||||
|
} else if (requestCode == REQUEST_MICROPHONE) {
|
||||||
|
startMicrophone();
|
||||||
|
} else if (requestCode == REQUEST_CAMERA_MICROPHONE) {
|
||||||
|
startCameraAndMicrophone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean configureWindow(String scenario) {
|
||||||
|
Window window = getWindow();
|
||||||
|
if (Build.VERSION.SDK_INT >= 28) {
|
||||||
|
window.getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario)) {
|
||||||
|
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||||
|
} else if (SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)) {
|
||||||
|
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
|
||||||
|
window.setStatusBarColor(Color.WHITE);
|
||||||
|
window.setNavigationBarColor(Color.WHITE);
|
||||||
|
return true;
|
||||||
|
} else if (SCENARIO_OPAQUE_DARK.equals(scenario)) {
|
||||||
|
window.setStatusBarColor(Color.BLACK);
|
||||||
|
window.setNavigationBarColor(Color.BLACK);
|
||||||
|
return false;
|
||||||
|
} else if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||||
|
window.setStatusBarColor(Color.TRANSPARENT);
|
||||||
|
window.setNavigationBarColor(Color.TRANSPARENT);
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
window.setDecorFitsSystemWindows(false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (isFullscreen(scenario)) {
|
||||||
|
window.setStatusBarColor(Color.TRANSPARENT);
|
||||||
|
window.setNavigationBarColor(Color.BLACK);
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
window.setDecorFitsSystemWindows(false);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
window.setStatusBarColor(0xff202124);
|
||||||
|
window.setNavigationBarColor(0xff202124);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private View createScenarioView(String scenario) {
|
||||||
|
LinearLayout root = new LinearLayout(this);
|
||||||
|
root.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
root.setGravity(Gravity.CENTER);
|
||||||
|
root.setPadding(dp(24), dp(60), dp(24), dp(60));
|
||||||
|
root.setBackgroundColor(backgroundColor(scenario));
|
||||||
|
|
||||||
|
TextView title = new TextView(this);
|
||||||
|
title.setText(titleFor(scenario));
|
||||||
|
title.setTextColor(textColor(scenario));
|
||||||
|
title.setTextSize(28);
|
||||||
|
title.setGravity(Gravity.CENTER);
|
||||||
|
root.addView(title, matchWrap());
|
||||||
|
|
||||||
|
statusText = new TextView(this);
|
||||||
|
statusText.setText(statusFor(scenario));
|
||||||
|
statusText.setTextColor(textColor(scenario));
|
||||||
|
statusText.setAlpha(0.85f);
|
||||||
|
statusText.setTextSize(16);
|
||||||
|
statusText.setGravity(Gravity.CENTER);
|
||||||
|
statusText.setPadding(0, dp(12), 0, dp(20));
|
||||||
|
root.addView(statusText, matchWrap());
|
||||||
|
|
||||||
|
if (isFullscreen(scenario)) {
|
||||||
|
addButton(root, "Temporarily show system bars", v -> showSystemBarsBriefly());
|
||||||
|
}
|
||||||
|
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||||
|
addButton(root, "Start camera privacy indicator", v -> startCamera());
|
||||||
|
addButton(root, "Stop camera", v -> stopCamera());
|
||||||
|
}
|
||||||
|
if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||||
|
addButton(root, "Start microphone privacy indicator", v -> startMicrophone());
|
||||||
|
addButton(root, "Stop microphone", v -> stopMicrophone());
|
||||||
|
}
|
||||||
|
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||||
|
addButton(root, "Start both indicators", v -> startCameraAndMicrophone());
|
||||||
|
addButton(root, "Stop both", v -> {
|
||||||
|
stopCamera();
|
||||||
|
stopMicrophone();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addButton(root, "Finish scenario", v -> finish());
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startScenarioIfNeeded(String scenario) {
|
||||||
|
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||||
|
startCamera();
|
||||||
|
} else if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||||
|
startMicrophone();
|
||||||
|
} else if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||||
|
startCameraAndMicrophone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int backgroundColor(String scenario) {
|
||||||
|
if (SCENARIO_OPAQUE_LIGHT.equals(scenario)) {
|
||||||
|
return Color.WHITE;
|
||||||
|
}
|
||||||
|
if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||||
|
return 0xff0d47a1;
|
||||||
|
}
|
||||||
|
if (isFullscreen(scenario)) {
|
||||||
|
return 0xff1b5e20;
|
||||||
|
}
|
||||||
|
return 0xff101214;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int textColor(String scenario) {
|
||||||
|
return SCENARIO_OPAQUE_LIGHT.equals(scenario) ? Color.BLACK : Color.WHITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String titleFor(String scenario) {
|
||||||
|
switch (scenario) {
|
||||||
|
case SCENARIO_OPAQUE_LIGHT:
|
||||||
|
return "Opaque light statusbar";
|
||||||
|
case SCENARIO_OPAQUE_DARK:
|
||||||
|
return "Opaque dark statusbar";
|
||||||
|
case SCENARIO_TRANSPARENT:
|
||||||
|
return "Transparent statusbar";
|
||||||
|
case SCENARIO_FULLSCREEN_PORTRAIT:
|
||||||
|
return "Fullscreen portrait";
|
||||||
|
case SCENARIO_FULLSCREEN_LANDSCAPE:
|
||||||
|
return "Fullscreen landscape";
|
||||||
|
case SCENARIO_CAMERA:
|
||||||
|
return "Camera privacy indicator";
|
||||||
|
case SCENARIO_MICROPHONE:
|
||||||
|
return "Microphone privacy indicator";
|
||||||
|
case SCENARIO_CAMERA_MICROPHONE:
|
||||||
|
return "Camera + microphone indicators";
|
||||||
|
default:
|
||||||
|
return "Scenario";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String statusFor(String scenario) {
|
||||||
|
if (isFullscreen(scenario)) {
|
||||||
|
return "System bars are hidden. Swipe from an edge or use the button below to reveal them.";
|
||||||
|
}
|
||||||
|
if (SCENARIO_TRANSPARENT.equals(scenario)) {
|
||||||
|
return "Content is laid out behind a transparent statusbar.";
|
||||||
|
}
|
||||||
|
if (SCENARIO_CAMERA.equals(scenario)) {
|
||||||
|
return "Camera should stay open until stopped or this screen is closed.";
|
||||||
|
}
|
||||||
|
if (SCENARIO_MICROPHONE.equals(scenario)) {
|
||||||
|
return "AudioRecord should stay active until stopped or this screen is closed.";
|
||||||
|
}
|
||||||
|
if (SCENARIO_CAMERA_MICROPHONE.equals(scenario)) {
|
||||||
|
return "Camera and AudioRecord should stay active until stopped or this screen is closed.";
|
||||||
|
}
|
||||||
|
return "This screen uses a normal app statusbar.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCamera() {
|
||||||
|
if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cameraDevice != null) {
|
||||||
|
setStatus("Camera already active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CameraManager manager = getSystemService(CameraManager.class);
|
||||||
|
if (manager == null) {
|
||||||
|
setStatus("CameraManager unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
String[] ids = manager.getCameraIdList();
|
||||||
|
if (ids.length == 0) {
|
||||||
|
setStatus("No camera reported by this device.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cameraThread = new HandlerThread("SbtHarnessCamera");
|
||||||
|
cameraThread.start();
|
||||||
|
cameraHandler = new Handler(cameraThread.getLooper());
|
||||||
|
manager.openCamera(ids[0], new CameraDevice.StateCallback() {
|
||||||
|
@Override
|
||||||
|
public void onOpened(CameraDevice camera) {
|
||||||
|
cameraDevice = camera;
|
||||||
|
setStatus("Camera active: " + ids[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisconnected(CameraDevice camera) {
|
||||||
|
stopCamera();
|
||||||
|
setStatus("Camera disconnected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(CameraDevice camera, int error) {
|
||||||
|
stopCamera();
|
||||||
|
setStatus("Camera error: " + error);
|
||||||
|
}
|
||||||
|
}, cameraHandler);
|
||||||
|
} catch (CameraAccessException | SecurityException | IllegalArgumentException e) {
|
||||||
|
stopCamera();
|
||||||
|
setStatus("Camera failed: " + e.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCameraAndMicrophone() {
|
||||||
|
boolean hasCamera = checkSelfPermission(Manifest.permission.CAMERA)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
boolean hasMic = checkSelfPermission(Manifest.permission.RECORD_AUDIO)
|
||||||
|
== PackageManager.PERMISSION_GRANTED;
|
||||||
|
if (!hasCamera || !hasMic) {
|
||||||
|
requestPermissions(
|
||||||
|
new String[]{Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO},
|
||||||
|
REQUEST_CAMERA_MICROPHONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startCamera();
|
||||||
|
startMicrophone();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopCamera() {
|
||||||
|
if (cameraDevice != null) {
|
||||||
|
cameraDevice.close();
|
||||||
|
cameraDevice = null;
|
||||||
|
}
|
||||||
|
if (cameraThread != null) {
|
||||||
|
cameraThread.quitSafely();
|
||||||
|
cameraThread = null;
|
||||||
|
cameraHandler = null;
|
||||||
|
}
|
||||||
|
setStatus("Camera stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startMicrophone() {
|
||||||
|
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissions(new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_MICROPHONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (audioRecord != null) {
|
||||||
|
setStatus("Microphone already active.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int sampleRate = 8000;
|
||||||
|
int minBuffer = AudioRecord.getMinBufferSize(
|
||||||
|
sampleRate,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT);
|
||||||
|
int bufferSize = Math.max(minBuffer, 2048);
|
||||||
|
try {
|
||||||
|
audioRecord = new AudioRecord(
|
||||||
|
MediaRecorder.AudioSource.MIC,
|
||||||
|
sampleRate,
|
||||||
|
AudioFormat.CHANNEL_IN_MONO,
|
||||||
|
AudioFormat.ENCODING_PCM_16BIT,
|
||||||
|
bufferSize);
|
||||||
|
audioRecord.startRecording();
|
||||||
|
audioRunning = true;
|
||||||
|
audioThread = new Thread(() -> {
|
||||||
|
byte[] buffer = new byte[bufferSize];
|
||||||
|
while (audioRunning && audioRecord != null) {
|
||||||
|
audioRecord.read(buffer, 0, buffer.length);
|
||||||
|
}
|
||||||
|
}, "SbtHarnessMic");
|
||||||
|
audioThread.start();
|
||||||
|
setStatus("Microphone active.");
|
||||||
|
} catch (SecurityException | IllegalStateException | IllegalArgumentException e) {
|
||||||
|
stopMicrophone();
|
||||||
|
setStatus("Microphone failed: " + e.getClass().getSimpleName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopMicrophone() {
|
||||||
|
audioRunning = false;
|
||||||
|
if (audioRecord != null) {
|
||||||
|
try {
|
||||||
|
audioRecord.stop();
|
||||||
|
} catch (IllegalStateException ignored) {
|
||||||
|
}
|
||||||
|
audioRecord.release();
|
||||||
|
audioRecord = null;
|
||||||
|
}
|
||||||
|
audioThread = null;
|
||||||
|
setStatus("Microphone stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setStatus(String status) {
|
||||||
|
if (statusText != null) {
|
||||||
|
runOnUiThread(() -> statusText.setText(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFullscreen(String scenario) {
|
||||||
|
return SCENARIO_FULLSCREEN_PORTRAIT.equals(scenario)
|
||||||
|
|| SCENARIO_FULLSCREEN_LANDSCAPE.equals(scenario);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideSystemBars() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
WindowInsetsController controller = getWindow().getInsetsController();
|
||||||
|
if (controller != null) {
|
||||||
|
controller.setSystemBarsBehavior(
|
||||||
|
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||||
|
controller.hide(WindowInsets.Type.systemBars());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(
|
||||||
|
View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||||
|
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSystemBarsBriefly() {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
WindowInsetsController controller = getWindow().getInsetsController();
|
||||||
|
if (controller != null) {
|
||||||
|
controller.show(WindowInsets.Type.systemBars());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
|
||||||
|
}
|
||||||
|
getWindow().getDecorView().postDelayed(this::hideSystemBars, 2500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setLightBars(boolean lightBars) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 30) {
|
||||||
|
WindowInsetsController controller = getWindow().getInsetsController();
|
||||||
|
if (controller != null) {
|
||||||
|
int mask = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
|
||||||
|
| WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
|
||||||
|
controller.setSystemBarsAppearance(lightBars ? mask : 0, mask);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int flags = getWindow().getDecorView().getSystemUiVisibility();
|
||||||
|
if (lightBars) {
|
||||||
|
flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||||
|
flags |= View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||||
|
} else {
|
||||||
|
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
|
||||||
|
flags &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
|
||||||
|
}
|
||||||
|
getWindow().getDecorView().setSystemUiVisibility(flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addButton(LinearLayout root, String label, View.OnClickListener listener) {
|
||||||
|
Button button = new Button(this);
|
||||||
|
button.setText(label);
|
||||||
|
button.setAllCaps(false);
|
||||||
|
button.setOnClickListener(listener);
|
||||||
|
LinearLayout.LayoutParams params = matchWrap();
|
||||||
|
params.setMargins(0, dp(8), 0, 0);
|
||||||
|
root.addView(button, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinearLayout.LayoutParams matchWrap() {
|
||||||
|
return new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int dp(int value) {
|
||||||
|
return Math.round(value * getResources().getDisplayMetrics().density);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="48dp"
|
||||||
|
android:height="48dp"
|
||||||
|
android:viewportWidth="48"
|
||||||
|
android:viewportHeight="48">
|
||||||
|
<path
|
||||||
|
android:fillColor="#202124"
|
||||||
|
android:pathData="M4,4h40v40h-40z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00E676"
|
||||||
|
android:pathData="M4,4h32v5h-32z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M8,16h32v4h-32zM8,24h24v4h-24zM8,32h16v4h-16z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M4,5h16v11h-12l-4,4z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="harness_accent">#00E676</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">SBT Test Harness</string>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<style name="Theme.StatusBarTweakHarness" parent="@android:style/Theme.Material.NoActionBar">
|
||||||
|
<item name="android:fontFamily">sans</item>
|
||||||
|
<item name="android:windowActionModeOverlay">true</item>
|
||||||
|
<item name="android:navigationBarColor">#111111</item>
|
||||||
|
<item name="android:colorAccent">@color/harness_accent</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
version=0.4
|
version=0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user