Initial clean project state
This commit is contained in:
+23
@@ -0,0 +1,23 @@
|
||||
# Local/private workspace files
|
||||
/local/
|
||||
/local.properties
|
||||
/project.md
|
||||
/build.sh
|
||||
|
||||
# Build output
|
||||
/.gradle/
|
||||
/.gradle-tmp/
|
||||
/build/
|
||||
/app/build/
|
||||
/.externalNativeBuild/
|
||||
/.cxx/
|
||||
/captures/
|
||||
|
||||
# IDE and OS noise
|
||||
*.iml
|
||||
/.idea/
|
||||
.DS_Store
|
||||
|
||||
# Generated logs/caches
|
||||
*.log
|
||||
__pycache__/
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,144 @@
|
||||
import java.io.File
|
||||
import java.util.Properties
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
}
|
||||
|
||||
data class SharedReleaseSigning(
|
||||
val storeFile: File,
|
||||
val keyAlias: String,
|
||||
val storePassword: String,
|
||||
val keyPassword: String,
|
||||
)
|
||||
|
||||
val fallbackVersionCode = Instant.now().epochSecond
|
||||
.coerceAtMost(Int.MAX_VALUE.toLong())
|
||||
.toInt()
|
||||
val fallbackVersionName = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss")
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(Instant.ofEpochSecond(fallbackVersionCode.toLong()))
|
||||
|
||||
fun loadSharedReleaseSigning(project: org.gradle.api.Project): SharedReleaseSigning? {
|
||||
val userHome = System.getProperty("user.home") ?: return null
|
||||
val configFile = File(userHome, ".config/android/release-signing.properties")
|
||||
if (!configFile.isFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
val props = Properties().apply {
|
||||
configFile.inputStream().use(::load)
|
||||
}
|
||||
|
||||
val rawStoreFile = props.getProperty("storeFile")?.trim().orEmpty()
|
||||
val keyAlias = props.getProperty("keyAlias")?.trim().orEmpty()
|
||||
val storePassword = project.providers.gradleProperty("androidReleaseStorePassword").orNull?.trim().orEmpty()
|
||||
val keyPassword = project.providers.gradleProperty("androidReleaseKeyPassword").orNull?.trim().orEmpty()
|
||||
|
||||
if (rawStoreFile.isEmpty() || keyAlias.isEmpty() || storePassword.isEmpty() || keyPassword.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val storeFile = File(rawStoreFile).let { candidate ->
|
||||
if (candidate.isAbsolute) candidate else File(configFile.parentFile, rawStoreFile)
|
||||
}
|
||||
if (!storeFile.isFile) {
|
||||
return null
|
||||
}
|
||||
|
||||
return SharedReleaseSigning(
|
||||
storeFile = storeFile,
|
||||
keyAlias = keyAlias,
|
||||
storePassword = storePassword,
|
||||
keyPassword = keyPassword,
|
||||
)
|
||||
}
|
||||
|
||||
val sharedReleaseSigning = loadSharedReleaseSigning(project)
|
||||
val releaseTaskRequested = gradle.startParameter.taskNames.any { taskName ->
|
||||
taskName.contains("Release", ignoreCase = true)
|
||||
|| taskName.contains("Bundle", ignoreCase = true)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "se.ajpanton.statusbartweak"
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "se.ajpanton.statusbartweak"
|
||||
minSdk = 35
|
||||
targetSdk = 36
|
||||
val buildVersionName = project.findProperty("sbtVersionName")?.toString()
|
||||
?: fallbackVersionName
|
||||
val buildVersionCode = project.findProperty("sbtVersionCode")?.toString()?.toIntOrNull()
|
||||
?: fallbackVersionCode
|
||||
versionCode = buildVersionCode
|
||||
versionName = buildVersionName
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
if (sharedReleaseSigning != null) {
|
||||
create("release") {
|
||||
storeFile = sharedReleaseSigning.storeFile
|
||||
storePassword = sharedReleaseSigning.storePassword
|
||||
keyAlias = sharedReleaseSigning.keyAlias
|
||||
keyPassword = sharedReleaseSigning.keyPassword
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
// Intentionally unchanged: local debug installs keep using the default debug signing.
|
||||
}
|
||||
getByName("release") {
|
||||
if (sharedReleaseSigning != null) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
// Modern replacement for packagingOptions
|
||||
packaging {
|
||||
resources {
|
||||
excludes += setOf(
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/NOTICE"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (releaseTaskRequested && sharedReleaseSigning == null) {
|
||||
throw GradleException(
|
||||
buildString {
|
||||
appendLine("Release signing is not configured.")
|
||||
appendLine("Expected:")
|
||||
appendLine("- ~/.config/android/release-signing.properties with storeFile and keyAlias")
|
||||
appendLine("- ~/.gradle/gradle.properties with androidReleaseStorePassword and androidReleaseKeyPassword")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
|
||||
// Modern Xposed API, provided by the official libxposed Maven artifact
|
||||
compileOnly("io.github.libxposed:api:101.0.1")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
}
|
||||
Vendored
+21
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,26 @@
|
||||
package se.ajpanton.statusbartweak;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
assertEquals("se.ajpanton.statusbartweak", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
@@ -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.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:label="@string/app_name"
|
||||
android:description="@string/module_description"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
<activity
|
||||
android:name=".shell.ui.MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.MoreNotis.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<receiver
|
||||
android:name=".shell.debug.DebugBootReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<receiver
|
||||
android:name=".shell.debug.DebugNotificationDeleteReceiver"
|
||||
android:exported="false" />
|
||||
<provider
|
||||
android:name=".shell.settings.SbtSettingsProvider"
|
||||
android:authorities="se.ajpanton.statusbartweak.settings"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,62 @@
|
||||
package se.ajpanton.statusbartweak.module;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.StandaloneFeatureRegistry;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
||||
|
||||
/**
|
||||
* Fresh rebuild entrypoint.
|
||||
*
|
||||
* Intentionally does not install layout/icon hooks yet. During the reset
|
||||
* phase, the settings app remains available while runtime behaviour is rebuilt
|
||||
* slice by slice under the runtime package tree.
|
||||
*/
|
||||
public final class ModuleCoordinator {
|
||||
|
||||
private static final String PACKAGE_ANDROID = "android";
|
||||
private static final String PACKAGE_SYSTEM = "system";
|
||||
private static final String PACKAGE_SYSTEMUI = "com.android.systemui";
|
||||
private static final String PACKAGE_AODSERVICE = "com.samsung.android.app.aodservice";
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final StandaloneFeatureRegistry standaloneFeatureRegistry;
|
||||
|
||||
public ModuleCoordinator(XposedInterface framework) {
|
||||
ModeStateRepository modeStateRepository = new ModeStateRepository();
|
||||
runtimeContext = new RuntimeContext(
|
||||
framework,
|
||||
modeStateRepository);
|
||||
standaloneFeatureRegistry = new StandaloneFeatureRegistry(runtimeContext);
|
||||
}
|
||||
|
||||
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
|
||||
String processName = param.getProcessName();
|
||||
if (!PACKAGE_SYSTEMUI.equals(processName)
|
||||
&& !PACKAGE_ANDROID.equals(processName)
|
||||
&& !PACKAGE_SYSTEM.equals(processName)
|
||||
&& !PACKAGE_AODSERVICE.equals(processName)) {
|
||||
return;
|
||||
}
|
||||
standaloneFeatureRegistry.onModuleLoaded(param);
|
||||
}
|
||||
|
||||
public void onPackageReady(XposedModuleInterface.PackageReadyParam param) {
|
||||
if (!shouldHandlePackage(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
standaloneFeatureRegistry.onPackageReady(param);
|
||||
}
|
||||
|
||||
public void onSystemServerStarting(XposedModuleInterface.SystemServerStartingParam param) {
|
||||
standaloneFeatureRegistry.onSystemServerStarting(param);
|
||||
}
|
||||
|
||||
private static boolean shouldHandlePackage(String packageName) {
|
||||
return PACKAGE_SYSTEMUI.equals(packageName)
|
||||
|| PACKAGE_ANDROID.equals(packageName)
|
||||
|| PACKAGE_SYSTEM.equals(packageName)
|
||||
|| PACKAGE_AODSERVICE.equals(packageName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package se.ajpanton.statusbartweak.module;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import io.github.libxposed.api.XposedModule;
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
|
||||
/**
|
||||
* Modern module entrypoint.
|
||||
*/
|
||||
public final class StatusBarTweakModule extends XposedModule {
|
||||
|
||||
private static final String FAILSAFE_PATH = "/data/local/tmp/disable_statusbar_fix";
|
||||
|
||||
private final ModuleCoordinator moduleCoordinator = new ModuleCoordinator(this);
|
||||
private boolean moduleLoadedHandled;
|
||||
|
||||
public StatusBarTweakModule() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compatibility constructor for frameworks that still instantiate modern
|
||||
* modules with the older constructor-based contract.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public StatusBarTweakModule(
|
||||
XposedInterface framework,
|
||||
XposedModuleInterface.ModuleLoadedParam param
|
||||
) {
|
||||
attachFrameworkIfAvailable(framework);
|
||||
handleModuleLoaded(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModuleLoaded(ModuleLoadedParam param) {
|
||||
handleModuleLoaded(param);
|
||||
}
|
||||
|
||||
private void handleModuleLoaded(ModuleLoadedParam param) {
|
||||
if (moduleLoadedHandled) {
|
||||
return;
|
||||
}
|
||||
moduleLoadedHandled = true;
|
||||
if (new File(FAILSAFE_PATH).exists()) {
|
||||
return;
|
||||
}
|
||||
moduleCoordinator.onModuleLoaded(param);
|
||||
}
|
||||
|
||||
private void attachFrameworkIfAvailable(XposedInterface framework) {
|
||||
if (framework == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
getClass()
|
||||
.getMethod("attachFramework", XposedInterface.class)
|
||||
.invoke(this, framework);
|
||||
} catch (Throwable ignored) {
|
||||
// Older modern-API LSPosed builds may still use the constructor-based
|
||||
// contract without exposing attachFramework() on the runtime API.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(PackageReadyParam param) {
|
||||
if (new File(FAILSAFE_PATH).exists()) {
|
||||
return;
|
||||
}
|
||||
moduleCoordinator.onPackageReady(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemServerStarting(XposedModuleInterface.SystemServerStartingParam param) {
|
||||
if (new File(FAILSAFE_PATH).exists()) {
|
||||
return;
|
||||
}
|
||||
moduleCoordinator.onSystemServerStarting(param);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.ModeStateRepository;
|
||||
|
||||
/**
|
||||
* Shared runtime services for independently rebuilt features.
|
||||
*
|
||||
* Standalone slices such as Battery Bar, Misc, and Status Chips should depend
|
||||
* on this narrow context instead of reaching back into one large controller.
|
||||
*/
|
||||
public final class RuntimeContext {
|
||||
|
||||
private static final String LOG_TAG = "StatusBarTweak";
|
||||
private static final int MAX_LOG_MESSAGE_CHARS = 1800;
|
||||
|
||||
private final XposedInterface framework;
|
||||
private final ModeStateRepository modeStateRepository;
|
||||
|
||||
public RuntimeContext(
|
||||
XposedInterface framework,
|
||||
ModeStateRepository modeStateRepository
|
||||
) {
|
||||
this.framework = Objects.requireNonNull(framework, "framework");
|
||||
this.modeStateRepository = Objects.requireNonNull(modeStateRepository, "modeStateRepository");
|
||||
}
|
||||
|
||||
public XposedInterface getFramework() {
|
||||
return framework;
|
||||
}
|
||||
|
||||
public ModeStateRepository getModeStateRepository() {
|
||||
return modeStateRepository;
|
||||
}
|
||||
|
||||
public boolean isLogEnabled() {
|
||||
return RuntimeLogGate.isEnabled();
|
||||
}
|
||||
|
||||
public void logInfo(String message) {
|
||||
logSplit(Log.INFO, message, null);
|
||||
}
|
||||
|
||||
public void logWarning(String message, Throwable throwable) {
|
||||
logSplit(Log.WARN, message, throwable);
|
||||
}
|
||||
|
||||
public void logError(String message, Throwable throwable) {
|
||||
logSplit(Log.ERROR, message, throwable);
|
||||
}
|
||||
|
||||
private void logSplit(int priority, String message, Throwable throwable) {
|
||||
if (!RuntimeLogGate.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
String safeMessage = message != null ? message : "";
|
||||
if (safeMessage.length() <= MAX_LOG_MESSAGE_CHARS) {
|
||||
if (throwable != null) {
|
||||
framework.log(priority, LOG_TAG, safeMessage, throwable);
|
||||
} else {
|
||||
framework.log(priority, LOG_TAG, safeMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
PrefixParts prefixParts = splitPrefix(safeMessage);
|
||||
int bodyChunkSize = Math.max(
|
||||
1,
|
||||
MAX_LOG_MESSAGE_CHARS - prefixParts.prefix.length() - 20);
|
||||
int partCount = (prefixParts.body.length() + bodyChunkSize - 1) / bodyChunkSize;
|
||||
int digits = String.valueOf(partCount).length();
|
||||
for (int i = 0; i < partCount; i++) {
|
||||
int start = i * bodyChunkSize;
|
||||
int end = Math.min(prefixParts.body.length(), start + bodyChunkSize);
|
||||
String part = prefixParts.prefix
|
||||
+ " [part "
|
||||
+ leftPad(i + 1, digits)
|
||||
+ "/"
|
||||
+ leftPad(partCount, digits)
|
||||
+ "]"
|
||||
+ prefixParts.separator
|
||||
+ prefixParts.body.substring(start, end);
|
||||
if (throwable != null && i == 0) {
|
||||
framework.log(priority, LOG_TAG, part, throwable);
|
||||
} else {
|
||||
framework.log(priority, LOG_TAG, part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PrefixParts splitPrefix(String message) {
|
||||
int colon = message.indexOf(':');
|
||||
if (colon <= 0 || colon > 80) {
|
||||
return new PrefixParts("", "", message);
|
||||
}
|
||||
String prefix = message.substring(0, colon);
|
||||
String body = message.substring(colon + 1);
|
||||
String separator = ":";
|
||||
if (body.startsWith(" ")) {
|
||||
separator = ": ";
|
||||
body = body.substring(1);
|
||||
}
|
||||
return new PrefixParts(prefix, separator, body);
|
||||
}
|
||||
|
||||
private String leftPad(int value, int width) {
|
||||
String text = String.valueOf(value);
|
||||
StringBuilder out = new StringBuilder(width);
|
||||
for (int i = text.length(); i < width; i++) {
|
||||
out.append('0');
|
||||
}
|
||||
out.append(text);
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private record PrefixParts(String prefix, String separator, String body) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
|
||||
/**
|
||||
* Small independent runtime slice.
|
||||
*
|
||||
* The rebuild is intentionally moving away from one giant Xposed init class.
|
||||
* Each feature gets its own lifecycle and can stay outside the icon-layout
|
||||
* engine until it truly needs to integrate with it.
|
||||
*/
|
||||
public interface RuntimeFeature {
|
||||
|
||||
String getName();
|
||||
|
||||
default void onModuleLoaded(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.ModuleLoadedParam param
|
||||
) throws Throwable {
|
||||
}
|
||||
|
||||
default void onPackageReady(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.PackageReadyParam param
|
||||
) throws Throwable {
|
||||
}
|
||||
|
||||
default void onSystemServerStarting(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.SystemServerStartingParam param
|
||||
) throws Throwable {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class RuntimeLogGate {
|
||||
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private static Boolean cachedEnabled;
|
||||
private static boolean receiverRegistered;
|
||||
|
||||
private RuntimeLogGate() {
|
||||
}
|
||||
|
||||
static boolean isEnabled() {
|
||||
Boolean enabled = cachedEnabled;
|
||||
if (enabled != null) {
|
||||
return enabled;
|
||||
}
|
||||
Context context = getSystemContext();
|
||||
if (context == null) {
|
||||
return SbtDefaults.DEBUG_WRITE_LOGS_DEFAULT;
|
||||
}
|
||||
registerReceiverIfNeeded(context);
|
||||
synchronized (LOCK) {
|
||||
if (cachedEnabled == null) {
|
||||
cachedEnabled = SbtSettings.isDebugLogWritingEnabled(context);
|
||||
}
|
||||
return cachedEnabled != null ? cachedEnabled : SbtDefaults.DEBUG_WRITE_LOGS_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
private static void invalidate() {
|
||||
synchronized (LOCK) {
|
||||
cachedEnabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerReceiverIfNeeded(Context context) {
|
||||
if (receiverRegistered || context == null) {
|
||||
return;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
if (receiverRegistered) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static Context getSystemContext() {
|
||||
try {
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Object app = ReflectionSupport.invokeStaticMethod(
|
||||
activityThreadClass,
|
||||
"currentApplication",
|
||||
new Class<?>[0]
|
||||
);
|
||||
return app instanceof Context context ? context : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.battery.BatteryBarFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.features.chips.StatusChipsFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.features.debug.DebugNotificationAutoGroupFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.features.misc.MiscFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.features.notifications.StockUnlockedNotificationIconsFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.layout.StockLayoutCanvasFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SystemUiStatusBarStateFeature;
|
||||
|
||||
/**
|
||||
* Tracks independently rebuilt features that do not need the custom icon
|
||||
* layout engine yet.
|
||||
*/
|
||||
public final class StandaloneFeatureRegistry {
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final List<RuntimeFeature> features;
|
||||
|
||||
public StandaloneFeatureRegistry(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
this.features = List.of(
|
||||
new SystemUiStatusBarStateFeature(),
|
||||
new StockLayoutCanvasFeature(),
|
||||
new ClockFeature(),
|
||||
new StockUnlockedNotificationIconsFeature(),
|
||||
new BatteryBarFeature(),
|
||||
new MiscFeature(),
|
||||
new StatusChipsFeature(),
|
||||
new DebugNotificationAutoGroupFeature()
|
||||
);
|
||||
}
|
||||
|
||||
public void onModuleLoaded(XposedModuleInterface.ModuleLoadedParam param) {
|
||||
forEachFeature(feature -> feature.onModuleLoaded(runtimeContext, param));
|
||||
}
|
||||
|
||||
public void onPackageReady(XposedModuleInterface.PackageReadyParam param) {
|
||||
forEachFeature(feature -> feature.onPackageReady(runtimeContext, param));
|
||||
}
|
||||
|
||||
public void onSystemServerStarting(XposedModuleInterface.SystemServerStartingParam param) {
|
||||
forEachFeature(feature -> feature.onSystemServerStarting(runtimeContext, param));
|
||||
}
|
||||
|
||||
private void forEachFeature(FeatureAction action) {
|
||||
for (RuntimeFeature feature : features) {
|
||||
try {
|
||||
action.run(feature);
|
||||
} catch (Throwable throwable) {
|
||||
runtimeContext.logError(
|
||||
"Feature bootstrap failed: " + feature.getName()
|
||||
+ " throwable=" + throwable.getClass().getName()
|
||||
+ ": " + throwable.getMessage(),
|
||||
throwable
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface FeatureAction {
|
||||
void run(RuntimeFeature feature) throws Throwable;
|
||||
}
|
||||
}
|
||||
+487
@@ -0,0 +1,487 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.battery;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class BatteryBarController {
|
||||
|
||||
private static final String[] ROOT_CLASSES = new String[] {
|
||||
"com.android.systemui.statusbar.phone.PhoneStatusBarView",
|
||||
"com.android.systemui.statusbar.phone.KeyguardStatusBarView"
|
||||
};
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final Set<View> roots = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Map<View, BatteryBarView> batteryBarViews = new WeakHashMap<>();
|
||||
private final Map<View, View.OnLayoutChangeListener> rootLayoutListeners = new WeakHashMap<>();
|
||||
private final Map<View, String> lastSignatures = new WeakHashMap<>();
|
||||
|
||||
private boolean hooksInstalled;
|
||||
private boolean receiverRegistered;
|
||||
private int batteryLevel = -1;
|
||||
private boolean plugged;
|
||||
|
||||
BatteryBarController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
public void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
for (String className : ROOT_CLASSES) {
|
||||
installHooksForClass(classLoader, className);
|
||||
}
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void installHooksForClass(ClassLoader classLoader, String className) {
|
||||
Class<?> rootClass = ReflectionSupport.findClassIfExists(className, classLoader);
|
||||
if (rootClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedInterface framework = runtimeContext.getFramework();
|
||||
XposedHookSupport.hookAllConstructors(framework, rootClass, chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof View root) {
|
||||
trackAndApplyRoot(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, rootClass, "onFinishInflate", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof View root) {
|
||||
trackAndApplyRoot(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, rootClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof View root) {
|
||||
trackAndApplyRoot(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, rootClass, "onDetachedFromWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof View root) {
|
||||
untrackRoot(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void trackAndApplyRoot(View root) {
|
||||
roots.add(root);
|
||||
registerReceiverIfNeeded(root.getContext());
|
||||
ensureRootLayoutListener(root);
|
||||
applyBatteryBar(root);
|
||||
}
|
||||
|
||||
private void untrackRoot(View root) {
|
||||
View.OnLayoutChangeListener listener = rootLayoutListeners.remove(root);
|
||||
if (listener != null) {
|
||||
root.removeOnLayoutChangeListener(listener);
|
||||
}
|
||||
roots.remove(root);
|
||||
lastSignatures.remove(root);
|
||||
removeBatteryBarView(root);
|
||||
}
|
||||
|
||||
private void registerReceiverIfNeeded(Context context) {
|
||||
if (context == null || receiverRegistered) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(action)) {
|
||||
updateBatteryState(intent);
|
||||
}
|
||||
if (Intent.ACTION_BATTERY_CHANGED.equals(action)
|
||||
|| SbtSettings.ACTION_SETTINGS_CHANGED.equals(action)) {
|
||||
refreshAllRoots();
|
||||
}
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||
filter.addAction(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
Intent sticky;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
sticky = appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
sticky = appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
if (sticky != null) {
|
||||
updateBatteryState(sticky);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBatteryState(Intent intent) {
|
||||
int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
|
||||
if (level >= 0 && scale > 0) {
|
||||
batteryLevel = Math.round((level * 100f) / scale);
|
||||
}
|
||||
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
|
||||
|| status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
}
|
||||
|
||||
private void refreshAllRoots() {
|
||||
for (View root : new ArrayList<>(roots)) {
|
||||
applyBatteryBar(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureRootLayoutListener(View root) {
|
||||
if (rootLayoutListeners.containsKey(root)) {
|
||||
return;
|
||||
}
|
||||
View.OnLayoutChangeListener listener = new View.OnLayoutChangeListener() {
|
||||
@Override
|
||||
public void onLayoutChange(
|
||||
View v,
|
||||
int left,
|
||||
int top,
|
||||
int right,
|
||||
int bottom,
|
||||
int oldLeft,
|
||||
int oldTop,
|
||||
int oldRight,
|
||||
int oldBottom
|
||||
) {
|
||||
int width = right - left;
|
||||
int height = bottom - top;
|
||||
int oldWidth = oldRight - oldLeft;
|
||||
int oldHeight = oldBottom - oldTop;
|
||||
if (width == oldWidth && height == oldHeight) {
|
||||
return;
|
||||
}
|
||||
if (!roots.contains(root) || !root.isAttachedToWindow()) {
|
||||
return;
|
||||
}
|
||||
applyBatteryBar(root);
|
||||
}
|
||||
};
|
||||
root.addOnLayoutChangeListener(listener);
|
||||
rootLayoutListeners.put(root, listener);
|
||||
}
|
||||
|
||||
private void applyBatteryBar(View root) {
|
||||
if (!(root instanceof ViewGroup rootGroup)) {
|
||||
removeBatteryBarView(root);
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
if (!settings.batteryBarEnabled || root.getWidth() <= 0 || root.getHeight() <= 0) {
|
||||
removeBatteryBarView(root);
|
||||
return;
|
||||
}
|
||||
int color = resolveBarColor(root, settings);
|
||||
String signature = buildSignature(root, settings, color);
|
||||
BatteryBarView existingView = batteryBarViews.get(root);
|
||||
String lastSignature = lastSignatures.get(root);
|
||||
if (signature.equals(lastSignature) && existingView != null && existingView.getParent() == rootGroup) {
|
||||
existingView.bringToFront();
|
||||
return;
|
||||
}
|
||||
BatteryBarView barView = ensureBatteryBarView(rootGroup);
|
||||
layoutBatteryBarView(rootGroup, barView);
|
||||
barView.update(rootGroup, settings, batteryLevel, plugged, color);
|
||||
barView.bringToFront();
|
||||
barView.invalidate();
|
||||
lastSignatures.put(root, signature);
|
||||
}
|
||||
|
||||
private BatteryBarView ensureBatteryBarView(ViewGroup root) {
|
||||
BatteryBarView view = batteryBarViews.get(root);
|
||||
if (view == null) {
|
||||
view = new BatteryBarView(root.getContext());
|
||||
view.setClickable(false);
|
||||
view.setFocusable(false);
|
||||
view.setFocusableInTouchMode(false);
|
||||
batteryBarViews.put(root, view);
|
||||
}
|
||||
ensureAttached(root, view);
|
||||
return view;
|
||||
}
|
||||
|
||||
private void ensureAttached(ViewGroup root, BatteryBarView view) {
|
||||
if (view.getParent() == root) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params == null
|
||||
|| params.width != ViewGroup.LayoutParams.MATCH_PARENT
|
||||
|| params.height != ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
view.setLayoutParams(createLayoutParams(root));
|
||||
}
|
||||
return;
|
||||
}
|
||||
removeFromParent(view);
|
||||
root.addView(view, createLayoutParams(root));
|
||||
layoutBatteryBarView(root, view);
|
||||
}
|
||||
|
||||
private void layoutBatteryBarView(ViewGroup root, BatteryBarView view) {
|
||||
int width = root.getWidth();
|
||||
int height = root.getHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY);
|
||||
view.measure(widthSpec, heightSpec);
|
||||
view.layout(0, 0, width, height);
|
||||
}
|
||||
|
||||
private ViewGroup.LayoutParams createLayoutParams(ViewGroup parent) {
|
||||
if (parent instanceof FrameLayout) {
|
||||
return new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
}
|
||||
return new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
);
|
||||
}
|
||||
|
||||
private String buildSignature(View root, SbtSettings settings, int color) {
|
||||
return root.getWidth() + "|"
|
||||
+ root.getHeight() + "|"
|
||||
+ batteryLevel + "|"
|
||||
+ plugged + "|"
|
||||
+ color + "|"
|
||||
+ settings.batteryBarEnabled + "|"
|
||||
+ settings.batteryBarPosition + "|"
|
||||
+ settings.batteryBarAlignment + "|"
|
||||
+ settings.batteryBarThicknessDp + "|"
|
||||
+ settings.batteryBarEdgeOffsetDp + "|"
|
||||
+ settings.batteryBarMinLevel + "|"
|
||||
+ settings.batteryBarMaxLevel + "|"
|
||||
+ settings.batteryBarDefaultDischargeColor + "|"
|
||||
+ settings.batteryBarDefaultChargeColor + "|"
|
||||
+ BatteryBarStyle.encodeThresholds(settings.batteryBarThresholds);
|
||||
}
|
||||
|
||||
private void removeBatteryBarView(View root) {
|
||||
BatteryBarView view = batteryBarViews.remove(root);
|
||||
lastSignatures.remove(root);
|
||||
if (view != null) {
|
||||
removeFromParent(view);
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveBarColor(View root, SbtSettings settings) {
|
||||
int fallback = resolveClockColor(root);
|
||||
return BatteryBarStyle.resolveColor(
|
||||
batteryLevel,
|
||||
plugged,
|
||||
settings.batteryBarDefaultDischargeColor,
|
||||
settings.batteryBarDefaultChargeColor,
|
||||
settings.batteryBarThresholds,
|
||||
fallback,
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
private int resolveClockColor(View root) {
|
||||
View stockClock = findClock(root);
|
||||
if (stockClock instanceof TextView textView) {
|
||||
return textView.getCurrentTextColor();
|
||||
}
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
private static View findClock(View root) {
|
||||
if (matchesClockId(root)) {
|
||||
return root;
|
||||
}
|
||||
if (!(root instanceof ViewGroup group)) {
|
||||
return null;
|
||||
}
|
||||
for (int index = 0; index < group.getChildCount(); index++) {
|
||||
View child = group.getChildAt(index);
|
||||
View found = findClock(child);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean matchesClockId(View view) {
|
||||
int id = view.getId();
|
||||
if (id == View.NO_ID) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return "clock".equals(view.getResources().getResourceEntryName(id));
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int dpToPx(Context context, int dp) {
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
private static void removeFromParent(View view) {
|
||||
ViewParent parent = view.getParent();
|
||||
if (parent instanceof ViewGroup group) {
|
||||
group.removeView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BatteryBarView extends View {
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Rect drawRect = new Rect();
|
||||
private ViewGroup host;
|
||||
private SbtSettings settings;
|
||||
private int batteryLevel = -1;
|
||||
private boolean charging;
|
||||
|
||||
BatteryBarView(Context context) {
|
||||
super(context);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
void update(
|
||||
ViewGroup host,
|
||||
SbtSettings settings,
|
||||
int batteryLevel,
|
||||
boolean charging,
|
||||
int color
|
||||
) {
|
||||
this.host = host;
|
||||
this.settings = settings;
|
||||
this.batteryLevel = batteryLevel;
|
||||
this.charging = charging;
|
||||
paint.setColor(color);
|
||||
paint.setAlpha(charging ? 255 : 230);
|
||||
buildDrawRect();
|
||||
}
|
||||
|
||||
private void buildDrawRect() {
|
||||
drawRect.setEmpty();
|
||||
if (host == null || settings == null) {
|
||||
return;
|
||||
}
|
||||
int width = getWidth() > 0 ? getWidth() : host.getWidth();
|
||||
int height = getHeight() > 0 ? getHeight() : host.getHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
int thickness = Math.max(1, dpToPx(getContext(), settings.batteryBarThicknessDp));
|
||||
int edgeOffset = Math.max(0, dpToPx(getContext(), settings.batteryBarEdgeOffsetDp));
|
||||
int left = edgeOffset;
|
||||
int right = width - edgeOffset;
|
||||
if (right <= left) {
|
||||
return;
|
||||
}
|
||||
int top = resolveTop(settings, thickness, height);
|
||||
int bottom = Math.min(height, top + thickness);
|
||||
if (bottom <= top) {
|
||||
return;
|
||||
}
|
||||
float fraction = resolveFraction(batteryLevel, settings);
|
||||
int availableWidth = right - left;
|
||||
int barWidth = Math.round(availableWidth * fraction);
|
||||
if (barWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
if ("rtl".equals(settings.batteryBarAlignment)) {
|
||||
drawRect.set(right - barWidth, top, right, bottom);
|
||||
} else if ("center".equals(settings.batteryBarAlignment)) {
|
||||
int centerX = (left + right) / 2;
|
||||
int half = barWidth / 2;
|
||||
drawRect.set(centerX - half, top, centerX - half + barWidth, bottom);
|
||||
} else {
|
||||
drawRect.set(left, top, left + barWidth, bottom);
|
||||
}
|
||||
}
|
||||
|
||||
private int resolveTop(SbtSettings settings, int thickness, int height) {
|
||||
if ("bottom".equals(settings.batteryBarPosition)) {
|
||||
return Math.max(0, height - thickness);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private float resolveFraction(int batteryLevel, SbtSettings settings) {
|
||||
if (batteryLevel < 0) {
|
||||
return 0f;
|
||||
}
|
||||
int min = settings.batteryBarMinLevel;
|
||||
int max = settings.batteryBarMaxLevel;
|
||||
if (max <= min) {
|
||||
return batteryLevel >= max ? 1f : 0f;
|
||||
}
|
||||
if (batteryLevel <= min) {
|
||||
return 0f;
|
||||
}
|
||||
if (batteryLevel >= max) {
|
||||
return 1f;
|
||||
}
|
||||
return (batteryLevel - min) / (float) (max - min);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
buildDrawRect();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (!drawRect.isEmpty()) {
|
||||
canvas.drawRect(drawRect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.battery;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* Reserved home for the rebuilt Battery Bar runtime.
|
||||
*
|
||||
* This feature is intentionally separate from the future icon layout engine.
|
||||
* The old implementation mostly owned its own overlays and can be ported as a
|
||||
* standalone slice first.
|
||||
*/
|
||||
public final class BatteryBarFeature implements RuntimeFeature {
|
||||
|
||||
private BatteryBarController controller;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "battery-bar";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.PackageReadyParam param
|
||||
) {
|
||||
if (param == null || !"com.android.systemui".equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
installController(context, param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (controller == null) {
|
||||
controller = new BatteryBarController(context);
|
||||
}
|
||||
controller.install(classLoader);
|
||||
}
|
||||
}
|
||||
+917
@@ -0,0 +1,917 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.chips;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.InstanceStateStore;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class StatusChipHider {
|
||||
|
||||
private static final String FIELD_FORCED_HIDDEN = "sbtStatusChipForcedHidden";
|
||||
private static final String FIELD_PREV_VIS = "sbtStatusChipPrevVis";
|
||||
private static final int TOP_REGION_DP = 220;
|
||||
private static final int MAX_TEXT_SCAN_LEN = 160;
|
||||
private static final int MAX_CLASS_SCAN_LEN = 480;
|
||||
private static final int MAX_TEXT_NODES = 48;
|
||||
private static final int MAX_ASCENT = 8;
|
||||
private static final int ZONE_NONE = 0;
|
||||
private static final int ZONE_TOP = 1;
|
||||
private static final int KIND_UNKNOWN = 0;
|
||||
private static final int KIND_MEDIA = 1;
|
||||
private static final int KIND_NAV = 2;
|
||||
private static final int KIND_CALL = 3;
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final InstanceStateStore instanceStateStore = new InstanceStateStore();
|
||||
private final Set<View> trackedRoots = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> scannedRoots = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> forcedHiddenViews = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<Object> ongoingControllers = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
|
||||
private boolean hooksInstalled;
|
||||
private boolean broadcastInstalled;
|
||||
private KeyguardManager keyguardManager;
|
||||
private Class<?> ongoingActivityDataHelperClass;
|
||||
private Class<?> notificationLockscreenUserManagerClass;
|
||||
|
||||
private boolean hideAll;
|
||||
private boolean hideMediaUnlocked;
|
||||
private boolean hideNavUnlocked;
|
||||
private boolean hideCallUnlocked;
|
||||
|
||||
StatusChipHider(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void installHooks(ClassLoader classLoader) {
|
||||
if (hooksInstalled) {
|
||||
return;
|
||||
}
|
||||
installBroadcastReceiver(getSystemContext());
|
||||
refreshSettings();
|
||||
installOngoingActivityHooks(classLoader);
|
||||
installTouchInterceptHooks(classLoader);
|
||||
installViewRootHooks();
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void installOngoingActivityHooks(ClassLoader classLoader) {
|
||||
Class<?> controllerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.ongoingactivity.OngoingActivityController",
|
||||
classLoader
|
||||
);
|
||||
ongoingActivityDataHelperClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.ongoingactivity.OngoingActivityDataHelper",
|
||||
classLoader
|
||||
);
|
||||
notificationLockscreenUserManagerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.NotificationLockscreenUserManager",
|
||||
classLoader
|
||||
);
|
||||
if (controllerClass == null
|
||||
|| ongoingActivityDataHelperClass == null
|
||||
|| notificationLockscreenUserManagerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllConstructors(runtimeContext.getFramework(), controllerClass, chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object controller = chain.getThisObject();
|
||||
if (controller != null) {
|
||||
ongoingControllers.add(controller);
|
||||
refreshSettings();
|
||||
if (isAnyChipHidingEnabled()) {
|
||||
refreshOngoingActivityController(controller);
|
||||
rescanTrackedRoots();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
controllerClass,
|
||||
"shouldVisible",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
refreshSettings();
|
||||
if (result instanceof Boolean bool && bool && shouldSuppressTopOngoingController()) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
runtimeContext.getFramework(),
|
||||
ongoingActivityDataHelperClass,
|
||||
"shouldHide",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (result instanceof Boolean bool && bool) {
|
||||
return true;
|
||||
}
|
||||
if (!shouldHideTopUnlocked() || chain.getArgs().size() < 2) {
|
||||
return result;
|
||||
}
|
||||
Object data = chain.getArg(1);
|
||||
boolean suppress = shouldHide(classifyOngoingData(data), ZONE_TOP);
|
||||
return suppress ? true : result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installViewRootHooks() {
|
||||
Class<?> viewRootClass = ReflectionSupport.requireClass("android.view.ViewRootImpl", null);
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), viewRootClass, "performTraversals", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object rootObj = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
|
||||
if (!(rootObj instanceof View root)) {
|
||||
return result;
|
||||
}
|
||||
installBroadcastReceiver(root.getContext());
|
||||
trackedRoots.add(root);
|
||||
if (!isAnyChipHidingEnabled()) {
|
||||
restoreForcedHiddenViews();
|
||||
return result;
|
||||
}
|
||||
if (scannedRoots.add(root)) {
|
||||
applyToViewTree(root, false);
|
||||
} else {
|
||||
keepForcedHiddenViewsHidden();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installTouchInterceptHooks(ClassLoader classLoader) {
|
||||
Class<?> targetClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.TouchInterceptFrameLayout",
|
||||
classLoader
|
||||
);
|
||||
if (targetClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), targetClass, "onLayout", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (!isAnyChipHidingEnabled()) {
|
||||
return result;
|
||||
}
|
||||
if (!(chain.getThisObject() instanceof View view)) {
|
||||
return result;
|
||||
}
|
||||
if (view.getWidth() > 0 && view.getHeight() > 0) {
|
||||
applySourceDecision(view, false);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), targetClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (!isAnyChipHidingEnabled()) {
|
||||
return result;
|
||||
}
|
||||
if (chain.getThisObject() instanceof View view && shouldPreemptivelyHide(view)) {
|
||||
forceHide(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), targetClass, "setVisibility", chain -> {
|
||||
if (!(chain.getThisObject() instanceof View view)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
if (isAnyChipHidingEnabled()
|
||||
&& !chain.getArgs().isEmpty()
|
||||
&& chain.getArg(0) instanceof Integer requestedVisibility
|
||||
&& requestedVisibility != View.GONE
|
||||
&& shouldPreemptivelyHide(view)) {
|
||||
markForcedHidden(view, requestedVisibility);
|
||||
Object[] args = chain.getArgs().toArray();
|
||||
args[0] = View.GONE;
|
||||
return chain.proceed(args);
|
||||
}
|
||||
Object result = chain.proceed();
|
||||
if (!isAnyChipHidingEnabled()) {
|
||||
return result;
|
||||
}
|
||||
if (view.getWidth() > 0 && view.getHeight() > 0) {
|
||||
applySourceDecision(view, false);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installBroadcastReceiver(Context context) {
|
||||
if (broadcastInstalled) {
|
||||
return;
|
||||
}
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
IntentFilter filter = new IntentFilter(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||
android.content.BroadcastReceiver receiver = new android.content.BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
RuntimeSettingsCache.invalidate();
|
||||
refreshSettings();
|
||||
scannedRoots.clear();
|
||||
refreshOngoingActivityLists();
|
||||
rescanTrackedRoots();
|
||||
}
|
||||
};
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
broadcastInstalled = true;
|
||||
}
|
||||
|
||||
private void refreshSettings() {
|
||||
Context context = getSystemContext();
|
||||
if (context == null) {
|
||||
hideAll = false;
|
||||
hideMediaUnlocked = false;
|
||||
hideNavUnlocked = false;
|
||||
hideCallUnlocked = false;
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
hideAll = settings.statusChipsHideAll;
|
||||
hideMediaUnlocked = settings.statusChipsHideMediaUnlocked;
|
||||
hideNavUnlocked = settings.statusChipsHideNavUnlocked;
|
||||
hideCallUnlocked = settings.statusChipsHideCallUnlocked;
|
||||
}
|
||||
|
||||
private boolean isAnyChipHidingEnabled() {
|
||||
return hideAll || hideMediaUnlocked || hideNavUnlocked || hideCallUnlocked;
|
||||
}
|
||||
|
||||
private void rescanTrackedRoots() {
|
||||
if (!isAnyChipHidingEnabled()) {
|
||||
restoreForcedHiddenViews();
|
||||
return;
|
||||
}
|
||||
for (View root : trackedRoots) {
|
||||
if (root != null) {
|
||||
scannedRoots.add(root);
|
||||
applyToViewTree(root, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshOngoingActivityLists() {
|
||||
if (ongoingActivityDataHelperClass == null || notificationLockscreenUserManagerClass == null) {
|
||||
return;
|
||||
}
|
||||
for (Object controller : ongoingControllers) {
|
||||
refreshOngoingActivityController(controller);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshOngoingActivityController(Object controller) {
|
||||
if (controller == null
|
||||
|| ongoingActivityDataHelperClass == null
|
||||
|| notificationLockscreenUserManagerClass == null) {
|
||||
return;
|
||||
}
|
||||
Object userManager = ReflectionSupport.getFieldValue(controller, "userManager");
|
||||
if (userManager == null) {
|
||||
return;
|
||||
}
|
||||
ReflectionSupport.invokeStaticMethod(
|
||||
ongoingActivityDataHelperClass,
|
||||
"updateOngoingList",
|
||||
new Class<?>[]{notificationLockscreenUserManagerClass},
|
||||
userManager
|
||||
);
|
||||
ReflectionSupport.invokeMethod(controller, "updateParentViewVisibility", new Class<?>[]{boolean.class}, false);
|
||||
ReflectionSupport.invokeMethod(controller, "updateAdapter", new Class<?>[0]);
|
||||
}
|
||||
|
||||
private boolean shouldSuppressTopOngoingController() {
|
||||
if (!shouldHideTopUnlocked()) {
|
||||
return false;
|
||||
}
|
||||
Object list = ReflectionSupport.getStaticFieldValue(
|
||||
ongoingActivityDataHelperClass,
|
||||
"mOngoingActivityLists");
|
||||
if (!(list instanceof Iterable<?> iterable)) {
|
||||
return false;
|
||||
}
|
||||
boolean sawItem = false;
|
||||
for (Object data : iterable) {
|
||||
sawItem = true;
|
||||
if (!shouldHide(classifyOngoingData(data), ZONE_TOP)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return sawItem;
|
||||
}
|
||||
|
||||
private void applyToViewTree(View root, boolean restoreOnly) {
|
||||
ArrayDeque<View> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
while (!stack.isEmpty()) {
|
||||
View source = stack.pop();
|
||||
if (source == null) {
|
||||
continue;
|
||||
}
|
||||
if (source instanceof ViewGroup group) {
|
||||
for (int index = group.getChildCount() - 1; index >= 0; index--) {
|
||||
stack.push(group.getChildAt(index));
|
||||
}
|
||||
}
|
||||
applySourceDecision(source, restoreOnly);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean applySourceDecision(View source, boolean restoreOnly) {
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
if (restoreOnly) {
|
||||
restoreIfForcedHidden(source);
|
||||
return false;
|
||||
}
|
||||
int zone = detectZone(source);
|
||||
if (zone == ZONE_NONE || !isSourceCandidate(source)) {
|
||||
restoreIfForcedHidden(source);
|
||||
return false;
|
||||
}
|
||||
View target = resolveHideTarget(source, zone);
|
||||
if (target == null) {
|
||||
restoreIfForcedHidden(source);
|
||||
return false;
|
||||
}
|
||||
int targetZone = detectZone(target);
|
||||
if (targetZone == ZONE_NONE) {
|
||||
restoreIfForcedHidden(target);
|
||||
return false;
|
||||
}
|
||||
String sourceText = collectText(source);
|
||||
String targetText = target == source ? sourceText : collectText(target);
|
||||
String kindText = sourceText.isEmpty() ? targetText : sourceText;
|
||||
String classNames = target == source
|
||||
? collectClassNames(source)
|
||||
: collectClassNames(source) + " " + collectClassNames(target);
|
||||
String lowerText = kindText.toLowerCase();
|
||||
if (looksLikePrivacy(target, kindText) || isDefinitelyNotChipText(lowerText)) {
|
||||
restoreIfForcedHidden(target);
|
||||
return false;
|
||||
}
|
||||
int kind = classifyKind(source, kindText, classNames);
|
||||
if (shouldHide(kind, targetZone)) {
|
||||
forceHide(target);
|
||||
return true;
|
||||
} else {
|
||||
restoreIfForcedHidden(target);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldPreemptivelyHide(View source) {
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
if (isForcedHidden(source) && shouldHideTopUnlocked()) {
|
||||
return true;
|
||||
}
|
||||
if (!isTransitionSourceCandidate(source)) {
|
||||
return false;
|
||||
}
|
||||
return shouldHideSource(source, true);
|
||||
}
|
||||
|
||||
private boolean shouldHideSource(View source, boolean allowRelaxedGeometry) {
|
||||
int zone = detectZone(source);
|
||||
boolean relaxedGeometry = false;
|
||||
if (zone == ZONE_NONE) {
|
||||
if (!allowRelaxedGeometry || !isTransitionSourceCandidate(source)) {
|
||||
return false;
|
||||
}
|
||||
if (source.getWidth() > 0 && source.getHeight() > 0) {
|
||||
return false;
|
||||
}
|
||||
zone = ZONE_TOP;
|
||||
relaxedGeometry = true;
|
||||
} else if (!isSourceCandidate(source)) {
|
||||
return false;
|
||||
}
|
||||
String sourceText = collectText(source);
|
||||
String lowerText = sourceText.toLowerCase();
|
||||
if (looksLikePrivacy(source, sourceText) || isDefinitelyNotChipText(lowerText)) {
|
||||
return false;
|
||||
}
|
||||
int kind = classifyKind(source, sourceText, collectClassNames(source));
|
||||
if (relaxedGeometry && kind == KIND_UNKNOWN && lowerText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return shouldHide(kind, zone);
|
||||
}
|
||||
|
||||
private boolean isSourceCandidate(View view) {
|
||||
if (isOngoingActivityCapsule(view)) {
|
||||
return false;
|
||||
}
|
||||
if (!isSystemUiView(view) || !looksLikeChipGeometry(view)) {
|
||||
return false;
|
||||
}
|
||||
String className = className(view).toLowerCase();
|
||||
if (className.contains("privacy") || isDefinitelyNotChipClass(className)) {
|
||||
return false;
|
||||
}
|
||||
return isChipClassName(className);
|
||||
}
|
||||
|
||||
private boolean isTransitionSourceCandidate(View view) {
|
||||
if (isOngoingActivityCapsule(view)) {
|
||||
return false;
|
||||
}
|
||||
if (!isSystemUiView(view)) {
|
||||
return false;
|
||||
}
|
||||
String className = className(view).toLowerCase();
|
||||
if (className.contains("privacy") || isDefinitelyNotChipClass(className)) {
|
||||
return false;
|
||||
}
|
||||
return isChipClassName(className);
|
||||
}
|
||||
|
||||
private View resolveHideTarget(View source, int sourceZone) {
|
||||
String sourceClass = className(source).toLowerCase();
|
||||
if (sourceClass.contains("touchinterceptframelayout")) {
|
||||
return source;
|
||||
}
|
||||
View best = null;
|
||||
View current = source;
|
||||
int depth = 0;
|
||||
int screenWidth = source.getResources().getDisplayMetrics().widthPixels;
|
||||
while (current != null && depth < MAX_ASCENT) {
|
||||
if (!isSystemUiView(current)) {
|
||||
break;
|
||||
}
|
||||
if (detectZone(current) != sourceZone) {
|
||||
break;
|
||||
}
|
||||
if (looksLikeHideTarget(current, source, screenWidth)) {
|
||||
best = current;
|
||||
}
|
||||
ViewParent parent = current.getParent();
|
||||
current = parent instanceof View view ? view : null;
|
||||
depth++;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private boolean looksLikeHideTarget(View candidate, View source, int screenWidth) {
|
||||
if (!looksLikeChipGeometry(candidate)) {
|
||||
return false;
|
||||
}
|
||||
String className = className(candidate).toLowerCase();
|
||||
if (className.contains("privacy") || isDefinitelyNotChipClass(className)) {
|
||||
return false;
|
||||
}
|
||||
if (!isChipClassName(className)) {
|
||||
return false;
|
||||
}
|
||||
int width = candidate.getWidth();
|
||||
int height = candidate.getHeight();
|
||||
if (width >= (int) (screenWidth * 0.98f) && height < dp(candidate, 90)) {
|
||||
return false;
|
||||
}
|
||||
if (candidate == source) {
|
||||
return true;
|
||||
}
|
||||
return candidate instanceof ViewGroup && width >= source.getWidth();
|
||||
}
|
||||
|
||||
private boolean isChipClassName(String lowerClass) {
|
||||
return lowerClass.contains("chip")
|
||||
|| lowerClass.contains("ongoing")
|
||||
|| lowerClass.contains("statusevent")
|
||||
|| lowerClass.contains("touchinterceptframelayout");
|
||||
}
|
||||
|
||||
private int detectZone(View view) {
|
||||
if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0) {
|
||||
return ZONE_NONE;
|
||||
}
|
||||
int[] location = new int[2];
|
||||
try {
|
||||
view.getLocationOnScreen(location);
|
||||
} catch (Throwable ignored) {
|
||||
return ZONE_NONE;
|
||||
}
|
||||
return location[1] >= 0 && location[1] <= dp(view, TOP_REGION_DP) ? ZONE_TOP : ZONE_NONE;
|
||||
}
|
||||
|
||||
private boolean shouldHide(int kind, int zone) {
|
||||
if (zone != ZONE_TOP || !shouldHideTopUnlocked()) {
|
||||
return false;
|
||||
}
|
||||
if (hideAll && kind == KIND_UNKNOWN) {
|
||||
return true;
|
||||
}
|
||||
if (kind == KIND_MEDIA) {
|
||||
return hideMediaUnlocked;
|
||||
}
|
||||
if (kind == KIND_NAV) {
|
||||
return hideNavUnlocked;
|
||||
}
|
||||
if (kind == KIND_CALL) {
|
||||
return hideCallUnlocked;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean shouldHideTopUnlocked() {
|
||||
return isAnyChipHidingEnabled() && !isKeyguardLocked();
|
||||
}
|
||||
|
||||
private int classifyKind(View view, String text, String classNames) {
|
||||
String lowerText = text != null ? text.toLowerCase() : "";
|
||||
String lowerClass = classNames != null && !classNames.isEmpty()
|
||||
? classNames.toLowerCase()
|
||||
: (view != null ? className(view).toLowerCase() : "");
|
||||
if (containsToken(lowerText, "call")
|
||||
|| containsPhrase(lowerText, "ongoing call")
|
||||
|| lowerClass.contains("call")) {
|
||||
return KIND_CALL;
|
||||
}
|
||||
if (containsToken(lowerText, "maps")
|
||||
|| containsTokenPrefix(lowerText, "navigat")
|
||||
|| containsToken(lowerText, "towards")
|
||||
|| containsToken(lowerText, "directions")
|
||||
|| containsTokenPrefix(lowerText, "route")
|
||||
|| containsToken(lowerText, "waze")
|
||||
|| containsDistanceToken(lowerText)
|
||||
|| containsAny(lowerClass, "map", "nav", "route")) {
|
||||
return KIND_NAV;
|
||||
}
|
||||
if (containsToken(lowerText, "music")
|
||||
|| containsToken(lowerText, "media")
|
||||
|| containsTokenPrefix(lowerText, "play")
|
||||
|| containsToken(lowerText, "spotify")
|
||||
|| containsToken(lowerText, "song")
|
||||
|| containsTokenPrefix(lowerText, "track")
|
||||
|| containsToken(lowerText, "album")
|
||||
|| containsToken(lowerText, "artist")
|
||||
|| containsAny(lowerClass, "media", "music", "player")) {
|
||||
return KIND_MEDIA;
|
||||
}
|
||||
if (lowerClass.contains("touchinterceptframelayout") && !lowerText.isEmpty()) {
|
||||
return KIND_MEDIA;
|
||||
}
|
||||
return KIND_UNKNOWN;
|
||||
}
|
||||
|
||||
private int classifyOngoingData(Object data) {
|
||||
if (data == null) {
|
||||
return KIND_UNKNOWN;
|
||||
}
|
||||
if (ReflectionSupport.getBooleanField(data, "mIsMediaOngoingData", false)) {
|
||||
return KIND_MEDIA;
|
||||
}
|
||||
String text = (textField(data, "mPackageName") + " "
|
||||
+ textField(data, "mPrimaryInfo") + " "
|
||||
+ textField(data, "mSecondaryInfo") + " "
|
||||
+ textField(data, "mDescription") + " "
|
||||
+ textField(data, "mExpandedChipText")).trim();
|
||||
return classifyKind(null, text, data.getClass().getName());
|
||||
}
|
||||
|
||||
private boolean looksLikePrivacy(View view, String text) {
|
||||
String lowerClass = className(view).toLowerCase();
|
||||
String lowerText = text != null ? text.toLowerCase() : "";
|
||||
return lowerClass.contains("privacy")
|
||||
|| containsToken(lowerText, "camera")
|
||||
|| containsToken(lowerText, "microphone")
|
||||
|| containsPhrase(lowerText, "location in use")
|
||||
|| containsPhrase(lowerText, "mic in use");
|
||||
}
|
||||
|
||||
private String collectClassNames(View root) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
appendClassName(out, root);
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return out.toString();
|
||||
}
|
||||
ArrayDeque<View> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
int nodes = 0;
|
||||
while (!stack.isEmpty() && out.length() < MAX_CLASS_SCAN_LEN && nodes < MAX_TEXT_NODES) {
|
||||
View current = stack.pop();
|
||||
nodes++;
|
||||
if (current != root) {
|
||||
appendClassName(out, current);
|
||||
}
|
||||
if (current instanceof ViewGroup group) {
|
||||
for (int index = 0; index < group.getChildCount(); index++) {
|
||||
stack.push(group.getChildAt(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private String collectText(View root) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
appendText(out, root.getContentDescription());
|
||||
if (root instanceof TextView textView) {
|
||||
appendText(out, textView.getText());
|
||||
}
|
||||
if (!(root instanceof ViewGroup)) {
|
||||
return out.toString();
|
||||
}
|
||||
ArrayDeque<View> stack = new ArrayDeque<>();
|
||||
stack.push(root);
|
||||
int nodes = 0;
|
||||
while (!stack.isEmpty() && out.length() < MAX_TEXT_SCAN_LEN && nodes < MAX_TEXT_NODES) {
|
||||
View current = stack.pop();
|
||||
nodes++;
|
||||
if (current != root) {
|
||||
appendText(out, current.getContentDescription());
|
||||
if (current instanceof TextView textView) {
|
||||
appendText(out, textView.getText());
|
||||
}
|
||||
}
|
||||
if (current instanceof ViewGroup group) {
|
||||
for (int index = 0; index < group.getChildCount(); index++) {
|
||||
stack.push(group.getChildAt(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private void forceHide(View view) {
|
||||
int restoreVisibility = view.getVisibility() != View.GONE ? view.getVisibility() : View.VISIBLE;
|
||||
markForcedHidden(view, restoreVisibility);
|
||||
if (view.getVisibility() != View.GONE) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void keepForcedHiddenViewsHidden() {
|
||||
if (forcedHiddenViews.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : forcedHiddenViews) {
|
||||
if (view == null || !view.isAttachedToWindow()) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
if (view.getVisibility() != View.GONE) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
forcedHiddenViews.remove(staleViews.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreForcedHiddenViews() {
|
||||
if (forcedHiddenViews.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> views = new ArrayDeque<>(forcedHiddenViews);
|
||||
while (!views.isEmpty()) {
|
||||
restoreIfForcedHidden(views.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private void markForcedHidden(View view, int restoreVisibility) {
|
||||
instanceStateStore.put(view, FIELD_PREV_VIS, restoreVisibility);
|
||||
instanceStateStore.put(view, FIELD_FORCED_HIDDEN, Boolean.TRUE);
|
||||
forcedHiddenViews.add(view);
|
||||
}
|
||||
|
||||
private void restoreIfForcedHidden(View view) {
|
||||
if (!isForcedHidden(view)) {
|
||||
return;
|
||||
}
|
||||
Object previous = instanceStateStore.get(view, FIELD_PREV_VIS);
|
||||
int restoreVisibility = previous instanceof Integer integer ? integer : View.VISIBLE;
|
||||
instanceStateStore.remove(view, FIELD_FORCED_HIDDEN);
|
||||
instanceStateStore.remove(view, FIELD_PREV_VIS);
|
||||
forcedHiddenViews.remove(view);
|
||||
if (view.getVisibility() != restoreVisibility) {
|
||||
view.setVisibility(restoreVisibility);
|
||||
}
|
||||
if (restoreVisibility == View.VISIBLE && view.getAlpha() == 0f) {
|
||||
view.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isForcedHidden(View view) {
|
||||
Object marker = instanceStateStore.get(view, FIELD_FORCED_HIDDEN);
|
||||
return marker instanceof Boolean bool && bool;
|
||||
}
|
||||
|
||||
private boolean isSystemUiView(View view) {
|
||||
return className(view).contains("systemui");
|
||||
}
|
||||
|
||||
private boolean looksLikeChipGeometry(View view) {
|
||||
if (view == null || view.getWidth() <= 0 || view.getHeight() <= 0) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth();
|
||||
int height = view.getHeight();
|
||||
int screenWidth = view.getResources().getDisplayMetrics().widthPixels;
|
||||
if (height < dp(view, 18) || height > dp(view, 160)) {
|
||||
return false;
|
||||
}
|
||||
if (width < height * 2) {
|
||||
return false;
|
||||
}
|
||||
return !(width > (int) (screenWidth * 0.95f) && height < dp(view, 110));
|
||||
}
|
||||
|
||||
private boolean isDefinitelyNotChipClass(String lowerClass) {
|
||||
return lowerClass.contains("clock")
|
||||
|| lowerClass.contains("date")
|
||||
|| lowerClass.contains("brightness")
|
||||
|| lowerClass.contains("tile")
|
||||
|| lowerClass.contains("qsbuttonscontainer")
|
||||
|| lowerClass.contains("sectionheaderview")
|
||||
|| lowerClass.contains("expandablenotificationrow")
|
||||
|| lowerClass.contains("carrier")
|
||||
|| lowerClass.contains("statusicon")
|
||||
|| lowerClass.contains("indicator")
|
||||
|| lowerClass.contains("launchabletextview")
|
||||
|| lowerClass.contains("qsshortendate")
|
||||
|| lowerClass.contains("qsclock")
|
||||
|| lowerClass.contains("quickqs");
|
||||
}
|
||||
|
||||
private boolean isDefinitelyNotChipText(String lowerText) {
|
||||
if (lowerText == null || lowerText.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return containsPhrase(lowerText, "notification settings")
|
||||
|| containsToken(lowerText, "brightness")
|
||||
|| containsToken(lowerText, "wifi")
|
||||
|| containsPhrase(lowerText, "smart view")
|
||||
|| containsToken(lowerText, "mirror");
|
||||
}
|
||||
|
||||
private String className(View view) {
|
||||
return view != null && view.getClass() != null ? view.getClass().getName() : "";
|
||||
}
|
||||
|
||||
private String textField(Object target, String fieldName) {
|
||||
Object value = ReflectionSupport.getFieldValue(target, fieldName);
|
||||
return value != null ? String.valueOf(value) : "";
|
||||
}
|
||||
|
||||
private boolean isOngoingActivityCapsule(View view) {
|
||||
if (view == null || view.getId() == View.NO_ID) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return "ongoing_activity_capsule".equals(view.getResources().getResourceEntryName(view.getId()));
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void appendText(StringBuilder out, CharSequence value) {
|
||||
if (value == null || value.length() == 0 || out.length() >= MAX_TEXT_SCAN_LEN) {
|
||||
return;
|
||||
}
|
||||
if (out.length() > 0) {
|
||||
out.append(' ');
|
||||
}
|
||||
int remaining = MAX_TEXT_SCAN_LEN - out.length();
|
||||
if (value.length() > remaining) {
|
||||
out.append(value, 0, remaining);
|
||||
} else {
|
||||
out.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendClassName(StringBuilder out, View view) {
|
||||
if (view == null || out.length() >= MAX_CLASS_SCAN_LEN) {
|
||||
return;
|
||||
}
|
||||
String value = className(view);
|
||||
if (value.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (out.length() > 0) {
|
||||
out.append(' ');
|
||||
}
|
||||
int remaining = MAX_CLASS_SCAN_LEN - out.length();
|
||||
if (value.length() > remaining) {
|
||||
out.append(value, 0, remaining);
|
||||
} else {
|
||||
out.append(value);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean containsAny(String value, String... needles) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (String needle : needles) {
|
||||
if (needle != null && value.contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsPhrase(String value, String phrase) {
|
||||
return value != null && phrase != null && value.contains(phrase);
|
||||
}
|
||||
|
||||
private boolean containsToken(String value, String token) {
|
||||
if (value == null || value.isEmpty() || token == null || token.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String[] parts = value.split("[^a-z0-9]+");
|
||||
for (String part : parts) {
|
||||
if (token.equals(part)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsTokenPrefix(String value, String prefix) {
|
||||
if (value == null || value.isEmpty() || prefix == null || prefix.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String[] parts = value.split("[^a-z0-9]+");
|
||||
for (String part : parts) {
|
||||
if (part.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean containsDistanceToken(String value) {
|
||||
return value != null && !value.isEmpty() && value.matches(".*\\b\\d+\\s*(m|km|mi|ft)\\b.*");
|
||||
}
|
||||
|
||||
private int dp(View view, int dp) {
|
||||
return Math.round(dp * view.getResources().getDisplayMetrics().density);
|
||||
}
|
||||
|
||||
private Context getSystemContext() {
|
||||
try {
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Object app = ReflectionSupport.invokeStaticMethod(
|
||||
activityThreadClass,
|
||||
"currentApplication",
|
||||
new Class<?>[0]
|
||||
);
|
||||
return app instanceof Context context ? context : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isKeyguardLocked() {
|
||||
try {
|
||||
Context context = getSystemContext();
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
keyguardManager = getKeyguardManager(context);
|
||||
return keyguardManager != null && keyguardManager.isKeyguardLocked();
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private KeyguardManager getKeyguardManager(Context context) {
|
||||
if (keyguardManager == null && context != null) {
|
||||
Object service = context.getSystemService(Context.KEYGUARD_SERVICE);
|
||||
if (service instanceof KeyguardManager manager) {
|
||||
keyguardManager = manager;
|
||||
}
|
||||
}
|
||||
return keyguardManager;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.chips;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* Reserved home for stock status-chip suppression.
|
||||
*
|
||||
* Status chips appear to be largely independent of the custom icon-layout
|
||||
* rebuild because they mostly suppress Samsung-owned chip views rather than
|
||||
* participating in our future icon solver.
|
||||
*/
|
||||
public final class StatusChipsFeature implements RuntimeFeature {
|
||||
|
||||
private StatusChipHider statusChipHider;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "status-chips";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.PackageReadyParam param
|
||||
) {
|
||||
if (param == null || !"com.android.systemui".equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
installController(context, param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (statusChipHider == null) {
|
||||
statusChipHider = new StatusChipHider(context);
|
||||
}
|
||||
statusChipHider.installHooks(classLoader);
|
||||
}
|
||||
}
|
||||
+337
@@ -0,0 +1,337 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle;
|
||||
|
||||
final class ClockColorResolver {
|
||||
|
||||
String resolveColorHex(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
int resolved = resolveColor(context, expression, referenceColor, fallbackColor);
|
||||
if (Color.alpha(resolved) >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
int resolveColor(
|
||||
Context context,
|
||||
String expression,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
if (TextUtils.isEmpty(expression)) {
|
||||
return fallbackColor;
|
||||
}
|
||||
ArrayList<String> segments = splitSegments(expression);
|
||||
if (segments.isEmpty() || segments.size() > 4) {
|
||||
throw new IllegalArgumentException("Invalid colour expression");
|
||||
}
|
||||
int brightColor;
|
||||
int darkColor;
|
||||
switch (segments.size()) {
|
||||
case 1:
|
||||
brightColor = resolveSource(context, segments.get(0), referenceColor, fallbackColor);
|
||||
darkColor = brightColor;
|
||||
break;
|
||||
case 2:
|
||||
brightColor = resolveSource(context, segments.get(0), referenceColor, fallbackColor);
|
||||
darkColor = resolveVariant(
|
||||
context,
|
||||
brightColor,
|
||||
segments.get(1),
|
||||
referenceColor,
|
||||
fallbackColor);
|
||||
break;
|
||||
case 3:
|
||||
int sharedSource = resolveSource(context, segments.get(0), referenceColor, fallbackColor);
|
||||
brightColor = resolveVariant(
|
||||
context,
|
||||
sharedSource,
|
||||
segments.get(1),
|
||||
referenceColor,
|
||||
fallbackColor);
|
||||
darkColor = resolveVariant(
|
||||
context,
|
||||
sharedSource,
|
||||
segments.get(2),
|
||||
referenceColor,
|
||||
fallbackColor);
|
||||
break;
|
||||
case 4:
|
||||
int darkSource = resolveSource(context, segments.get(0), referenceColor, fallbackColor);
|
||||
int brightSource = resolveSource(context, segments.get(1), referenceColor, fallbackColor);
|
||||
brightColor = resolveVariant(
|
||||
context,
|
||||
brightSource,
|
||||
segments.get(2),
|
||||
referenceColor,
|
||||
fallbackColor);
|
||||
darkColor = resolveVariant(
|
||||
context,
|
||||
darkSource,
|
||||
segments.get(3),
|
||||
referenceColor,
|
||||
fallbackColor);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported colour expression");
|
||||
}
|
||||
return isDark(referenceColor) ? darkColor : brightColor;
|
||||
}
|
||||
|
||||
private int resolveVariant(
|
||||
Context context,
|
||||
int sourceColor,
|
||||
String spec,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
String value = spec != null ? spec.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty variant");
|
||||
}
|
||||
if (value.charAt(0) == '#') {
|
||||
return resolveSource(context, value, referenceColor, fallbackColor);
|
||||
}
|
||||
return applyMathExpression(sourceColor, value);
|
||||
}
|
||||
|
||||
private int resolveSource(
|
||||
Context context,
|
||||
String source,
|
||||
int referenceColor,
|
||||
int fallbackColor
|
||||
) {
|
||||
String value = source != null ? source.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour source");
|
||||
}
|
||||
if ("#batterybar".equalsIgnoreCase(value)) {
|
||||
return BatteryBarStyle.resolveCurrentColor(context, referenceColor, fallbackColor);
|
||||
}
|
||||
return parseHexColor(value);
|
||||
}
|
||||
|
||||
private int applyMathExpression(int sourceColor, String expression) {
|
||||
String value = expression != null ? expression.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour math expression");
|
||||
}
|
||||
double[] rgb = new double[] {
|
||||
Color.red(sourceColor),
|
||||
Color.green(sourceColor),
|
||||
Color.blue(sourceColor)
|
||||
};
|
||||
double alpha = Color.alpha(sourceColor);
|
||||
int index = 0;
|
||||
if (value.regionMatches(true, 0, "inv", 0, 3)) {
|
||||
rgb[0] = 255d - rgb[0];
|
||||
rgb[1] = 255d - rgb[1];
|
||||
rgb[2] = 255d - rgb[2];
|
||||
index = 3;
|
||||
}
|
||||
while (index < value.length()) {
|
||||
while (index < value.length() && Character.isWhitespace(value.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= value.length()) {
|
||||
break;
|
||||
}
|
||||
char operator = value.charAt(index);
|
||||
if (operator != '+' && operator != '-' && operator != '*' && operator != '/') {
|
||||
throw new IllegalArgumentException("Invalid colour operation");
|
||||
}
|
||||
index++;
|
||||
while (index < value.length() && Character.isWhitespace(value.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= value.length()) {
|
||||
throw new IllegalArgumentException("Missing colour operand");
|
||||
}
|
||||
int operandStart = index;
|
||||
if (value.charAt(index) == '#') {
|
||||
index++;
|
||||
while (index < value.length() && isHexDigit(value.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
} else {
|
||||
while (index < value.length()) {
|
||||
char c = value.charAt(index);
|
||||
if (!Character.isDigit(c) && c != '.') {
|
||||
break;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
String operandText = value.substring(operandStart, index).trim();
|
||||
if (operandText.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing colour operand");
|
||||
}
|
||||
Operand operand = parseOperand(operandText);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double rhs = operand.rgb[i];
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
rgb[i] = applyOperator(rgb[i], rhs, operator);
|
||||
}
|
||||
if (operand.hasAlpha) {
|
||||
double rhs = operand.alpha;
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
alpha = applyOperator(alpha, rhs, operator);
|
||||
}
|
||||
}
|
||||
return Color.argb(
|
||||
clampChannel(alpha),
|
||||
clampChannel(rgb[0]),
|
||||
clampChannel(rgb[1]),
|
||||
clampChannel(rgb[2]));
|
||||
}
|
||||
|
||||
private double applyOperator(double left, double right, char operator) {
|
||||
switch (operator) {
|
||||
case '+':
|
||||
return left + right;
|
||||
case '-':
|
||||
return left - right;
|
||||
case '*':
|
||||
return left * right;
|
||||
case '/':
|
||||
return right == 0d ? left : left / right;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unsupported operator");
|
||||
}
|
||||
}
|
||||
|
||||
private Operand parseOperand(String operandText) {
|
||||
if (TextUtils.isEmpty(operandText)) {
|
||||
throw new IllegalArgumentException("Missing operand");
|
||||
}
|
||||
if (operandText.charAt(0) == '#') {
|
||||
return parseHexOperand(operandText);
|
||||
}
|
||||
double value;
|
||||
try {
|
||||
value = Double.parseDouble(operandText);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid numeric operand", e);
|
||||
}
|
||||
return new Operand(new double[] { value, value, value }, 0d, false, false);
|
||||
}
|
||||
|
||||
private Operand parseHexOperand(String raw) {
|
||||
String hex = expandHex(raw);
|
||||
boolean hasAlpha = hex.length() == 8;
|
||||
int offset = hasAlpha ? 2 : 0;
|
||||
double alpha = hasAlpha ? parseHexByte(hex.substring(0, 2)) : 0d;
|
||||
return new Operand(
|
||||
new double[] {
|
||||
parseHexByte(hex.substring(offset, offset + 2)),
|
||||
parseHexByte(hex.substring(offset + 2, offset + 4)),
|
||||
parseHexByte(hex.substring(offset + 4, offset + 6))
|
||||
},
|
||||
alpha,
|
||||
hasAlpha,
|
||||
true);
|
||||
}
|
||||
|
||||
private int parseHexColor(String raw) {
|
||||
String hex = expandHex(raw);
|
||||
try {
|
||||
return Color.parseColor("#" + hex);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid colour literal", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String expandHex(String raw) {
|
||||
String value = raw != null ? raw.trim() : "";
|
||||
if (value.isEmpty() || value.charAt(0) != '#') {
|
||||
throw new IllegalArgumentException("Expected colour literal");
|
||||
}
|
||||
String hex = value.substring(1);
|
||||
if (hex.length() == 3 || hex.length() == 4) {
|
||||
StringBuilder out = new StringBuilder(hex.length() * 2);
|
||||
for (int i = 0; i < hex.length(); i++) {
|
||||
out.append(hex.charAt(i)).append(hex.charAt(i));
|
||||
}
|
||||
hex = out.toString();
|
||||
} else if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Unsupported colour literal length");
|
||||
}
|
||||
for (int i = 0; i < hex.length(); i++) {
|
||||
if (!isHexDigit(hex.charAt(i))) {
|
||||
throw new IllegalArgumentException("Invalid colour literal");
|
||||
}
|
||||
}
|
||||
return hex.toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private ArrayList<String> splitSegments(String expression) {
|
||||
ArrayList<String> segments = new ArrayList<>();
|
||||
if (expression == null) {
|
||||
return segments;
|
||||
}
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (int i = 0; i < expression.length(); i++) {
|
||||
char c = expression.charAt(i);
|
||||
if (c == '|') {
|
||||
segments.add(current.toString().trim());
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
current.append(c);
|
||||
}
|
||||
segments.add(current.toString().trim());
|
||||
return segments;
|
||||
}
|
||||
|
||||
private boolean isDark(int color) {
|
||||
double darkness = 1d
|
||||
- ((0.299d * Color.red(color))
|
||||
+ (0.587d * Color.green(color))
|
||||
+ (0.114d * Color.blue(color))) / 255d;
|
||||
return darkness > 0.5d;
|
||||
}
|
||||
|
||||
private boolean isHexDigit(char c) {
|
||||
return (c >= '0' && c <= '9')
|
||||
|| (c >= 'a' && c <= 'f')
|
||||
|| (c >= 'A' && c <= 'F');
|
||||
}
|
||||
|
||||
private double parseHexByte(String raw) {
|
||||
return Integer.parseInt(raw, 16);
|
||||
}
|
||||
|
||||
private int clampChannel(double value) {
|
||||
return Math.max(0, Math.min(255, (int) Math.round(value)));
|
||||
}
|
||||
|
||||
private static final class Operand {
|
||||
final double[] rgb;
|
||||
final double alpha;
|
||||
final boolean hasAlpha;
|
||||
final boolean isHex;
|
||||
|
||||
Operand(double[] rgb, double alpha, boolean hasAlpha, boolean isHex) {
|
||||
this.rgb = rgb;
|
||||
this.alpha = alpha;
|
||||
this.hasAlpha = hasAlpha;
|
||||
this.isHex = isHex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* First clock slice for the rebuild.
|
||||
*
|
||||
* For now this only targets the stock SystemUI clock while the custom Layout
|
||||
* engine is off. Later, when the rewritten layout owns the clock container,
|
||||
* this feature can hand off to that path instead of owning the stock view.
|
||||
*/
|
||||
public final class ClockFeature implements RuntimeFeature {
|
||||
|
||||
private StockClockController stockClockController;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "clock";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.PackageReadyParam param
|
||||
) {
|
||||
if (param == null || !"com.android.systemui".equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
installController(context, param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (stockClockController == null) {
|
||||
stockClockController = new StockClockController(context);
|
||||
}
|
||||
stockClockController.install(classLoader);
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
public final class ClockLayoutTextFactory {
|
||||
public static final int ROW_GAP_AUTO = ClockPatternParser.ROW_GAP_AUTO;
|
||||
private static final ClockPatternParser PATTERN_PARSER = new ClockPatternParser();
|
||||
private static final ClockTextRenderer TEXT_RENDERER = new ClockTextRenderer();
|
||||
private static final WeakHashMap<TextView, CachedModel> MODEL_CACHE = new WeakHashMap<>();
|
||||
|
||||
private ClockLayoutTextFactory() {
|
||||
}
|
||||
|
||||
public static Model build(TextView source, SbtSettings settings) {
|
||||
Context context = source != null ? source.getContext() : null;
|
||||
if (context == null || settings == null) {
|
||||
return Model.empty();
|
||||
}
|
||||
CacheKey cacheKey = cacheKey(context, source, settings);
|
||||
CachedModel cached = MODEL_CACHE.get(source);
|
||||
if (cached != null && cached.key.equals(cacheKey)) {
|
||||
return cached.model;
|
||||
}
|
||||
Model model = buildUncached(context, source, settings);
|
||||
MODEL_CACHE.put(source, new CachedModel(cacheKey, model));
|
||||
return model;
|
||||
}
|
||||
|
||||
private static Model buildUncached(Context context, TextView source, SbtSettings settings) {
|
||||
if (!hasCustomClockTextEnabled(settings)) {
|
||||
ArrayList<Row> rows = new ArrayList<>();
|
||||
CharSequence text = source.getText();
|
||||
rows.add(new Row(text, "", "", 0, 0));
|
||||
CharSequence description = source.getContentDescription();
|
||||
return new Model(rows, description != null ? description.toString() : String.valueOf(text), false);
|
||||
}
|
||||
if (settings.clockCustomFormatEnabled
|
||||
&& settings.clockCustomFormat != null
|
||||
&& !settings.clockCustomFormat.isEmpty()) {
|
||||
ClockParsedPattern parsedPattern = PATTERN_PARSER.parse(settings.clockCustomFormat);
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> renderedRows =
|
||||
TEXT_RENDERER.renderLayoutRows(context, parsedPattern, source.getCurrentTextColor());
|
||||
ArrayList<Row> rows = new ArrayList<>();
|
||||
StringBuilder description = new StringBuilder();
|
||||
for (int i = 0; i < renderedRows.size(); i++) {
|
||||
ClockTextRenderer.LayoutRowResult row = renderedRows.get(i);
|
||||
if (i > 0) {
|
||||
description.append('\n');
|
||||
}
|
||||
description.append(row.contentDescription);
|
||||
rows.add(new Row(
|
||||
row.text,
|
||||
row.leftText,
|
||||
row.rightText,
|
||||
row.pipeCount,
|
||||
row.gapBeforePx));
|
||||
}
|
||||
return new Model(rows, description.toString(), parsedPattern.containsSeconds);
|
||||
}
|
||||
CharSequence text = renderFallbackClockText(context, settings);
|
||||
ArrayList<Row> rows = new ArrayList<>();
|
||||
rows.add(new Row(text, "", "", 0, 0));
|
||||
return new Model(rows, text != null ? text.toString() : "", settings.clockShowSeconds);
|
||||
}
|
||||
|
||||
private static CacheKey cacheKey(Context context, TextView source, SbtSettings settings) {
|
||||
String pattern = settings.clockCustomFormat != null ? settings.clockCustomFormat : "";
|
||||
boolean customPattern = settings.clockCustomFormatEnabled && !pattern.isEmpty();
|
||||
boolean generatedText = hasCustomClockTextEnabled(settings);
|
||||
boolean seconds = settings.clockShowSeconds
|
||||
|| (customPattern && ClockMarkupSupport.containsSecondsPattern(pattern));
|
||||
long divisor = seconds ? 1_000L : 60_000L;
|
||||
CharSequence sourceText = source.getText();
|
||||
CharSequence sourceDescription = source.getContentDescription();
|
||||
return new CacheKey(
|
||||
localeFor(context).toLanguageTag(),
|
||||
System.currentTimeMillis() / divisor,
|
||||
source.getCurrentTextColor(),
|
||||
settings.clockShowSeconds,
|
||||
settings.clockShowDatePrefix,
|
||||
customPattern,
|
||||
pattern,
|
||||
!generatedText && sourceText != null ? sourceText.toString() : "",
|
||||
!generatedText && sourceDescription != null ? sourceDescription.toString() : "");
|
||||
}
|
||||
|
||||
private static boolean hasCustomClockTextEnabled(SbtSettings settings) {
|
||||
if (settings == null) {
|
||||
return false;
|
||||
}
|
||||
String pattern = settings.clockCustomFormat != null ? settings.clockCustomFormat : "";
|
||||
return settings.clockShowSeconds
|
||||
|| settings.clockShowDatePrefix
|
||||
|| (settings.clockCustomFormatEnabled && !pattern.isEmpty());
|
||||
}
|
||||
|
||||
private static CharSequence renderFallbackClockText(Context context, SbtSettings settings) {
|
||||
Locale locale = localeFor(context);
|
||||
Date now = new Date();
|
||||
String timePattern = DateFormat.getBestDateTimePattern(
|
||||
locale,
|
||||
settings.clockShowSeconds ? "Hmss" : "Hm");
|
||||
String timeText = new SimpleDateFormat(timePattern, locale).format(now);
|
||||
if (!settings.clockShowDatePrefix) {
|
||||
return timeText;
|
||||
}
|
||||
String datePattern = DateFormat.getBestDateTimePattern(locale, "EEE d MMM");
|
||||
String dateText = new SimpleDateFormat(datePattern, locale).format(now);
|
||||
return dateText + " " + timeText;
|
||||
}
|
||||
|
||||
private static Locale localeFor(Context context) {
|
||||
android.content.res.Configuration configuration = context.getResources().getConfiguration();
|
||||
android.os.LocaleList locales = configuration.getLocales();
|
||||
if (locales != null && !locales.isEmpty()) {
|
||||
return locales.get(0);
|
||||
}
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
public static final class Model {
|
||||
public final ArrayList<Row> rows;
|
||||
public final String contentDescription;
|
||||
public final boolean containsSeconds;
|
||||
|
||||
Model(ArrayList<Row> rows, String contentDescription, boolean containsSeconds) {
|
||||
this.rows = rows != null ? rows : new ArrayList<>();
|
||||
this.contentDescription = contentDescription != null ? contentDescription : "";
|
||||
this.containsSeconds = containsSeconds;
|
||||
}
|
||||
|
||||
static Model empty() {
|
||||
return new Model(new ArrayList<>(), "", false);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return rows.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Row {
|
||||
public final CharSequence text;
|
||||
public final CharSequence leftText;
|
||||
public final CharSequence rightText;
|
||||
public final int pipeCount;
|
||||
public final int gapBeforePx;
|
||||
|
||||
Row(
|
||||
CharSequence text,
|
||||
CharSequence leftText,
|
||||
CharSequence rightText,
|
||||
int pipeCount,
|
||||
int gapBeforePx
|
||||
) {
|
||||
this.text = text != null ? text : "";
|
||||
this.leftText = leftText != null ? leftText : "";
|
||||
this.rightText = rightText != null ? rightText : "";
|
||||
this.pipeCount = Math.max(0, pipeCount);
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
}
|
||||
}
|
||||
|
||||
private record CacheKey(
|
||||
String locale,
|
||||
long timeBucket,
|
||||
int referenceColor,
|
||||
boolean showSeconds,
|
||||
boolean showDatePrefix,
|
||||
boolean customPattern,
|
||||
String pattern,
|
||||
String sourceText,
|
||||
String sourceDescription
|
||||
) {
|
||||
}
|
||||
|
||||
private record CachedModel(CacheKey key, Model model) {
|
||||
}
|
||||
}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
final class ClockMarkupSupport {
|
||||
|
||||
private ClockMarkupSupport() {
|
||||
}
|
||||
|
||||
enum TagCategory {
|
||||
OTHER,
|
||||
FONT_COLOR,
|
||||
FONT_FACE
|
||||
}
|
||||
|
||||
static final class OpenTag {
|
||||
final String name;
|
||||
final String openLiteral;
|
||||
final String closeLiteral;
|
||||
final TagCategory category;
|
||||
|
||||
OpenTag(String name, String openLiteral, String closeLiteral, TagCategory category) {
|
||||
this.name = name;
|
||||
this.openLiteral = openLiteral;
|
||||
this.closeLiteral = closeLiteral;
|
||||
this.category = category != null ? category : TagCategory.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
static final class TagState {
|
||||
final ArrayList<OpenTag> stack = new ArrayList<>();
|
||||
|
||||
TagState() {
|
||||
}
|
||||
|
||||
TagState(TagState other) {
|
||||
if (other != null) {
|
||||
stack.addAll(other.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static final class ParsedTag {
|
||||
final boolean closing;
|
||||
final boolean selfClosing;
|
||||
final String name;
|
||||
final String literal;
|
||||
final TagCategory category;
|
||||
|
||||
ParsedTag(
|
||||
boolean closing,
|
||||
boolean selfClosing,
|
||||
String name,
|
||||
String literal,
|
||||
TagCategory category
|
||||
) {
|
||||
this.closing = closing;
|
||||
this.selfClosing = selfClosing;
|
||||
this.name = name != null ? name : "";
|
||||
this.literal = literal != null ? literal : "";
|
||||
this.category = category != null ? category : TagCategory.OTHER;
|
||||
}
|
||||
}
|
||||
|
||||
static void appendOpenTags(StringBuilder out, TagState state) {
|
||||
for (OpenTag tag : state.stack) {
|
||||
out.append(tag.openLiteral);
|
||||
}
|
||||
}
|
||||
|
||||
static void closeAllOpenTags(StringBuilder out, TagState state) {
|
||||
for (int i = state.stack.size() - 1; i >= 0; i--) {
|
||||
out.append(state.stack.get(i).closeLiteral);
|
||||
}
|
||||
state.stack.clear();
|
||||
}
|
||||
|
||||
static void closeAllFontTags(StringBuilder out, TagState state) {
|
||||
for (int i = state.stack.size() - 1; i >= 0; i--) {
|
||||
OpenTag tag = state.stack.get(i);
|
||||
if (tag.category != TagCategory.FONT_COLOR && tag.category != TagCategory.FONT_FACE) {
|
||||
continue;
|
||||
}
|
||||
out.append(tag.closeLiteral);
|
||||
state.stack.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
static void applyParsedTag(StringBuilder out, TagState state, ParsedTag parsedTag) {
|
||||
if (TextUtils.isEmpty(parsedTag.name)) {
|
||||
return;
|
||||
}
|
||||
if (parsedTag.selfClosing) {
|
||||
if (out != null) {
|
||||
out.append(parsedTag.literal);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (parsedTag.closing) {
|
||||
closeTag(out, state, parsedTag.name);
|
||||
return;
|
||||
}
|
||||
openTag(out, state, parsedTag.name, parsedTag.literal, parsedTag.category);
|
||||
}
|
||||
|
||||
static void consumeTagLiteral(TagState state, String literal) {
|
||||
ParsedTag parsedTag = parseTagLiteral(literal);
|
||||
if (parsedTag == null) {
|
||||
throw new IllegalArgumentException("Malformed tag");
|
||||
}
|
||||
applyParsedTag(null, state, parsedTag);
|
||||
}
|
||||
|
||||
private static void openTag(
|
||||
StringBuilder out,
|
||||
TagState state,
|
||||
String name,
|
||||
String literal,
|
||||
TagCategory category
|
||||
) {
|
||||
if (TextUtils.isEmpty(name) || TextUtils.isEmpty(literal)) {
|
||||
return;
|
||||
}
|
||||
if (category == TagCategory.FONT_COLOR) {
|
||||
closeLastCategory(out, state, TagCategory.FONT_COLOR);
|
||||
} else if (category == TagCategory.FONT_FACE) {
|
||||
closeLastCategory(out, state, TagCategory.FONT_FACE);
|
||||
}
|
||||
if (out != null) {
|
||||
out.append(literal);
|
||||
}
|
||||
state.stack.add(new OpenTag(name, literal, "</" + name + ">", category));
|
||||
}
|
||||
|
||||
private static void closeTag(StringBuilder out, TagState state, String name) {
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
return;
|
||||
}
|
||||
String normalized = name.toLowerCase(Locale.ROOT);
|
||||
if ("font".equals(normalized)) {
|
||||
for (int i = state.stack.size() - 1; i >= 0; i--) {
|
||||
OpenTag tag = state.stack.get(i);
|
||||
if (!"font".equals(tag.name)) {
|
||||
continue;
|
||||
}
|
||||
if (out != null) {
|
||||
out.append(tag.closeLiteral);
|
||||
}
|
||||
state.stack.remove(i);
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException("Unmatched closing tag");
|
||||
}
|
||||
for (int i = state.stack.size() - 1; i >= 0; i--) {
|
||||
OpenTag tag = state.stack.get(i);
|
||||
if (!normalized.equals(tag.name)) {
|
||||
continue;
|
||||
}
|
||||
if (out != null) {
|
||||
out.append(tag.closeLiteral);
|
||||
}
|
||||
state.stack.remove(i);
|
||||
return;
|
||||
}
|
||||
throw new IllegalArgumentException("Unmatched closing tag");
|
||||
}
|
||||
|
||||
private static void closeLastCategory(StringBuilder out, TagState state, TagCategory category) {
|
||||
for (int i = state.stack.size() - 1; i >= 0; i--) {
|
||||
OpenTag tag = state.stack.get(i);
|
||||
if (tag.category != category) {
|
||||
continue;
|
||||
}
|
||||
if (out != null) {
|
||||
out.append(tag.closeLiteral);
|
||||
}
|
||||
state.stack.remove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
static ParsedTag parseTagLiteral(String literal) {
|
||||
if (TextUtils.isEmpty(literal)) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = literal.trim();
|
||||
if (!trimmed.startsWith("<") || !trimmed.endsWith(">")) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.matches("(?i)<\\s*br\\s*/?\\s*>")) {
|
||||
return new ParsedTag(false, true, "br", trimmed, TagCategory.OTHER);
|
||||
}
|
||||
if (trimmed.matches("(?i)<\\s*/\\s*([a-z0-9_-]+)\\s*>")) {
|
||||
String name = trimmed.replaceAll(
|
||||
"(?i)<\\s*/\\s*([a-z0-9_-]+)\\s*>",
|
||||
"$1").toLowerCase(Locale.ROOT);
|
||||
return new ParsedTag(true, false, name, trimmed, TagCategory.OTHER);
|
||||
}
|
||||
if (trimmed.matches("(?i)<\\s*([a-z0-9_-]+)(?:\\s+[^>]*)?/\\s*>")) {
|
||||
String name = trimmed.replaceAll(
|
||||
"(?i)<\\s*([a-z0-9_-]+)(?:\\s+[^>]*)?/\\s*>",
|
||||
"$1").toLowerCase(Locale.ROOT);
|
||||
return new ParsedTag(false, true, name, trimmed, TagCategory.OTHER);
|
||||
}
|
||||
java.util.regex.Matcher matcher = java.util.regex.Pattern
|
||||
.compile("(?i)<\\s*([a-z0-9_-]+)([^>]*)>")
|
||||
.matcher(trimmed);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
String name = matcher.group(1).toLowerCase(Locale.ROOT);
|
||||
String attributes = matcher.group(2) != null ? matcher.group(2) : "";
|
||||
TagCategory category = TagCategory.OTHER;
|
||||
if ("font".equals(name)) {
|
||||
boolean hasColor = attributes.matches("(?is).*\\bcolor\\s*=\\s*['\"][^'\"]+['\"].*");
|
||||
boolean hasFace = attributes.matches("(?is).*\\bface\\s*=\\s*['\"][^'\"]+['\"].*");
|
||||
if (hasColor && !hasFace) {
|
||||
category = TagCategory.FONT_COLOR;
|
||||
} else if (hasFace && !hasColor) {
|
||||
category = TagCategory.FONT_FACE;
|
||||
}
|
||||
} else if ("sbt-clock-color".equals(name)) {
|
||||
category = TagCategory.FONT_COLOR;
|
||||
}
|
||||
return new ParsedTag(false, false, name, trimmed, category);
|
||||
}
|
||||
|
||||
static List<Integer> findPipePositions(String markup) {
|
||||
if (TextUtils.isEmpty(markup)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
ArrayList<Integer> positions = new ArrayList<>();
|
||||
boolean insideTag = false;
|
||||
boolean insideQuote = false;
|
||||
for (int i = 0; i < markup.length(); i++) {
|
||||
char c = markup.charAt(i);
|
||||
if (insideTag) {
|
||||
if (c == '>') {
|
||||
insideTag = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '<') {
|
||||
insideTag = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'') {
|
||||
insideQuote = !insideQuote;
|
||||
continue;
|
||||
}
|
||||
if (c == '|' && !insideQuote) {
|
||||
positions.add(i);
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
static boolean containsSecondsPattern(String pattern) {
|
||||
if (TextUtils.isEmpty(pattern)) {
|
||||
return false;
|
||||
}
|
||||
boolean insideTag = false;
|
||||
boolean insideBlock = false;
|
||||
boolean insideQuote = false;
|
||||
for (int i = 0; i < pattern.length(); i++) {
|
||||
char c = pattern.charAt(i);
|
||||
if (insideTag) {
|
||||
if (c == '>') {
|
||||
insideTag = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (insideBlock) {
|
||||
if (c == '}') {
|
||||
insideBlock = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '<') {
|
||||
insideTag = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '{') {
|
||||
insideBlock = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'') {
|
||||
insideQuote = !insideQuote;
|
||||
continue;
|
||||
}
|
||||
if (!insideQuote && c == 's') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ClockParsedPattern {
|
||||
|
||||
final List<ClockParsedRow> rows;
|
||||
final boolean containsSeconds;
|
||||
|
||||
ClockParsedPattern(List<ClockParsedRow> rows, boolean containsSeconds) {
|
||||
this.rows = rows != null
|
||||
? Collections.unmodifiableList(new ArrayList<>(rows))
|
||||
: Collections.emptyList();
|
||||
this.containsSeconds = containsSeconds;
|
||||
}
|
||||
|
||||
boolean isMultiline() {
|
||||
return rows.size() > 1;
|
||||
}
|
||||
|
||||
boolean hasPipeRows() {
|
||||
for (ClockParsedRow row : rows) {
|
||||
if (row != null && row.pipeCount > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static final class ClockParsedRow {
|
||||
final String markup;
|
||||
final int gapBeforePx;
|
||||
final boolean syntaxError;
|
||||
final int pipeCount;
|
||||
final String leftSegmentMarkup;
|
||||
final String middleSegmentMarkup;
|
||||
final String rightSegmentMarkup;
|
||||
|
||||
ClockParsedRow(
|
||||
String markup,
|
||||
int gapBeforePx,
|
||||
boolean syntaxError,
|
||||
int pipeCount,
|
||||
String leftSegmentMarkup,
|
||||
String middleSegmentMarkup,
|
||||
String rightSegmentMarkup
|
||||
) {
|
||||
this.markup = markup != null ? markup : "";
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
this.syntaxError = syntaxError;
|
||||
this.pipeCount = Math.max(0, pipeCount);
|
||||
this.leftSegmentMarkup = leftSegmentMarkup != null ? leftSegmentMarkup : "";
|
||||
this.middleSegmentMarkup = middleSegmentMarkup != null ? middleSegmentMarkup : "";
|
||||
this.rightSegmentMarkup = rightSegmentMarkup != null ? rightSegmentMarkup : "";
|
||||
}
|
||||
|
||||
static ClockParsedRow plain(String markup, int gapBeforePx) {
|
||||
String safeMarkup = markup != null ? markup : "";
|
||||
return new ClockParsedRow(
|
||||
safeMarkup,
|
||||
gapBeforePx,
|
||||
false,
|
||||
0,
|
||||
safeMarkup,
|
||||
"",
|
||||
"");
|
||||
}
|
||||
|
||||
static ClockParsedRow piped(
|
||||
String markup,
|
||||
int gapBeforePx,
|
||||
int pipeCount,
|
||||
String leftSegmentMarkup,
|
||||
String middleSegmentMarkup,
|
||||
String rightSegmentMarkup
|
||||
) {
|
||||
return new ClockParsedRow(
|
||||
markup,
|
||||
gapBeforePx,
|
||||
false,
|
||||
pipeCount,
|
||||
leftSegmentMarkup,
|
||||
middleSegmentMarkup,
|
||||
rightSegmentMarkup);
|
||||
}
|
||||
|
||||
static ClockParsedRow syntaxError(int gapBeforePx) {
|
||||
return new ClockParsedRow(
|
||||
"Syntax error",
|
||||
gapBeforePx,
|
||||
true,
|
||||
0,
|
||||
"Syntax error",
|
||||
"",
|
||||
"");
|
||||
}
|
||||
|
||||
boolean hasPipeSplit() {
|
||||
return pipeCount > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
final class ClockPatternParser {
|
||||
|
||||
static final int ROW_GAP_AUTO = Integer.MIN_VALUE;
|
||||
|
||||
ClockParsedPattern parse(String rawPattern) {
|
||||
String pattern = rawPattern;
|
||||
ArrayList<ClockParsedPattern.ClockParsedRow> rows = new ArrayList<>();
|
||||
boolean containsSeconds = ClockMarkupSupport.containsSecondsPattern(pattern);
|
||||
|
||||
ClockMarkupSupport.TagState carriedState = new ClockMarkupSupport.TagState();
|
||||
ClockMarkupSupport.TagState rowState = new ClockMarkupSupport.TagState(carriedState);
|
||||
StringBuilder rowMarkup = new StringBuilder();
|
||||
ClockMarkupSupport.appendOpenTags(rowMarkup, carriedState);
|
||||
boolean rowSyntaxError = false;
|
||||
int gapBeforePx = 0;
|
||||
|
||||
int index = 0;
|
||||
while (index < pattern.length()) {
|
||||
LineBreakToken lineBreak;
|
||||
try {
|
||||
lineBreak = tryParseLineBreak(pattern, index);
|
||||
} catch (IllegalArgumentException e) {
|
||||
rowSyntaxError = true;
|
||||
int blockEnd = pattern.indexOf('}', index + 1);
|
||||
index = blockEnd >= 0 ? blockEnd + 1 : pattern.length();
|
||||
continue;
|
||||
}
|
||||
if (lineBreak != null) {
|
||||
rows.add(finalizeRow(rowMarkup, rowState, gapBeforePx, rowSyntaxError));
|
||||
carriedState = (lineBreak.resetPropagation || rowSyntaxError)
|
||||
? new ClockMarkupSupport.TagState()
|
||||
: new ClockMarkupSupport.TagState(rowState);
|
||||
rowState = new ClockMarkupSupport.TagState(carriedState);
|
||||
rowMarkup = new StringBuilder();
|
||||
ClockMarkupSupport.appendOpenTags(rowMarkup, carriedState);
|
||||
rowSyntaxError = false;
|
||||
gapBeforePx = lineBreak.gapBeforePx;
|
||||
index = lineBreak.endIndex;
|
||||
continue;
|
||||
}
|
||||
if (rowSyntaxError) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
char c = pattern.charAt(index);
|
||||
if (c == '{') {
|
||||
int blockEnd = pattern.indexOf('}', index + 1);
|
||||
if (blockEnd < 0) {
|
||||
rowSyntaxError = true;
|
||||
index = pattern.length();
|
||||
continue;
|
||||
}
|
||||
String content = pattern.substring(index + 1, blockEnd);
|
||||
try {
|
||||
appendShorthandBlock(rowMarkup, rowState, content);
|
||||
} catch (IllegalArgumentException e) {
|
||||
rowSyntaxError = true;
|
||||
}
|
||||
index = blockEnd + 1;
|
||||
continue;
|
||||
}
|
||||
if (c == '<') {
|
||||
int tagEnd = pattern.indexOf('>', index + 1);
|
||||
if (tagEnd < 0) {
|
||||
rowSyntaxError = true;
|
||||
index = pattern.length();
|
||||
continue;
|
||||
}
|
||||
String literal = pattern.substring(index, tagEnd + 1);
|
||||
try {
|
||||
ClockMarkupSupport.ParsedTag parsedTag = ClockMarkupSupport.parseTagLiteral(literal);
|
||||
if (parsedTag == null) {
|
||||
throw new IllegalArgumentException("Malformed tag");
|
||||
}
|
||||
ClockMarkupSupport.applyParsedTag(rowMarkup, rowState, parsedTag);
|
||||
} catch (IllegalArgumentException e) {
|
||||
rowSyntaxError = true;
|
||||
}
|
||||
index = tagEnd + 1;
|
||||
continue;
|
||||
}
|
||||
rowMarkup.append(c);
|
||||
index++;
|
||||
}
|
||||
rows.add(finalizeRow(rowMarkup, rowState, gapBeforePx, rowSyntaxError));
|
||||
return new ClockParsedPattern(rows, containsSeconds);
|
||||
}
|
||||
|
||||
private ClockParsedPattern.ClockParsedRow finalizeRow(
|
||||
StringBuilder rowMarkup,
|
||||
ClockMarkupSupport.TagState rowState,
|
||||
int gapBeforePx,
|
||||
boolean rowSyntaxError
|
||||
) {
|
||||
if (rowSyntaxError) {
|
||||
return ClockParsedPattern.ClockParsedRow.syntaxError(gapBeforePx);
|
||||
}
|
||||
StringBuilder balanced = new StringBuilder(rowMarkup);
|
||||
ClockMarkupSupport.closeAllOpenTags(
|
||||
balanced,
|
||||
new ClockMarkupSupport.TagState(rowState));
|
||||
return buildParsedRow(balanced.toString(), gapBeforePx);
|
||||
}
|
||||
|
||||
private ClockParsedPattern.ClockParsedRow buildParsedRow(String markup, int gapBeforePx) {
|
||||
if (TextUtils.isEmpty(markup)) {
|
||||
return ClockParsedPattern.ClockParsedRow.plain("", gapBeforePx);
|
||||
}
|
||||
java.util.List<Integer> pipePositions = ClockMarkupSupport.findPipePositions(markup);
|
||||
if (pipePositions.size() > 2) {
|
||||
return ClockParsedPattern.ClockParsedRow.syntaxError(gapBeforePx);
|
||||
}
|
||||
if (pipePositions.isEmpty()) {
|
||||
return ClockParsedPattern.ClockParsedRow.plain(markup, gapBeforePx);
|
||||
}
|
||||
if (pipePositions.size() == 1) {
|
||||
int split = pipePositions.get(0);
|
||||
return ClockParsedPattern.ClockParsedRow.piped(
|
||||
markup,
|
||||
gapBeforePx,
|
||||
1,
|
||||
markup.substring(0, split),
|
||||
"",
|
||||
markup.substring(split + 1));
|
||||
}
|
||||
int first = pipePositions.get(0);
|
||||
int second = pipePositions.get(1);
|
||||
return ClockParsedPattern.ClockParsedRow.piped(
|
||||
markup,
|
||||
gapBeforePx,
|
||||
2,
|
||||
markup.substring(0, first),
|
||||
markup.substring(first + 1, second),
|
||||
markup.substring(second + 1));
|
||||
}
|
||||
|
||||
private void appendShorthandBlock(
|
||||
StringBuilder out,
|
||||
ClockMarkupSupport.TagState tagState,
|
||||
String content
|
||||
) {
|
||||
String trimmed = content.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String[] tokens = trimmed.split("\\s+");
|
||||
for (String token : tokens) {
|
||||
if (TextUtils.isEmpty(token)) {
|
||||
continue;
|
||||
}
|
||||
if ("/font".equalsIgnoreCase(token)) {
|
||||
ClockMarkupSupport.closeAllFontTags(out, tagState);
|
||||
continue;
|
||||
}
|
||||
if (token.charAt(0) == '/') {
|
||||
String name = token.substring(1).trim();
|
||||
if (name.isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid shorthand closing tag");
|
||||
}
|
||||
ClockMarkupSupport.applyParsedTag(
|
||||
out,
|
||||
tagState,
|
||||
new ClockMarkupSupport.ParsedTag(
|
||||
true,
|
||||
false,
|
||||
name.toLowerCase(java.util.Locale.ROOT),
|
||||
"</" + name + ">",
|
||||
ClockMarkupSupport.TagCategory.OTHER));
|
||||
continue;
|
||||
}
|
||||
if (token.charAt(0) == '#') {
|
||||
String literal = "<sbt-clock-color value='"
|
||||
+ android.text.TextUtils.htmlEncode(token)
|
||||
+ "'>";
|
||||
ClockMarkupSupport.applyParsedTag(
|
||||
out,
|
||||
tagState,
|
||||
new ClockMarkupSupport.ParsedTag(
|
||||
false,
|
||||
false,
|
||||
"sbt-clock-color",
|
||||
literal,
|
||||
ClockMarkupSupport.TagCategory.FONT_COLOR));
|
||||
continue;
|
||||
}
|
||||
if (token.charAt(0) == '@' && token.length() > 1) {
|
||||
String face = android.text.TextUtils.htmlEncode(token.substring(1));
|
||||
String literal = "<font face='" + face + "'>";
|
||||
ClockMarkupSupport.applyParsedTag(
|
||||
out,
|
||||
tagState,
|
||||
new ClockMarkupSupport.ParsedTag(
|
||||
false,
|
||||
false,
|
||||
"font",
|
||||
literal,
|
||||
ClockMarkupSupport.TagCategory.FONT_FACE));
|
||||
continue;
|
||||
}
|
||||
String literal = "<" + token + ">";
|
||||
ClockMarkupSupport.applyParsedTag(
|
||||
out,
|
||||
tagState,
|
||||
new ClockMarkupSupport.ParsedTag(
|
||||
false,
|
||||
false,
|
||||
token.toLowerCase(java.util.Locale.ROOT),
|
||||
literal,
|
||||
ClockMarkupSupport.TagCategory.OTHER));
|
||||
}
|
||||
}
|
||||
|
||||
private LineBreakToken tryParseLineBreak(String pattern, int index) {
|
||||
if (pattern.startsWith("\\n", index)) {
|
||||
return new LineBreakToken(index + 2, false, ROW_GAP_AUTO);
|
||||
}
|
||||
if (pattern.charAt(index) != '{') {
|
||||
return null;
|
||||
}
|
||||
int end = pattern.indexOf('}', index + 1);
|
||||
if (end < 0) {
|
||||
return null;
|
||||
}
|
||||
String body = pattern.substring(index + 1, end).trim();
|
||||
boolean resetPropagation = false;
|
||||
if (body.startsWith("/")) {
|
||||
resetPropagation = true;
|
||||
body = body.substring(1).trim();
|
||||
}
|
||||
if (!body.startsWith("\\n")) {
|
||||
return null;
|
||||
}
|
||||
String gapText = body.substring(2).trim();
|
||||
int gapBeforePx = ROW_GAP_AUTO;
|
||||
if (!gapText.isEmpty()) {
|
||||
try {
|
||||
gapBeforePx = Integer.parseInt(gapText);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid row gap", e);
|
||||
}
|
||||
}
|
||||
return new LineBreakToken(end + 1, resetPropagation, gapBeforePx);
|
||||
}
|
||||
|
||||
private static final class LineBreakToken {
|
||||
final int endIndex;
|
||||
final boolean resetPropagation;
|
||||
final int gapBeforePx;
|
||||
|
||||
LineBreakToken(int endIndex, boolean resetPropagation, int gapBeforePx) {
|
||||
this.endIndex = endIndex;
|
||||
this.resetPropagation = resetPropagation;
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
}
|
||||
}
|
||||
}
|
||||
+607
@@ -0,0 +1,607 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
final class ClockTextRenderer {
|
||||
|
||||
private static final String RESOLVED_COLOR_TAG_PREFIX = "sbt-color-";
|
||||
private static final Pattern INTERNAL_COLOR_OPEN_TAG_PATTERN =
|
||||
Pattern.compile("(?is)<\\s*sbt-clock-color\\b[^>]*\\bvalue\\s*=\\s*(['\"])(.*?)\\1[^>]*>");
|
||||
private static final Pattern INTERNAL_COLOR_CLOSE_TAG_PATTERN =
|
||||
Pattern.compile("(?is)<\\s*/\\s*sbt-clock-color\\s*>");
|
||||
private static final Pattern FONT_COLOR_TAG_PATTERN =
|
||||
Pattern.compile("(?i)<\\s*font\\b[^>]*\\bcolor\\s*=\\s*['\"](#(?:[0-9a-f]{8}|[0-9a-f]{6}))['\"][^>]*>");
|
||||
private static final Pattern FONT_COLOR_ATTRIBUTE_PATTERN =
|
||||
Pattern.compile("(?i)(<\\s*font\\b[^>]*\\bcolor\\s*=\\s*['\"])([^'\"]+)(['\"])");
|
||||
|
||||
private final ClockColorResolver colorResolver = new ClockColorResolver();
|
||||
|
||||
ArrayList<RowResult> renderRows(
|
||||
Context context,
|
||||
ClockParsedPattern parsedPattern,
|
||||
int referenceColor
|
||||
) {
|
||||
ArrayList<RowResult> rowResults = new ArrayList<>();
|
||||
if (parsedPattern.rows.isEmpty()) {
|
||||
return rowResults;
|
||||
}
|
||||
Locale locale = localeFor(context);
|
||||
Date now = new Date();
|
||||
for (ClockParsedPattern.ClockParsedRow row : parsedPattern.rows) {
|
||||
rowResults.add(renderRow(context, locale, now, row, referenceColor));
|
||||
}
|
||||
return rowResults;
|
||||
}
|
||||
|
||||
ArrayList<LayoutRowResult> renderLayoutRows(
|
||||
Context context,
|
||||
ClockParsedPattern parsedPattern,
|
||||
int referenceColor
|
||||
) {
|
||||
ArrayList<LayoutRowResult> rowResults = new ArrayList<>();
|
||||
if (parsedPattern.rows.isEmpty()) {
|
||||
return rowResults;
|
||||
}
|
||||
Locale locale = localeFor(context);
|
||||
Date now = new Date();
|
||||
for (ClockParsedPattern.ClockParsedRow row : parsedPattern.rows) {
|
||||
rowResults.add(renderLayoutRow(context, locale, now, row, referenceColor));
|
||||
}
|
||||
return rowResults;
|
||||
}
|
||||
|
||||
private LayoutRowResult renderLayoutRow(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
ClockParsedPattern.ClockParsedRow row,
|
||||
int referenceColor
|
||||
) {
|
||||
if (row == null || row.syntaxError) {
|
||||
return new LayoutRowResult("Syntax error", "", "", "Syntax error", 0, row != null ? row.gapBeforePx : 0);
|
||||
}
|
||||
try {
|
||||
if (row.pipeCount <= 0) {
|
||||
CharSequence text = formatMarkup(context, locale, now, row.markup, referenceColor);
|
||||
return new LayoutRowResult(text, "", "", text != null ? text.toString() : "", 0, row.gapBeforePx);
|
||||
}
|
||||
ClockMarkupSupport.TagState tagState = new ClockMarkupSupport.TagState();
|
||||
CharSequence left = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
row.leftSegmentMarkup,
|
||||
tagState);
|
||||
CharSequence middle = "";
|
||||
if (!TextUtils.isEmpty(row.middleSegmentMarkup)) {
|
||||
middle = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
row.middleSegmentMarkup,
|
||||
tagState);
|
||||
}
|
||||
CharSequence right = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
row.rightSegmentMarkup,
|
||||
tagState);
|
||||
SpannableStringBuilder text = new SpannableStringBuilder();
|
||||
if (left != null) {
|
||||
text.append(left);
|
||||
}
|
||||
if (middle != null) {
|
||||
text.append(middle);
|
||||
}
|
||||
if (right != null) {
|
||||
text.append(right);
|
||||
}
|
||||
return new LayoutRowResult(
|
||||
text,
|
||||
left,
|
||||
right,
|
||||
text.toString(),
|
||||
row.pipeCount,
|
||||
row.gapBeforePx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new LayoutRowResult("Syntax error", "", "", "Syntax error", 0, row.gapBeforePx);
|
||||
}
|
||||
}
|
||||
|
||||
private RowResult renderRow(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
ClockParsedPattern.ClockParsedRow row,
|
||||
int referenceColor
|
||||
) {
|
||||
if (row == null || row.syntaxError) {
|
||||
return new RowResult("Syntax error", "Syntax error", row != null ? row.gapBeforePx : 0);
|
||||
}
|
||||
try {
|
||||
CharSequence display;
|
||||
if (row.pipeCount <= 0) {
|
||||
display = formatMarkup(context, locale, now, row.markup, referenceColor);
|
||||
} else if (row.pipeCount == 1) {
|
||||
display = renderSplitDisplay(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
row.leftSegmentMarkup,
|
||||
null,
|
||||
row.rightSegmentMarkup);
|
||||
} else {
|
||||
display = renderSplitDisplay(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
row.leftSegmentMarkup,
|
||||
row.middleSegmentMarkup,
|
||||
row.rightSegmentMarkup);
|
||||
}
|
||||
return new RowResult(display, display != null ? display.toString() : "", row.gapBeforePx);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return new RowResult("Syntax error", "Syntax error", row.gapBeforePx);
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence renderSplitDisplay(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
int referenceColor,
|
||||
String leftSegment,
|
||||
String middleSegment,
|
||||
String rightSegment
|
||||
) {
|
||||
ClockMarkupSupport.TagState tagState = new ClockMarkupSupport.TagState();
|
||||
CharSequence left = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
leftSegment,
|
||||
tagState);
|
||||
CharSequence middle = "";
|
||||
if (!TextUtils.isEmpty(middleSegment)) {
|
||||
middle = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
middleSegment,
|
||||
tagState);
|
||||
}
|
||||
CharSequence right = renderSegmentWithCarry(
|
||||
context,
|
||||
locale,
|
||||
now,
|
||||
referenceColor,
|
||||
rightSegment,
|
||||
tagState);
|
||||
SpannableStringBuilder out = new SpannableStringBuilder();
|
||||
if (left != null) {
|
||||
out.append(left);
|
||||
}
|
||||
if (middle != null) {
|
||||
out.append(middle);
|
||||
}
|
||||
if (right != null) {
|
||||
out.append(right);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private CharSequence renderSegmentWithCarry(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
int referenceColor,
|
||||
String segment,
|
||||
ClockMarkupSupport.TagState tagState
|
||||
) {
|
||||
if (TextUtils.isEmpty(segment)) {
|
||||
consumeSegmentTags(segment, tagState);
|
||||
return "";
|
||||
}
|
||||
StringBuilder markup = new StringBuilder();
|
||||
ClockMarkupSupport.appendOpenTags(markup, tagState);
|
||||
markup.append(segment);
|
||||
|
||||
ClockMarkupSupport.TagState previewState = new ClockMarkupSupport.TagState(tagState);
|
||||
consumeSegmentTags(segment, previewState);
|
||||
ClockMarkupSupport.closeAllOpenTags(markup, new ClockMarkupSupport.TagState(previewState));
|
||||
consumeSegmentTags(segment, tagState);
|
||||
return formatMarkup(context, locale, now, markup.toString(), referenceColor);
|
||||
}
|
||||
|
||||
private void consumeSegmentTags(String segment, ClockMarkupSupport.TagState tagState) {
|
||||
if (TextUtils.isEmpty(segment)) {
|
||||
return;
|
||||
}
|
||||
int index = 0;
|
||||
while (index < segment.length()) {
|
||||
int start = segment.indexOf('<', index);
|
||||
if (start < 0) {
|
||||
break;
|
||||
}
|
||||
int end = segment.indexOf('>', start + 1);
|
||||
if (end < 0) {
|
||||
throw new IllegalArgumentException("Malformed tag");
|
||||
}
|
||||
ClockMarkupSupport.consumeTagLiteral(tagState, segment.substring(start, end + 1));
|
||||
index = end + 1;
|
||||
}
|
||||
}
|
||||
|
||||
private CharSequence formatMarkup(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
String markup,
|
||||
int referenceColor
|
||||
) {
|
||||
if (TextUtils.isEmpty(markup)) {
|
||||
return "";
|
||||
}
|
||||
if (markup.indexOf('<') < 0 || markup.indexOf('>') < markup.indexOf('<')) {
|
||||
return new SimpleDateFormat(markup, locale).format(now);
|
||||
}
|
||||
String renderedHtml = formatPatternHtml(context, locale, now, markup, referenceColor);
|
||||
CharSequence parsed = HtmlCompat.fromHtml(
|
||||
renderedHtml,
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY,
|
||||
null,
|
||||
this::handleCustomHtmlTag);
|
||||
return applyAlphaAwareClockColours(parsed, renderedHtml);
|
||||
}
|
||||
|
||||
private String formatPatternHtml(
|
||||
Context context,
|
||||
Locale locale,
|
||||
Date now,
|
||||
String markup,
|
||||
int referenceColor
|
||||
) {
|
||||
StringBuilder out = new StringBuilder();
|
||||
StringBuilder textChunk = new StringBuilder();
|
||||
boolean insideTag = false;
|
||||
StringBuilder tagChunk = new StringBuilder();
|
||||
ArrayList<String> internalColorTagStack = new ArrayList<>();
|
||||
for (int i = 0; i < markup.length(); i++) {
|
||||
char c = markup.charAt(i);
|
||||
if (insideTag) {
|
||||
tagChunk.append(c);
|
||||
if (c == '>') {
|
||||
insideTag = false;
|
||||
out.append(normalizeHtmlTag(
|
||||
context,
|
||||
tagChunk.toString(),
|
||||
referenceColor,
|
||||
internalColorTagStack));
|
||||
tagChunk.setLength(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (c == '<') {
|
||||
if (textChunk.length() > 0) {
|
||||
out.append(encodeHtmlTextPreservingSpaces(
|
||||
new SimpleDateFormat(textChunk.toString(), locale).format(now)));
|
||||
textChunk.setLength(0);
|
||||
}
|
||||
insideTag = true;
|
||||
tagChunk.append(c);
|
||||
continue;
|
||||
}
|
||||
textChunk.append(c);
|
||||
}
|
||||
if (insideTag) {
|
||||
throw new IllegalArgumentException("Unterminated tag");
|
||||
}
|
||||
if (textChunk.length() > 0) {
|
||||
out.append(encodeHtmlTextPreservingSpaces(
|
||||
new SimpleDateFormat(textChunk.toString(), locale).format(now)));
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private String normalizeHtmlTag(
|
||||
Context context,
|
||||
String rawTag,
|
||||
int referenceColor,
|
||||
ArrayList<String> internalColorTagStack
|
||||
) {
|
||||
if (TextUtils.isEmpty(rawTag)) {
|
||||
return "";
|
||||
}
|
||||
Matcher internalColorOpen = INTERNAL_COLOR_OPEN_TAG_PATTERN.matcher(rawTag);
|
||||
if (internalColorOpen.matches()) {
|
||||
String colorExpression = decodeHtmlAttribute(internalColorOpen.group(2));
|
||||
String resolved = colorResolver.resolveColorHex(
|
||||
context,
|
||||
colorExpression,
|
||||
referenceColor,
|
||||
Color.WHITE);
|
||||
String tagName = resolvedColorTagName(resolved);
|
||||
internalColorTagStack.add(tagName);
|
||||
return "<" + tagName + ">";
|
||||
}
|
||||
if (INTERNAL_COLOR_CLOSE_TAG_PATTERN.matcher(rawTag).matches()) {
|
||||
if (internalColorTagStack.isEmpty()) {
|
||||
throw new IllegalArgumentException("Unmatched colour tag");
|
||||
}
|
||||
String tagName = internalColorTagStack.remove(internalColorTagStack.size() - 1);
|
||||
return "</" + tagName + ">";
|
||||
}
|
||||
Matcher matcher = FONT_COLOR_ATTRIBUTE_PATTERN.matcher(rawTag);
|
||||
StringBuffer out = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
String resolved = colorResolver.resolveColorHex(
|
||||
context,
|
||||
matcher.group(2),
|
||||
referenceColor,
|
||||
Color.WHITE);
|
||||
String replacement = matcher.group(1)
|
||||
+ Matcher.quoteReplacement(resolved)
|
||||
+ matcher.group(3);
|
||||
matcher.appendReplacement(out, replacement);
|
||||
}
|
||||
matcher.appendTail(out);
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private String resolvedColorTagName(String resolvedColor) {
|
||||
String hex = resolvedColor != null ? resolvedColor.trim() : "";
|
||||
if (hex.startsWith("#")) {
|
||||
hex = hex.substring(1);
|
||||
}
|
||||
if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Invalid resolved colour");
|
||||
}
|
||||
return RESOLVED_COLOR_TAG_PREFIX + hex.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String decodeHtmlAttribute(String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return "";
|
||||
}
|
||||
return value
|
||||
.replace(""", "\"")
|
||||
.replace("'", "'")
|
||||
.replace("'", "'")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("&", "&");
|
||||
}
|
||||
|
||||
private void handleCustomHtmlTag(
|
||||
boolean opening,
|
||||
String tag,
|
||||
Editable output,
|
||||
XMLReader xmlReader
|
||||
) {
|
||||
Integer color = colorFromResolvedTag(tag);
|
||||
if (color == null || output == null) {
|
||||
return;
|
||||
}
|
||||
if (opening) {
|
||||
output.setSpan(
|
||||
new ColorMarker(color),
|
||||
output.length(),
|
||||
output.length(),
|
||||
Spanned.SPAN_MARK_MARK);
|
||||
} else {
|
||||
endColorSpan(output);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer colorFromResolvedTag(String tag) {
|
||||
if (TextUtils.isEmpty(tag)) {
|
||||
return null;
|
||||
}
|
||||
String normalized = tag.toLowerCase(Locale.ROOT);
|
||||
if (!normalized.startsWith(RESOLVED_COLOR_TAG_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
String hex = normalized.substring(RESOLVED_COLOR_TAG_PREFIX.length());
|
||||
if (hex.length() != 6 && hex.length() != 8) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Color.parseColor("#" + hex);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void endColorSpan(Editable output) {
|
||||
ColorMarker marker = getLastSpan(output, ColorMarker.class);
|
||||
if (marker == null) {
|
||||
return;
|
||||
}
|
||||
int start = output.getSpanStart(marker);
|
||||
int end = output.length();
|
||||
output.removeSpan(marker);
|
||||
if (start < 0 || start >= end) {
|
||||
return;
|
||||
}
|
||||
output.setSpan(
|
||||
new ForegroundColorSpan(marker.color),
|
||||
start,
|
||||
end,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
private <T> T getLastSpan(Editable output, Class<T> kind) {
|
||||
T[] spans = output.getSpans(0, output.length(), kind);
|
||||
if (spans == null || spans.length == 0) {
|
||||
return null;
|
||||
}
|
||||
T last = null;
|
||||
int lastStart = -1;
|
||||
for (T span : spans) {
|
||||
int start = output.getSpanStart(span);
|
||||
if (start >= lastStart) {
|
||||
last = span;
|
||||
lastStart = start;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
private CharSequence applyAlphaAwareClockColours(CharSequence parsed, String renderedHtml) {
|
||||
if (!(parsed instanceof Spanned) || TextUtils.isEmpty(renderedHtml)) {
|
||||
return parsed;
|
||||
}
|
||||
ArrayList<Integer> colourSequence = new ArrayList<>();
|
||||
Matcher matcher = FONT_COLOR_TAG_PATTERN.matcher(renderedHtml);
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
colourSequence.add(Color.parseColor(matcher.group(1)));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
if (colourSequence.isEmpty()) {
|
||||
return parsed;
|
||||
}
|
||||
SpannableStringBuilder out = new SpannableStringBuilder(parsed);
|
||||
ForegroundColorSpan[] spans = out.getSpans(0, out.length(), ForegroundColorSpan.class);
|
||||
if (spans == null || spans.length == 0) {
|
||||
return parsed;
|
||||
}
|
||||
Arrays.sort(spans, Comparator
|
||||
.comparingInt((ForegroundColorSpan span) -> out.getSpanStart(span))
|
||||
.thenComparingInt(span -> out.getSpanEnd(span)));
|
||||
int count = Math.min(spans.length, colourSequence.size());
|
||||
boolean changed = false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int color = colourSequence.get(i);
|
||||
if (Color.alpha(color) >= 255) {
|
||||
continue;
|
||||
}
|
||||
ForegroundColorSpan span = spans[i];
|
||||
int start = out.getSpanStart(span);
|
||||
int end = out.getSpanEnd(span);
|
||||
int flags = out.getSpanFlags(span);
|
||||
if (start < 0 || end <= start) {
|
||||
continue;
|
||||
}
|
||||
out.removeSpan(span);
|
||||
out.setSpan(new ForegroundColorSpan(color), start, end, flags);
|
||||
changed = true;
|
||||
}
|
||||
return changed ? out : parsed;
|
||||
}
|
||||
|
||||
private String encodeHtmlTextPreservingSpaces(String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return "";
|
||||
}
|
||||
String encoded = TextUtils.htmlEncode(text);
|
||||
StringBuilder out = new StringBuilder(encoded.length());
|
||||
int index = 0;
|
||||
while (index < encoded.length()) {
|
||||
char c = encoded.charAt(index);
|
||||
if (c != ' ') {
|
||||
out.append(c);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
int runStart = index;
|
||||
while (index < encoded.length() && encoded.charAt(index) == ' ') {
|
||||
index++;
|
||||
}
|
||||
int runLength = index - runStart;
|
||||
boolean leading = runStart == 0;
|
||||
boolean trailing = index == encoded.length();
|
||||
if (!leading && !trailing && runLength == 1) {
|
||||
out.append(' ');
|
||||
continue;
|
||||
}
|
||||
for (int i = 0; i < runLength; i++) {
|
||||
out.append(" ");
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private Locale localeFor(Context context) {
|
||||
android.content.res.Configuration configuration = context.getResources().getConfiguration();
|
||||
android.os.LocaleList locales = configuration.getLocales();
|
||||
if (locales != null && !locales.isEmpty()) {
|
||||
return locales.get(0);
|
||||
}
|
||||
return Locale.getDefault();
|
||||
}
|
||||
|
||||
static final class RowResult {
|
||||
final CharSequence text;
|
||||
final String contentDescription;
|
||||
final int gapBeforePx;
|
||||
|
||||
RowResult(CharSequence text, String contentDescription, int gapBeforePx) {
|
||||
this.text = text != null ? text : "";
|
||||
this.contentDescription = contentDescription != null ? contentDescription : "";
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
}
|
||||
}
|
||||
|
||||
static final class LayoutRowResult {
|
||||
final CharSequence text;
|
||||
final CharSequence leftText;
|
||||
final CharSequence rightText;
|
||||
final String contentDescription;
|
||||
final int pipeCount;
|
||||
final int gapBeforePx;
|
||||
|
||||
LayoutRowResult(
|
||||
CharSequence text,
|
||||
CharSequence leftText,
|
||||
CharSequence rightText,
|
||||
String contentDescription,
|
||||
int pipeCount,
|
||||
int gapBeforePx
|
||||
) {
|
||||
this.text = text != null ? text : "";
|
||||
this.leftText = leftText != null ? leftText : "";
|
||||
this.rightText = rightText != null ? rightText : "";
|
||||
this.contentDescription = contentDescription != null ? contentDescription : "";
|
||||
this.pipeCount = Math.max(0, pipeCount);
|
||||
this.gapBeforePx = gapBeforePx;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ColorMarker {
|
||||
final int color;
|
||||
|
||||
ColorMarker(int color) {
|
||||
this.color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
+773
@@ -0,0 +1,773 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroupOverlay;
|
||||
import android.view.ViewParent;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class StockClockController {
|
||||
|
||||
private static final String CLOCK_CLASS_NAME =
|
||||
"com.android.systemui.statusbar.policy.QSClockIndicatorView";
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final Set<TextView> trackedClocks =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<TextView> ownedClocks =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Map<TextView, Runnable> tickers = new WeakHashMap<>();
|
||||
private final Map<TextView, ClockRowsOverlay> rowOverlays = new WeakHashMap<>();
|
||||
private final Map<TextView, ViewTreeObserver.OnPreDrawListener> rowOverlaySyncListeners =
|
||||
new WeakHashMap<>();
|
||||
private final Map<TextView, StockClockSnapshot> stockSnapshots = new WeakHashMap<>();
|
||||
private boolean hooksInstalled;
|
||||
private boolean receiverRegistered;
|
||||
private boolean layoutModeListenerRegistered;
|
||||
|
||||
StockClockController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
registerLayoutModeListenerIfNeeded();
|
||||
Class<?> clockClass = ReflectionSupport.requireClass(CLOCK_CLASS_NAME, classLoader);
|
||||
XposedInterface framework = runtimeContext.getFramework();
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView clockView) {
|
||||
trackAndApply(clockView, null);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onDetachedFromWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView clockView) {
|
||||
untrackClock(clockView);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setVisibility", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView) {
|
||||
TextView clockView = (TextView) target;
|
||||
if (clockView.getVisibility() == View.VISIBLE) {
|
||||
trackAndApply(clockView, null);
|
||||
}
|
||||
if (rowOverlays.containsKey(clockView)) {
|
||||
syncRowOverlayVisuals(clockView);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setAlpha", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView clockView && rowOverlays.containsKey(clockView)) {
|
||||
syncRowOverlayVisuals(clockView);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(framework, clockClass, "notifyTimeChanged", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (!(target instanceof TextView)) {
|
||||
return result;
|
||||
}
|
||||
TextView clockView = (TextView) target;
|
||||
java.util.List<Object> args = chain.getArgs();
|
||||
Object bellSound = args != null && !args.isEmpty()
|
||||
? args.get(0)
|
||||
: null;
|
||||
captureBellSoundSnapshot(clockView, bellSound);
|
||||
if (isDemoBellSound(bellSound)) {
|
||||
stopTicker(clockView);
|
||||
return result;
|
||||
}
|
||||
trackAndApply(clockView, bellSound);
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "setTextColor", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView) {
|
||||
TextView clockView = (TextView) target;
|
||||
if (hasCustomClockTextEnabled(RuntimeSettingsCache.get(clockView.getContext()))
|
||||
|| ownedClocks.contains(clockView)) {
|
||||
applyClockText(clockView);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, clockClass, "onDarkChanged", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object target = chain.getThisObject();
|
||||
if (target instanceof TextView) {
|
||||
TextView clockView = (TextView) target;
|
||||
if (hasCustomClockTextEnabled(RuntimeSettingsCache.get(clockView.getContext()))
|
||||
|| ownedClocks.contains(clockView)) {
|
||||
applyClockText(clockView);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void registerLayoutModeListenerIfNeeded() {
|
||||
if (layoutModeListenerRegistered) {
|
||||
return;
|
||||
}
|
||||
runtimeContext.getModeStateRepository().addLayoutEnabledListener(enabled -> {
|
||||
if (enabled) {
|
||||
removeAllRowOverlays();
|
||||
} else {
|
||||
refreshAllClocks();
|
||||
}
|
||||
});
|
||||
layoutModeListenerRegistered = true;
|
||||
}
|
||||
|
||||
private void trackAndApply(TextView clockView, Object bellSound) {
|
||||
trackedClocks.add(clockView);
|
||||
registerReceiverIfNeeded(clockView.getContext());
|
||||
if (bellSound != null) {
|
||||
captureBellSoundSnapshot(clockView, bellSound);
|
||||
} else {
|
||||
captureCurrentStockSnapshot(clockView);
|
||||
}
|
||||
applyClockText(clockView);
|
||||
}
|
||||
|
||||
private void untrackClock(TextView clockView) {
|
||||
trackedClocks.remove(clockView);
|
||||
ownedClocks.remove(clockView);
|
||||
removeRowOverlay(clockView);
|
||||
stopRowOverlayVisualSync(clockView);
|
||||
stockSnapshots.remove(clockView);
|
||||
stopTicker(clockView);
|
||||
}
|
||||
|
||||
private void registerReceiverIfNeeded(Context context) {
|
||||
if (context == null || receiverRegistered) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
refreshAllClocks();
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||
filter.addAction(Intent.ACTION_BATTERY_CHANGED);
|
||||
filter.addAction(Intent.ACTION_TIME_TICK);
|
||||
filter.addAction(Intent.ACTION_TIME_CHANGED);
|
||||
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
|
||||
filter.addAction(Intent.ACTION_LOCALE_CHANGED);
|
||||
filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
}
|
||||
|
||||
private void refreshAllClocks() {
|
||||
for (TextView clockView : new ArrayList<>(trackedClocks)) {
|
||||
applyClockText(clockView);
|
||||
}
|
||||
}
|
||||
|
||||
private void removeAllRowOverlays() {
|
||||
for (TextView clockView : new ArrayList<>(trackedClocks)) {
|
||||
removeRowOverlay(clockView);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyClockText(TextView clockView) {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(clockView.getContext());
|
||||
if (!hasCustomClockTextEnabled(settings)) {
|
||||
stopTicker(clockView);
|
||||
if (ownedClocks.contains(clockView)) {
|
||||
restoreStockText(clockView);
|
||||
ownedClocks.remove(clockView);
|
||||
}
|
||||
return;
|
||||
}
|
||||
captureCurrentStockSnapshot(clockView);
|
||||
|
||||
ClockLayoutTextFactory.Model model = ClockLayoutTextFactory.build(clockView, settings);
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> renderedRows = layoutRowsFromModel(model);
|
||||
ClockTextRenderer.LayoutRowResult lastRow = renderedRows.isEmpty()
|
||||
? new ClockTextRenderer.LayoutRowResult("", "", "", "", 0, 0)
|
||||
: renderedRows.get(renderedRows.size() - 1);
|
||||
CharSequence renderedText = lastRow.text;
|
||||
String contentDescription = model.contentDescription;
|
||||
boolean useSecondsTicker = model.containsSeconds;
|
||||
|
||||
applyClockLineMode(clockView);
|
||||
if (!sameTextSignature(clockView.getText(), renderedText)) {
|
||||
clockView.setText(renderedText);
|
||||
}
|
||||
applyRowOverlay(clockView, renderedRows);
|
||||
if (!TextUtils.equals(clockView.getContentDescription(), contentDescription)) {
|
||||
clockView.setContentDescription(contentDescription);
|
||||
}
|
||||
ownedClocks.add(clockView);
|
||||
if (useSecondsTicker) {
|
||||
startTicker(clockView);
|
||||
} else {
|
||||
stopTicker(clockView);
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<ClockTextRenderer.LayoutRowResult> layoutRowsFromModel(
|
||||
ClockLayoutTextFactory.Model model
|
||||
) {
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> rows = new ArrayList<>();
|
||||
if (model == null || model.rows == null) {
|
||||
return rows;
|
||||
}
|
||||
for (ClockLayoutTextFactory.Row row : model.rows) {
|
||||
rows.add(new ClockTextRenderer.LayoutRowResult(
|
||||
row.text,
|
||||
row.leftText,
|
||||
row.rightText,
|
||||
row.text != null ? row.text.toString() : "",
|
||||
row.pipeCount,
|
||||
row.gapBeforePx));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private void restoreStockText(TextView clockView) {
|
||||
StockClockSnapshot snapshot = stockSnapshots.get(clockView);
|
||||
if (snapshot == null) {
|
||||
return;
|
||||
}
|
||||
removeRowOverlay(clockView);
|
||||
applyClockLineMode(clockView);
|
||||
if (!sameTextSignature(clockView.getText(), snapshot.text)) {
|
||||
clockView.setText(snapshot.text);
|
||||
}
|
||||
if (!TextUtils.equals(clockView.getContentDescription(), snapshot.contentDescription)) {
|
||||
clockView.setContentDescription(snapshot.contentDescription);
|
||||
}
|
||||
}
|
||||
|
||||
private void captureCurrentStockSnapshot(TextView clockView) {
|
||||
if (ownedClocks.contains(clockView)) {
|
||||
return;
|
||||
}
|
||||
stockSnapshots.put(clockView, new StockClockSnapshot(
|
||||
clockView.getText(),
|
||||
stringValue(clockView.getContentDescription())));
|
||||
}
|
||||
|
||||
private void captureBellSoundSnapshot(TextView clockView, Object bellSound) {
|
||||
if (bellSound == null) {
|
||||
return;
|
||||
}
|
||||
String text = stringValue(ReflectionSupport.getFieldValue(bellSound, "TimeText"));
|
||||
String description = stringValue(ReflectionSupport.getFieldValue(
|
||||
bellSound,
|
||||
"TimeContentDescription"));
|
||||
if (text == null && description == null) {
|
||||
return;
|
||||
}
|
||||
stockSnapshots.put(clockView, new StockClockSnapshot(text, description));
|
||||
}
|
||||
|
||||
private boolean hasCustomClockTextEnabled(SbtSettings settings) {
|
||||
if (settings == null) {
|
||||
return false;
|
||||
}
|
||||
String pattern = settings.clockCustomFormat != null ? settings.clockCustomFormat : "";
|
||||
return settings.clockShowSeconds
|
||||
|| settings.clockShowDatePrefix
|
||||
|| (settings.clockCustomFormatEnabled && !pattern.isEmpty());
|
||||
}
|
||||
|
||||
private void applyClockLineMode(TextView clockView) {
|
||||
clockView.setSingleLine(true);
|
||||
clockView.setHorizontallyScrolling(false);
|
||||
clockView.setMaxLines(1);
|
||||
}
|
||||
|
||||
private void applyRowOverlay(
|
||||
TextView clockView,
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> rows
|
||||
) {
|
||||
if (rows == null
|
||||
|| rows.size() <= 1
|
||||
|| runtimeContext.getModeStateRepository().isLayoutEnabled()
|
||||
|| !isUnlockedForOverlay(clockView.getContext())) {
|
||||
removeRowOverlay(clockView);
|
||||
stopRowOverlayVisualSync(clockView);
|
||||
return;
|
||||
}
|
||||
ViewGroup host = findOverlayHost(clockView);
|
||||
if (host == null) {
|
||||
removeRowOverlay(clockView);
|
||||
stopRowOverlayVisualSync(clockView);
|
||||
return;
|
||||
}
|
||||
disableAncestorClipping(host);
|
||||
ClockRowsOverlay overlay = rowOverlays.get(clockView);
|
||||
if (overlay == null || overlay.host != host) {
|
||||
removeRowOverlay(clockView);
|
||||
overlay = new ClockRowsOverlay(host);
|
||||
rowOverlays.put(clockView, overlay);
|
||||
}
|
||||
overlay.update(clockView, rows);
|
||||
ensureRowOverlayVisualSync(clockView);
|
||||
}
|
||||
|
||||
private boolean isUnlockedForOverlay(Context context) {
|
||||
if (isKeyguardLocked(context)) {
|
||||
return false;
|
||||
}
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
return activeScene != null && activeScene.isUnlocked();
|
||||
}
|
||||
|
||||
private boolean isKeyguardLocked(Context context) {
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
|
||||
return keyguardManager != null && keyguardManager.isKeyguardLocked();
|
||||
} catch (RuntimeException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void disableAncestorClipping(View view) {
|
||||
Object parent = view;
|
||||
int depth = 0;
|
||||
while (parent instanceof ViewGroup && depth < 12) {
|
||||
ViewGroup group = (ViewGroup) parent;
|
||||
group.setClipChildren(false);
|
||||
group.setClipToPadding(false);
|
||||
group.setClipToOutline(false);
|
||||
parent = group.getParent();
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
private ViewGroup findOverlayHost(TextView clockView) {
|
||||
android.view.ViewParent parent = clockView.getParent();
|
||||
ViewGroup best = null;
|
||||
int depth = 0;
|
||||
while (parent instanceof ViewGroup && depth < 12) {
|
||||
ViewGroup group = (ViewGroup) parent;
|
||||
best = group;
|
||||
parent = group.getParent();
|
||||
depth++;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private void removeRowOverlay(TextView clockView) {
|
||||
ClockRowsOverlay overlay = rowOverlays.remove(clockView);
|
||||
if (overlay != null) {
|
||||
overlay.remove();
|
||||
}
|
||||
stopRowOverlayVisualSync(clockView);
|
||||
}
|
||||
|
||||
private void ensureRowOverlayVisualSync(TextView clockView) {
|
||||
if (clockView == null || rowOverlaySyncListeners.containsKey(clockView)) {
|
||||
return;
|
||||
}
|
||||
ViewTreeObserver observer = clockView.getViewTreeObserver();
|
||||
if (observer == null || !observer.isAlive()) {
|
||||
return;
|
||||
}
|
||||
ViewTreeObserver.OnPreDrawListener listener = () -> {
|
||||
syncRowOverlayVisuals(clockView);
|
||||
return true;
|
||||
};
|
||||
observer.addOnPreDrawListener(listener);
|
||||
rowOverlaySyncListeners.put(clockView, listener);
|
||||
}
|
||||
|
||||
private void stopRowOverlayVisualSync(TextView clockView) {
|
||||
ViewTreeObserver.OnPreDrawListener listener = rowOverlaySyncListeners.remove(clockView);
|
||||
if (listener == null) {
|
||||
return;
|
||||
}
|
||||
ViewTreeObserver observer = clockView.getViewTreeObserver();
|
||||
if (observer != null && observer.isAlive()) {
|
||||
observer.removeOnPreDrawListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void syncRowOverlayVisuals(TextView clockView) {
|
||||
ClockRowsOverlay overlay = rowOverlays.get(clockView);
|
||||
if (overlay == null) {
|
||||
stopRowOverlayVisualSync(clockView);
|
||||
return;
|
||||
}
|
||||
overlay.syncFromSource(clockView);
|
||||
}
|
||||
|
||||
private void startTicker(TextView clockView) {
|
||||
Runnable ticker = tickers.get(clockView);
|
||||
if (ticker == null) {
|
||||
ticker = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (clockView.getHandler() == null || !clockView.isAttachedToWindow()) {
|
||||
stopTicker(clockView);
|
||||
return;
|
||||
}
|
||||
applyClockText(clockView);
|
||||
scheduleNextTick(clockView, this);
|
||||
}
|
||||
};
|
||||
tickers.put(clockView, ticker);
|
||||
}
|
||||
clockView.removeCallbacks(ticker);
|
||||
scheduleNextTick(clockView, ticker);
|
||||
}
|
||||
|
||||
private void stopTicker(TextView clockView) {
|
||||
Runnable ticker = tickers.get(clockView);
|
||||
if (ticker != null) {
|
||||
clockView.removeCallbacks(ticker);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleNextTick(TextView clockView, Runnable ticker) {
|
||||
long now = System.currentTimeMillis();
|
||||
long next = now - (now % 1000L) + 1000L;
|
||||
clockView.postDelayed(ticker, Math.max(1L, next - now));
|
||||
}
|
||||
|
||||
private boolean isDemoBellSound(Object bellSound) {
|
||||
if (bellSound == null) {
|
||||
return false;
|
||||
}
|
||||
Object demoMode = ReflectionSupport.getFieldValue(bellSound, "demo");
|
||||
return demoMode instanceof Boolean && (Boolean) demoMode;
|
||||
}
|
||||
|
||||
private boolean sameTextSignature(CharSequence left, CharSequence right) {
|
||||
return TextUtils.equals(buildTextSignature(left), buildTextSignature(right));
|
||||
}
|
||||
|
||||
private String buildTextSignature(CharSequence text) {
|
||||
if (text == null) {
|
||||
return "<null>";
|
||||
}
|
||||
StringBuilder out = new StringBuilder();
|
||||
out.append(text);
|
||||
if (!(text instanceof Spanned)) {
|
||||
return out.toString();
|
||||
}
|
||||
Spanned spanned = (Spanned) text;
|
||||
Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||
if (spans == null || spans.length == 0) {
|
||||
return out.toString();
|
||||
}
|
||||
Arrays.sort(spans, Comparator
|
||||
.comparingInt((Object span) -> spanned.getSpanStart(span))
|
||||
.thenComparingInt(span -> spanned.getSpanEnd(span))
|
||||
.thenComparing(span -> span.getClass().getName()));
|
||||
for (Object span : spans) {
|
||||
int start = spanned.getSpanStart(span);
|
||||
int end = spanned.getSpanEnd(span);
|
||||
int flags = spanned.getSpanFlags(span);
|
||||
out.append('|')
|
||||
.append(span.getClass().getName())
|
||||
.append('@')
|
||||
.append(start)
|
||||
.append(':')
|
||||
.append(end)
|
||||
.append(':')
|
||||
.append(flags);
|
||||
if (span instanceof ForegroundColorSpan) {
|
||||
out.append(':').append(((ForegroundColorSpan) span).getForegroundColor());
|
||||
} else if (span instanceof TypefaceSpan) {
|
||||
out.append(':').append(((TypefaceSpan) span).getFamily());
|
||||
} else if (span instanceof RelativeSizeSpan) {
|
||||
out.append(':').append(((RelativeSizeSpan) span).getSizeChange());
|
||||
} else if (span instanceof StyleSpan) {
|
||||
out.append(':').append(((StyleSpan) span).getStyle());
|
||||
} else if (span instanceof UnderlineSpan) {
|
||||
out.append(":u");
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value != null ? String.valueOf(value) : null;
|
||||
}
|
||||
|
||||
private static final class StockClockSnapshot {
|
||||
final CharSequence text;
|
||||
final String contentDescription;
|
||||
|
||||
StockClockSnapshot(CharSequence text, String contentDescription) {
|
||||
this.text = text != null ? text : "";
|
||||
this.contentDescription = contentDescription != null ? contentDescription : "";
|
||||
}
|
||||
}
|
||||
|
||||
private final class ClockRowsOverlay {
|
||||
final ViewGroup host;
|
||||
private final ViewGroupOverlay overlay;
|
||||
private final ArrayList<TextView> rowViews = new ArrayList<>();
|
||||
private ArrayList<ClockTextRenderer.LayoutRowResult> lastRows;
|
||||
private boolean removed;
|
||||
|
||||
ClockRowsOverlay(ViewGroup host) {
|
||||
this.host = host;
|
||||
this.overlay = host.getOverlay();
|
||||
}
|
||||
|
||||
void update(TextView source, ArrayList<ClockTextRenderer.LayoutRowResult> rows) {
|
||||
if (removed
|
||||
|| rows.size() <= 1) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
lastRows = new ArrayList<>(rows);
|
||||
if (!source.isAttachedToWindow() || source.getBaseline() < 0 || source.getWidth() <= 0) {
|
||||
source.post(() -> {
|
||||
if (!removed) {
|
||||
update(source, rows);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
int visibleRows = rows.size() - 1;
|
||||
ensureViewCount(source, visibleRows);
|
||||
|
||||
int[] sourceLocation = new int[2];
|
||||
int[] hostLocation = new int[2];
|
||||
source.getLocationOnScreen(sourceLocation);
|
||||
host.getLocationOnScreen(hostLocation);
|
||||
|
||||
int sourceBaselineY = sourceLocation[1] + source.getBaseline();
|
||||
ArrayList<RowMetrics> rowMetrics = measureRows(source, rows);
|
||||
int anchorOffset = alignmentOffset(rowMetrics.get(rows.size() - 1), rowMetrics);
|
||||
int cumulativeOffset = 0;
|
||||
for (int rowIndex = rows.size() - 2; rowIndex >= 0; rowIndex--) {
|
||||
ClockTextRenderer.LayoutRowResult nextRow = rows.get(rowIndex + 1);
|
||||
cumulativeOffset += resolveGap(nextRow != null ? nextRow.gapBeforePx : 0, source);
|
||||
TextView rowView = rowViews.get(rowIndex);
|
||||
ClockTextRenderer.LayoutRowResult row = rows.get(rowIndex);
|
||||
rowView.setText(row != null ? row.text : "");
|
||||
copyTextViewStyle(source, rowView);
|
||||
positionRowView(
|
||||
source,
|
||||
rowView,
|
||||
sourceLocation[0] - hostLocation[0]
|
||||
+ alignmentOffset(rowMetrics.get(rowIndex), rowMetrics)
|
||||
- anchorOffset,
|
||||
sourceBaselineY - hostLocation[1] - cumulativeOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void syncFromSource(TextView source) {
|
||||
if (lastRows == null || lastRows.size() <= 1) {
|
||||
remove();
|
||||
return;
|
||||
}
|
||||
update(source, lastRows);
|
||||
}
|
||||
|
||||
private ArrayList<RowMetrics> measureRows(
|
||||
TextView source,
|
||||
ArrayList<ClockTextRenderer.LayoutRowResult> rows
|
||||
) {
|
||||
ArrayList<RowMetrics> metrics = new ArrayList<>();
|
||||
TextView probe = new TextView(source.getContext());
|
||||
probe.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
copyTextViewStyle(source, probe);
|
||||
for (ClockTextRenderer.LayoutRowResult row : rows) {
|
||||
metrics.add(measureRow(probe, row));
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private RowMetrics measureRow(
|
||||
TextView probe,
|
||||
ClockTextRenderer.LayoutRowResult row
|
||||
) {
|
||||
if (row == null || row.pipeCount <= 0) {
|
||||
return new RowMetrics(0, 0, 0);
|
||||
}
|
||||
int width = measureTextWidth(probe, row.text);
|
||||
int leftPipe = measureTextWidth(probe, row.leftText);
|
||||
int rightPipe = width - measureTextWidth(probe, row.rightText);
|
||||
int pipePosition = row.pipeCount > 1
|
||||
? Math.round((leftPipe + rightPipe) / 2f)
|
||||
: leftPipe;
|
||||
return new RowMetrics(width, pipePosition, row.pipeCount);
|
||||
}
|
||||
|
||||
private int measureTextWidth(TextView probe, CharSequence text) {
|
||||
probe.setText(text != null ? text : "");
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
probe.measure(widthSpec, heightSpec);
|
||||
return Math.max(0, probe.getMeasuredWidth());
|
||||
}
|
||||
|
||||
private int alignmentOffset(RowMetrics row, ArrayList<RowMetrics> rows) {
|
||||
if (row == null || row.pipeCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int maxPipePosition = 0;
|
||||
for (RowMetrics candidate : rows) {
|
||||
if (candidate != null && candidate.pipeCount > 0) {
|
||||
maxPipePosition = Math.max(maxPipePosition, candidate.pipePosition);
|
||||
}
|
||||
}
|
||||
return maxPipePosition - row.pipePosition;
|
||||
}
|
||||
|
||||
void remove() {
|
||||
removed = true;
|
||||
lastRows = null;
|
||||
for (TextView view : rowViews) {
|
||||
overlay.remove(view);
|
||||
}
|
||||
rowViews.clear();
|
||||
}
|
||||
|
||||
private void ensureViewCount(TextView source, int count) {
|
||||
while (rowViews.size() > count) {
|
||||
TextView view = rowViews.remove(rowViews.size() - 1);
|
||||
overlay.remove(view);
|
||||
}
|
||||
while (rowViews.size() < count) {
|
||||
TextView view = new TextView(source.getContext());
|
||||
view.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
view.setIncludeFontPadding(source.getIncludeFontPadding());
|
||||
view.setSingleLine(true);
|
||||
view.setMaxLines(1);
|
||||
view.setHorizontallyScrolling(true);
|
||||
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
rowViews.add(view);
|
||||
overlay.add(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyTextViewStyle(TextView source, TextView target) {
|
||||
target.setTextColor(source.getCurrentTextColor());
|
||||
target.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, source.getTextSize());
|
||||
target.setTypeface(source.getTypeface());
|
||||
target.setGravity(source.getGravity());
|
||||
target.setIncludeFontPadding(source.getIncludeFontPadding());
|
||||
target.setPadding(
|
||||
source.getPaddingLeft(),
|
||||
source.getPaddingTop(),
|
||||
source.getPaddingRight(),
|
||||
source.getPaddingBottom());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
target.setLetterSpacing(source.getLetterSpacing());
|
||||
target.setFontFeatureSettings(source.getFontFeatureSettings());
|
||||
}
|
||||
VisualState visualState = resolveVisualState(source);
|
||||
target.setVisibility(visualState.visible ? View.VISIBLE : View.INVISIBLE);
|
||||
target.setAlpha(visualState.alpha);
|
||||
}
|
||||
|
||||
int rowCount() {
|
||||
return rowViews.size();
|
||||
}
|
||||
|
||||
private VisualState resolveVisualState(TextView source) {
|
||||
float alpha = 1f;
|
||||
boolean visible = source.isShown();
|
||||
View current = source;
|
||||
while (current != null && current != host) {
|
||||
if (current.getVisibility() != View.VISIBLE) {
|
||||
visible = false;
|
||||
}
|
||||
alpha *= current.getAlpha();
|
||||
ViewParent parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
return new VisualState(visible, alpha);
|
||||
}
|
||||
|
||||
private void positionRowView(
|
||||
TextView source,
|
||||
TextView rowView,
|
||||
int left,
|
||||
int baselineY
|
||||
) {
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
rowView.measure(widthSpec, heightSpec);
|
||||
int baseline = Math.max(0, rowView.getBaseline());
|
||||
int top = baselineY - baseline;
|
||||
rowView.layout(left, top, left + rowView.getMeasuredWidth(), top + rowView.getMeasuredHeight());
|
||||
}
|
||||
|
||||
private int resolveGap(int gapBeforePx, TextView source) {
|
||||
if (gapBeforePx == ClockPatternParser.ROW_GAP_AUTO) {
|
||||
return Math.max(1, source.getLineHeight());
|
||||
}
|
||||
return gapBeforePx;
|
||||
}
|
||||
|
||||
private record VisualState(boolean visible, float alpha) {
|
||||
}
|
||||
|
||||
private record RowMetrics(int width, int pipePosition, int pipeCount) {
|
||||
}
|
||||
}
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.debug;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class DebugNotificationAutoGroupController {
|
||||
private static final String CLASS_GROUP_HELPER =
|
||||
"com.android.server.notification.GroupHelper";
|
||||
private static final String CLASS_NOTIFICATION_RECORD =
|
||||
"com.android.server.notification.NotificationRecord";
|
||||
private static final String CLASS_NOTIFICATION_MANAGER_SERVICE =
|
||||
"com.android.server.notification.NotificationManagerService";
|
||||
private static final String EXTRA_DEBUG_NOTIFICATION = "sbt_debug_notification";
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private boolean hooksInstalled;
|
||||
|
||||
DebugNotificationAutoGroupController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled) {
|
||||
return;
|
||||
}
|
||||
Class<?> groupHelper = ReflectionSupport.findClassIfExists(CLASS_GROUP_HELPER, classLoader);
|
||||
Class<?> recordClass = ReflectionSupport.findClassIfExists(CLASS_NOTIFICATION_RECORD, classLoader);
|
||||
Class<?> nmsClass = ReflectionSupport.findClassIfExists(
|
||||
CLASS_NOTIFICATION_MANAGER_SERVICE,
|
||||
classLoader);
|
||||
if (groupHelper == null || recordClass == null || nmsClass == null) {
|
||||
runtimeContext.logWarning("Debug notification autogroup hook target missing", null);
|
||||
return;
|
||||
}
|
||||
|
||||
XposedInterface.Hooker blockDebugGrouping = chain -> {
|
||||
if (chain.getArgs().isEmpty() || !isOurNotificationRecord(chain.getArg(0))) {
|
||||
return chain.proceed();
|
||||
}
|
||||
if (chain.getExecutable() instanceof Method method
|
||||
&& method.getReturnType() == Boolean.TYPE) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
hookIfExists(groupHelper, "onNotificationPosted", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "onNotificationRemoved", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "maybeGroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "maybeUngroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "autoGroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "autogroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "maybeAutoGroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "shouldAutoGroup", blockDebugGrouping);
|
||||
hookIfExists(groupHelper, "isEligibleForAutoGroup", blockDebugGrouping);
|
||||
|
||||
hookIfExists(recordClass, "setOverrideGroupKey", chain -> {
|
||||
if (isOurNotificationRecord(chain.getThisObject())) {
|
||||
return null;
|
||||
}
|
||||
return chain.proceed();
|
||||
});
|
||||
|
||||
XposedInterface.Hooker dropAutoSummary = chain -> {
|
||||
NotificationInfo info = extractNotificationInfo(chain.getArgs().toArray());
|
||||
if (info != null
|
||||
&& SbtSettings.MODULE_PACKAGE.equals(info.packageName)
|
||||
&& !info.isDebugNotification) {
|
||||
return null;
|
||||
}
|
||||
return chain.proceed();
|
||||
};
|
||||
hookIfExists(nmsClass, "enqueueNotificationInternal", dropAutoSummary);
|
||||
hookIfExists(nmsClass, "enqueueNotificationInternalLocked", dropAutoSummary);
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void hookIfExists(Class<?> type, String methodName, XposedInterface.Hooker hooker) {
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
runtimeContext.getFramework().hook(method).intercept(hooker);
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationInfo extractNotificationInfo(Object[] args) {
|
||||
StatusBarNotification sbn = null;
|
||||
Notification notification = null;
|
||||
String packageName = null;
|
||||
|
||||
for (Object arg : args) {
|
||||
if (arg instanceof StatusBarNotification statusBarNotification) {
|
||||
sbn = statusBarNotification;
|
||||
packageName = sbn.getPackageName();
|
||||
notification = sbn.getNotification();
|
||||
break;
|
||||
}
|
||||
if (arg instanceof Notification candidateNotification && notification == null) {
|
||||
notification = candidateNotification;
|
||||
}
|
||||
if (arg instanceof String candidatePackage && packageName == null) {
|
||||
packageName = candidatePackage;
|
||||
}
|
||||
if (arg != null && arg.getClass().getName().contains("NotificationRecord")) {
|
||||
Object sbnObj = ReflectionSupport.invokeMethod(arg, "getSbn", new Class<?>[0]);
|
||||
if (sbnObj instanceof StatusBarNotification statusBarNotification) {
|
||||
sbn = statusBarNotification;
|
||||
packageName = sbn.getPackageName();
|
||||
notification = sbn.getNotification();
|
||||
break;
|
||||
}
|
||||
Object packageObj = ReflectionSupport.invokeMethod(
|
||||
arg,
|
||||
"getPackageName",
|
||||
new Class<?>[0]);
|
||||
if (packageObj instanceof String stringPackage) {
|
||||
packageName = stringPackage;
|
||||
}
|
||||
Object notificationObj = ReflectionSupport.invokeMethod(
|
||||
arg,
|
||||
"getNotification",
|
||||
new Class<?>[0]);
|
||||
if (notificationObj instanceof Notification candidateNotification) {
|
||||
notification = candidateNotification;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (notification == null && sbn != null) {
|
||||
notification = sbn.getNotification();
|
||||
}
|
||||
if (packageName == null || notification == null) {
|
||||
return null;
|
||||
}
|
||||
Bundle extras = notification.extras;
|
||||
boolean isDebug = extras != null && extras.getBoolean(EXTRA_DEBUG_NOTIFICATION, false);
|
||||
return new NotificationInfo(packageName, isDebug);
|
||||
}
|
||||
|
||||
private boolean isOurNotificationRecord(Object record) {
|
||||
if (record == null) {
|
||||
return false;
|
||||
}
|
||||
Object sbnObj = ReflectionSupport.invokeMethod(record, "getSbn", new Class<?>[0]);
|
||||
if (sbnObj instanceof StatusBarNotification sbn) {
|
||||
return SbtSettings.MODULE_PACKAGE.equals(sbn.getPackageName());
|
||||
}
|
||||
if (sbnObj != null) {
|
||||
Object packageObj = ReflectionSupport.invokeMethod(
|
||||
sbnObj,
|
||||
"getPackageName",
|
||||
new Class<?>[0]);
|
||||
if (packageObj instanceof String packageName) {
|
||||
return SbtSettings.MODULE_PACKAGE.equals(packageName);
|
||||
}
|
||||
}
|
||||
Object packageObj = ReflectionSupport.invokeMethod(record, "getPackageName", new Class<?>[0]);
|
||||
return packageObj instanceof String packageName
|
||||
&& SbtSettings.MODULE_PACKAGE.equals(packageName);
|
||||
}
|
||||
|
||||
private record NotificationInfo(String packageName, boolean isDebugNotification) {
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.debug;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
public final class DebugNotificationAutoGroupFeature implements RuntimeFeature {
|
||||
private DebugNotificationAutoGroupController controller;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "debug-notification-autogroup";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemServerStarting(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.SystemServerStartingParam param
|
||||
) {
|
||||
if (controller == null) {
|
||||
controller = new DebugNotificationAutoGroupController(context);
|
||||
}
|
||||
controller.install(param.getClassLoader());
|
||||
}
|
||||
}
|
||||
+346
@@ -0,0 +1,346 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
|
||||
final class AodBottomBatteryFallbackController {
|
||||
|
||||
private static final int OVERRIDE_TAG_KEY = 0x7f0b0a01;
|
||||
private static final int VISIBILITY_TAG_KEY = 0x7f0b0a02;
|
||||
private static final long[] INDICATION_RETRY_DELAYS_MS = new long[] {96L, 224L, 480L};
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private Object currentManager;
|
||||
private FrameLayout currentHost;
|
||||
private final Set<Runnable> pendingRetries = new HashSet<>();
|
||||
|
||||
AodBottomBatteryFallbackController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
hookSetBottomArea(classLoader);
|
||||
hookDozing(classLoader);
|
||||
hookBatteryRefresh(classLoader);
|
||||
}
|
||||
|
||||
private void hookSetBottomArea(ClassLoader classLoader) {
|
||||
Class<?> callbackClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.doze.PluginAODManager$6",
|
||||
classLoader
|
||||
);
|
||||
if (callbackClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), callbackClass, "setBottomArea", chain -> {
|
||||
Object result = chain.proceed();
|
||||
View addedView = chain.getArgs().isEmpty() && !(result instanceof View)
|
||||
? null
|
||||
: (chain.getArgs().isEmpty() ? null : (chain.getArg(0) instanceof View view ? view : null));
|
||||
currentManager = resolveManager(chain.getThisObject());
|
||||
currentHost = resolveBottomDozeAreaFromManager(currentManager);
|
||||
updateFallback(currentHost, addedView);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void hookDozing(ClassLoader classLoader) {
|
||||
Class<?> managerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.doze.PluginAODManager",
|
||||
classLoader
|
||||
);
|
||||
if (managerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), managerClass, "setIsDozing", chain -> {
|
||||
Object result = chain.proceed();
|
||||
currentManager = chain.getThisObject();
|
||||
currentHost = resolveBottomDozeAreaFromManager(currentManager);
|
||||
boolean dozing = chain.getArgs().size() > 0
|
||||
&& chain.getArg(0) instanceof Boolean bool
|
||||
&& bool;
|
||||
View target = currentHost != null && currentHost.getChildCount() > 0
|
||||
? currentHost.getChildAt(0)
|
||||
: null;
|
||||
if (!dozing) {
|
||||
clearAll(currentHost, target);
|
||||
return result;
|
||||
}
|
||||
updateFallback(currentHost, target);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void hookBatteryRefresh(ClassLoader classLoader) {
|
||||
Class<?> callbackClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.KeyguardSecIndicationController$SecKeyguardCallback",
|
||||
classLoader
|
||||
);
|
||||
if (callbackClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), callbackClass, "onRefreshBatteryInfo", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (currentManager == null || currentHost == null) {
|
||||
return result;
|
||||
}
|
||||
View target = currentHost.getChildCount() > 0 ? currentHost.getChildAt(0) : null;
|
||||
updateFallback(currentHost, target);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateFallback(FrameLayout host, View addedView) {
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
BatteryStatusAccess.BatteryState batteryState = resolveVisibleBatteryState(host);
|
||||
if (batteryState == null) {
|
||||
clearAll(host, addedView);
|
||||
return;
|
||||
}
|
||||
CharSequence text = buildBatteryPercentageText(batteryState);
|
||||
if (text == null || text.length() == 0) {
|
||||
clearAll(host, addedView);
|
||||
return;
|
||||
}
|
||||
if (canApplyIndicationOnlyFallback(host, addedView)) {
|
||||
clearIndicationOnlyFallback(addedView, host);
|
||||
cancelRetries(host);
|
||||
applyIndicationOnlyFallback(host, addedView, text.toString());
|
||||
scheduleIndicationOnlyRetries(host, addedView, text.toString());
|
||||
return;
|
||||
}
|
||||
clearAll(host, addedView);
|
||||
}
|
||||
|
||||
private void clearAll(FrameLayout host, View addedView) {
|
||||
cancelRetries(host);
|
||||
clearIndicationOnlyFallback(addedView, host);
|
||||
}
|
||||
|
||||
private BatteryStatusAccess.BatteryState resolveVisibleBatteryState(View host) {
|
||||
if (host == null) {
|
||||
return null;
|
||||
}
|
||||
if (!RuntimeSettingsCache.get(host.getContext()).batteryAodUnpluggedPercent) {
|
||||
return null;
|
||||
}
|
||||
BatteryStatusAccess.BatteryState state =
|
||||
BatteryStatusAccess.getCurrentBatteryState(host.getContext());
|
||||
if (state == null || state.plugged() || state.charged()) {
|
||||
return null;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private CharSequence buildBatteryPercentageText(BatteryStatusAccess.BatteryState state) {
|
||||
if (state == null || state.level() < 0) {
|
||||
return null;
|
||||
}
|
||||
return NumberFormat.getPercentInstance().format(state.level() / 100.0d);
|
||||
}
|
||||
|
||||
private boolean canApplyIndicationOnlyFallback(FrameLayout host, View addedView) {
|
||||
Object component = getChargingComponent(resolvePluginRoot(addedView, host));
|
||||
return resolveNativeTextView(component, "d") != null;
|
||||
}
|
||||
|
||||
private boolean applyIndicationOnlyFallback(FrameLayout host, View addedView, String text) {
|
||||
Object component = getChargingComponent(resolvePluginRoot(addedView, host));
|
||||
if (component == null) {
|
||||
return false;
|
||||
}
|
||||
TextView indicationText = resolveNativeTextView(component, "d");
|
||||
if (indicationText == null) {
|
||||
return false;
|
||||
}
|
||||
TextView remainingText = resolveNativeTextView(component, "k");
|
||||
View batteryContainer = resolveNativeBatteryContainer(component);
|
||||
View hostView = resolveNativeHostView(component);
|
||||
View infoHolder = getObjectFieldAs(hostView, "b", View.class);
|
||||
View usbContainer = getObjectFieldAs(hostView, "l", View.class);
|
||||
|
||||
rememberAndApplyText(indicationText, text);
|
||||
rememberAndApplyVisibility(indicationText, View.VISIBLE);
|
||||
rememberAndApplyVisibility(remainingText, View.GONE);
|
||||
rememberAndApplyVisibility(batteryContainer, View.GONE);
|
||||
rememberAndApplyVisibility(usbContainer, View.GONE);
|
||||
|
||||
forceViewChainVisible(indicationText);
|
||||
forceViewChainVisible(infoHolder);
|
||||
forceViewChainVisible(hostView);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void clearIndicationOnlyFallback(View addedView, FrameLayout host) {
|
||||
Object component = getChargingComponent(resolvePluginRoot(addedView, host));
|
||||
if (component == null) {
|
||||
return;
|
||||
}
|
||||
restoreRememberedText(resolveNativeTextView(component, "d"));
|
||||
restoreRememberedVisibility(resolveNativeTextView(component, "k"));
|
||||
restoreRememberedVisibility(resolveNativeBatteryContainer(component));
|
||||
restoreRememberedVisibility(getObjectFieldAs(resolveNativeHostView(component), "l", View.class));
|
||||
}
|
||||
|
||||
private void scheduleIndicationOnlyRetries(FrameLayout host, View addedView, String text) {
|
||||
for (long delayMs : INDICATION_RETRY_DELAYS_MS) {
|
||||
Runnable retry = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
pendingRetries.remove(this);
|
||||
if (currentManager == null || currentHost != host) {
|
||||
return;
|
||||
}
|
||||
if (resolveVisibleBatteryState(host) == null) {
|
||||
clearAll(host, addedView);
|
||||
return;
|
||||
}
|
||||
applyIndicationOnlyFallback(host, addedView, text);
|
||||
}
|
||||
};
|
||||
pendingRetries.add(retry);
|
||||
host.postDelayed(retry, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
private void cancelRetries(FrameLayout host) {
|
||||
if (host == null) {
|
||||
pendingRetries.clear();
|
||||
return;
|
||||
}
|
||||
for (Runnable pending : pendingRetries) {
|
||||
host.removeCallbacks(pending);
|
||||
}
|
||||
pendingRetries.clear();
|
||||
}
|
||||
|
||||
private View resolvePluginRoot(View source, FrameLayout host) {
|
||||
if (source instanceof ViewGroup) {
|
||||
return source;
|
||||
}
|
||||
if (host != null && host.getChildCount() > 0) {
|
||||
return host.getChildAt(0);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Object getChargingComponent(Object root) {
|
||||
return BatteryStatusAccess.getObjectField(root, "p");
|
||||
}
|
||||
|
||||
private TextView resolveNativeTextView(Object component, String fieldName) {
|
||||
View hostView = resolveNativeHostView(component);
|
||||
return getObjectFieldAs(hostView, fieldName, TextView.class);
|
||||
}
|
||||
|
||||
private ViewGroup resolveNativeBatteryContainer(Object component) {
|
||||
View hostView = resolveNativeHostView(component);
|
||||
return getObjectFieldAs(hostView, "c", ViewGroup.class);
|
||||
}
|
||||
|
||||
private View resolveNativeHostView(Object component) {
|
||||
return getObjectFieldAs(component, "c", View.class);
|
||||
}
|
||||
|
||||
private void rememberAndApplyText(TextView view, String text) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
if (!(view.getTag(OVERRIDE_TAG_KEY) instanceof Object[])) {
|
||||
view.setTag(OVERRIDE_TAG_KEY, new Object[] {view.getText(), view.getVisibility()});
|
||||
}
|
||||
view.setText(text);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void restoreRememberedText(TextView view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
Object tag = view.getTag(OVERRIDE_TAG_KEY);
|
||||
if (!(tag instanceof Object[] saved)) {
|
||||
return;
|
||||
}
|
||||
view.setText(saved.length > 0 && saved[0] instanceof CharSequence sequence ? sequence : "");
|
||||
view.setVisibility(saved.length > 1 && saved[1] instanceof Integer integer ? integer : View.VISIBLE);
|
||||
view.setTag(OVERRIDE_TAG_KEY, null);
|
||||
}
|
||||
|
||||
private void rememberAndApplyVisibility(View view, int visibility) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
if (!(view.getTag(VISIBILITY_TAG_KEY) instanceof Integer)) {
|
||||
view.setTag(VISIBILITY_TAG_KEY, view.getVisibility());
|
||||
}
|
||||
view.setVisibility(visibility);
|
||||
}
|
||||
|
||||
private void restoreRememberedVisibility(View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
Object tag = view.getTag(VISIBILITY_TAG_KEY);
|
||||
if (!(tag instanceof Integer integer)) {
|
||||
return;
|
||||
}
|
||||
view.setVisibility(integer);
|
||||
view.setTag(VISIBILITY_TAG_KEY, null);
|
||||
}
|
||||
|
||||
private void forceViewChainVisible(View view) {
|
||||
View current = view;
|
||||
while (current != null) {
|
||||
current.setVisibility(View.VISIBLE);
|
||||
current.setAlpha(1f);
|
||||
current.setTranslationX(0f);
|
||||
current.setTranslationY(0f);
|
||||
current.setScaleX(1f);
|
||||
current.setScaleY(1f);
|
||||
current.requestLayout();
|
||||
current.invalidate();
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View next ? next : null;
|
||||
}
|
||||
}
|
||||
|
||||
private Object resolveManager(Object callback) {
|
||||
return BatteryStatusAccess.getObjectField(callback, "this$0");
|
||||
}
|
||||
|
||||
private FrameLayout resolveBottomDozeAreaFromManager(Object manager) {
|
||||
if (manager == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Object panelLazy = ReflectionSupport.getFieldValue(manager, "mPanelViewControllerLazy");
|
||||
Object panel = ReflectionSupport.invokeMethod(panelLazy, "get", new Class<?>[0]);
|
||||
Object bottomController = ReflectionSupport.getFieldValue(panel, "mKeyguardSecBottomAreaViewController");
|
||||
Object bottomView = ReflectionSupport.invokeMethod(bottomController, "getView", new Class<?>[0]);
|
||||
Object delegate = ReflectionSupport.getFieldValue(bottomView, "bottomDozeArea$delegate");
|
||||
Object host = ReflectionSupport.invokeMethod(delegate, "getValue", new Class<?>[0]);
|
||||
return host instanceof FrameLayout frameLayout ? frameLayout : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T getObjectFieldAs(Object target, String fieldName, Class<T> cls) {
|
||||
Object value = BatteryStatusAccess.getObjectField(target, fieldName);
|
||||
return cls.isInstance(value) ? cls.cast(value) : null;
|
||||
}
|
||||
|
||||
}
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.telecom.TelecomManager;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
|
||||
final class AodCallVisibilityController {
|
||||
|
||||
private static final int PLUGIN_KEY_PHONE_STATE = 0x2;
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
|
||||
AodCallVisibilityController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
hookSuppressAmbientDisplay(classLoader);
|
||||
hookFrontScrim(classLoader);
|
||||
hookAlwaysOnSuppressedChanged(classLoader);
|
||||
hookDozeMachineStateOverrides(classLoader);
|
||||
hookPluginPhoneStateReadOverride(classLoader);
|
||||
}
|
||||
|
||||
private void hookSuppressAmbientDisplay(ClassLoader classLoader) {
|
||||
hookBooleanVoidMethod(
|
||||
classLoader,
|
||||
"com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks",
|
||||
"suppressAmbientDisplay"
|
||||
);
|
||||
hookBooleanVoidMethod(
|
||||
classLoader,
|
||||
"com.android.systemui.statusbar.CommandQueue",
|
||||
"suppressAmbientDisplay"
|
||||
);
|
||||
}
|
||||
|
||||
private void hookFrontScrim(ClassLoader classLoader) {
|
||||
hookBooleanVoidMethod(
|
||||
classLoader,
|
||||
"com.android.systemui.statusbar.phone.SecLsScrimControlHelper",
|
||||
"setFrontScrimToBlack"
|
||||
);
|
||||
}
|
||||
|
||||
private void hookAlwaysOnSuppressedChanged(ClassLoader classLoader) {
|
||||
hookBooleanVoidMethod(
|
||||
classLoader,
|
||||
"com.android.systemui.doze.DozeSuppressor$1",
|
||||
"onAlwaysOnSuppressedChanged"
|
||||
);
|
||||
hookBooleanVoidMethod(
|
||||
classLoader,
|
||||
"com.android.systemui.doze.AODUi$1",
|
||||
"onAlwaysOnSuppressedChanged"
|
||||
);
|
||||
}
|
||||
|
||||
private void hookDozeMachineStateOverrides(ClassLoader classLoader) {
|
||||
Class<?> dozeMachineClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.doze.DozeMachine",
|
||||
classLoader
|
||||
);
|
||||
Class<?> stateClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.doze.DozeMachine$State",
|
||||
classLoader
|
||||
);
|
||||
if (dozeMachineClass == null || stateClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedInterface framework = runtimeContext.getFramework();
|
||||
XposedHookSupport.hookAllMethods(framework, dozeMachineClass, "requestState", chain -> {
|
||||
Object state = chain.getArgs().isEmpty() ? null : chain.getArg(0);
|
||||
if (!(state instanceof Enum<?> target) || !shouldForceAodForCall()) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Enum<?> replacement = overrideDozeState(target);
|
||||
if (replacement == null) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Object[] args = chain.getArgs().toArray();
|
||||
args[0] = replacement;
|
||||
return chain.proceed(args);
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(framework, dozeMachineClass, "transitionTo", chain -> {
|
||||
Object state = chain.getArgs().isEmpty() ? null : chain.getArg(0);
|
||||
if (!(state instanceof Enum<?> target) || !shouldForceAodForCall()) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Enum<?> replacement = overrideDozeState(target);
|
||||
if (replacement == null) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Object[] args = chain.getArgs().toArray();
|
||||
args[0] = replacement;
|
||||
return chain.proceed(args);
|
||||
});
|
||||
}
|
||||
|
||||
private void hookPluginPhoneStateReadOverride(ClassLoader classLoader) {
|
||||
Class<?> configClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.plugins.aod.PluginAODSystemUIConfiguration",
|
||||
classLoader
|
||||
);
|
||||
if (configClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), configClass, "get", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (!shouldForceAodForCall() || chain.getArgs().size() < 2) {
|
||||
return result;
|
||||
}
|
||||
Object keyObj = chain.getArg(0);
|
||||
if (!(keyObj instanceof Integer key) || key != PLUGIN_KEY_PHONE_STATE) {
|
||||
return result;
|
||||
}
|
||||
int phoneState = result instanceof Integer integer
|
||||
? integer
|
||||
: (chain.getArg(1) instanceof Integer fallback ? fallback : TelephonyManager.CALL_STATE_IDLE);
|
||||
if (phoneState == TelephonyManager.CALL_STATE_IDLE) {
|
||||
return result;
|
||||
}
|
||||
return TelephonyManager.CALL_STATE_IDLE;
|
||||
});
|
||||
}
|
||||
|
||||
private void hookBooleanVoidMethod(ClassLoader classLoader, String className, String methodName) {
|
||||
Class<?> targetClass = ReflectionSupport.findClassIfExists(className, classLoader);
|
||||
if (targetClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), targetClass, methodName, chain -> {
|
||||
if (chain.getArgs().isEmpty() || !(chain.getArg(0) instanceof Boolean booleanArg)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
if (!booleanArg || !shouldForceAodForCall()) {
|
||||
return chain.proceed();
|
||||
}
|
||||
Object[] args = chain.getArgs().toArray();
|
||||
args[0] = false;
|
||||
return chain.proceed(args);
|
||||
});
|
||||
}
|
||||
|
||||
private Enum<?> overrideDozeState(Enum<?> requested) {
|
||||
String stateName = requested.name();
|
||||
if (!"DOZE".equals(stateName) && !"FINISH".equals(stateName)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
Enum<?> dozeAod = Enum.valueOf((Class) requested.getDeclaringClass(), "DOZE_AOD");
|
||||
return dozeAod;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldForceAodForCall() {
|
||||
Context context = getSystemContext();
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
if (!RuntimeSettingsCache.get(context).keepAodVisibleDuringCall) {
|
||||
return false;
|
||||
}
|
||||
return isInCallLikeState(context);
|
||||
}
|
||||
|
||||
private boolean isInCallLikeState(Context context) {
|
||||
try {
|
||||
TelecomManager telecomManager = context.getSystemService(TelecomManager.class);
|
||||
if (telecomManager != null && telecomManager.isInCall()) {
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class);
|
||||
if (telephonyManager != null
|
||||
&& telephonyManager.getCallState() != TelephonyManager.CALL_STATE_IDLE) {
|
||||
return true;
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
try {
|
||||
AudioManager audioManager = context.getSystemService(AudioManager.class);
|
||||
if (audioManager == null) {
|
||||
return false;
|
||||
}
|
||||
int mode = audioManager.getMode();
|
||||
return mode == AudioManager.MODE_IN_CALL || mode == AudioManager.MODE_IN_COMMUNICATION;
|
||||
} catch (Throwable ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Context getSystemContext() {
|
||||
try {
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Object app = ReflectionSupport.invokeStaticMethod(
|
||||
activityThreadClass,
|
||||
"currentApplication",
|
||||
new Class<?>[0]
|
||||
);
|
||||
return app instanceof Context context ? context : null;
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
|
||||
final class BatteryIndicationController {
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private Object batteryEvent;
|
||||
private Object batteryRestingEvent;
|
||||
|
||||
BatteryIndicationController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
initEventTypes(classLoader);
|
||||
hookAddInitialIndication(classLoader);
|
||||
}
|
||||
|
||||
private void initEventTypes(ClassLoader classLoader) {
|
||||
Class<?> eventTypeClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.IndicationEventType",
|
||||
classLoader
|
||||
);
|
||||
if (eventTypeClass == null) {
|
||||
return;
|
||||
}
|
||||
batteryEvent = ReflectionSupport.getStaticFieldValue(eventTypeClass, "BATTERY");
|
||||
batteryRestingEvent = ReflectionSupport.getStaticFieldValue(eventTypeClass, "BATTERY_RESTING");
|
||||
}
|
||||
|
||||
private void hookAddInitialIndication(ClassLoader classLoader) {
|
||||
Class<?> controllerClass = ReflectionSupport.findClassIfExists(
|
||||
"com.android.systemui.statusbar.KeyguardSecIndicationController",
|
||||
classLoader
|
||||
);
|
||||
if (controllerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(
|
||||
runtimeContext.getFramework(),
|
||||
controllerClass,
|
||||
"addInitialIndication",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object controller = chain.getThisObject();
|
||||
BatteryStatusAccess.BatteryState batteryState = resolveVisibleBatteryState(controller);
|
||||
CharSequence text = buildBatteryPercentageText(batteryState);
|
||||
if (text == null || text.length() == 0) {
|
||||
return result;
|
||||
}
|
||||
ColorStateList color = null;
|
||||
Object colorField = ReflectionSupport.getFieldValue(controller, "mInitialTextColorState");
|
||||
if (colorField instanceof ColorStateList colorStateList) {
|
||||
color = colorStateList;
|
||||
}
|
||||
ReflectionSupport.invokeMethod(
|
||||
controller,
|
||||
"addIndicationTimeout",
|
||||
new Class<?>[] {
|
||||
batteryEvent != null ? batteryEvent.getClass() : Object.class,
|
||||
CharSequence.class,
|
||||
ColorStateList.class,
|
||||
boolean.class
|
||||
},
|
||||
batteryEvent,
|
||||
text,
|
||||
color,
|
||||
false
|
||||
);
|
||||
ReflectionSupport.invokeMethod(
|
||||
controller,
|
||||
"addIndication",
|
||||
new Class<?>[] {
|
||||
batteryRestingEvent != null ? batteryRestingEvent.getClass() : Object.class,
|
||||
CharSequence.class
|
||||
},
|
||||
batteryRestingEvent,
|
||||
text
|
||||
);
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private BatteryStatusAccess.BatteryState resolveVisibleBatteryState(Object controller) {
|
||||
if (controller == null) {
|
||||
return null;
|
||||
}
|
||||
Context context = BatteryStatusAccess.getContextField(controller, "mContext");
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
if (!RuntimeSettingsCache.get(context).batteryLockscreenUnpluggedPercent) {
|
||||
return null;
|
||||
}
|
||||
if (BatteryStatusAccess.getBooleanField(controller, "mDozing", false)) {
|
||||
return null;
|
||||
}
|
||||
BatteryStatusAccess.BatteryState state = BatteryStatusAccess.getCurrentBatteryState(context);
|
||||
if (state == null || state.plugged() || state.charged()) {
|
||||
return null;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private CharSequence buildBatteryPercentageText(BatteryStatusAccess.BatteryState state) {
|
||||
if (state == null || state.level() < 0) {
|
||||
return null;
|
||||
}
|
||||
NumberFormat format = NumberFormat.getPercentInstance();
|
||||
return format.format(state.level() / 100.0d);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.BatteryManager;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
|
||||
final class BatteryStatusAccess {
|
||||
|
||||
private BatteryStatusAccess() {
|
||||
}
|
||||
|
||||
static BatteryState getCurrentBatteryState(Context context) {
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
Intent batteryIntent = context.registerReceiver(
|
||||
null,
|
||||
new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
|
||||
if (batteryIntent == null) {
|
||||
return null;
|
||||
}
|
||||
int level = batteryIntent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = batteryIntent.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
|
||||
if (level < 0 || scale <= 0) {
|
||||
return null;
|
||||
}
|
||||
int percent = Math.round((level * 100f) / scale);
|
||||
int status = batteryIntent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean plugged = batteryIntent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
|
||||
|| status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
boolean charged = status == BatteryManager.BATTERY_STATUS_FULL || percent >= 100;
|
||||
return new BatteryState(percent, plugged, charged);
|
||||
}
|
||||
|
||||
static Context getContextField(Object target, String fieldName) {
|
||||
Object value = ReflectionSupport.getFieldValue(target, fieldName);
|
||||
return value instanceof Context context ? context : null;
|
||||
}
|
||||
|
||||
static int getIntField(Object target, String fieldName, int fallback) {
|
||||
Object value = ReflectionSupport.getFieldValue(target, fieldName);
|
||||
return value instanceof Integer integer ? integer : fallback;
|
||||
}
|
||||
|
||||
static boolean getBooleanField(Object target, String fieldName, boolean fallback) {
|
||||
Object value = ReflectionSupport.getFieldValue(target, fieldName);
|
||||
return value instanceof Boolean bool ? bool : fallback;
|
||||
}
|
||||
|
||||
static Object getObjectField(Object target, String fieldName) {
|
||||
return ReflectionSupport.getFieldValue(target, fieldName);
|
||||
}
|
||||
|
||||
record BatteryState(int level, boolean plugged, boolean charged) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.misc;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* Reserved home for Misc runtime behaviours that stay outside the icon
|
||||
* placement engine.
|
||||
*/
|
||||
public final class MiscFeature implements RuntimeFeature {
|
||||
|
||||
private AodCallVisibilityController aodCallVisibilityController;
|
||||
private BatteryIndicationController batteryIndicationController;
|
||||
private AodBottomBatteryFallbackController aodBottomBatteryFallbackController;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "misc";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(
|
||||
RuntimeContext context,
|
||||
XposedModuleInterface.PackageReadyParam param
|
||||
) {
|
||||
if (param == null || !"com.android.systemui".equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
installControllers(context, param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installControllers(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (aodCallVisibilityController == null) {
|
||||
aodCallVisibilityController = new AodCallVisibilityController(context);
|
||||
}
|
||||
if (batteryIndicationController == null) {
|
||||
batteryIndicationController = new BatteryIndicationController(context);
|
||||
}
|
||||
if (aodBottomBatteryFallbackController == null) {
|
||||
aodBottomBatteryFallbackController = new AodBottomBatteryFallbackController(context);
|
||||
}
|
||||
aodCallVisibilityController.install(classLoader);
|
||||
batteryIndicationController.install(classLoader);
|
||||
aodBottomBatteryFallbackController.install(classLoader);
|
||||
}
|
||||
}
|
||||
+273
@@ -0,0 +1,273 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.notifications;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class StockUnlockedNotificationIconsController {
|
||||
|
||||
private static final String CONTAINER_CLASS_NAME =
|
||||
"com.android.systemui.statusbar.phone.NotificationIconContainer";
|
||||
private static final int INFINITE_ICON_LIMIT = 1000;
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final Set<View> trackedContainers =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final WeakHashMap<View, Integer> stockMaxStaticIcons = new WeakHashMap<>();
|
||||
|
||||
private boolean hooksInstalled;
|
||||
private boolean receiverRegistered;
|
||||
|
||||
StockUnlockedNotificationIconsController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
Class<?> containerClass = ReflectionSupport.requireClass(CONTAINER_CLASS_NAME, classLoader);
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onAttachedToWindow", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) {
|
||||
trackContainer(view);
|
||||
view.post(() -> refreshContainer(view));
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "calculateIconXTranslations", chain -> {
|
||||
if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) {
|
||||
trackContainer(view);
|
||||
applyLimitIfNeeded(view);
|
||||
}
|
||||
return chain.proceed();
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "onLayout", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view && shouldManageContainer(view)) {
|
||||
trackContainer(view);
|
||||
if (isFeatureEnabled(view.getContext())) {
|
||||
enforceParentWidth(view);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), containerClass, "shouldForceOverflow", chain -> {
|
||||
if (!(chain.getThisObject() instanceof View view)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
if (!shouldManageContainer(view)) {
|
||||
return chain.proceed();
|
||||
}
|
||||
trackContainer(view);
|
||||
if (chain.getArgs().size() < 4 || !isFeatureEnabled(view.getContext())) {
|
||||
return chain.proceed();
|
||||
}
|
||||
int override = resolveUnlockedLimit(view.getContext());
|
||||
if (override <= 0) {
|
||||
return chain.proceed();
|
||||
}
|
||||
applyOverrideToContainer(view, override);
|
||||
int index = chain.getArg(0) instanceof Integer value ? value : -1;
|
||||
int speedBumpIndex = chain.getArg(1) instanceof Integer value ? value : -1;
|
||||
float appear = chain.getArg(2) instanceof Float value ? value : 0f;
|
||||
return (speedBumpIndex != -1 && index >= speedBumpIndex && appear > 0f)
|
||||
|| index >= override;
|
||||
});
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void trackContainer(View view) {
|
||||
trackedContainers.add(view);
|
||||
captureStockMaxStaticIcons(view);
|
||||
registerReceiverIfNeeded(view.getContext());
|
||||
}
|
||||
|
||||
private void captureStockMaxStaticIcons(View view) {
|
||||
if (stockMaxStaticIcons.containsKey(view)) {
|
||||
return;
|
||||
}
|
||||
Object value = ReflectionSupport.getFieldValue(view, "mMaxStaticIcons");
|
||||
if (value instanceof Integer integer) {
|
||||
stockMaxStaticIcons.put(view, integer);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReceiverIfNeeded(Context context) {
|
||||
if (receiverRegistered || context == null) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
refreshTrackedContainers();
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
}
|
||||
|
||||
private void refreshTrackedContainers() {
|
||||
for (View view : trackedContainers) {
|
||||
if (shouldManageContainer(view)) {
|
||||
view.post(() -> refreshContainer(view));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshContainer(View view) {
|
||||
applyLimitIfNeeded(view);
|
||||
forceLayout(view);
|
||||
tryRecalculateIcons(view);
|
||||
}
|
||||
|
||||
private void applyLimitIfNeeded(View view) {
|
||||
captureStockMaxStaticIcons(view);
|
||||
if (!isFeatureEnabled(view.getContext())) {
|
||||
restoreStockLimit(view);
|
||||
return;
|
||||
}
|
||||
int override = resolveUnlockedLimit(view.getContext());
|
||||
if (override <= 0) {
|
||||
restoreStockLimit(view);
|
||||
return;
|
||||
}
|
||||
applyOverrideToContainer(view, override);
|
||||
enforceParentWidth(view);
|
||||
}
|
||||
|
||||
private void restoreStockLimit(View view) {
|
||||
Integer stock = stockMaxStaticIcons.get(view);
|
||||
if (stock != null) {
|
||||
ReflectionSupport.setFieldValue(view, "mMaxStaticIcons", stock);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyOverrideToContainer(View view, int override) {
|
||||
ReflectionSupport.setFieldValue(view, "mMaxStaticIcons", override);
|
||||
}
|
||||
|
||||
private void enforceParentWidth(View view) {
|
||||
ViewParent parent = view.getParent();
|
||||
int depth = 0;
|
||||
while (parent instanceof View && depth < 3) {
|
||||
View parentView = (View) parent;
|
||||
String name = parentView.getClass().getName();
|
||||
if (name.contains("AlphaOptimizedLinearLayout")
|
||||
|| name.contains("AlphaOptimizedFrameLayout")) {
|
||||
ViewGroup.LayoutParams lp = parentView.getLayoutParams();
|
||||
if (lp != null && (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.width == 0)) {
|
||||
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
parentView.setLayoutParams(lp);
|
||||
parentView.requestLayout();
|
||||
}
|
||||
}
|
||||
parent = parentView.getParent();
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
private void forceLayout(View view) {
|
||||
view.forceLayout();
|
||||
view.requestLayout();
|
||||
view.invalidate();
|
||||
ViewParent parent = view.getParent();
|
||||
if (parent instanceof View parentView) {
|
||||
parentView.requestLayout();
|
||||
parentView.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void tryRecalculateIcons(View view) {
|
||||
ReflectionSupport.invokeMethod(view, "calculateIconXTranslations", new Class<?>[0]);
|
||||
ReflectionSupport.invokeMethod(view, "applyIconStates", new Class<?>[0]);
|
||||
ReflectionSupport.invokeMethod(view, "onNotificationInfoUpdated", new Class<?>[0]);
|
||||
}
|
||||
|
||||
private boolean isFeatureEnabled(Context context) {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
return settings != null && !settings.clockEnabled;
|
||||
}
|
||||
|
||||
private int resolveUnlockedLimit(Context context) {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
if (settings == null || settings.clockEnabled) {
|
||||
return -1;
|
||||
}
|
||||
int value = settings.unlockedMaxIconsPerRow;
|
||||
if (value == Integer.MAX_VALUE || value == 0) {
|
||||
return INFINITE_ICON_LIMIT;
|
||||
}
|
||||
if (value < SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN) {
|
||||
return SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isKeyguardContainer(Object container) {
|
||||
return ReflectionSupport.getBooleanField(container, "mOnKeyguardStatusBar", false);
|
||||
}
|
||||
|
||||
private boolean shouldManageContainer(View view) {
|
||||
if (isKeyguardContainer(view)) {
|
||||
return false;
|
||||
}
|
||||
ViewParent parent = view.getParent();
|
||||
boolean inUnlockedStatusBarIconArea = false;
|
||||
int depth = 0;
|
||||
while (parent instanceof View parentView && depth < 8) {
|
||||
String idName = resolveIdName(parentView);
|
||||
if (idName.equals("notification_icon_area")
|
||||
|| idName.equals("notification_icon_area_inner")) {
|
||||
inUnlockedStatusBarIconArea = true;
|
||||
}
|
||||
String className = parentView.getClass().getName();
|
||||
if (className.contains("KeyguardRootView")
|
||||
|| className.contains("NotificationShelf")
|
||||
|| className.contains("NotificationStackScrollLayout")) {
|
||||
return false;
|
||||
}
|
||||
parent = parentView.getParent();
|
||||
depth++;
|
||||
}
|
||||
return inUnlockedStatusBarIconArea;
|
||||
}
|
||||
|
||||
private String resolveIdName(View view) {
|
||||
if (view == null || view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.notifications;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* Keeps the original simple unlocked stock-icon count override available while
|
||||
* the rewritten custom layout engine is still disabled.
|
||||
*/
|
||||
public final class StockUnlockedNotificationIconsFeature implements RuntimeFeature {
|
||||
|
||||
private StockUnlockedNotificationIconsController controller;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stock-unlocked-notification-icons";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(RuntimeContext context, XposedModuleInterface.PackageReadyParam param) {
|
||||
if (param == null || !"com.android.systemui".equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
installController(context, param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installController(RuntimeContext context, ClassLoader classLoader) {
|
||||
if (controller == null) {
|
||||
controller = new StockUnlockedNotificationIconsController(context);
|
||||
}
|
||||
controller.install(classLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package se.ajpanton.statusbartweak.runtime.hooks;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* Small reflection helpers used by rebuilt runtime slices.
|
||||
*/
|
||||
public final class ReflectionSupport {
|
||||
private ReflectionSupport() {
|
||||
}
|
||||
|
||||
public static Class<?> findClassIfExists(String className, ClassLoader classLoader) {
|
||||
if (className == null || className.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return Class.forName(className, false, classLoader);
|
||||
} catch (Throwable ignored) {
|
||||
if (classLoader == null) {
|
||||
try {
|
||||
return Class.forName(className);
|
||||
} catch (Throwable ignoredAgain) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Class<?> requireClass(String className, ClassLoader classLoader) {
|
||||
Class<?> type = findClassIfExists(className, classLoader);
|
||||
if (type == null) {
|
||||
throw new IllegalStateException("Required class not found: " + className);
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
public static Object getFieldValue(Object target, String fieldName) {
|
||||
if (target == null || fieldName == null || fieldName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Field field = findField(target.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return field.get(target);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object requireFieldValue(Object target, String fieldName) {
|
||||
if (target == null) {
|
||||
throw new IllegalArgumentException("Target is null for field: " + fieldName);
|
||||
}
|
||||
Field field = requireField(target.getClass(), fieldName);
|
||||
try {
|
||||
return field.get(target);
|
||||
} catch (IllegalAccessException exception) {
|
||||
throw new IllegalStateException(
|
||||
"Required field not readable: " + target.getClass().getName() + "#" + fieldName,
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
public static Object getStaticFieldValue(Class<?> type, String fieldName) {
|
||||
if (type == null || fieldName == null || fieldName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Field field = findField(type, fieldName);
|
||||
if (field == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return field.get(null);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean getBooleanField(Object target, String fieldName, boolean fallback) {
|
||||
Object value = getFieldValue(target, fieldName);
|
||||
return value instanceof Boolean ? (Boolean) value : fallback;
|
||||
}
|
||||
|
||||
public static void setFieldValue(Object target, String fieldName, Object value) {
|
||||
if (target == null || fieldName == null || fieldName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Field field = findField(target.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
field.set(target, value);
|
||||
} catch (Throwable ignored) {
|
||||
// Optional reflection path; absence/failure means no override.
|
||||
}
|
||||
}
|
||||
|
||||
public static Object invokeMethod(
|
||||
Object target,
|
||||
String methodName,
|
||||
Class<?>[] parameterTypes,
|
||||
Object... args
|
||||
) {
|
||||
if (target == null || methodName == null || methodName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Method method = findMethod(target.getClass(), methodName, parameterTypes);
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return method.invoke(target, args);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static Object invokeStaticMethod(
|
||||
Class<?> type,
|
||||
String methodName,
|
||||
Class<?>[] parameterTypes,
|
||||
Object... args
|
||||
) {
|
||||
if (type == null || methodName == null || methodName.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Method method = findMethod(type, methodName, parameterTypes);
|
||||
if (method == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return method.invoke(null, args);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Field requireField(Class<?> type, String fieldName) {
|
||||
if (fieldName == null || fieldName.isEmpty()) {
|
||||
throw new IllegalArgumentException("Field name is empty");
|
||||
}
|
||||
Field field = findField(type, fieldName);
|
||||
if (field == null) {
|
||||
throw new IllegalStateException("Required field not found: " + type.getName() + "#" + fieldName);
|
||||
}
|
||||
return field;
|
||||
}
|
||||
|
||||
private static Field findField(Class<?> type, String fieldName) {
|
||||
Class<?> current = type;
|
||||
while (current != null) {
|
||||
try {
|
||||
Field field = current.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?> type, String methodName, Class<?>[] parameterTypes) {
|
||||
Class<?> current = type;
|
||||
while (current != null) {
|
||||
try {
|
||||
Method method = current.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
} catch (NoSuchMethodException ignored) {
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package se.ajpanton.statusbartweak.runtime.hooks;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
|
||||
/**
|
||||
* Narrow libxposed hook helpers.
|
||||
*
|
||||
* The rebuilt runtime should depend on a small adapter layer instead of
|
||||
* spreading raw framework hook calls across every feature port.
|
||||
*/
|
||||
public final class XposedHookSupport {
|
||||
|
||||
private XposedHookSupport() {
|
||||
}
|
||||
|
||||
public static List<XposedInterface.HookHandle> hookAllMethods(
|
||||
XposedInterface framework,
|
||||
Class<?> type,
|
||||
String methodName,
|
||||
XposedInterface.Hooker hooker
|
||||
) {
|
||||
Objects.requireNonNull(framework, "framework");
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(methodName, "methodName");
|
||||
Objects.requireNonNull(hooker, "hooker");
|
||||
List<XposedInterface.HookHandle> handles = new ArrayList<>();
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
handles.add(framework.hook(method).intercept(hooker));
|
||||
}
|
||||
if (handles.isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"No methods matched hook target: " + type.getName() + "#" + methodName);
|
||||
}
|
||||
return handles;
|
||||
}
|
||||
|
||||
public static List<XposedInterface.HookHandle> hookAllMethodsIfExists(
|
||||
XposedInterface framework,
|
||||
Class<?> type,
|
||||
String methodName,
|
||||
XposedInterface.Hooker hooker
|
||||
) {
|
||||
Objects.requireNonNull(framework, "framework");
|
||||
Objects.requireNonNull(type, "type");
|
||||
Objects.requireNonNull(methodName, "methodName");
|
||||
Objects.requireNonNull(hooker, "hooker");
|
||||
List<XposedInterface.HookHandle> handles = new ArrayList<>();
|
||||
for (Method method : type.getDeclaredMethods()) {
|
||||
if (!methodName.equals(method.getName())) {
|
||||
continue;
|
||||
}
|
||||
method.setAccessible(true);
|
||||
handles.add(framework.hook(method).intercept(hooker));
|
||||
}
|
||||
return handles;
|
||||
}
|
||||
|
||||
public static List<XposedInterface.HookHandle> hookAllConstructors(
|
||||
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));
|
||||
}
|
||||
if (handles.isEmpty()) {
|
||||
throw new IllegalStateException("No constructors matched hook target: " + type.getName());
|
||||
}
|
||||
return handles;
|
||||
}
|
||||
}
|
||||
+903
@@ -0,0 +1,903 @@
|
||||
package se.ajpanton.statusbartweak.runtime.layout;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.PowerManager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Collections;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.NotificationDisplayStyle;
|
||||
import se.ajpanton.statusbartweak.runtime.render.DebugBoundsOverlayController;
|
||||
import se.ajpanton.statusbartweak.runtime.render.OwnedIconHostManager;
|
||||
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController;
|
||||
import se.ajpanton.statusbartweak.runtime.render.UnlockedIconSnapshotRenderController.RenderResult;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class StockLayoutCanvasController {
|
||||
|
||||
private static final String CLASS_PLUGIN_AOD_MANAGER =
|
||||
"com.android.systemui.doze.PluginAODManager";
|
||||
private static final String CLASS_AOD_DIRECT_CONTROLLER = "aod.QI";
|
||||
private static final String CLASS_AOD_ICONS_CONTROLLER = "aod.T4";
|
||||
private static final String CLASS_AOD_ICONS_ONLY_CONTAINER =
|
||||
"com.samsung.android.uniform.widget.notification.NotificationIconsOnlyContainer";
|
||||
|
||||
private static final Set<String> TARGET_ID_NAMES = Set.of(
|
||||
"left_clock_container",
|
||||
"notification_icon_area",
|
||||
"notification_icon_area_inner",
|
||||
"system_icon_area",
|
||||
"statusIcons",
|
||||
"battery",
|
||||
"notification_icononly_container",
|
||||
"notification_icononly_area",
|
||||
"keyguard_icononly_container_view",
|
||||
"common_notification_widget"
|
||||
);
|
||||
|
||||
private static final Set<String> TARGET_CLASS_SUBSTRINGS = Set.of(
|
||||
"QSClockIndicatorView",
|
||||
"SecShelfNotificationIconContainer"
|
||||
);
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final Map<View, HiddenViewState> hiddenViewStates = new WeakHashMap<>();
|
||||
private final Map<View, RootSize> rootSizes = new WeakHashMap<>();
|
||||
private final Map<View, Integer> debugOverlaySignatures = new WeakHashMap<>();
|
||||
private final Set<View> confirmedAodPluginViews =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> layoutOffCleanedRoots =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> trackedStatusChipSources =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> trackedUnlockedStockSurfaces =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<View> trackedLockedStatusBarIconSurfaces =
|
||||
Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private final Set<ClassLoader> hookedPluginClassLoaders =
|
||||
Collections.newSetFromMap(new IdentityHashMap<>());
|
||||
private final Map<View, UnlockedHosts> unlockedHostsByRoot = new WeakHashMap<>();
|
||||
private final OwnedIconHostManager ownedIconHostManager = new OwnedIconHostManager();
|
||||
private final DebugBoundsOverlayController debugBoundsOverlayController =
|
||||
new DebugBoundsOverlayController();
|
||||
private final UnlockedIconSnapshotRenderController unlockedIconRenderController;
|
||||
|
||||
private boolean hooksInstalled;
|
||||
|
||||
StockLayoutCanvasController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
unlockedIconRenderController = new UnlockedIconSnapshotRenderController();
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
installPluginManagerHooks(classLoader);
|
||||
installViewRootHook(classLoader);
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void installPluginManagerHooks(ClassLoader classLoader) {
|
||||
Class<?> managerClass = ReflectionSupport.findClassIfExists(CLASS_PLUGIN_AOD_MANAGER, classLoader);
|
||||
if (managerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), managerClass, "setAODPlugin", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object plugin = firstArg(chain.getArgs());
|
||||
if (plugin == null) {
|
||||
return result;
|
||||
}
|
||||
ClassLoader pluginClassLoader = plugin.getClass().getClassLoader();
|
||||
if (pluginClassLoader != null && hookedPluginClassLoaders.add(pluginClassLoader)) {
|
||||
installPluginClassLoaderHooks(pluginClassLoader);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void installPluginClassLoaderHooks(ClassLoader pluginClassLoader) {
|
||||
Class<?> qiClass = ReflectionSupport.findClassIfExists(CLASS_AOD_DIRECT_CONTROLLER, pluginClassLoader);
|
||||
if (qiClass != null) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), qiClass, "b", chain -> {
|
||||
Object result = chain.proceed();
|
||||
enforceDirectAodControllerPolicy(chain.getThisObject());
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), qiClass, "e", chain -> {
|
||||
Object result = chain.proceed();
|
||||
enforceDirectAodControllerPolicy(chain.getThisObject());
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Class<?> iconsControllerClass =
|
||||
ReflectionSupport.findClassIfExists(CLASS_AOD_ICONS_CONTROLLER, pluginClassLoader);
|
||||
if (iconsControllerClass != null) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), iconsControllerClass, "f", chain -> {
|
||||
Object result = chain.proceed();
|
||||
enforceIconsOnlyControllerPolicy(firstArg(chain.getArgs()));
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
Class<?> iconsOnlyClass =
|
||||
ReflectionSupport.findClassIfExists(CLASS_AOD_ICONS_ONLY_CONTAINER, pluginClassLoader);
|
||||
if (iconsOnlyClass != null) {
|
||||
XposedHookSupport.hookAllMethodsIfExists(runtimeContext.getFramework(), iconsOnlyClass, "onLayout", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (chain.getThisObject() instanceof View view) {
|
||||
applyConfirmedAodPluginPolicy(view);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void installViewRootHook(ClassLoader classLoader) {
|
||||
Class<?> viewRootClass = ReflectionSupport.requireClass("android.view.ViewRootImpl", classLoader);
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), viewRootClass, "performDraw", chain -> {
|
||||
Object rootObj = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
|
||||
View root = rootObj instanceof View candidate ? candidate : null;
|
||||
if (root != null) {
|
||||
applyBeforeDraw(root);
|
||||
}
|
||||
return chain.proceed();
|
||||
});
|
||||
XposedHookSupport.hookAllMethods(runtimeContext.getFramework(), viewRootClass, "performTraversals", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object rootObj = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
|
||||
if (rootObj instanceof View root) {
|
||||
applyToRoot(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void applyBeforeDraw(View root) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
if (!settings.clockEnabled) {
|
||||
return;
|
||||
}
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
if (activeScene != null && activeScene.isAod()) {
|
||||
hideConfirmedAodPluginViews();
|
||||
}
|
||||
if (activeScene != null && !activeScene.isUnlocked()) {
|
||||
hideTrackedLockedStatusBarIconSurfaces(null);
|
||||
}
|
||||
if (!isUnlockedStatusBarRoot(root)) {
|
||||
return;
|
||||
}
|
||||
if (activeScene != null && activeScene.isUnlocked()) {
|
||||
if (isKnownRootSizeChanged(root)) {
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, false);
|
||||
removeDebugOverlay(root);
|
||||
}
|
||||
hideTrackedUnlockedStockSurfaces(root);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyToRoot(View root) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
Context context = root.getContext();
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
boolean layoutEnabled = settings.clockEnabled;
|
||||
boolean statusBarRoot = isUnlockedStatusBarRoot(root);
|
||||
runtimeContext.getModeStateRepository().setLayoutEnabled(layoutEnabled);
|
||||
if (!layoutEnabled) {
|
||||
if (statusBarRoot) {
|
||||
applyDebugOverlayIfNeeded(root, false, settings, false);
|
||||
} else {
|
||||
removeDebugOverlay(root);
|
||||
}
|
||||
cleanupLayoutOffRoot(root);
|
||||
return;
|
||||
}
|
||||
layoutOffCleanedRoots.remove(root);
|
||||
updateActiveScene(root, context);
|
||||
SceneKey activeScene = runtimeContext.getModeStateRepository().getActiveScene();
|
||||
boolean unlockedScene = activeScene != null && activeScene.isUnlocked();
|
||||
if (!statusBarRoot) {
|
||||
if (!unlockedScene && canContainLockedStatusBarIconSurfaces(root)) {
|
||||
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
||||
}
|
||||
removeDebugOverlay(root);
|
||||
return;
|
||||
}
|
||||
UnlockedHosts unlockedHosts = updateUnlockedOwnedHosts(root, activeScene);
|
||||
if (!unlockedScene) {
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
restoreTransientUnlockedSources();
|
||||
hideOrDiscoverLockedStatusBarIconSurfaces(root);
|
||||
removeDebugOverlay(root);
|
||||
return;
|
||||
}
|
||||
RenderResult renderResult = new RenderResult(false, false);
|
||||
if (unlockedHosts != null) {
|
||||
markRootSizeBeforeDraw(root);
|
||||
renderResult = unlockedIconRenderController.renderUnlocked(
|
||||
root,
|
||||
unlockedHosts.clockHost,
|
||||
unlockedHosts.chipHost,
|
||||
unlockedHosts.statusHost,
|
||||
unlockedHosts.notificationHost,
|
||||
activeScene);
|
||||
ownedIconHostManager.setUnlockedHostsVisible(root, true);
|
||||
}
|
||||
boolean scanTree = renderResult.stockSourcesChanged() || !hasTrackedUnlockedStockSurfaces(root);
|
||||
applyDebugOverlayIfNeeded(root, true, settings, renderResult.layoutChanged());
|
||||
hideTrackedStatusChipSources(root);
|
||||
if (renderResult.chipSourcesChanged()
|
||||
&& !scanTree
|
||||
&& !hasTrackedStatusChipSources(root)) {
|
||||
trackAndHideStatusChipSources(root);
|
||||
}
|
||||
if (!scanTree) {
|
||||
return;
|
||||
}
|
||||
walkTree(root, view -> {
|
||||
boolean statusChipSource = isStatusChipSource(view);
|
||||
if (statusChipSource) {
|
||||
trackedStatusChipSources.add(view);
|
||||
}
|
||||
boolean shouldHide = statusChipSource || shouldHideNonChipView(view);
|
||||
if (shouldHide) {
|
||||
trackedUnlockedStockSurfaces.add(view);
|
||||
hideView(view, true);
|
||||
} else {
|
||||
trackedUnlockedStockSurfaces.remove(view);
|
||||
restoreView(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void applyDebugOverlay(
|
||||
View root,
|
||||
boolean layoutEnabled,
|
||||
SbtSettings settings
|
||||
) {
|
||||
if (!debugBoundsOverlayController.hasEnabledVisualizer(layoutEnabled, settings)) {
|
||||
debugBoundsOverlayController.remove(root);
|
||||
return;
|
||||
}
|
||||
debugBoundsOverlayController.apply(root, layoutEnabled, settings);
|
||||
}
|
||||
|
||||
private void applyDebugOverlayIfNeeded(
|
||||
View root,
|
||||
boolean layoutEnabled,
|
||||
SbtSettings settings,
|
||||
boolean force
|
||||
) {
|
||||
int signature = debugOverlaySignature(root, layoutEnabled, settings);
|
||||
Integer previous = debugOverlaySignatures.get(root);
|
||||
if (!force && previous != null && previous == signature) {
|
||||
return;
|
||||
}
|
||||
debugOverlaySignatures.put(root, signature);
|
||||
applyDebugOverlay(root, layoutEnabled, settings);
|
||||
}
|
||||
|
||||
private int debugOverlaySignature(
|
||||
View root,
|
||||
boolean layoutEnabled,
|
||||
SbtSettings settings
|
||||
) {
|
||||
int result = 17;
|
||||
result = 31 * result + (layoutEnabled ? 1 : 0);
|
||||
result = 31 * result + (root != null ? root.getWidth() : 0);
|
||||
result = 31 * result + (root != null ? root.getHeight() : 0);
|
||||
if (settings != null) {
|
||||
result = 31 * result + (settings.debugVisualizeCameraCutout ? 1 : 0);
|
||||
result = 31 * result + (settings.debugVisualizeNotificationContainer ? 1 : 0);
|
||||
result = 31 * result + (settings.debugVisualizeStatusContainer ? 1 : 0);
|
||||
result = 31 * result + (settings.debugVisualizeClockContainer ? 1 : 0);
|
||||
result = 31 * result + (settings.debugVisualizeChipContainer ? 1 : 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void removeDebugOverlay(View root) {
|
||||
debugOverlaySignatures.remove(root);
|
||||
debugBoundsOverlayController.remove(root);
|
||||
}
|
||||
|
||||
private void restoreTransientUnlockedSources() {
|
||||
if (hiddenViewStates.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> views = new ArrayDeque<>(hiddenViewStates.keySet());
|
||||
while (!views.isEmpty()) {
|
||||
View view = views.removeFirst();
|
||||
if (confirmedAodPluginViews.contains(view)
|
||||
|| trackedLockedStatusBarIconSurfaces.contains(view)) {
|
||||
continue;
|
||||
}
|
||||
restoreView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void hideTrackedStatusChipSources(View root) {
|
||||
if (trackedStatusChipSources.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : trackedStatusChipSources) {
|
||||
if (view == null
|
||||
|| !view.isAttachedToWindow()
|
||||
|| !isDescendantOf(view, root)
|
||||
|| ownedIconHostManager.isOwnedHost(view)) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
hideView(view, true);
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
trackedStatusChipSources.remove(staleViews.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private void trackAndHideStatusChipSources(View root) {
|
||||
walkTree(root, view -> {
|
||||
if (isStatusChipSource(view)) {
|
||||
trackedStatusChipSources.add(view);
|
||||
hideView(view, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void hideTrackedUnlockedStockSurfaces(View root) {
|
||||
if (trackedUnlockedStockSurfaces.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : trackedUnlockedStockSurfaces) {
|
||||
if (view == null
|
||||
|| !view.isAttachedToWindow()
|
||||
|| !isDescendantOf(view, root)
|
||||
|| ownedIconHostManager.isOwnedHost(view)) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
hideView(view, true);
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
trackedUnlockedStockSurfaces.remove(staleViews.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasTrackedUnlockedStockSurfaces(View root) {
|
||||
if (trackedUnlockedStockSurfaces.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (View view : trackedUnlockedStockSurfaces) {
|
||||
if (view != null && view.isAttachedToWindow() && isDescendantOf(view, root)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void hideConfirmedAodPluginViews() {
|
||||
hideTrackedViews(confirmedAodPluginViews, view -> true, false);
|
||||
}
|
||||
|
||||
private void trackAndHideLockedStatusBarIconSurfaces(View root) {
|
||||
walkTree(root, view -> {
|
||||
if (isLockedStatusBarIconSurface(view)) {
|
||||
trackedLockedStatusBarIconSurfaces.add(view);
|
||||
hideView(view);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void hideOrDiscoverLockedStatusBarIconSurfaces(View root) {
|
||||
if (!hideTrackedLockedStatusBarIconSurfaces(root)) {
|
||||
trackAndHideLockedStatusBarIconSurfaces(root);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hideTrackedLockedStatusBarIconSurfaces(View root) {
|
||||
if (trackedLockedStatusBarIconSurfaces.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
boolean foundInRoot = false;
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : trackedLockedStatusBarIconSurfaces) {
|
||||
if (view == null || !view.isAttachedToWindow() || !isLockedStatusBarIconSurface(view)) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
if (root == null || isDescendantOf(view, root)) {
|
||||
foundInRoot = true;
|
||||
hideView(view);
|
||||
}
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
trackedLockedStatusBarIconSurfaces.remove(staleViews.removeFirst());
|
||||
}
|
||||
return foundInRoot;
|
||||
}
|
||||
|
||||
private boolean hasTrackedStatusChipSources(View root) {
|
||||
if (trackedStatusChipSources.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (View view : trackedStatusChipSources) {
|
||||
if (view != null
|
||||
&& view.isAttachedToWindow()
|
||||
&& isDescendantOf(view, root)
|
||||
&& isStatusChipSource(view)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void hideTrackedViews(
|
||||
Set<View> trackedViews,
|
||||
Predicate<View> keepTracked,
|
||||
boolean keepLaidOut
|
||||
) {
|
||||
if (trackedViews.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> staleViews = new ArrayDeque<>();
|
||||
for (View view : trackedViews) {
|
||||
if (view == null || !view.isAttachedToWindow() || !keepTracked.test(view)) {
|
||||
staleViews.add(view);
|
||||
continue;
|
||||
}
|
||||
hideView(view, keepLaidOut);
|
||||
}
|
||||
while (!staleViews.isEmpty()) {
|
||||
trackedViews.remove(staleViews.removeFirst());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLockedStatusBarIconSurface(View view) {
|
||||
if (view == null || ownedIconHostManager.isOwnedHost(view)) {
|
||||
return false;
|
||||
}
|
||||
String idName = resolveIdName(view);
|
||||
String viewClassName = view.getClass().getName();
|
||||
boolean shelfSurface = viewClassName.contains("NotificationShelf");
|
||||
boolean shadeIconOnlySurface = "notification_icononly_container".equals(idName)
|
||||
|| "notification_icononly_area".equals(idName)
|
||||
|| "keyguard_icononly_container_view".equals(idName)
|
||||
|| "common_notification_widget".equals(idName);
|
||||
if (!("system_icons".equals(idName)
|
||||
|| "system_icon_area".equals(idName)
|
||||
|| "statusIcons".equals(idName)
|
||||
|| "battery".equals(idName)
|
||||
|| "notification_icon_area".equals(idName)
|
||||
|| "notification_icon_area_inner".equals(idName)
|
||||
|| "notificationIcons".equals(idName)
|
||||
|| "notification_icononly_container".equals(idName)
|
||||
|| "notification_icononly_area".equals(idName)
|
||||
|| "keyguard_icononly_container_view".equals(idName)
|
||||
|| "common_notification_widget".equals(idName)
|
||||
|| shelfSurface)) {
|
||||
return false;
|
||||
}
|
||||
Object current = view;
|
||||
while (current instanceof View currentView) {
|
||||
String className = currentView.getClass().getName();
|
||||
if (className.contains("KeyguardStatusBarView")
|
||||
|| className.contains("PhoneStatusBarView")
|
||||
|| className.contains("StatusBarWindowView")) {
|
||||
return true;
|
||||
}
|
||||
if (shelfSurface && className.contains("NotificationShadeWindowView")) {
|
||||
return true;
|
||||
}
|
||||
if (shadeIconOnlySurface && className.contains("NotificationShadeWindowView")) {
|
||||
return true;
|
||||
}
|
||||
current = currentView.getParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean canContainLockedStatusBarIconSurfaces(View root) {
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
String className = root.getClass().getName();
|
||||
return className.contains("NotificationShadeWindowView")
|
||||
|| className.contains("KeyguardStatusBarView")
|
||||
|| className.contains("PhoneStatusBarView")
|
||||
|| className.contains("StatusBarWindowView");
|
||||
}
|
||||
|
||||
private boolean shouldHideNonChipView(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
if (ownedIconHostManager.isOwnedHost(view)) {
|
||||
return false;
|
||||
}
|
||||
if (confirmedAodPluginViews.contains(view)) {
|
||||
return true;
|
||||
}
|
||||
String idName = resolveIdName(view);
|
||||
if (TARGET_ID_NAMES.contains(idName)) {
|
||||
return true;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
if (className.contains("NotificationShelf")) {
|
||||
return true;
|
||||
}
|
||||
for (String token : TARGET_CLASS_SUBSTRINGS) {
|
||||
if (className.contains(token)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isStatusChipSource(View view) {
|
||||
if (view == null || view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
int screenWidth = view.getResources().getDisplayMetrics().widthPixels;
|
||||
if (screenWidth > 0 && width >= screenWidth / 2) {
|
||||
return false;
|
||||
}
|
||||
if (!isStatusChipCandidate(view)) {
|
||||
return false;
|
||||
}
|
||||
Object parent = view.getParent();
|
||||
while (parent instanceof View parentView) {
|
||||
if (isStatusChipCandidate(parentView)) {
|
||||
return false;
|
||||
}
|
||||
parent = parentView.getParent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isStatusChipCandidate(View view) {
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
if (className.contains("privacy") || idName.contains("privacy")) {
|
||||
return false;
|
||||
}
|
||||
return className.contains("touchinterceptframelayout")
|
||||
|| className.contains("ongoingactivity")
|
||||
|| "ongoing_activity_capsule".equals(idName);
|
||||
}
|
||||
|
||||
private void markRootSizeBeforeDraw(View root) {
|
||||
if (!isUnlockedStatusBarRoot(root)) {
|
||||
return;
|
||||
}
|
||||
markRootSize(root, new RootSize(root.getWidth(), root.getHeight()));
|
||||
}
|
||||
|
||||
private boolean isKnownRootSizeChanged(View root) {
|
||||
if (!isUnlockedStatusBarRoot(root)) {
|
||||
return false;
|
||||
}
|
||||
RootSize previous = rootSizes.get(root);
|
||||
return previous != null
|
||||
&& (previous.width != root.getWidth() || previous.height != root.getHeight());
|
||||
}
|
||||
|
||||
private void markRootSize(View root, RootSize size) {
|
||||
if (size.width <= 0 || size.height <= 0) {
|
||||
return;
|
||||
}
|
||||
RootSize previous = rootSizes.put(root, size);
|
||||
if (previous != null && !previous.equals(size)) {
|
||||
unlockedIconRenderController.beginRootTransition();
|
||||
}
|
||||
}
|
||||
|
||||
private void enforceDirectAodControllerPolicy(Object controller) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "d")));
|
||||
applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "l")));
|
||||
applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "k")));
|
||||
applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "m")));
|
||||
applyConfirmedAodPluginPolicy(castView(ReflectionSupport.requireFieldValue(controller, "s")));
|
||||
}
|
||||
|
||||
private void enforceIconsOnlyControllerPolicy(Object controller) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
View container = castView(ReflectionSupport.requireFieldValue(controller, "mNotificationContainer"));
|
||||
applyConfirmedAodPluginPolicy(container);
|
||||
}
|
||||
|
||||
private void applyConfirmedAodPluginPolicy(View view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
applyConditionalAodPluginPolicy(view, isLayoutEnabled(view.getContext()));
|
||||
}
|
||||
|
||||
private void applyConditionalAodPluginPolicy(View view, boolean shouldHide) {
|
||||
if (shouldHide) {
|
||||
confirmedAodPluginViews.add(view);
|
||||
hideView(view);
|
||||
} else {
|
||||
confirmedAodPluginViews.remove(view);
|
||||
restoreView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hideView(View view) {
|
||||
return hideView(view, false);
|
||||
}
|
||||
|
||||
private boolean hideView(View view, boolean keepLaidOut) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
int targetVisibility = keepLaidOut ? View.VISIBLE : View.GONE;
|
||||
if (hiddenViewStates.containsKey(view)
|
||||
&& view.getVisibility() == targetVisibility
|
||||
&& view.getAlpha() == 0f) {
|
||||
return false;
|
||||
}
|
||||
hiddenViewStates.computeIfAbsent(view, ignored -> new HiddenViewState(
|
||||
view.getVisibility(),
|
||||
view.getAlpha()));
|
||||
view.setAlpha(0f);
|
||||
view.setVisibility(targetVisibility);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean restoreView(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
HiddenViewState state = hiddenViewStates.remove(view);
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
view.setAlpha(state.alpha);
|
||||
view.setVisibility(state.visibility);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void releaseHiddenViews() {
|
||||
ArrayDeque<View> views = new ArrayDeque<>(hiddenViewStates.keySet());
|
||||
while (!views.isEmpty()) {
|
||||
View view = views.removeFirst();
|
||||
hiddenViewStates.remove(view);
|
||||
view.setAlpha(1f);
|
||||
view.setVisibility(View.VISIBLE);
|
||||
}
|
||||
hiddenViewStates.clear();
|
||||
trackedStatusChipSources.clear();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
}
|
||||
|
||||
private void cleanupLayoutOffRoot(View root) {
|
||||
if (!hiddenViewStates.isEmpty()) {
|
||||
releaseHiddenViews();
|
||||
}
|
||||
trackedStatusChipSources.clear();
|
||||
trackedLockedStatusBarIconSurfaces.clear();
|
||||
trackedUnlockedStockSurfaces.clear();
|
||||
if (!isUnlockedStatusBarRoot(root) || !layoutOffCleanedRoots.add(root)) {
|
||||
return;
|
||||
}
|
||||
rootSizes.remove(root);
|
||||
unlockedIconRenderController.clearAll();
|
||||
ownedIconHostManager.removeUnlockedHosts(root);
|
||||
}
|
||||
|
||||
private boolean isDescendantOf(View child, View ancestor) {
|
||||
if (child == null || ancestor == null) {
|
||||
return false;
|
||||
}
|
||||
View current = child;
|
||||
while (current != null) {
|
||||
if (current == ancestor) {
|
||||
return true;
|
||||
}
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void walkTree(View root, ViewVisitor visitor) {
|
||||
if (root == null || visitor == null) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
visitor.visit(view);
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private UnlockedHosts updateUnlockedOwnedHosts(
|
||||
View root,
|
||||
SceneKey activeScene
|
||||
) {
|
||||
if (!isUnlockedStatusBarRoot(root)) {
|
||||
return null;
|
||||
}
|
||||
if (activeScene != null && activeScene.isUnlocked()) {
|
||||
UnlockedHosts cached = unlockedHostsByRoot.get(root);
|
||||
if (areUnlockedHostsAttached(root, cached)) {
|
||||
return cached;
|
||||
}
|
||||
View statusHost = ownedIconHostManager.ensureUnlockedStatusHost(root);
|
||||
View notificationHost = ownedIconHostManager.ensureUnlockedNotificationHost(root);
|
||||
View clockHost = ownedIconHostManager.ensureUnlockedClockHost(root);
|
||||
View chipHost = ownedIconHostManager.ensureUnlockedChipHost(root);
|
||||
if (statusHost instanceof ViewGroup statusGroup
|
||||
&& notificationHost instanceof ViewGroup notificationGroup
|
||||
&& clockHost instanceof ViewGroup clockGroup
|
||||
&& chipHost instanceof ViewGroup chipGroup) {
|
||||
UnlockedHosts hosts = new UnlockedHosts(clockGroup, chipGroup, statusGroup, notificationGroup);
|
||||
unlockedHostsByRoot.put(root, hosts);
|
||||
return hosts;
|
||||
}
|
||||
unlockedHostsByRoot.remove(root);
|
||||
return null;
|
||||
}
|
||||
unlockedIconRenderController.clearAll();
|
||||
ownedIconHostManager.removeUnlockedHosts(root);
|
||||
unlockedHostsByRoot.remove(root);
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean areUnlockedHostsAttached(View root, UnlockedHosts hosts) {
|
||||
return hosts != null
|
||||
&& hosts.clockHost.getParent() == root
|
||||
&& hosts.chipHost.getParent() == root
|
||||
&& hosts.statusHost.getParent() == root
|
||||
&& hosts.notificationHost.getParent() == root;
|
||||
}
|
||||
|
||||
private boolean isUnlockedStatusBarRoot(View root) {
|
||||
if (root == null) {
|
||||
return false;
|
||||
}
|
||||
String className = root.getClass().getName();
|
||||
return className.contains("PhoneStatusBarView")
|
||||
|| className.contains("StatusBarWindowView");
|
||||
}
|
||||
|
||||
private void updateActiveScene(View root, Context context) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
if (runtimeContext.getModeStateRepository().hasSystemUiStatusBarState()) {
|
||||
return;
|
||||
}
|
||||
if (isAodRoot(root) || (isKeyguardLocked(context) && !isInteractive(context))) {
|
||||
runtimeContext.getModeStateRepository().setAod(NotificationDisplayStyle.UNKNOWN);
|
||||
return;
|
||||
}
|
||||
if (isKeyguardLocked(context)) {
|
||||
runtimeContext.getModeStateRepository().setLockscreen(NotificationDisplayStyle.UNKNOWN);
|
||||
return;
|
||||
}
|
||||
runtimeContext.getModeStateRepository().setUnlocked();
|
||||
}
|
||||
|
||||
private boolean isAodRoot(View root) {
|
||||
return root != null && root.getClass().getName().toLowerCase(java.util.Locale.ROOT).contains("aod");
|
||||
}
|
||||
|
||||
private View castView(Object value) {
|
||||
return value instanceof View ? (View) value : null;
|
||||
}
|
||||
|
||||
private Object firstArg(java.util.List<Object> args) {
|
||||
return args != null && !args.isEmpty() ? args.get(0) : null;
|
||||
}
|
||||
|
||||
private String resolveIdName(View view) {
|
||||
if (view == null) {
|
||||
return "";
|
||||
}
|
||||
int id = view.getId();
|
||||
if (id == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(id);
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isKeyguardLocked(Context context) {
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
KeyguardManager keyguardManager = context.getSystemService(KeyguardManager.class);
|
||||
return keyguardManager != null && keyguardManager.isKeyguardLocked();
|
||||
}
|
||||
|
||||
private boolean isInteractive(Context context) {
|
||||
if (context == null) {
|
||||
return true;
|
||||
}
|
||||
PowerManager powerManager = context.getSystemService(PowerManager.class);
|
||||
return powerManager == null || powerManager.isInteractive();
|
||||
}
|
||||
|
||||
private boolean isLayoutEnabled(Context context) {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(context);
|
||||
return settings.clockEnabled;
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ViewVisitor {
|
||||
void visit(View view);
|
||||
}
|
||||
|
||||
private static final class HiddenViewState {
|
||||
final int visibility;
|
||||
final float alpha;
|
||||
|
||||
HiddenViewState(int visibility, float alpha) {
|
||||
this.visibility = visibility;
|
||||
this.alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
private record UnlockedHosts(
|
||||
ViewGroup clockHost,
|
||||
ViewGroup chipHost,
|
||||
ViewGroup statusHost,
|
||||
ViewGroup notificationHost
|
||||
) {
|
||||
}
|
||||
|
||||
private record RootSize(int width, int height) {
|
||||
}
|
||||
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package se.ajpanton.statusbartweak.runtime.layout;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
|
||||
/**
|
||||
* First layout-engine behavioral slice: clear stock icon surfaces so the new
|
||||
* layout runtime starts from an empty canvas when Layout is enabled.
|
||||
*/
|
||||
public final class StockLayoutCanvasFeature implements RuntimeFeature {
|
||||
|
||||
private static final String PACKAGE_SYSTEM_UI = "com.android.systemui";
|
||||
|
||||
private StockLayoutCanvasController systemUiController;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "stock-layout-canvas";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(RuntimeContext context, XposedModuleInterface.PackageReadyParam param) {
|
||||
if (param == null) {
|
||||
return;
|
||||
}
|
||||
installForPackage(context, param.getPackageName(), param.getClassLoader());
|
||||
}
|
||||
|
||||
private void installForPackage(RuntimeContext context, String packageName, ClassLoader classLoader) {
|
||||
if (PACKAGE_SYSTEM_UI.equals(packageName)) {
|
||||
if (systemUiController == null) {
|
||||
systemUiController = new StockLayoutCanvasController(context);
|
||||
}
|
||||
systemUiController.install(classLoader);
|
||||
}
|
||||
}
|
||||
}
|
||||
+751
@@ -0,0 +1,751 @@
|
||||
package se.ajpanton.statusbartweak.runtime.layoutsolver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
public final class StatusBarLayoutSolver {
|
||||
private static final int MIDDLE_COLLISION_MARGIN_PX = 2;
|
||||
|
||||
private StatusBarLayoutSolver() {
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> ArrayList<T> bandsForPosition(
|
||||
ArrayList<T> bands,
|
||||
LayoutPosition position,
|
||||
ArrayList<LayoutItemKind> itemOrder
|
||||
) {
|
||||
ArrayList<T> result = new ArrayList<>();
|
||||
for (LayoutItemKind kind : itemOrder) {
|
||||
for (T band : bands) {
|
||||
if (band.kind == kind
|
||||
&& band.position == position
|
||||
&& band.splitRegion == SplitRegion.NONE) {
|
||||
result.add(band);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> ArrayList<T> middleBandsForPlacement(
|
||||
ArrayList<T> bands,
|
||||
ArrayList<LayoutItemKind> itemOrder,
|
||||
AnchorSide side
|
||||
) {
|
||||
ArrayList<T> result = new ArrayList<>();
|
||||
SplitRegion sideRegion = side == AnchorSide.LEFT ? SplitRegion.LEFT : SplitRegion.RIGHT;
|
||||
for (T band : bands) {
|
||||
if (band.splitRegion == sideRegion) {
|
||||
result.add(band);
|
||||
}
|
||||
}
|
||||
result.addAll(bandsForPosition(bands, LayoutPosition.MIDDLE, itemOrder));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> RegionPlan<T> planRegions(
|
||||
int rootWidth,
|
||||
LayoutEdges edges,
|
||||
ArrayList<T> bands,
|
||||
ArrayList<LayoutItemKind> itemOrder,
|
||||
Bounds cutoutBounds,
|
||||
int itemPadding
|
||||
) {
|
||||
int leftEdge = Math.max(0, Math.min(rootWidth, edges.left));
|
||||
int rightEdge = Math.max(leftEdge, Math.min(rootWidth, edges.right));
|
||||
Region<T> leftRegion;
|
||||
Region<T> rightRegion = null;
|
||||
if (cutoutBounds == null) {
|
||||
leftRegion = new Region<>(leftEdge, rightEdge);
|
||||
} else {
|
||||
leftRegion = new Region<>(leftEdge, Math.min(cutoutBounds.left, rightEdge));
|
||||
rightRegion = new Region<>(
|
||||
Math.max(cutoutBounds.left + cutoutBounds.width, leftEdge),
|
||||
rightEdge);
|
||||
}
|
||||
|
||||
ArrayList<T> leftBands = bandsForPosition(bands, LayoutPosition.LEFT, itemOrder);
|
||||
ArrayList<T> middleBands = bandsForPosition(bands, LayoutPosition.MIDDLE, itemOrder);
|
||||
ArrayList<T> rightBands = bandsForPosition(bands, LayoutPosition.RIGHT, itemOrder);
|
||||
AnchorSide middleSide = sideForMiddleRegion(rootWidth, middleBands, cutoutBounds, itemPadding);
|
||||
if (rightRegion == null) {
|
||||
addSplitRegionBands(bands, leftRegion, SplitRegion.LEFT);
|
||||
addSplitRegionBands(bands, leftRegion, SplitRegion.RIGHT);
|
||||
leftRegion.bands.addAll(leftBands);
|
||||
leftRegion.bands.addAll(middleBands);
|
||||
Collections.reverse(rightBands);
|
||||
leftRegion.bands.addAll(rightBands);
|
||||
} else {
|
||||
leftRegion.bands.addAll(leftBands);
|
||||
ArrayList<T> middleRegionBands = new ArrayList<>(middleBands);
|
||||
Collections.reverse(middleRegionBands);
|
||||
if (middleSide == AnchorSide.LEFT) {
|
||||
leftRegion.bands.addAll(middleRegionBands);
|
||||
}
|
||||
addSplitRegionBands(bands, leftRegion, SplitRegion.LEFT);
|
||||
|
||||
addSplitRegionBands(bands, rightRegion, SplitRegion.RIGHT);
|
||||
if (middleSide == AnchorSide.RIGHT) {
|
||||
rightRegion.bands.addAll(middleRegionBands);
|
||||
}
|
||||
Collections.reverse(rightBands);
|
||||
rightRegion.bands.addAll(rightBands);
|
||||
}
|
||||
|
||||
ArrayList<Region<T>> regions = new ArrayList<>();
|
||||
regions.add(leftRegion);
|
||||
if (rightRegion != null) {
|
||||
regions.add(rightRegion);
|
||||
}
|
||||
return new RegionPlan<>(regions, middleSide);
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> void addSplitRegionBands(
|
||||
ArrayList<T> source,
|
||||
Region<T> target,
|
||||
SplitRegion region
|
||||
) {
|
||||
if (source == null || target == null || region == SplitRegion.NONE) {
|
||||
return;
|
||||
}
|
||||
for (T band : source) {
|
||||
if (band.splitRegion == region) {
|
||||
target.bands.add(band);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> AnchorSide sideForMiddleRegion(
|
||||
int rootWidth,
|
||||
ArrayList<T> middleBands,
|
||||
Bounds cutoutBounds,
|
||||
int itemPadding
|
||||
) {
|
||||
int middle = rootWidth / 2;
|
||||
if (middleBands == null || middleBands.isEmpty()) {
|
||||
return cutoutBounds == null || cutoutBounds.left + cutoutBounds.width / 2 < middle
|
||||
? AnchorSide.RIGHT
|
||||
: AnchorSide.LEFT;
|
||||
}
|
||||
T firstBand = middleBands.get(0);
|
||||
int width = packedWidth(middleBands, itemPadding);
|
||||
int cameraDistance = distanceFromMiddle(cutoutBounds, middle);
|
||||
boolean centered = cutoutBounds != null
|
||||
&& cutoutBounds.left <= middle + MIDDLE_COLLISION_MARGIN_PX
|
||||
&& cutoutBounds.left + cutoutBounds.width >= middle - MIDDLE_COLLISION_MARGIN_PX;
|
||||
if (cameraDistance < width / 2 && (centered || !firstBand.middleAutoNearestSide)) {
|
||||
return firstBand.middleSide;
|
||||
}
|
||||
if (cutoutBounds == null || cutoutBounds.left + cutoutBounds.width / 2 < middle) {
|
||||
return AnchorSide.RIGHT;
|
||||
}
|
||||
return AnchorSide.LEFT;
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> Anchor anchorForMiddle(
|
||||
int rootWidth,
|
||||
ArrayList<T> bands,
|
||||
ArrayList<PlacedBand> placedBands,
|
||||
Bounds cutoutBounds,
|
||||
AnchorSide side,
|
||||
int itemPadding
|
||||
) {
|
||||
int middle = rootWidth / 2;
|
||||
int width = packedWidth(bands, itemPadding);
|
||||
if (coversMiddle(cutoutBounds, middle)) {
|
||||
return obstacleAnchor(cutoutBounds, side, AnchorKind.WALL);
|
||||
}
|
||||
Bounds nearestItem = nearestPlacedBand(placedBands, middle);
|
||||
int cameraDistance = distanceFromMiddle(cutoutBounds, middle);
|
||||
int itemDistance = distanceFromMiddle(nearestItem, middle);
|
||||
if (Math.min(cameraDistance, itemDistance) < width / 2) {
|
||||
if (cameraDistance < itemDistance) {
|
||||
return obstacleAnchor(cutoutBounds, side, AnchorKind.WALL);
|
||||
}
|
||||
return obstacleAnchor(nearestItem, side, AnchorKind.ITEM);
|
||||
}
|
||||
return middleAnchor(rootWidth, width, side);
|
||||
}
|
||||
|
||||
private static Anchor obstacleAnchor(Bounds obstacle, AnchorSide side, AnchorKind kind) {
|
||||
if (obstacle == null) {
|
||||
return new Anchor(0, side, kind);
|
||||
}
|
||||
int anchorPoint = side == AnchorSide.RIGHT ? obstacle.left + obstacle.width : obstacle.left;
|
||||
return new Anchor(anchorPoint, side, kind);
|
||||
}
|
||||
|
||||
private static Anchor middleAnchor(int rootWidth, int width, AnchorSide side) {
|
||||
int middle = rootWidth / 2;
|
||||
if (side == AnchorSide.RIGHT) {
|
||||
return new Anchor(middle - width / 2, AnchorSide.RIGHT, AnchorKind.CENTER);
|
||||
}
|
||||
return new Anchor(middle + width / 2, AnchorSide.LEFT, AnchorKind.CENTER);
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> void placeStackedBands(
|
||||
ArrayList<T> bands,
|
||||
Anchor anchor,
|
||||
ArrayList<PlacedBand> placedBands,
|
||||
String sortOrder,
|
||||
boolean avoidPlacedBands,
|
||||
int itemPadding
|
||||
) {
|
||||
if (bands == null || bands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (T band : bands) {
|
||||
band.prepareForSide(anchor.side, sortOrder);
|
||||
}
|
||||
ArrayList<StackedBand<T>> stackedBands = stackBands(bands, itemPadding);
|
||||
int shift = avoidPlacedBands ? middleShift(stackedBands, anchor, placedBands, itemPadding) : 0;
|
||||
for (StackedBand<T> stackedBand : stackedBands) {
|
||||
T band = stackedBand.band;
|
||||
int wallDotSlack = anchor.kind == AnchorKind.WALL
|
||||
&& stackedBand.left == 0
|
||||
&& shift == 0
|
||||
? band.wallDotSlack(anchor.side)
|
||||
: 0;
|
||||
int left = stackedLeft(stackedBand, anchor, shift, wallDotSlack, itemPadding);
|
||||
band.place(left);
|
||||
ArrayList<Bounds> bounds = band.placedCollisionBounds();
|
||||
if (!bounds.isEmpty()) {
|
||||
placedBands.add(new PlacedBand(band.kind, band.position, bounds));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> int middleShift(
|
||||
ArrayList<StackedBand<T>> stackedBands,
|
||||
Anchor anchor,
|
||||
ArrayList<PlacedBand> placedBands,
|
||||
int itemPadding
|
||||
) {
|
||||
int shift = 0;
|
||||
for (StackedBand<T> stackedBand : stackedBands) {
|
||||
ArrayList<Bounds> bounds = boundsForStackedBand(stackedBand, anchor, shift, itemPadding);
|
||||
int leftOverstep = overstepFromPlacedBands(bounds, placedBands, LayoutPosition.LEFT, itemPadding);
|
||||
if (leftOverstep > 0) {
|
||||
shift += leftOverstep;
|
||||
continue;
|
||||
}
|
||||
int rightOverstep = overstepFromPlacedBands(bounds, placedBands, LayoutPosition.RIGHT, itemPadding);
|
||||
if (rightOverstep > 0) {
|
||||
shift -= rightOverstep;
|
||||
}
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> ArrayList<Bounds> boundsForStackedBand(
|
||||
StackedBand<T> stackedBand,
|
||||
Anchor anchor,
|
||||
int shift,
|
||||
int itemPadding
|
||||
) {
|
||||
ArrayList<Bounds> result = new ArrayList<>();
|
||||
int left = stackedLeft(stackedBand, anchor, shift, 0, itemPadding);
|
||||
for (Bounds box : stackedBand.boxes) {
|
||||
result.add(new Bounds(left + box.left, box.top, box.width, box.height));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> int stackedLeft(
|
||||
StackedBand<T> stackedBand,
|
||||
Anchor anchor,
|
||||
int shift,
|
||||
int wallDotSlack,
|
||||
int itemPadding
|
||||
) {
|
||||
T band = stackedBand.band;
|
||||
int anchorGap = anchor.kind == AnchorKind.ITEM ? Math.max(0, itemPadding) : 0;
|
||||
if (anchor.side == AnchorSide.RIGHT) {
|
||||
return anchor.point + anchorGap + stackedBand.left + shift - wallDotSlack;
|
||||
}
|
||||
return anchor.point - anchorGap - stackedBand.left - band.width() + shift + wallDotSlack;
|
||||
}
|
||||
|
||||
private static int overstepFromPlacedBands(
|
||||
ArrayList<Bounds> bounds,
|
||||
ArrayList<PlacedBand> placedBands,
|
||||
LayoutPosition position,
|
||||
int itemPadding
|
||||
) {
|
||||
int overstep = 0;
|
||||
if (bounds == null || placedBands == null) {
|
||||
return overstep;
|
||||
}
|
||||
for (Bounds boundsItem : bounds) {
|
||||
if (boundsItem == null) {
|
||||
continue;
|
||||
}
|
||||
for (PlacedBand placedBand : placedBands) {
|
||||
if (placedBand.position != position) {
|
||||
continue;
|
||||
}
|
||||
for (Bounds obstacle : placedBand.bounds) {
|
||||
if (obstacle == null
|
||||
|| !verticalRangesOverlap(
|
||||
boundsItem.top,
|
||||
boundsItem.height,
|
||||
obstacle.top,
|
||||
obstacle.height)) {
|
||||
continue;
|
||||
}
|
||||
overstep = Math.max(overstep, horizontalOverstep(boundsItem, obstacle, position, itemPadding));
|
||||
}
|
||||
}
|
||||
}
|
||||
return overstep;
|
||||
}
|
||||
|
||||
private static int horizontalOverstep(
|
||||
Bounds item,
|
||||
Bounds obstacle,
|
||||
LayoutPosition obstaclePosition,
|
||||
int itemPadding
|
||||
) {
|
||||
int gap = Math.max(0, itemPadding);
|
||||
if (obstaclePosition == LayoutPosition.LEFT) {
|
||||
return Math.max(0, obstacle.left + blockingWidth(obstacle) + gap - item.left);
|
||||
}
|
||||
if (obstaclePosition == LayoutPosition.RIGHT) {
|
||||
return Math.max(0, item.left + blockingWidth(item) + gap - obstacle.left);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> void truncateToFit(
|
||||
ArrayList<T> bands,
|
||||
int freeSpace,
|
||||
AnchorSide middleSide,
|
||||
String sortOrder,
|
||||
int itemPadding
|
||||
) {
|
||||
int target = Math.max(0, freeSpace);
|
||||
for (T band : bands) {
|
||||
band.prepareForSide(truncationAnchorSide(band, middleSide), sortOrder);
|
||||
}
|
||||
int guard = 64;
|
||||
while (packedWidthForTruncation(bands, middleSide, itemPadding) > target && guard-- > 0) {
|
||||
if (shrinkToReducePackedWidth(bands, LayoutItemKind.NOTIFICATIONS, middleSide, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
if (shrinkToReducePackedWidth(bands, LayoutItemKind.STATUS, middleSide, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
if (clipToReducePackedWidth(bands, LayoutItemKind.CHIP, middleSide, target, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
if (removeDotToReducePackedWidth(bands, LayoutItemKind.NOTIFICATIONS, middleSide, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
if (removeDotToReducePackedWidth(bands, LayoutItemKind.STATUS, middleSide, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
if (clipToReducePackedWidth(bands, LayoutItemKind.CLOCK, middleSide, target, itemPadding)) {
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> boolean shrinkToReducePackedWidth(
|
||||
ArrayList<T> bands,
|
||||
LayoutItemKind kind,
|
||||
AnchorSide middleSide,
|
||||
int itemPadding
|
||||
) {
|
||||
return mutateBandToReducePackedWidth(
|
||||
bands,
|
||||
kind,
|
||||
middleSide,
|
||||
itemPadding,
|
||||
LayoutBand::shrinkOneIcon,
|
||||
false,
|
||||
true);
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> boolean removeDotToReducePackedWidth(
|
||||
ArrayList<T> bands,
|
||||
LayoutItemKind kind,
|
||||
AnchorSide middleSide,
|
||||
int itemPadding
|
||||
) {
|
||||
return mutateBandToReducePackedWidth(
|
||||
bands,
|
||||
kind,
|
||||
middleSide,
|
||||
itemPadding,
|
||||
LayoutBand::removeOverflowDot,
|
||||
true,
|
||||
false);
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> boolean mutateBandToReducePackedWidth(
|
||||
ArrayList<T> bands,
|
||||
LayoutItemKind kind,
|
||||
AnchorSide middleSide,
|
||||
int itemPadding,
|
||||
BandMutation mutation,
|
||||
boolean acceptEqualWidth,
|
||||
boolean acceptCreatedDot
|
||||
) {
|
||||
T band = findBand(bands, kind);
|
||||
if (band == null) {
|
||||
return false;
|
||||
}
|
||||
int before = packedWidthForTruncation(bands, middleSide, itemPadding);
|
||||
Object state = band.captureState();
|
||||
if (!mutation.apply(band)) {
|
||||
return false;
|
||||
}
|
||||
int after = packedWidthForTruncation(bands, middleSide, itemPadding);
|
||||
if (after < before
|
||||
|| (acceptEqualWidth && after == before)
|
||||
|| (acceptCreatedDot && band.createdOverflowDotSince(state))) {
|
||||
return true;
|
||||
}
|
||||
band.restoreState(state);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> boolean clipToReducePackedWidth(
|
||||
ArrayList<T> bands,
|
||||
LayoutItemKind kind,
|
||||
AnchorSide middleSide,
|
||||
int target,
|
||||
int itemPadding
|
||||
) {
|
||||
T band = findBand(bands, kind);
|
||||
if (band == null || !band.canClipContinuously()) {
|
||||
return false;
|
||||
}
|
||||
int before = packedWidthForTruncation(bands, middleSide, itemPadding);
|
||||
int amount = Math.max(1, before - target);
|
||||
if (!band.clipContinuous(truncationAnchorSide(band, middleSide), amount)) {
|
||||
return false;
|
||||
}
|
||||
return packedWidthForTruncation(bands, middleSide, itemPadding) <= before;
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> int packedWidth(ArrayList<T> bands) {
|
||||
return packedWidth(bands, 0);
|
||||
}
|
||||
|
||||
public static <T extends LayoutBand> int packedWidth(ArrayList<T> bands, int itemPadding) {
|
||||
return stackBounds(stackBands(bands, itemPadding)).width;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> int packedWidthForTruncation(
|
||||
ArrayList<T> bands,
|
||||
AnchorSide middleSide,
|
||||
int itemPadding
|
||||
) {
|
||||
ArrayList<StackedBand<T>> stackedBands = stackBands(bands, itemPadding);
|
||||
Bounds bounds = stackBounds(stackedBands);
|
||||
int width = bounds.width;
|
||||
if (width <= 0 || stackedBands.isEmpty()) {
|
||||
return width;
|
||||
}
|
||||
int leftEdge = bounds.left;
|
||||
int rightEdge = bounds.left + bounds.width;
|
||||
Integer leftSlack = null;
|
||||
Integer rightSlack = null;
|
||||
for (StackedBand<T> stackedBand : stackedBands) {
|
||||
T band = stackedBand.band;
|
||||
Bounds bandBounds = shiftedBounds(stackedBand.boxes, stackedBand.left);
|
||||
int bandLeft = bandBounds != null ? bandBounds.left : stackedBand.left;
|
||||
int bandRight = bandBounds != null ? bandBounds.left + bandBounds.width : bandLeft;
|
||||
if (bandLeft == leftEdge) {
|
||||
int slack = band.wallDotSlack(AnchorSide.RIGHT);
|
||||
leftSlack = leftSlack == null ? slack : Math.min(leftSlack, slack);
|
||||
}
|
||||
if (bandRight == rightEdge) {
|
||||
int slack = band.wallDotSlack(AnchorSide.LEFT);
|
||||
rightSlack = rightSlack == null ? slack : Math.min(rightSlack, slack);
|
||||
}
|
||||
}
|
||||
return Math.max(0, width - (leftSlack != null ? leftSlack : 0) - (rightSlack != null ? rightSlack : 0));
|
||||
}
|
||||
|
||||
private static AnchorSide truncationAnchorSide(LayoutBand band, AnchorSide middleSide) {
|
||||
if (band.splitRegion == SplitRegion.LEFT) {
|
||||
return AnchorSide.LEFT;
|
||||
}
|
||||
if (band.splitRegion == SplitRegion.RIGHT) {
|
||||
return AnchorSide.RIGHT;
|
||||
}
|
||||
if (band.position == LayoutPosition.LEFT) {
|
||||
return AnchorSide.RIGHT;
|
||||
}
|
||||
if (band.position == LayoutPosition.RIGHT) {
|
||||
return AnchorSide.LEFT;
|
||||
}
|
||||
return middleSide;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> ArrayList<StackedBand<T>> stackBands(
|
||||
ArrayList<T> bands,
|
||||
int itemPadding
|
||||
) {
|
||||
ArrayList<StackedBand<T>> stackedBands = new ArrayList<>();
|
||||
if (bands == null) {
|
||||
return stackedBands;
|
||||
}
|
||||
int gap = Math.max(0, itemPadding);
|
||||
for (T band : bands) {
|
||||
int left = 0;
|
||||
ArrayList<Bounds> boxes = band.collisionBoxes();
|
||||
for (StackedBand<T> stackedBand : stackedBands) {
|
||||
for (Bounds box : boxes) {
|
||||
for (Bounds placedBox : stackedBand.boxes) {
|
||||
if (verticalRangesOverlap(box.top, box.height, placedBox.top, placedBox.height)) {
|
||||
left = Math.max(
|
||||
left,
|
||||
stackedBand.left + placedBox.left + blockingWidth(placedBox) + gap - box.left);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stackedBands.add(new StackedBand<>(band, left, boxes));
|
||||
}
|
||||
return stackedBands;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> Bounds stackBounds(ArrayList<StackedBand<T>> stackedBands) {
|
||||
Bounds bounds = new Bounds(0, 0, 0, 0);
|
||||
if (stackedBands == null || stackedBands.isEmpty()) {
|
||||
return bounds;
|
||||
}
|
||||
Bounds union = null;
|
||||
for (StackedBand<T> stackedBand : stackedBands) {
|
||||
Bounds bandBounds = shiftedBounds(stackedBand.boxes, stackedBand.left);
|
||||
if (bandBounds != null) {
|
||||
union = union == null ? bandBounds : union.union(bandBounds);
|
||||
}
|
||||
}
|
||||
return union != null ? union : bounds;
|
||||
}
|
||||
|
||||
private static Bounds shiftedBounds(ArrayList<Bounds> boxes, int left) {
|
||||
Bounds union = null;
|
||||
if (boxes == null) {
|
||||
return null;
|
||||
}
|
||||
for (Bounds box : boxes) {
|
||||
Bounds shifted = new Bounds(left + box.left, box.top, blockingWidth(box), box.height);
|
||||
union = union == null ? shifted : union.union(shifted);
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
private static boolean verticalRangesOverlap(int topA, int heightA, int topB, int heightB) {
|
||||
return topA < topB + heightB && topB < topA + heightA;
|
||||
}
|
||||
|
||||
private static int blockingWidth(Bounds bounds) {
|
||||
return bounds != null && bounds.height > 0 ? Math.max(1, bounds.width) : 0;
|
||||
}
|
||||
|
||||
private static <T extends LayoutBand> T findBand(ArrayList<T> bands, LayoutItemKind kind) {
|
||||
for (T band : bands) {
|
||||
if (band.kind == kind) {
|
||||
return band;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Bounds nearestPlacedBand(ArrayList<PlacedBand> placedBands, int middle) {
|
||||
Bounds nearest = null;
|
||||
int nearestDistance = Integer.MAX_VALUE;
|
||||
for (PlacedBand placedBand : placedBands) {
|
||||
for (Bounds bounds : placedBand.bounds) {
|
||||
int distance = distanceFromMiddle(bounds, middle);
|
||||
if (distance < nearestDistance) {
|
||||
nearest = bounds;
|
||||
nearestDistance = distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
public static int distanceFromMiddle(Bounds bounds, int middle) {
|
||||
if (bounds == null) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
if (bounds.left <= middle && bounds.left + bounds.width >= middle) {
|
||||
return 0;
|
||||
}
|
||||
return Math.min(Math.abs(middle - bounds.left), Math.abs(middle - (bounds.left + bounds.width)));
|
||||
}
|
||||
|
||||
public static boolean coversMiddle(Bounds bounds, int middle) {
|
||||
return bounds != null
|
||||
&& bounds.left <= middle + MIDDLE_COLLISION_MARGIN_PX
|
||||
&& bounds.left + bounds.width >= middle - MIDDLE_COLLISION_MARGIN_PX;
|
||||
}
|
||||
|
||||
public abstract static class LayoutBand {
|
||||
public final LayoutItemKind kind;
|
||||
public final LayoutPosition position;
|
||||
public final boolean middleAutoNearestSide;
|
||||
public final AnchorSide middleSide;
|
||||
public final SplitRegion splitRegion;
|
||||
|
||||
protected LayoutBand(
|
||||
LayoutItemKind kind,
|
||||
LayoutPosition position,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide middleSide,
|
||||
SplitRegion splitRegion
|
||||
) {
|
||||
this.kind = kind;
|
||||
this.position = position;
|
||||
this.middleAutoNearestSide = middleAutoNearestSide;
|
||||
this.middleSide = middleSide;
|
||||
this.splitRegion = splitRegion != null ? splitRegion : SplitRegion.NONE;
|
||||
}
|
||||
|
||||
protected abstract void prepareForSide(AnchorSide side, String sortOrder);
|
||||
|
||||
protected abstract int width();
|
||||
|
||||
protected abstract ArrayList<Bounds> collisionBoxes();
|
||||
|
||||
protected abstract void place(int left);
|
||||
|
||||
protected abstract ArrayList<Bounds> placedCollisionBounds();
|
||||
|
||||
protected int wallDotSlack(AnchorSide side) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected boolean shrinkOneIcon() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean removeOverflowDot() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Object captureState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void restoreState(Object state) {
|
||||
}
|
||||
|
||||
protected boolean createdOverflowDotSince(Object state) {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean canClipContinuously() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean clipContinuous(AnchorSide wallSide, int amountPx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private interface BandMutation {
|
||||
boolean apply(LayoutBand band);
|
||||
}
|
||||
|
||||
private record StackedBand<T extends LayoutBand>(
|
||||
T band,
|
||||
int left,
|
||||
ArrayList<Bounds> boxes
|
||||
) {
|
||||
}
|
||||
|
||||
public enum LayoutItemKind {
|
||||
CLOCK,
|
||||
CHIP,
|
||||
NOTIFICATIONS,
|
||||
STATUS
|
||||
}
|
||||
|
||||
public enum LayoutPosition {
|
||||
LEFT,
|
||||
MIDDLE,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
public enum AnchorSide {
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
public enum AnchorKind {
|
||||
WALL,
|
||||
ITEM,
|
||||
CENTER
|
||||
}
|
||||
|
||||
public enum SplitRegion {
|
||||
NONE,
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
public record Anchor(int point, AnchorSide side, AnchorKind kind) {
|
||||
}
|
||||
|
||||
public static final class Bounds {
|
||||
public final int left;
|
||||
public final int top;
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
public Bounds(int left, int top, int width, int height) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public Bounds union(Bounds other) {
|
||||
int right = Math.max(left + width, other.left + other.width);
|
||||
int bottom = Math.max(top + height, other.top + other.height);
|
||||
int newLeft = Math.min(left, other.left);
|
||||
int newTop = Math.min(top, other.top);
|
||||
return new Bounds(newLeft, newTop, right - newLeft, bottom - newTop);
|
||||
}
|
||||
}
|
||||
|
||||
public record PlacedBand(
|
||||
LayoutItemKind kind,
|
||||
LayoutPosition position,
|
||||
ArrayList<Bounds> bounds
|
||||
) {
|
||||
}
|
||||
|
||||
public record RegionPlan<T extends LayoutBand>(
|
||||
ArrayList<Region<T>> regions,
|
||||
AnchorSide middleSide
|
||||
) {
|
||||
}
|
||||
|
||||
public record LayoutEdges(int left, int right) {
|
||||
}
|
||||
|
||||
public static final class Region<T extends LayoutBand> {
|
||||
public final int left;
|
||||
public final int right;
|
||||
public final ArrayList<T> bands = new ArrayList<>();
|
||||
|
||||
Region(int left, int right) {
|
||||
this.left = left;
|
||||
this.right = Math.max(left, right);
|
||||
}
|
||||
|
||||
public int width() {
|
||||
return Math.max(0, right - left);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Central source of truth for the current scene.
|
||||
*
|
||||
* This is intentionally small at the reset stage: it only tracks the active
|
||||
* scene and whether the custom layout runtime should currently output anything.
|
||||
* Hook-driven updates can be wired in later without changing the rest of the
|
||||
* runtime API.
|
||||
*/
|
||||
public final class ModeStateRepository {
|
||||
|
||||
private SceneKey activeScene = SceneKey.unlocked();
|
||||
private boolean systemUiStatusBarStateKnown;
|
||||
private boolean layoutEnabled;
|
||||
private final List<LayoutEnabledListener> layoutEnabledListeners = new ArrayList<>();
|
||||
|
||||
public synchronized SceneKey getActiveScene() {
|
||||
return activeScene;
|
||||
}
|
||||
|
||||
public synchronized boolean hasSystemUiStatusBarState() {
|
||||
return systemUiStatusBarStateKnown;
|
||||
}
|
||||
|
||||
public synchronized boolean isLayoutEnabled() {
|
||||
return layoutEnabled;
|
||||
}
|
||||
|
||||
public void setLayoutEnabled(boolean enabled) {
|
||||
List<LayoutEnabledListener> listeners;
|
||||
synchronized (this) {
|
||||
if (layoutEnabled == enabled) {
|
||||
return;
|
||||
}
|
||||
layoutEnabled = enabled;
|
||||
listeners = new ArrayList<>(layoutEnabledListeners);
|
||||
}
|
||||
for (LayoutEnabledListener listener : listeners) {
|
||||
listener.onLayoutEnabledChanged(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void addLayoutEnabledListener(LayoutEnabledListener listener) {
|
||||
if (listener != null && !layoutEnabledListeners.contains(listener)) {
|
||||
layoutEnabledListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void setUnlocked() {
|
||||
activeScene = SceneKey.unlocked();
|
||||
}
|
||||
|
||||
public synchronized void setLockscreen(NotificationDisplayStyle style) {
|
||||
activeScene = SceneKey.lockscreen(requireDisplayStyle(style));
|
||||
}
|
||||
|
||||
public synchronized void setAod(NotificationDisplayStyle style) {
|
||||
activeScene = SceneKey.aod(requireDisplayStyle(style));
|
||||
}
|
||||
|
||||
public synchronized void setScene(SceneKey sceneKey) {
|
||||
activeScene = Objects.requireNonNull(sceneKey, "sceneKey");
|
||||
}
|
||||
|
||||
public synchronized void setSystemUiStatusBarState(int state, boolean dozing) {
|
||||
systemUiStatusBarStateKnown = true;
|
||||
if (dozing) {
|
||||
activeScene = SceneKey.aod(NotificationDisplayStyle.UNKNOWN);
|
||||
} else if (state == 0) {
|
||||
activeScene = SceneKey.unlocked();
|
||||
} else {
|
||||
activeScene = SceneKey.lockscreen(NotificationDisplayStyle.UNKNOWN);
|
||||
}
|
||||
}
|
||||
|
||||
private static NotificationDisplayStyle requireDisplayStyle(NotificationDisplayStyle style) {
|
||||
if (style == null || style == NotificationDisplayStyle.NONE) {
|
||||
throw new IllegalArgumentException("Lockscreen/AOD scenes cannot use NONE");
|
||||
}
|
||||
return style;
|
||||
}
|
||||
|
||||
public interface LayoutEnabledListener {
|
||||
void onLayoutEnabledChanged(boolean enabled);
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
/**
|
||||
* How Android is currently representing notifications for a scene.
|
||||
*
|
||||
* Unlocked mode does not use the lockscreen/AOD notification style setting, so
|
||||
* it should use {@link #NONE}.
|
||||
*/
|
||||
public enum NotificationDisplayStyle {
|
||||
UNKNOWN,
|
||||
NONE,
|
||||
ICONS,
|
||||
CARDS,
|
||||
DOT
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Stable scene identity for the new runtime pipeline.
|
||||
*
|
||||
* Cards/icons/dot are notification display styles within lockscreen/AOD, not
|
||||
* separate top-level modes. Unlocked scenes always use {@link
|
||||
* NotificationDisplayStyle#NONE}.
|
||||
*/
|
||||
public record SceneKey(
|
||||
SurfaceMode surfaceMode,
|
||||
NotificationDisplayStyle notificationDisplayStyle
|
||||
) {
|
||||
|
||||
public SceneKey {
|
||||
surfaceMode = Objects.requireNonNull(surfaceMode, "surfaceMode");
|
||||
notificationDisplayStyle = Objects.requireNonNull(
|
||||
notificationDisplayStyle,
|
||||
"notificationDisplayStyle"
|
||||
);
|
||||
if (surfaceMode == SurfaceMode.UNLOCKED
|
||||
&& notificationDisplayStyle != NotificationDisplayStyle.NONE) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unlocked scenes must use NotificationDisplayStyle.NONE"
|
||||
);
|
||||
}
|
||||
if (surfaceMode != SurfaceMode.UNLOCKED
|
||||
&& notificationDisplayStyle == NotificationDisplayStyle.NONE) {
|
||||
throw new IllegalArgumentException(
|
||||
"Lockscreen/AOD scenes cannot use NotificationDisplayStyle.NONE"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static SceneKey unlocked() {
|
||||
return new SceneKey(SurfaceMode.UNLOCKED, NotificationDisplayStyle.NONE);
|
||||
}
|
||||
|
||||
public static SceneKey lockscreen(NotificationDisplayStyle style) {
|
||||
return new SceneKey(SurfaceMode.LOCKSCREEN, requireStyle(style));
|
||||
}
|
||||
|
||||
public static SceneKey aod(NotificationDisplayStyle style) {
|
||||
return new SceneKey(SurfaceMode.AOD, requireStyle(style));
|
||||
}
|
||||
|
||||
public boolean isUnlocked() {
|
||||
return surfaceMode == SurfaceMode.UNLOCKED;
|
||||
}
|
||||
|
||||
public boolean isLockscreen() {
|
||||
return surfaceMode == SurfaceMode.LOCKSCREEN;
|
||||
}
|
||||
|
||||
public boolean isAod() {
|
||||
return surfaceMode == SurfaceMode.AOD;
|
||||
}
|
||||
|
||||
private static NotificationDisplayStyle requireStyle(NotificationDisplayStyle style) {
|
||||
if (style == null || style == NotificationDisplayStyle.NONE) {
|
||||
throw new IllegalArgumentException("A non-NONE notification display style is required");
|
||||
}
|
||||
return style;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
/**
|
||||
* High-level SystemUI surface we are rendering into.
|
||||
*/
|
||||
public enum SurfaceMode {
|
||||
UNLOCKED,
|
||||
LOCKSCREEN,
|
||||
AOD
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package se.ajpanton.statusbartweak.runtime.mode;
|
||||
|
||||
import io.github.libxposed.api.XposedModuleInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeContext;
|
||||
import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.XposedHookSupport;
|
||||
|
||||
/**
|
||||
* Feeds the runtime scene from SystemUI's own status-bar state controller.
|
||||
*
|
||||
* KeyguardManager lags during unlock/drawer transitions; StatusBarState is the
|
||||
* state SystemUI itself uses for SHADE / KEYGUARD / SHADE_LOCKED decisions.
|
||||
*/
|
||||
public final class SystemUiStatusBarStateFeature implements RuntimeFeature {
|
||||
|
||||
private static final String PACKAGE_SYSTEM_UI = "com.android.systemui";
|
||||
private static final String CLASS_STATUS_BAR_STATE_CONTROLLER =
|
||||
"com.android.systemui.statusbar.StatusBarStateControllerImpl";
|
||||
|
||||
private boolean installed;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "systemui-status-bar-state";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPackageReady(RuntimeContext context, XposedModuleInterface.PackageReadyParam param) {
|
||||
if (param == null || installed || !PACKAGE_SYSTEM_UI.equals(param.getPackageName())) {
|
||||
return;
|
||||
}
|
||||
Class<?> controllerClass = ReflectionSupport.findClassIfExists(
|
||||
CLASS_STATUS_BAR_STATE_CONTROLLER,
|
||||
param.getClassLoader());
|
||||
if (controllerClass == null) {
|
||||
return;
|
||||
}
|
||||
XposedHookSupport.hookAllConstructors(context.getFramework(), controllerClass, chain -> {
|
||||
Object result = chain.proceed();
|
||||
updateScene(context, chain.getThisObject());
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(context.getFramework(), controllerClass, "setState", chain -> {
|
||||
Object result = chain.proceed();
|
||||
updateScene(context, chain.getThisObject());
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(
|
||||
context.getFramework(),
|
||||
controllerClass,
|
||||
"setDozeAmountInternal",
|
||||
chain -> {
|
||||
Object result = chain.proceed();
|
||||
updateScene(context, chain.getThisObject());
|
||||
return result;
|
||||
});
|
||||
installed = true;
|
||||
}
|
||||
|
||||
private void updateScene(RuntimeContext context, Object controller) {
|
||||
if (context == null || controller == null) {
|
||||
return;
|
||||
}
|
||||
Object rawState = ReflectionSupport.getFieldValue(controller, "mState");
|
||||
int state = rawState instanceof Integer integer ? integer : 0;
|
||||
boolean dozing = ReflectionSupport.getBooleanField(controller, "mIsDozing", false);
|
||||
context.getModeStateRepository().setSystemUiStatusBarState(state, dozing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition;
|
||||
|
||||
final class ClockLayout {
|
||||
private static final int MIDDLE_COLLISION_MARGIN_PX = 2;
|
||||
|
||||
private final TextView source;
|
||||
private final ClockLayoutTextFactory.Model model;
|
||||
private final ArrayList<ClockRow> rows;
|
||||
private final ArrayList<MeasuredClockRow> measuredRows;
|
||||
private final int top;
|
||||
private final boolean fixedPlacement;
|
||||
private final int width;
|
||||
private final int height;
|
||||
private int placedLeft;
|
||||
|
||||
private ClockLayout(
|
||||
TextView source,
|
||||
ClockLayoutTextFactory.Model model,
|
||||
ArrayList<ClockRow> rows,
|
||||
ArrayList<MeasuredClockRow> measuredRows,
|
||||
int top,
|
||||
int placedLeft,
|
||||
boolean fixedPlacement,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
this.source = source;
|
||||
this.model = model;
|
||||
this.rows = rows;
|
||||
this.measuredRows = measuredRows;
|
||||
this.top = top;
|
||||
this.placedLeft = placedLeft;
|
||||
this.fixedPlacement = fixedPlacement;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
static ClockLayout measure(
|
||||
TextView source,
|
||||
ClockLayoutTextFactory.Model model,
|
||||
LayoutPosition position,
|
||||
int sourceBaselineY,
|
||||
int rootWidth,
|
||||
Bounds cutoutBounds,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide middleSide
|
||||
) {
|
||||
if (source == null || model == null || model.rows.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
TextView probe = new TextView(source.getContext());
|
||||
probe.setLayoutParams(new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
ClockProxyRenderer.copyTextStyleIfChanged(source, probe);
|
||||
ArrayList<MeasuredClockRow> measuredRows = new ArrayList<>();
|
||||
for (int i = 0; i < model.rows.size(); i++) {
|
||||
ClockLayoutTextFactory.Row row = model.rows.get(i);
|
||||
MeasuredText text = measureText(probe, row.text);
|
||||
MeasuredText leftText = measureText(probe, row.leftText);
|
||||
MeasuredText rightText = measureText(probe, row.rightText);
|
||||
int rightPipe = text.width - rightText.width;
|
||||
int pipePosition = row.pipeCount > 1
|
||||
? Math.round((leftText.width + rightPipe) / 2f)
|
||||
: leftText.width;
|
||||
measuredRows.add(new MeasuredClockRow(
|
||||
row,
|
||||
text.width,
|
||||
text.height,
|
||||
text.baseline,
|
||||
row.pipeCount > 0 ? pipePosition : 0,
|
||||
0,
|
||||
leftText,
|
||||
rightText,
|
||||
0,
|
||||
i));
|
||||
}
|
||||
measuredRows = positionRowsFromBaseline(measuredRows, sourceBaselineY, source);
|
||||
|
||||
RowAlignment alignment = alignRows(measuredRows, position);
|
||||
ArrayList<ClockRow> rows = alignment.rows;
|
||||
Bounds bounds = null;
|
||||
for (ClockRow row : rows) {
|
||||
Bounds rowBounds = new Bounds(row.x, row.y, row.width, row.height);
|
||||
bounds = bounds == null ? rowBounds : bounds.union(rowBounds);
|
||||
}
|
||||
|
||||
if (bounds == null || alignment.width <= 0 || bounds.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
int top = bounds.top;
|
||||
ArrayList<ClockRow> normalized = new ArrayList<>();
|
||||
for (ClockRow row : rows) {
|
||||
normalized.add(new ClockRow(
|
||||
row.text,
|
||||
row.x,
|
||||
row.y - bounds.top,
|
||||
row.width,
|
||||
row.height,
|
||||
0,
|
||||
row.rowIndex,
|
||||
row.segment));
|
||||
}
|
||||
return new ClockLayout(
|
||||
source,
|
||||
model,
|
||||
normalized,
|
||||
measuredRows,
|
||||
top,
|
||||
0,
|
||||
false,
|
||||
alignment.width,
|
||||
bounds.height);
|
||||
}
|
||||
|
||||
private static ArrayList<MeasuredClockRow> positionRowsFromBaseline(
|
||||
ArrayList<MeasuredClockRow> measuredRows,
|
||||
int sourceBaselineY,
|
||||
TextView source
|
||||
) {
|
||||
ArrayList<MeasuredClockRow> positionedRows = new ArrayList<>(measuredRows);
|
||||
int baselineY = sourceBaselineY;
|
||||
for (int rowIndex = measuredRows.size() - 1; rowIndex >= 0; rowIndex--) {
|
||||
MeasuredClockRow row = measuredRows.get(rowIndex);
|
||||
int top = baselineY - row.baseline;
|
||||
positionedRows.set(rowIndex, new MeasuredClockRow(
|
||||
row.source,
|
||||
row.width,
|
||||
row.height,
|
||||
row.baseline,
|
||||
row.pipePosition,
|
||||
baselineY,
|
||||
row.leftSegment,
|
||||
row.rightSegment,
|
||||
top,
|
||||
row.rowIndex));
|
||||
if (rowIndex > 0) {
|
||||
baselineY -= resolveGap(row.source.gapBeforePx, source);
|
||||
}
|
||||
}
|
||||
return positionedRows;
|
||||
}
|
||||
|
||||
private static boolean shouldSplitAroundCutout(
|
||||
LayoutPosition position,
|
||||
ArrayList<ClockRow> rows,
|
||||
Bounds bounds,
|
||||
int rootWidth,
|
||||
Bounds cutoutBounds
|
||||
) {
|
||||
if (position != LayoutPosition.MIDDLE
|
||||
|| cutoutBounds == null
|
||||
|| rootWidth <= 0
|
||||
|| rows == null
|
||||
|| rows.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
int left = rootWidth / 2 - bounds.width / 2;
|
||||
for (ClockRow row : rows) {
|
||||
int rowLeft = left + row.x;
|
||||
int rowRight = rowLeft + row.width;
|
||||
if (rowRight > cutoutBounds.left
|
||||
&& rowLeft < cutoutBounds.left + cutoutBounds.width) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canSplitAroundCutout(int rootWidth, Bounds cutoutBounds) {
|
||||
return shouldSplitAroundCutout(LayoutPosition.MIDDLE, rows, bounds(), rootWidth, cutoutBounds);
|
||||
}
|
||||
|
||||
ArrayList<ClockLayout> splitAroundCutout(
|
||||
int rootWidth,
|
||||
Bounds cutoutBounds,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide explicitSide
|
||||
) {
|
||||
ArrayList<ClockLayout> result = new ArrayList<>();
|
||||
if (measuredRows == null || measuredRows.isEmpty() || cutoutBounds == null) {
|
||||
return result;
|
||||
}
|
||||
AnchorSide side = splitSide(rootWidth, cutoutBounds, middleAutoNearestSide, explicitSide);
|
||||
ArrayList<ClockRow> leftRows = new ArrayList<>();
|
||||
ArrayList<ClockRow> rightRows = new ArrayList<>();
|
||||
for (MeasuredClockRow row : measuredRows) {
|
||||
if (row.source.pipeCount > 0) {
|
||||
addSplitSegment(
|
||||
leftRows,
|
||||
row.source.leftText,
|
||||
row.leftSegment,
|
||||
cutoutBounds.left,
|
||||
false,
|
||||
row.baselineY,
|
||||
row.rowIndex,
|
||||
ClockRowSegment.LEFT);
|
||||
addSplitSegment(
|
||||
rightRows,
|
||||
row.source.rightText,
|
||||
row.rightSegment,
|
||||
cutoutBounds.left + cutoutBounds.width,
|
||||
true,
|
||||
row.baselineY,
|
||||
row.rowIndex,
|
||||
ClockRowSegment.RIGHT);
|
||||
} else {
|
||||
int x = side == AnchorSide.LEFT
|
||||
? cutoutBounds.left - row.width
|
||||
: cutoutBounds.left + cutoutBounds.width;
|
||||
ArrayList<ClockRow> targetRows = side == AnchorSide.LEFT ? leftRows : rightRows;
|
||||
targetRows.add(new ClockRow(
|
||||
row.source.text,
|
||||
x,
|
||||
row.y,
|
||||
row.width,
|
||||
row.height,
|
||||
0,
|
||||
row.rowIndex,
|
||||
ClockRowSegment.FULL));
|
||||
}
|
||||
}
|
||||
ClockLayout left = fixedLayoutFromAbsoluteRows(leftRows);
|
||||
ClockLayout right = fixedLayoutFromAbsoluteRows(rightRows);
|
||||
if (left != null) {
|
||||
result.add(left);
|
||||
}
|
||||
if (right != null) {
|
||||
result.add(right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ClockLayout fixedLayoutFromAbsoluteRows(ArrayList<ClockRow> rows) {
|
||||
Bounds bounds = null;
|
||||
for (ClockRow row : rows) {
|
||||
Bounds rowBounds = new Bounds(row.x, row.y, row.width, row.height);
|
||||
bounds = bounds == null ? rowBounds : bounds.union(rowBounds);
|
||||
}
|
||||
if (bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<ClockRow> normalized = new ArrayList<>();
|
||||
for (ClockRow row : rows) {
|
||||
normalized.add(new ClockRow(
|
||||
row.text,
|
||||
row.x - bounds.left,
|
||||
row.y - bounds.top,
|
||||
row.width,
|
||||
row.height,
|
||||
0,
|
||||
row.rowIndex,
|
||||
row.segment));
|
||||
}
|
||||
return new ClockLayout(
|
||||
source,
|
||||
model,
|
||||
normalized,
|
||||
null,
|
||||
bounds.top,
|
||||
bounds.left,
|
||||
true,
|
||||
bounds.width,
|
||||
bounds.height);
|
||||
}
|
||||
|
||||
private static void addSplitSegment(
|
||||
ArrayList<ClockRow> rows,
|
||||
CharSequence text,
|
||||
MeasuredText measuredText,
|
||||
int anchor,
|
||||
boolean extendRight,
|
||||
int baselineY,
|
||||
int rowIndex,
|
||||
ClockRowSegment segment
|
||||
) {
|
||||
if (text == null || text.length() <= 0 || measuredText == null || measuredText.width <= 0) {
|
||||
return;
|
||||
}
|
||||
int x = extendRight ? anchor : anchor - measuredText.width;
|
||||
int y = baselineY - measuredText.baseline;
|
||||
rows.add(new ClockRow(
|
||||
text,
|
||||
x,
|
||||
y,
|
||||
measuredText.width,
|
||||
measuredText.height,
|
||||
0,
|
||||
rowIndex,
|
||||
segment));
|
||||
}
|
||||
|
||||
private static AnchorSide splitSide(
|
||||
int rootWidth,
|
||||
Bounds cutoutBounds,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide explicitSide
|
||||
) {
|
||||
if (!middleAutoNearestSide || cutoutBounds == null || rootWidth <= 0) {
|
||||
return explicitSide;
|
||||
}
|
||||
int middle = rootWidth / 2;
|
||||
int cutoutCenter = cutoutBounds.left + cutoutBounds.width / 2;
|
||||
if (cutoutCenter > middle + MIDDLE_COLLISION_MARGIN_PX) {
|
||||
return AnchorSide.LEFT;
|
||||
}
|
||||
if (cutoutCenter < middle - MIDDLE_COLLISION_MARGIN_PX) {
|
||||
return AnchorSide.RIGHT;
|
||||
}
|
||||
return explicitSide;
|
||||
}
|
||||
|
||||
private static RowAlignment alignRows(
|
||||
ArrayList<MeasuredClockRow> measuredRows,
|
||||
LayoutPosition position
|
||||
) {
|
||||
int maxPipeLeft = 0;
|
||||
int maxPipeRight = 0;
|
||||
int maxStandaloneWidth = 0;
|
||||
for (MeasuredClockRow row : measuredRows) {
|
||||
if (row.source.pipeCount > 0) {
|
||||
maxPipeLeft = Math.max(maxPipeLeft, row.pipePosition);
|
||||
maxPipeRight = Math.max(maxPipeRight, row.width - row.pipePosition);
|
||||
} else {
|
||||
maxStandaloneWidth = Math.max(maxStandaloneWidth, row.width);
|
||||
}
|
||||
}
|
||||
int pipeGroupWidth = maxPipeLeft + maxPipeRight;
|
||||
int alignedWidth = Math.max(maxStandaloneWidth, pipeGroupWidth);
|
||||
int pipeGroupOffset = alignedOffset(alignedWidth, pipeGroupWidth, position);
|
||||
ArrayList<ClockRow> rows = new ArrayList<>();
|
||||
for (MeasuredClockRow row : measuredRows) {
|
||||
int x;
|
||||
if (row.source.pipeCount > 0) {
|
||||
x = pipeGroupOffset + maxPipeLeft - row.pipePosition;
|
||||
} else {
|
||||
x = alignedOffset(alignedWidth, row.width, position);
|
||||
}
|
||||
rows.add(new ClockRow(
|
||||
row.source.text,
|
||||
x,
|
||||
row.y,
|
||||
row.width,
|
||||
row.height,
|
||||
0,
|
||||
row.rowIndex,
|
||||
ClockRowSegment.FULL));
|
||||
}
|
||||
return new RowAlignment(rows, alignedWidth);
|
||||
}
|
||||
|
||||
private static int alignedOffset(int containerWidth, int contentWidth, LayoutPosition position) {
|
||||
int free = Math.max(0, containerWidth - contentWidth);
|
||||
if (position == LayoutPosition.RIGHT) {
|
||||
return free;
|
||||
}
|
||||
if (position == LayoutPosition.MIDDLE) {
|
||||
return free / 2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static MeasuredText measureText(TextView view, CharSequence text) {
|
||||
view.setText(text);
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||
view.measure(widthSpec, heightSpec);
|
||||
return new MeasuredText(
|
||||
Math.max(0, view.getMeasuredWidth()),
|
||||
Math.max(1, view.getMeasuredHeight()),
|
||||
Math.max(0, view.getBaseline()));
|
||||
}
|
||||
|
||||
private static int resolveGap(int gapBeforePx, TextView source) {
|
||||
if (gapBeforePx == ClockLayoutTextFactory.ROW_GAP_AUTO) {
|
||||
return Math.max(1, source.getLineHeight());
|
||||
}
|
||||
return gapBeforePx;
|
||||
}
|
||||
|
||||
int width() {
|
||||
return width;
|
||||
}
|
||||
|
||||
int height() {
|
||||
return height;
|
||||
}
|
||||
|
||||
int top() {
|
||||
return top;
|
||||
}
|
||||
|
||||
Bounds bounds() {
|
||||
return new Bounds(0, top, width, height);
|
||||
}
|
||||
|
||||
ArrayList<Bounds> collisionBoxes() {
|
||||
ArrayList<Bounds> boxes = new ArrayList<>();
|
||||
if (rows == null) {
|
||||
return boxes;
|
||||
}
|
||||
for (ClockRow row : rows) {
|
||||
if (row.width > 0 && row.height > 0) {
|
||||
boxes.add(new Bounds(row.x, top + row.y, row.width, row.height));
|
||||
}
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
Bounds place(int left) {
|
||||
if (!fixedPlacement) {
|
||||
placedLeft = left;
|
||||
}
|
||||
return new Bounds(placedLeft, top, width, height);
|
||||
}
|
||||
|
||||
ClockPlacement placement() {
|
||||
return new ClockPlacement(source, model.contentDescription, rows, new Bounds(placedLeft, top, width, height));
|
||||
}
|
||||
|
||||
private record MeasuredClockRow(
|
||||
ClockLayoutTextFactory.Row source,
|
||||
int width,
|
||||
int height,
|
||||
int baseline,
|
||||
int pipePosition,
|
||||
int baselineY,
|
||||
MeasuredText leftSegment,
|
||||
MeasuredText rightSegment,
|
||||
int y,
|
||||
int rowIndex
|
||||
) {
|
||||
}
|
||||
|
||||
private record MeasuredText(int width, int height, int baseline) {
|
||||
}
|
||||
|
||||
private record RowAlignment(ArrayList<ClockRow> rows, int width) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
final class ClockProxyRenderer {
|
||||
private final WeakHashMap<ViewGroup, ArrayList<TextView>> rowsByHost = new WeakHashMap<>();
|
||||
|
||||
void render(ViewGroup host, ClockPlacement placement) {
|
||||
if (host == null || placement == null || placement.rows.isEmpty()) {
|
||||
clear(host);
|
||||
return;
|
||||
}
|
||||
ArrayList<TextView> views = rowsByHost.get(host);
|
||||
if (views == null) {
|
||||
views = new ArrayList<>();
|
||||
rowsByHost.put(host, views);
|
||||
}
|
||||
boolean hostChanged = false;
|
||||
if (!views.isEmpty() && views.size() != placement.rows.size()) {
|
||||
for (TextView view : views) {
|
||||
detachFromParent(view);
|
||||
}
|
||||
views.clear();
|
||||
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()) {
|
||||
TextView view = new TextView(host.getContext());
|
||||
view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
views.add(view);
|
||||
host.addView(view);
|
||||
hostChanged = true;
|
||||
}
|
||||
for (int i = 0; i < placement.rows.size(); i++) {
|
||||
ClockRow row = placement.rows.get(i);
|
||||
TextView view = views.get(i);
|
||||
if (copyTextStyleIfChanged(placement.source, view)) {
|
||||
hostChanged = true;
|
||||
}
|
||||
if (!String.valueOf(view.getText()).contentEquals(row.text)) {
|
||||
view.setText(row.text);
|
||||
hostChanged = true;
|
||||
}
|
||||
if (!java.util.Objects.equals(view.getContentDescription(), placement.contentDescription)) {
|
||||
view.setContentDescription(placement.contentDescription);
|
||||
hostChanged = true;
|
||||
}
|
||||
if (view.getVisibility() != View.VISIBLE) {
|
||||
view.setVisibility(View.VISIBLE);
|
||||
hostChanged = true;
|
||||
}
|
||||
if (view.getAlpha() != 1f) {
|
||||
view.setAlpha(1f);
|
||||
hostChanged = true;
|
||||
}
|
||||
int left = placement.bounds.left + row.x;
|
||||
int top = placement.bounds.top + row.y;
|
||||
int scrollX = Math.max(0, row.clipLeft);
|
||||
if (view.getScrollX() != scrollX) {
|
||||
view.setScrollX(scrollX);
|
||||
hostChanged = true;
|
||||
}
|
||||
if (applyTextLayoutParams(host, view, left, top, row.width, row.height)) {
|
||||
hostChanged = true;
|
||||
}
|
||||
if (view.getLeft() != left
|
||||
|| view.getTop() != top
|
||||
|| view.getRight() != left + row.width
|
||||
|| view.getBottom() != top + row.height) {
|
||||
view.layout(left, top, left + row.width, top + row.height);
|
||||
hostChanged = true;
|
||||
}
|
||||
}
|
||||
if (hostChanged) {
|
||||
host.requestLayout();
|
||||
host.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void clear(ViewGroup host) {
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<TextView> views = rowsByHost.remove(host);
|
||||
if (views == null) {
|
||||
return;
|
||||
}
|
||||
for (TextView view : views) {
|
||||
detachFromParent(view);
|
||||
}
|
||||
host.requestLayout();
|
||||
host.invalidate();
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
for (ViewGroup host : new ArrayList<>(rowsByHost.keySet())) {
|
||||
clear(host);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean copyTextStyleIfChanged(TextView source, TextView target) {
|
||||
boolean changed = false;
|
||||
if (target.getCurrentTextColor() != source.getCurrentTextColor()) {
|
||||
target.setTextColor(source.getCurrentTextColor());
|
||||
changed = true;
|
||||
}
|
||||
if (target.getTextSize() != source.getTextSize()) {
|
||||
target.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, source.getTextSize());
|
||||
changed = true;
|
||||
}
|
||||
if (!java.util.Objects.equals(target.getTypeface(), source.getTypeface())) {
|
||||
target.setTypeface(source.getTypeface());
|
||||
changed = true;
|
||||
}
|
||||
if (target.getGravity() != source.getGravity()) {
|
||||
target.setGravity(source.getGravity());
|
||||
changed = true;
|
||||
}
|
||||
if (target.getIncludeFontPadding() != source.getIncludeFontPadding()) {
|
||||
target.setIncludeFontPadding(source.getIncludeFontPadding());
|
||||
changed = true;
|
||||
}
|
||||
if (target.getMaxLines() != 1) {
|
||||
target.setSingleLine(true);
|
||||
target.setMaxLines(1);
|
||||
changed = true;
|
||||
}
|
||||
if (!target.isHorizontallyScrollable()) {
|
||||
target.setHorizontallyScrolling(true);
|
||||
changed = true;
|
||||
}
|
||||
if (target.getPaddingLeft() != source.getPaddingLeft()
|
||||
|| target.getPaddingTop() != source.getPaddingTop()
|
||||
|| target.getPaddingRight() != source.getPaddingRight()
|
||||
|| target.getPaddingBottom() != source.getPaddingBottom()) {
|
||||
target.setPadding(
|
||||
source.getPaddingLeft(),
|
||||
source.getPaddingTop(),
|
||||
source.getPaddingRight(),
|
||||
source.getPaddingBottom());
|
||||
changed = true;
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (target.getLetterSpacing() != source.getLetterSpacing()) {
|
||||
target.setLetterSpacing(source.getLetterSpacing());
|
||||
changed = true;
|
||||
}
|
||||
if (!java.util.Objects.equals(target.getFontFeatureSettings(), source.getFontFeatureSettings())) {
|
||||
target.setFontFeatureSettings(source.getFontFeatureSettings());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
private static boolean applyTextLayoutParams(
|
||||
ViewGroup host,
|
||||
TextView view,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
ViewGroup.LayoutParams current = view.getLayoutParams();
|
||||
if (current != null
|
||||
&& current.width == width
|
||||
&& current.height == height
|
||||
&& current instanceof ViewGroup.MarginLayoutParams marginParams
|
||||
&& marginParams.leftMargin == left
|
||||
&& marginParams.topMargin == top) {
|
||||
return false;
|
||||
}
|
||||
ViewGroup.MarginLayoutParams lp = host instanceof FrameLayout
|
||||
? new FrameLayout.LayoutParams(width, height)
|
||||
: new ViewGroup.MarginLayoutParams(width, height);
|
||||
lp.leftMargin = left;
|
||||
lp.topMargin = top;
|
||||
view.setLayoutParams(lp);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void detachFromParent(View view) {
|
||||
if (view != null && view.getParent() instanceof ViewGroup parent) {
|
||||
parent.removeView(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
|
||||
final class ClockPlacement {
|
||||
final TextView source;
|
||||
final String contentDescription;
|
||||
final ArrayList<ClockRow> rows;
|
||||
final Bounds bounds;
|
||||
|
||||
ClockPlacement(
|
||||
TextView source,
|
||||
String contentDescription,
|
||||
ArrayList<ClockRow> rows,
|
||||
Bounds bounds
|
||||
) {
|
||||
this.source = source;
|
||||
this.contentDescription = contentDescription;
|
||||
this.rows = rows;
|
||||
this.bounds = bounds;
|
||||
}
|
||||
}
|
||||
|
||||
final class ClockRow {
|
||||
final CharSequence text;
|
||||
final int x;
|
||||
final int y;
|
||||
final int width;
|
||||
final int height;
|
||||
final int clipLeft;
|
||||
final int rowIndex;
|
||||
final ClockRowSegment segment;
|
||||
|
||||
ClockRow(
|
||||
CharSequence text,
|
||||
int x,
|
||||
int y,
|
||||
int width,
|
||||
int height,
|
||||
int clipLeft,
|
||||
int rowIndex,
|
||||
ClockRowSegment segment
|
||||
) {
|
||||
this.text = text;
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.clipLeft = clipLeft;
|
||||
this.rowIndex = rowIndex;
|
||||
this.segment = segment;
|
||||
}
|
||||
}
|
||||
|
||||
enum ClockRowSegment {
|
||||
FULL,
|
||||
LEFT,
|
||||
RIGHT
|
||||
}
|
||||
+378
@@ -0,0 +1,378 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
public final class DebugBoundsOverlayController {
|
||||
|
||||
private static final String TAG_OVERLAY_HOST = "statusbartweak.debug.bounds.host";
|
||||
private static final String TAG_CLOCK_HOST = "statusbartweak.unlocked.clock.host";
|
||||
private static final String OVERFLOW_DOT_KEY = "statusbartweak:overflow_dot";
|
||||
private static final int COLOR_NOTIFICATION = Color.argb(96, 0, 128, 255);
|
||||
private static final int COLOR_STATUS = Color.argb(96, 255, 64, 64);
|
||||
private static final int COLOR_CLOCK = Color.argb(96, 64, 220, 96);
|
||||
private static final int COLOR_CHIP = Color.argb(112, 255, 0, 220);
|
||||
private static final int COLOR_CUTOUT = Color.argb(112, 255, 180, 0);
|
||||
|
||||
public boolean hasEnabledVisualizer(boolean layoutEnabled, SbtSettings settings) {
|
||||
if (settings == null) {
|
||||
return false;
|
||||
}
|
||||
return settings.debugVisualizeCameraCutout
|
||||
|| (layoutEnabled
|
||||
&& (settings.debugVisualizeNotificationContainer
|
||||
|| settings.debugVisualizeStatusContainer
|
||||
|| settings.debugVisualizeClockContainer
|
||||
|| settings.debugVisualizeChipContainer));
|
||||
}
|
||||
|
||||
public void apply(View root, boolean layoutEnabled, SbtSettings settings) {
|
||||
if (!(root instanceof ViewGroup parent) || settings == null) {
|
||||
return;
|
||||
}
|
||||
if (!hasEnabledVisualizer(layoutEnabled, settings)) {
|
||||
removeOverlay(parent);
|
||||
return;
|
||||
}
|
||||
ArrayList<OverlaySpec> specs = new ArrayList<>();
|
||||
if (settings.debugVisualizeCameraCutout) {
|
||||
addCutoutSpecs(root, specs);
|
||||
}
|
||||
if (layoutEnabled && settings.debugVisualizeNotificationContainer) {
|
||||
Bounds bounds = unionHostChildren(
|
||||
root,
|
||||
parent,
|
||||
OwnedIconHostManager.TAG_UNLOCKED_NOTIFICATION_HOST,
|
||||
true);
|
||||
if (bounds != null) {
|
||||
specs.add(new OverlaySpec(bounds, COLOR_NOTIFICATION));
|
||||
}
|
||||
}
|
||||
if (layoutEnabled && settings.debugVisualizeStatusContainer) {
|
||||
Bounds bounds = unionHostChildren(
|
||||
root,
|
||||
parent,
|
||||
OwnedIconHostManager.TAG_UNLOCKED_STATUS_HOST,
|
||||
false);
|
||||
if (bounds != null) {
|
||||
specs.add(new OverlaySpec(bounds, COLOR_STATUS));
|
||||
}
|
||||
}
|
||||
if (layoutEnabled && settings.debugVisualizeClockContainer) {
|
||||
addHostChildSpecs(root, parent, TAG_CLOCK_HOST, COLOR_CLOCK, specs);
|
||||
}
|
||||
if (layoutEnabled && settings.debugVisualizeChipContainer) {
|
||||
Bounds bounds = unionHostChildren(
|
||||
root,
|
||||
parent,
|
||||
OwnedIconHostManager.TAG_UNLOCKED_CHIP_HOST,
|
||||
false);
|
||||
if (bounds != null) {
|
||||
specs.add(new OverlaySpec(bounds, COLOR_CHIP));
|
||||
}
|
||||
}
|
||||
if (specs.isEmpty()) {
|
||||
removeOverlay(parent);
|
||||
return;
|
||||
}
|
||||
OverlayView host = ensureOverlay(parent);
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
int width = Math.max(0, root.getWidth());
|
||||
int height = Math.max(0, root.getHeight());
|
||||
if (host.getLeft() != 0
|
||||
|| host.getTop() != 0
|
||||
|| host.getRight() != width
|
||||
|| host.getBottom() != height) {
|
||||
host.layout(0, 0, width, height);
|
||||
}
|
||||
host.setSpecs(specs);
|
||||
if (parent.getChildAt(parent.getChildCount() - 1) != host) {
|
||||
host.bringToFront();
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(View root) {
|
||||
if (root instanceof ViewGroup parent) {
|
||||
removeOverlay(parent);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCutoutSpecs(View root, ArrayList<OverlaySpec> specs) {
|
||||
WindowInsets insets = root.getRootWindowInsets();
|
||||
DisplayCutout cutout = insets != null ? insets.getDisplayCutout() : null;
|
||||
List<Rect> rects = cutout != null ? cutout.getBoundingRects() : null;
|
||||
if (rects == null || rects.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Rect rect : rects) {
|
||||
Bounds bounds = clampToRoot(root, rect);
|
||||
if (bounds != null) {
|
||||
specs.add(new OverlaySpec(bounds, COLOR_CUTOUT));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Bounds clampToRoot(View root, Rect rect) {
|
||||
if (root == null || rect == null || rect.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int left = clamp(rect.left, 0, root.getWidth());
|
||||
int top = clamp(rect.top, 0, root.getHeight());
|
||||
int right = clamp(rect.right, 0, root.getWidth());
|
||||
int bottom = clamp(rect.bottom, 0, root.getHeight());
|
||||
if (right <= left || bottom <= top) {
|
||||
return null;
|
||||
}
|
||||
return new Bounds(left, top, right - left, bottom - top);
|
||||
}
|
||||
|
||||
private Bounds unionHostChildren(
|
||||
View root,
|
||||
ViewGroup parent,
|
||||
String hostTag,
|
||||
boolean stableRegularIconSlots
|
||||
) {
|
||||
View host = findDirectTaggedChild(parent, hostTag);
|
||||
if (!(host instanceof ViewGroup group)) {
|
||||
return null;
|
||||
}
|
||||
Bounds union = null;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child == null || child.getVisibility() == View.GONE) {
|
||||
continue;
|
||||
}
|
||||
Bounds bounds = overlayBoundsRelativeTo(root, child, stableRegularIconSlots);
|
||||
if (bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
union = union == null ? bounds : union.union(bounds);
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
private boolean isOverflowDot(View view) {
|
||||
Object tag = view != null ? view.getTag() : null;
|
||||
return tag instanceof String string && string.startsWith(OVERFLOW_DOT_KEY);
|
||||
}
|
||||
|
||||
private Bounds overlayBoundsRelativeTo(View root, View child, boolean stableRegularIconSlots) {
|
||||
if (stableRegularIconSlots && !isOverflowDot(child)) {
|
||||
return boundsRelativeTo(root, child);
|
||||
}
|
||||
Bounds contentBounds = contentBoundsRelativeTo(root, child);
|
||||
Bounds slotBounds = boundsRelativeTo(root, child);
|
||||
if (isOverflowDot(child) && contentBounds != null && slotBounds != null) {
|
||||
return new Bounds(
|
||||
contentBounds.left,
|
||||
slotBounds.top,
|
||||
contentBounds.width,
|
||||
slotBounds.height);
|
||||
}
|
||||
return contentBounds;
|
||||
}
|
||||
|
||||
private void addHostChildSpecs(
|
||||
View root,
|
||||
ViewGroup parent,
|
||||
String hostTag,
|
||||
int color,
|
||||
ArrayList<OverlaySpec> specs
|
||||
) {
|
||||
View host = findDirectTaggedChild(parent, hostTag);
|
||||
if (!(host instanceof ViewGroup group) || specs == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child == null || child.getVisibility() == View.GONE) {
|
||||
continue;
|
||||
}
|
||||
Bounds bounds = contentBoundsRelativeTo(root, child);
|
||||
if (bounds != null && bounds.width > 0 && bounds.height > 0) {
|
||||
specs.add(new OverlaySpec(bounds, color));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private OverlayView ensureOverlay(ViewGroup parent) {
|
||||
View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST);
|
||||
if (existing instanceof OverlayView overlayView) {
|
||||
return overlayView;
|
||||
}
|
||||
OverlayView host = new OverlayView(parent.getContext());
|
||||
host.setTag(TAG_OVERLAY_HOST);
|
||||
host.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
host.setClickable(false);
|
||||
host.setFocusable(false);
|
||||
parent.addView(host, new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
return host;
|
||||
}
|
||||
|
||||
private void removeOverlay(ViewGroup parent) {
|
||||
View existing = findDirectTaggedChild(parent, TAG_OVERLAY_HOST);
|
||||
if (existing != null) {
|
||||
parent.removeView(existing);
|
||||
}
|
||||
}
|
||||
|
||||
private View findDirectTaggedChild(ViewGroup parent, String tag) {
|
||||
if (parent == null || tag == null) {
|
||||
return null;
|
||||
}
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
View child = parent.getChildAt(i);
|
||||
if (tag.equals(child.getTag())) {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Bounds boundsRelativeTo(View root, View view) {
|
||||
if (root == null || view == null) {
|
||||
return null;
|
||||
}
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
View current = view;
|
||||
while (current != null && current != root) {
|
||||
left += current.getLeft() + Math.round(current.getTranslationX());
|
||||
top += current.getTop() + Math.round(current.getTranslationY());
|
||||
Object parent = current.getParent();
|
||||
if (parent instanceof View parentView) {
|
||||
left -= parentView.getScrollX();
|
||||
top -= parentView.getScrollY();
|
||||
}
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
if (current != root) {
|
||||
return null;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
return new Bounds(left, top, Math.max(0, width), Math.max(0, height));
|
||||
}
|
||||
|
||||
private Bounds contentBoundsRelativeTo(View root, View view) {
|
||||
Bounds viewBounds = boundsRelativeTo(root, view);
|
||||
if (viewBounds == null) {
|
||||
return null;
|
||||
}
|
||||
Bounds bitmapBounds = bitmapContentBounds(view);
|
||||
if (bitmapBounds == null) {
|
||||
return viewBounds;
|
||||
}
|
||||
return new Bounds(
|
||||
viewBounds.left + bitmapBounds.left,
|
||||
viewBounds.top + bitmapBounds.top,
|
||||
bitmapBounds.width,
|
||||
bitmapBounds.height);
|
||||
}
|
||||
|
||||
private Bounds bitmapContentBounds(View view) {
|
||||
if (!(view instanceof ImageView imageView)) {
|
||||
return null;
|
||||
}
|
||||
Drawable drawable = imageView.getDrawable();
|
||||
if (!(drawable instanceof BitmapDrawable bitmapDrawable)) {
|
||||
return null;
|
||||
}
|
||||
Bitmap bitmap = bitmapDrawable.getBitmap();
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return null;
|
||||
}
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
int left = width;
|
||||
int top = height;
|
||||
int right = -1;
|
||||
int bottom = -1;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (Color.alpha(bitmap.getPixel(x, y)) == 0) {
|
||||
continue;
|
||||
}
|
||||
left = Math.min(left, x);
|
||||
top = Math.min(top, y);
|
||||
right = Math.max(right, x);
|
||||
bottom = Math.max(bottom, y);
|
||||
}
|
||||
}
|
||||
if (right < left || bottom < top) {
|
||||
return null;
|
||||
}
|
||||
return new Bounds(left, top, right - left + 1, bottom - top + 1);
|
||||
}
|
||||
|
||||
private int clamp(int value, int min, int max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
private record OverlaySpec(Bounds bounds, int color) {
|
||||
}
|
||||
|
||||
private record Bounds(int left, int top, int width, int height) {
|
||||
Bounds union(Bounds other) {
|
||||
int right = Math.max(left + width, other.left + other.width);
|
||||
int bottom = Math.max(top + height, other.top + other.height);
|
||||
int newLeft = Math.min(left, other.left);
|
||||
int newTop = Math.min(top, other.top);
|
||||
return new Bounds(newLeft, newTop, right - newLeft, bottom - newTop);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OverlayView extends View {
|
||||
private final Paint paint = new Paint();
|
||||
private ArrayList<OverlaySpec> specs = new ArrayList<>();
|
||||
|
||||
OverlayView(android.content.Context context) {
|
||||
super(context);
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
void setSpecs(ArrayList<OverlaySpec> specs) {
|
||||
ArrayList<OverlaySpec> next = specs != null ? new ArrayList<>(specs) : new ArrayList<>();
|
||||
if (Objects.equals(this.specs, next)) {
|
||||
return;
|
||||
}
|
||||
this.specs = next;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
for (OverlaySpec spec : specs) {
|
||||
Bounds bounds = spec.bounds;
|
||||
paint.setColor(spec.color);
|
||||
canvas.drawRect(
|
||||
bounds.left,
|
||||
bounds.top,
|
||||
bounds.left + bounds.width,
|
||||
bounds.top + bounds.height,
|
||||
paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Creates and owns runtime icon hosts inside SystemUI roots.
|
||||
*
|
||||
* This class only manages host containers. It does not solve layout or rehost
|
||||
* icon views; those steps stay in the layout and render layers.
|
||||
*/
|
||||
public final class OwnedIconHostManager {
|
||||
|
||||
public static final String TAG_UNLOCKED_STATUS_HOST =
|
||||
"statusbartweak.unlocked.status.host";
|
||||
public static final String TAG_UNLOCKED_NOTIFICATION_HOST =
|
||||
"statusbartweak.unlocked.notification.host";
|
||||
public static final String TAG_UNLOCKED_CLOCK_HOST =
|
||||
"statusbartweak.unlocked.clock.host";
|
||||
public static final String TAG_UNLOCKED_CHIP_HOST =
|
||||
"statusbartweak.unlocked.chip.host";
|
||||
|
||||
public FrameLayout ensureUnlockedStatusHost(View root) {
|
||||
return ensureHost(root, TAG_UNLOCKED_STATUS_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureUnlockedNotificationHost(View root) {
|
||||
return ensureHost(root, TAG_UNLOCKED_NOTIFICATION_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureUnlockedChipHost(View root) {
|
||||
return ensureHost(root, TAG_UNLOCKED_CHIP_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureUnlockedClockHost(View root) {
|
||||
return ensureHost(root, TAG_UNLOCKED_CLOCK_HOST);
|
||||
}
|
||||
|
||||
public FrameLayout ensureHost(View root, String tag) {
|
||||
Objects.requireNonNull(tag, "tag");
|
||||
if (!(root instanceof ViewGroup parent)) {
|
||||
return null;
|
||||
}
|
||||
FrameLayout existing = findDirectHost(parent, tag);
|
||||
if (existing != null) {
|
||||
existing.bringToFront();
|
||||
return existing;
|
||||
}
|
||||
FrameLayout host = new FrameLayout(parent.getContext());
|
||||
host.setTag(tag);
|
||||
host.setClipChildren(false);
|
||||
host.setClipToPadding(false);
|
||||
host.setClipToOutline(false);
|
||||
host.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
parent.addView(host, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
disableAncestorClipping(parent);
|
||||
host.bringToFront();
|
||||
return host;
|
||||
}
|
||||
|
||||
public void removeHost(View root, String tag) {
|
||||
if (!(root instanceof ViewGroup parent) || tag == null) {
|
||||
return;
|
||||
}
|
||||
FrameLayout host = findDirectHost(parent, tag);
|
||||
if (host != null) {
|
||||
parent.removeView(host);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeUnlockedHosts(View root) {
|
||||
removeHost(root, TAG_UNLOCKED_STATUS_HOST);
|
||||
removeHost(root, TAG_UNLOCKED_NOTIFICATION_HOST);
|
||||
removeHost(root, TAG_UNLOCKED_CLOCK_HOST);
|
||||
removeHost(root, TAG_UNLOCKED_CHIP_HOST);
|
||||
}
|
||||
|
||||
public void setUnlockedHostsVisible(View root, boolean visible) {
|
||||
setHostVisible(root, TAG_UNLOCKED_STATUS_HOST, visible);
|
||||
setHostVisible(root, TAG_UNLOCKED_NOTIFICATION_HOST, visible);
|
||||
setHostVisible(root, TAG_UNLOCKED_CLOCK_HOST, visible);
|
||||
setHostVisible(root, TAG_UNLOCKED_CHIP_HOST, visible);
|
||||
}
|
||||
|
||||
public boolean isOwnedHost(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
Object tag = view.getTag();
|
||||
return TAG_UNLOCKED_STATUS_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_NOTIFICATION_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_CLOCK_HOST.equals(tag)
|
||||
|| TAG_UNLOCKED_CHIP_HOST.equals(tag);
|
||||
}
|
||||
|
||||
private FrameLayout findDirectHost(ViewGroup parent, String tag) {
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
View child = parent.getChildAt(i);
|
||||
if (child instanceof FrameLayout frameLayout && tag.equals(child.getTag())) {
|
||||
return frameLayout;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void setHostVisible(View root, String tag, boolean visible) {
|
||||
if (!(root instanceof ViewGroup parent) || tag == null) {
|
||||
return;
|
||||
}
|
||||
FrameLayout host = findDirectHost(parent, tag);
|
||||
if (host == null) {
|
||||
return;
|
||||
}
|
||||
host.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
|
||||
private void disableAncestorClipping(View view) {
|
||||
Object current = view;
|
||||
int depth = 0;
|
||||
while (current instanceof ViewGroup group && depth < 8) {
|
||||
group.setClipChildren(false);
|
||||
group.setClipToPadding(false);
|
||||
group.setClipToOutline(false);
|
||||
current = group.getParent();
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
|
||||
record SnapshotSource(
|
||||
View view,
|
||||
SnapshotMode mode
|
||||
) {
|
||||
}
|
||||
|
||||
enum SnapshotMode {
|
||||
VIEW,
|
||||
NOTIFICATION_DRAWABLE
|
||||
}
|
||||
|
||||
final class SnapshotKeys {
|
||||
static final String OVERFLOW_DOT = "statusbartweak:overflow_dot";
|
||||
static final String EMPTY_CHIP = "statusbartweak:empty_chip";
|
||||
static final String EMPTY_ICON_CONTAINER = "statusbartweak:empty_icon_container";
|
||||
|
||||
private SnapshotKeys() {
|
||||
}
|
||||
}
|
||||
|
||||
final class SnapshotPlacement {
|
||||
final SnapshotSource source;
|
||||
final Bounds bounds;
|
||||
final String key;
|
||||
final boolean signal;
|
||||
final boolean overflowDot;
|
||||
final HeldSignal heldSignal;
|
||||
final Bounds contentBounds;
|
||||
final int sourceOffsetX;
|
||||
|
||||
SnapshotPlacement(
|
||||
SnapshotSource source,
|
||||
Bounds bounds,
|
||||
String key,
|
||||
boolean signal,
|
||||
boolean overflowDot,
|
||||
HeldSignal heldSignal,
|
||||
Bounds contentBounds,
|
||||
int sourceOffsetX
|
||||
) {
|
||||
this.source = source;
|
||||
this.bounds = bounds;
|
||||
this.key = key;
|
||||
this.signal = signal;
|
||||
this.overflowDot = overflowDot;
|
||||
this.heldSignal = heldSignal;
|
||||
this.contentBounds = contentBounds;
|
||||
this.sourceOffsetX = sourceOffsetX;
|
||||
}
|
||||
|
||||
SnapshotSource source() {
|
||||
return source;
|
||||
}
|
||||
|
||||
Bounds bounds() {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
String key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
boolean signal() {
|
||||
return signal;
|
||||
}
|
||||
|
||||
boolean overflowDot() {
|
||||
return overflowDot;
|
||||
}
|
||||
|
||||
HeldSignal heldSignal() {
|
||||
return heldSignal;
|
||||
}
|
||||
|
||||
Bounds contentBounds() {
|
||||
return contentBounds;
|
||||
}
|
||||
|
||||
int sourceOffsetX() {
|
||||
return sourceOffsetX;
|
||||
}
|
||||
}
|
||||
|
||||
record BitmapResult(Bitmap bitmap, boolean created) {
|
||||
}
|
||||
|
||||
record SnapshotRenderState(
|
||||
String key,
|
||||
int left,
|
||||
int top,
|
||||
int width,
|
||||
int height,
|
||||
boolean signal,
|
||||
boolean overflowDot,
|
||||
int sourceOffsetX,
|
||||
String heldSignalKey,
|
||||
int heldBitmapId,
|
||||
SnapshotMode sourceMode,
|
||||
int sourceSignature
|
||||
) {
|
||||
}
|
||||
|
||||
final class HeldSignal {
|
||||
final String key;
|
||||
final Bounds bounds;
|
||||
final Bitmap bitmap;
|
||||
|
||||
HeldSignal(String key, Bounds bounds, Bitmap bitmap) {
|
||||
this.key = key;
|
||||
this.bounds = bounds;
|
||||
this.bitmap = bitmap;
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
if (bitmap != null && !bitmap.isRecycled()) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class RootAnchors {
|
||||
final View clockContainer;
|
||||
final TextView clockView;
|
||||
final View statusRoot;
|
||||
final View statusEndContent;
|
||||
final View statusIcons;
|
||||
final View battery;
|
||||
final View notificationRoot;
|
||||
|
||||
RootAnchors(
|
||||
View clockContainer,
|
||||
TextView clockView,
|
||||
View statusRoot,
|
||||
View statusEndContent,
|
||||
View statusIcons,
|
||||
View battery,
|
||||
View notificationRoot
|
||||
) {
|
||||
this.clockContainer = clockContainer;
|
||||
this.clockView = clockView;
|
||||
this.statusRoot = statusRoot;
|
||||
this.statusEndContent = statusEndContent;
|
||||
this.statusIcons = statusIcons;
|
||||
this.battery = battery;
|
||||
this.notificationRoot = notificationRoot;
|
||||
}
|
||||
|
||||
boolean isUsable() {
|
||||
return isAttached(statusRoot)
|
||||
&& isAttached(battery)
|
||||
&& isAttached(notificationRoot)
|
||||
&& (clockContainer == null || isAttached(clockContainer));
|
||||
}
|
||||
|
||||
private static boolean isAttached(View view) {
|
||||
return view != null && view.isAttachedToWindow();
|
||||
}
|
||||
}
|
||||
|
||||
final class CollectedIcons {
|
||||
final TextView clockView;
|
||||
final ArrayList<SnapshotSource> statusIcons;
|
||||
final ArrayList<SnapshotSource> notificationIcons;
|
||||
final ArrayList<SnapshotSource> statusChips;
|
||||
final ArrayList<SnapshotSource> statusChipMetrics;
|
||||
|
||||
CollectedIcons(
|
||||
TextView clockView,
|
||||
ArrayList<SnapshotSource> statusIcons,
|
||||
ArrayList<SnapshotSource> notificationIcons,
|
||||
ArrayList<SnapshotSource> statusChips,
|
||||
ArrayList<SnapshotSource> statusChipMetrics
|
||||
) {
|
||||
this.clockView = clockView;
|
||||
this.statusIcons = statusIcons;
|
||||
this.notificationIcons = notificationIcons;
|
||||
this.statusChips = statusChips;
|
||||
this.statusChipMetrics = statusChipMetrics;
|
||||
}
|
||||
}
|
||||
|
||||
final class LayoutInputSignature {
|
||||
final String scene;
|
||||
final int rootWidth;
|
||||
final int rootHeight;
|
||||
final int rootPaddingLeft;
|
||||
final int rootPaddingRight;
|
||||
final int cutoutLeft;
|
||||
final int cutoutTop;
|
||||
final int cutoutRight;
|
||||
final int cutoutBottom;
|
||||
final int settings;
|
||||
final int clockTime;
|
||||
final int clockView;
|
||||
final int clockContainer;
|
||||
final int statusRoot;
|
||||
final int statusEndContent;
|
||||
final int statusIcons;
|
||||
final int battery;
|
||||
final int notificationRoot;
|
||||
final int statusSources;
|
||||
final int notificationSources;
|
||||
final int chipSources;
|
||||
|
||||
LayoutInputSignature(
|
||||
String scene,
|
||||
int rootWidth,
|
||||
int rootHeight,
|
||||
int rootPaddingLeft,
|
||||
int rootPaddingRight,
|
||||
int cutoutLeft,
|
||||
int cutoutTop,
|
||||
int cutoutRight,
|
||||
int cutoutBottom,
|
||||
int settings,
|
||||
int clockTime,
|
||||
int clockView,
|
||||
int clockContainer,
|
||||
int statusRoot,
|
||||
int statusEndContent,
|
||||
int statusIcons,
|
||||
int battery,
|
||||
int notificationRoot,
|
||||
int statusSources,
|
||||
int notificationSources,
|
||||
int chipSources
|
||||
) {
|
||||
this.scene = scene;
|
||||
this.rootWidth = rootWidth;
|
||||
this.rootHeight = rootHeight;
|
||||
this.rootPaddingLeft = rootPaddingLeft;
|
||||
this.rootPaddingRight = rootPaddingRight;
|
||||
this.cutoutLeft = cutoutLeft;
|
||||
this.cutoutTop = cutoutTop;
|
||||
this.cutoutRight = cutoutRight;
|
||||
this.cutoutBottom = cutoutBottom;
|
||||
this.settings = settings;
|
||||
this.clockTime = clockTime;
|
||||
this.clockView = clockView;
|
||||
this.clockContainer = clockContainer;
|
||||
this.statusRoot = statusRoot;
|
||||
this.statusEndContent = statusEndContent;
|
||||
this.statusIcons = statusIcons;
|
||||
this.battery = battery;
|
||||
this.notificationRoot = notificationRoot;
|
||||
this.statusSources = statusSources;
|
||||
this.notificationSources = notificationSources;
|
||||
this.chipSources = chipSources;
|
||||
}
|
||||
|
||||
int stockSurfaceSignature() {
|
||||
return java.util.Objects.hash(
|
||||
rootWidth,
|
||||
rootHeight,
|
||||
rootPaddingLeft,
|
||||
rootPaddingRight,
|
||||
settings,
|
||||
statusRoot,
|
||||
statusEndContent,
|
||||
statusIcons,
|
||||
battery,
|
||||
notificationRoot,
|
||||
statusSources,
|
||||
notificationSources);
|
||||
}
|
||||
|
||||
int nonClockLayoutSignature() {
|
||||
return java.util.Objects.hash(
|
||||
scene,
|
||||
rootWidth,
|
||||
rootHeight,
|
||||
rootPaddingLeft,
|
||||
rootPaddingRight,
|
||||
cutoutLeft,
|
||||
cutoutTop,
|
||||
cutoutRight,
|
||||
cutoutBottom,
|
||||
settings,
|
||||
statusRoot,
|
||||
statusEndContent,
|
||||
statusIcons,
|
||||
battery,
|
||||
notificationRoot,
|
||||
statusSources,
|
||||
notificationSources,
|
||||
chipSources);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (!(other instanceof LayoutInputSignature that)) {
|
||||
return false;
|
||||
}
|
||||
return rootWidth == that.rootWidth
|
||||
&& rootHeight == that.rootHeight
|
||||
&& rootPaddingLeft == that.rootPaddingLeft
|
||||
&& rootPaddingRight == that.rootPaddingRight
|
||||
&& cutoutLeft == that.cutoutLeft
|
||||
&& cutoutTop == that.cutoutTop
|
||||
&& cutoutRight == that.cutoutRight
|
||||
&& cutoutBottom == that.cutoutBottom
|
||||
&& settings == that.settings
|
||||
&& clockTime == that.clockTime
|
||||
&& clockView == that.clockView
|
||||
&& clockContainer == that.clockContainer
|
||||
&& statusRoot == that.statusRoot
|
||||
&& statusEndContent == that.statusEndContent
|
||||
&& statusIcons == that.statusIcons
|
||||
&& battery == that.battery
|
||||
&& notificationRoot == that.notificationRoot
|
||||
&& statusSources == that.statusSources
|
||||
&& notificationSources == that.notificationSources
|
||||
&& chipSources == that.chipSources
|
||||
&& java.util.Objects.equals(scene, that.scene);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return java.util.Objects.hash(
|
||||
scene,
|
||||
rootWidth,
|
||||
rootHeight,
|
||||
rootPaddingLeft,
|
||||
rootPaddingRight,
|
||||
cutoutLeft,
|
||||
cutoutTop,
|
||||
cutoutRight,
|
||||
cutoutBottom,
|
||||
settings,
|
||||
clockTime,
|
||||
clockView,
|
||||
clockContainer,
|
||||
statusRoot,
|
||||
statusEndContent,
|
||||
statusIcons,
|
||||
battery,
|
||||
notificationRoot,
|
||||
statusSources,
|
||||
notificationSources,
|
||||
chipSources);
|
||||
}
|
||||
}
|
||||
|
||||
final class LayoutPlan {
|
||||
final ClockPlacement clockPlacement;
|
||||
final ArrayList<SnapshotPlacement> chipPlacements;
|
||||
final ArrayList<SnapshotPlacement> statusPlacements;
|
||||
final ArrayList<SnapshotPlacement> notificationPlacements;
|
||||
|
||||
LayoutPlan(
|
||||
ClockPlacement clockPlacement,
|
||||
ArrayList<SnapshotPlacement> chipPlacements,
|
||||
ArrayList<SnapshotPlacement> statusPlacements,
|
||||
ArrayList<SnapshotPlacement> notificationPlacements
|
||||
) {
|
||||
this.clockPlacement = clockPlacement;
|
||||
this.chipPlacements = chipPlacements;
|
||||
this.statusPlacements = statusPlacements;
|
||||
this.notificationPlacements = notificationPlacements;
|
||||
}
|
||||
|
||||
LayoutPlan withClockPlacement(ClockPlacement placement) {
|
||||
return new LayoutPlan(
|
||||
placement,
|
||||
chipPlacements,
|
||||
statusPlacements,
|
||||
notificationPlacements);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,697 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
|
||||
final class SnapshotRenderer {
|
||||
private static final long TRANSITION_SIGNAL_HOLD_MS = 700L;
|
||||
|
||||
private final boolean holdSignalsDuringTransition;
|
||||
private final boolean sortPlacementsByX;
|
||||
private final boolean trimViewContentForCollision;
|
||||
private final WeakHashMap<ViewGroup, LinkedHashMap<String, ImageView>> proxiesByHost =
|
||||
new WeakHashMap<>();
|
||||
private final WeakHashMap<ImageView, SnapshotRenderState> statesByProxy =
|
||||
new WeakHashMap<>();
|
||||
private final LinkedHashMap<String, HeldSignal> heldSignals = new LinkedHashMap<>();
|
||||
|
||||
private long holdSignalsUntilElapsedMs;
|
||||
|
||||
SnapshotRenderer(
|
||||
boolean holdSignalsDuringTransition,
|
||||
boolean sortPlacementsByX,
|
||||
boolean trimViewContentForCollision
|
||||
) {
|
||||
this.holdSignalsDuringTransition = holdSignalsDuringTransition;
|
||||
this.sortPlacementsByX = sortPlacementsByX;
|
||||
this.trimViewContentForCollision = trimViewContentForCollision;
|
||||
}
|
||||
|
||||
void beginTransitionHold() {
|
||||
if (holdSignalsDuringTransition) {
|
||||
holdSignalsUntilElapsedMs = SystemClock.elapsedRealtime() + TRANSITION_SIGNAL_HOLD_MS;
|
||||
}
|
||||
}
|
||||
|
||||
ArrayList<SnapshotPlacement> rawPlacements(
|
||||
View root,
|
||||
ArrayList<SnapshotSource> sources
|
||||
) {
|
||||
ArrayList<SnapshotPlacement> placements = new ArrayList<>();
|
||||
int order = 0;
|
||||
for (SnapshotSource source : sources) {
|
||||
Bounds bounds = visualBoundsRelativeTo(root, source.view());
|
||||
if (bounds != null && bounds.width > 0 && bounds.height > 0) {
|
||||
if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE) {
|
||||
bounds = notificationDrawableBounds(bounds);
|
||||
}
|
||||
String key = normalizedKeyFor(source, order);
|
||||
boolean signal = isSignalKey(key);
|
||||
boolean trimContent = !signal && shouldTrimContent(source);
|
||||
Bounds contentBounds = trimContent ? snapshotContentBounds(source, bounds) : null;
|
||||
if (trimContent && contentBounds == null) {
|
||||
continue;
|
||||
}
|
||||
HeldSignal heldSignal = heldSignalForTransition(key);
|
||||
if (signal
|
||||
&& heldSignal != null
|
||||
&& !isSignalSettled(source.view(), key, bounds)) {
|
||||
placements.add(new SnapshotPlacement(
|
||||
null,
|
||||
heldBoundsForCurrentRow(heldSignal, bounds),
|
||||
key,
|
||||
true,
|
||||
false,
|
||||
heldSignal,
|
||||
null,
|
||||
0));
|
||||
} else {
|
||||
Bounds placementBounds = signal
|
||||
? normalizedStatusBounds(new SnapshotPlacement(
|
||||
source,
|
||||
bounds,
|
||||
key,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
0))
|
||||
: bounds;
|
||||
placements.add(new SnapshotPlacement(
|
||||
source,
|
||||
placementBounds,
|
||||
key,
|
||||
signal,
|
||||
false,
|
||||
null,
|
||||
contentBounds,
|
||||
0));
|
||||
}
|
||||
}
|
||||
order++;
|
||||
}
|
||||
if (sortPlacementsByX) {
|
||||
placements.sort(Comparator.comparingInt(placement -> placement.bounds().left));
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
Bounds snapshotContentBounds(SnapshotSource source, Bounds bounds) {
|
||||
if (source == null || bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createBitmap(bounds.width, bounds.height, Bitmap.Config.ARGB_8888);
|
||||
try {
|
||||
drawSnapshot(source, bitmap);
|
||||
return bitmapContentBounds(bitmap);
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
Bounds renderPlacements(
|
||||
ViewGroup host,
|
||||
ArrayList<SnapshotPlacement> placements
|
||||
) {
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
clear(host);
|
||||
return null;
|
||||
}
|
||||
LinkedHashMap<String, ImageView> proxies = proxiesFor(host);
|
||||
HashSet<String> desiredKeys = new HashSet<>();
|
||||
boolean structureChanged = false;
|
||||
Bounds renderedBounds = null;
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds bounds = placement.bounds();
|
||||
if (bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
String key = placement.key();
|
||||
desiredKeys.add(key);
|
||||
ImageView proxy = proxies.get(key);
|
||||
boolean proxyChanged = false;
|
||||
if (proxy == null) {
|
||||
proxy = new ImageView(host.getContext());
|
||||
proxy.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
proxies.put(key, proxy);
|
||||
host.addView(proxy);
|
||||
structureChanged = true;
|
||||
proxyChanged = true;
|
||||
} else if (proxy.getParent() != host) {
|
||||
detachFromParent(proxy);
|
||||
host.addView(proxy);
|
||||
structureChanged = true;
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (!java.util.Objects.equals(proxy.getTag(), key)) {
|
||||
proxy.setTag(key);
|
||||
proxyChanged = true;
|
||||
}
|
||||
BitmapResult bitmapResult = bitmapFor(proxy, bounds);
|
||||
Bitmap bitmap = bitmapResult.bitmap();
|
||||
SnapshotRenderState desiredState = renderStateFor(placement);
|
||||
boolean bitmapChanged = bitmapResult.created()
|
||||
|| !desiredState.equals(statesByProxy.get(proxy));
|
||||
if (bitmapChanged) {
|
||||
if (placement.overflowDot()) {
|
||||
drawOverflowDot(bitmap);
|
||||
} else if (placement.heldSignal() != null) {
|
||||
drawHeldSignal(placement.heldSignal(), bitmap);
|
||||
} else if (placement.source() != null) {
|
||||
drawSnapshot(placement.source(), bitmap, placement.sourceOffsetX());
|
||||
if (placement.signal()) {
|
||||
saveHeldSignal(key, bounds, bitmap);
|
||||
}
|
||||
} else {
|
||||
bitmap.eraseColor(Color.TRANSPARENT);
|
||||
}
|
||||
statesByProxy.put(proxy, desiredState);
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (proxy.getDrawable() == null) {
|
||||
proxy.setImageBitmap(bitmap);
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (proxy.getAlpha() != 1f) {
|
||||
proxy.setAlpha(1f);
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (proxy.getVisibility() != View.VISIBLE) {
|
||||
proxy.setVisibility(View.VISIBLE);
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (applyLayoutParamsIfChanged(host, proxy, bounds)) {
|
||||
structureChanged = true;
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (proxy.getLeft() != bounds.left
|
||||
|| proxy.getTop() != bounds.top
|
||||
|| proxy.getRight() != bounds.left + bounds.width
|
||||
|| proxy.getBottom() != bounds.top + bounds.height) {
|
||||
proxy.layout(bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height);
|
||||
proxyChanged = true;
|
||||
}
|
||||
if (proxyChanged) {
|
||||
proxy.invalidate();
|
||||
}
|
||||
renderedBounds = renderedBounds == null ? bounds : renderedBounds.union(bounds);
|
||||
}
|
||||
int removedMissing = removeMissing(proxies, desiredKeys);
|
||||
if (removedMissing > 0) {
|
||||
structureChanged = true;
|
||||
}
|
||||
pruneHeldSignals(placements);
|
||||
if (structureChanged) {
|
||||
host.requestLayout();
|
||||
host.invalidate();
|
||||
}
|
||||
return renderedBounds;
|
||||
}
|
||||
|
||||
void clear(ViewGroup host) {
|
||||
LinkedHashMap<String, ImageView> proxies = proxiesByHost.remove(host);
|
||||
if (proxies == null) {
|
||||
return;
|
||||
}
|
||||
for (ImageView proxy : proxies.values()) {
|
||||
detachFromParent(proxy);
|
||||
statesByProxy.remove(proxy);
|
||||
}
|
||||
host.requestLayout();
|
||||
host.invalidate();
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
for (ViewGroup host : new ArrayList<>(proxiesByHost.keySet())) {
|
||||
clear(host);
|
||||
}
|
||||
}
|
||||
|
||||
void clearHeldSignals() {
|
||||
for (HeldSignal heldSignal : heldSignals.values()) {
|
||||
heldSignal.recycle();
|
||||
}
|
||||
heldSignals.clear();
|
||||
holdSignalsUntilElapsedMs = 0L;
|
||||
}
|
||||
|
||||
private Bounds notificationDrawableBounds(Bounds sourceBounds) {
|
||||
int size = Math.max(1, Math.min(sourceBounds.width, sourceBounds.height));
|
||||
int left = sourceBounds.left + Math.max(0, (sourceBounds.width - size) / 2);
|
||||
int top = sourceBounds.top + Math.max(0, (sourceBounds.height - size) / 2);
|
||||
return new Bounds(left, top, size, size);
|
||||
}
|
||||
|
||||
private boolean shouldTrimContent(SnapshotSource source) {
|
||||
return trimViewContentForCollision && source != null && source.mode() == SnapshotMode.VIEW;
|
||||
}
|
||||
|
||||
private Bounds bitmapContentBounds(Bitmap bitmap) {
|
||||
if (bitmap == null || bitmap.isRecycled()) {
|
||||
return null;
|
||||
}
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
int left = width;
|
||||
int top = height;
|
||||
int right = -1;
|
||||
int bottom = -1;
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (Color.alpha(bitmap.getPixel(x, y)) == 0) {
|
||||
continue;
|
||||
}
|
||||
left = Math.min(left, x);
|
||||
top = Math.min(top, y);
|
||||
right = Math.max(right, x);
|
||||
bottom = Math.max(bottom, y);
|
||||
}
|
||||
}
|
||||
if (right < left || bottom < top) {
|
||||
return null;
|
||||
}
|
||||
return new Bounds(left, top, right - left + 1, bottom - top + 1);
|
||||
}
|
||||
|
||||
private Bounds heldBoundsForCurrentRow(HeldSignal heldSignal, Bounds currentBounds) {
|
||||
int height = Math.max(1, currentBounds.height);
|
||||
int width = heldSignal.bounds.width;
|
||||
if (heldSignal.bounds.height > 0) {
|
||||
width = Math.round((float) heldSignal.bounds.width * height / heldSignal.bounds.height);
|
||||
}
|
||||
return new Bounds(currentBounds.left, currentBounds.top, Math.max(1, width), height);
|
||||
}
|
||||
|
||||
private Bounds normalizedStatusBounds(SnapshotPlacement placement) {
|
||||
Bounds bounds = placement.bounds();
|
||||
int stableWidth = bounds.width;
|
||||
if (placement.signal()) {
|
||||
stableWidth = Math.min(stableWidth, stableSignalWidth(placement.key(), bounds.height));
|
||||
}
|
||||
return new Bounds(bounds.left, bounds.top, Math.max(1, stableWidth), bounds.height);
|
||||
}
|
||||
|
||||
private int stableSignalWidth(String key, int height) {
|
||||
if (key.contains("wifi")) {
|
||||
return Math.max(1, Math.round(height * 1.25f));
|
||||
}
|
||||
if (key.contains("mobile")) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return Math.max(1, height);
|
||||
}
|
||||
|
||||
private LinkedHashMap<String, ImageView> proxiesFor(ViewGroup host) {
|
||||
LinkedHashMap<String, ImageView> proxies = proxiesByHost.get(host);
|
||||
if (proxies != null) {
|
||||
return proxies;
|
||||
}
|
||||
LinkedHashMap<String, ImageView> created = new LinkedHashMap<>();
|
||||
proxiesByHost.put(host, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private String normalizedKeyFor(SnapshotSource source, int order) {
|
||||
String key = keyFor(source, order);
|
||||
String kind = signalKind(source.view(), key);
|
||||
return kind != null ? "signal:" + kind : key;
|
||||
}
|
||||
|
||||
private String keyFor(SnapshotSource source, int order) {
|
||||
View view = source.view();
|
||||
String slot = reflectString(view, "getSlot", "mSlot", "slot");
|
||||
if (!slot.isEmpty()) {
|
||||
return slot;
|
||||
}
|
||||
String key = reflectString(view, "getNotificationKey", "mNotificationKey", "notificationKey", "mKey", "key");
|
||||
if (!key.isEmpty()) {
|
||||
return key;
|
||||
}
|
||||
return view.getClass().getName() + "#" + order;
|
||||
}
|
||||
|
||||
private String signalKind(View view, String key) {
|
||||
String lowerKey = key.toLowerCase(java.util.Locale.ROOT);
|
||||
if (lowerKey.contains("wifi")) {
|
||||
return "wifi";
|
||||
}
|
||||
if (lowerKey.contains("mobile")) {
|
||||
return "mobile";
|
||||
}
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
if (idName.contains("wifi") || className.contains("wifi")) {
|
||||
return "wifi";
|
||||
}
|
||||
if (idName.contains("mobile") || className.contains("mobile")) {
|
||||
return "mobile";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isSignalKey(String key) {
|
||||
return key.startsWith("signal:");
|
||||
}
|
||||
|
||||
private HeldSignal heldSignalForTransition(String key) {
|
||||
return isTransitionHoldActive() ? heldSignals.get(key) : null;
|
||||
}
|
||||
|
||||
private boolean isTransitionHoldActive() {
|
||||
return holdSignalsDuringTransition
|
||||
&& SystemClock.elapsedRealtime() <= holdSignalsUntilElapsedMs;
|
||||
}
|
||||
|
||||
private boolean isSignalSettled(View source, String key, Bounds bounds) {
|
||||
if (source.getAlpha() < 0.1f) {
|
||||
return false;
|
||||
}
|
||||
return bounds.width <= stableSignalWidth(key, bounds.height) + 2;
|
||||
}
|
||||
|
||||
private void saveHeldSignal(String key, Bounds bounds, Bitmap bitmap) {
|
||||
if (!holdSignalsDuringTransition || !isSignalKey(key)) {
|
||||
return;
|
||||
}
|
||||
Bitmap copy = bitmap.copy(Bitmap.Config.ARGB_8888, false);
|
||||
HeldSignal previous = heldSignals.put(key, new HeldSignal(key, bounds, copy));
|
||||
if (previous != null) {
|
||||
previous.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private void pruneHeldSignals(ArrayList<SnapshotPlacement> placements) {
|
||||
if (!holdSignalsDuringTransition || isTransitionHoldActive() || heldSignals.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
HashSet<String> liveSignalKeys = new HashSet<>();
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (placement.signal() && placement.source() != null) {
|
||||
liveSignalKeys.add(placement.key());
|
||||
}
|
||||
}
|
||||
ArrayList<String> staleKeys = new ArrayList<>();
|
||||
for (String key : heldSignals.keySet()) {
|
||||
if (!liveSignalKeys.contains(key)) {
|
||||
staleKeys.add(key);
|
||||
}
|
||||
}
|
||||
for (String key : staleKeys) {
|
||||
HeldSignal stale = heldSignals.remove(key);
|
||||
if (stale != null) {
|
||||
stale.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int removeMissing(
|
||||
LinkedHashMap<String, ImageView> proxies,
|
||||
HashSet<String> desiredKeys
|
||||
) {
|
||||
ArrayList<String> staleKeys = new ArrayList<>();
|
||||
for (Map.Entry<String, ImageView> entry : proxies.entrySet()) {
|
||||
if (!desiredKeys.contains(entry.getKey())) {
|
||||
ImageView proxy = entry.getValue();
|
||||
detachFromParent(proxy);
|
||||
statesByProxy.remove(proxy);
|
||||
staleKeys.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
for (String key : staleKeys) {
|
||||
proxies.remove(key);
|
||||
}
|
||||
return staleKeys.size();
|
||||
}
|
||||
|
||||
private BitmapResult bitmapFor(ImageView proxy, Bounds bounds) {
|
||||
Drawable drawable = proxy.getDrawable();
|
||||
if (drawable instanceof BitmapDrawable bitmapDrawable) {
|
||||
Bitmap bitmap = bitmapDrawable.getBitmap();
|
||||
if (bitmap != null
|
||||
&& !bitmap.isRecycled()
|
||||
&& bitmap.getWidth() == bounds.width
|
||||
&& bitmap.getHeight() == bounds.height) {
|
||||
return new BitmapResult(bitmap, false);
|
||||
}
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createBitmap(bounds.width, bounds.height, Bitmap.Config.ARGB_8888);
|
||||
proxy.setImageBitmap(bitmap);
|
||||
return new BitmapResult(bitmap, true);
|
||||
}
|
||||
|
||||
private SnapshotRenderState renderStateFor(SnapshotPlacement placement) {
|
||||
Bounds bounds = placement.bounds();
|
||||
return new SnapshotRenderState(
|
||||
placement.key(),
|
||||
bounds.left,
|
||||
bounds.top,
|
||||
bounds.width,
|
||||
bounds.height,
|
||||
placement.signal(),
|
||||
placement.overflowDot(),
|
||||
placement.sourceOffsetX(),
|
||||
placement.heldSignal() != null ? placement.heldSignal().key : "",
|
||||
placement.heldSignal() != null ? System.identityHashCode(placement.heldSignal().bitmap) : 0,
|
||||
placement.source() != null ? placement.source().mode() : null,
|
||||
placement.source() != null ? sourceTreeSignature(placement.source().view()) : 0);
|
||||
}
|
||||
|
||||
private int sourceTreeSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(view);
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(view.getAlpha());
|
||||
result = 31 * result + view.getWidth();
|
||||
result = 31 * result + view.getHeight();
|
||||
result = 31 * result + view.getScrollX();
|
||||
result = 31 * result + view.getScrollY();
|
||||
CharSequence contentDescription = view.getContentDescription();
|
||||
result = 31 * result + (contentDescription != null ? contentDescription.toString().hashCode() : 0);
|
||||
result = 31 * result + java.util.Arrays.hashCode(view.getDrawableState());
|
||||
if (view instanceof TextView textView) {
|
||||
CharSequence text = textView.getText();
|
||||
result = 31 * result + (text != null ? text.toString().hashCode() : 0);
|
||||
result = 31 * result + textView.getCurrentTextColor();
|
||||
result = 31 * result + Float.floatToIntBits(textView.getTextSize());
|
||||
}
|
||||
if (view instanceof ImageView imageView) {
|
||||
result = 31 * result + drawableSignature(imageView.getDrawable());
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
result = 31 * result + sourceTreeSignature(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int drawableSignature(Drawable drawable) {
|
||||
if (drawable == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(drawable);
|
||||
result = 31 * result + drawable.getLevel();
|
||||
result = 31 * result + drawable.getAlpha();
|
||||
result = 31 * result + java.util.Arrays.hashCode(drawable.getState());
|
||||
Rect bounds = drawable.getBounds();
|
||||
result = 31 * result + bounds.left;
|
||||
result = 31 * result + bounds.top;
|
||||
result = 31 * result + bounds.right;
|
||||
result = 31 * result + bounds.bottom;
|
||||
return result;
|
||||
}
|
||||
|
||||
private void drawSnapshot(SnapshotSource source, Bitmap bitmap) {
|
||||
drawSnapshot(source, bitmap, 0);
|
||||
}
|
||||
|
||||
private void drawSnapshot(SnapshotSource source, Bitmap bitmap, int offsetX) {
|
||||
bitmap.eraseColor(Color.TRANSPARENT);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
if (offsetX != 0) {
|
||||
canvas.translate(-offsetX, 0);
|
||||
}
|
||||
View sourceView = source.view();
|
||||
int oldVisibility = sourceView.getVisibility();
|
||||
float oldAlpha = sourceView.getAlpha();
|
||||
try {
|
||||
if (source.mode() == SnapshotMode.NOTIFICATION_DRAWABLE
|
||||
&& drawNotificationDrawable(
|
||||
sourceView,
|
||||
canvas,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight())) {
|
||||
return;
|
||||
}
|
||||
sourceView.setVisibility(View.VISIBLE);
|
||||
sourceView.setAlpha(1f);
|
||||
sourceView.draw(canvas);
|
||||
} finally {
|
||||
sourceView.setAlpha(oldAlpha);
|
||||
sourceView.setVisibility(oldVisibility);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawHeldSignal(HeldSignal heldSignal, Bitmap bitmap) {
|
||||
bitmap.eraseColor(Color.TRANSPARENT);
|
||||
if (heldSignal.bitmap == null || heldSignal.bitmap.isRecycled()) {
|
||||
return;
|
||||
}
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Rect src = new Rect(0, 0, heldSignal.bitmap.getWidth(), heldSignal.bitmap.getHeight());
|
||||
Rect dst = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
canvas.drawBitmap(heldSignal.bitmap, src, dst, null);
|
||||
}
|
||||
|
||||
private void drawOverflowDot(Bitmap bitmap) {
|
||||
bitmap.eraseColor(Color.TRANSPARENT);
|
||||
int base = Math.min(bitmap.getWidth(), bitmap.getHeight());
|
||||
if (base <= 0) {
|
||||
return;
|
||||
}
|
||||
float radius = Math.max(1f, base * 0.16f);
|
||||
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
paint.setColor(Color.WHITE);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
canvas.drawCircle(bitmap.getWidth() / 2f, bitmap.getHeight() / 2f, radius, paint);
|
||||
}
|
||||
|
||||
private boolean drawNotificationDrawable(View source, Canvas canvas, int width, int height) {
|
||||
Drawable drawable = findDrawable(source);
|
||||
if (drawable == null || width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
Rect oldBounds = new Rect(drawable.getBounds());
|
||||
int size = Math.min(width, height);
|
||||
if (size <= 0) {
|
||||
size = Math.max(width, height);
|
||||
}
|
||||
int left = Math.max(0, (width - size) / 2);
|
||||
int top = Math.max(0, (height - size) / 2);
|
||||
drawable.setBounds(left, top, left + size, top + size);
|
||||
drawable.draw(canvas);
|
||||
drawable.setBounds(oldBounds);
|
||||
return true;
|
||||
}
|
||||
|
||||
private Drawable findDrawable(View source) {
|
||||
if (source instanceof ImageView imageView && imageView.getDrawable() != null) {
|
||||
return imageView.getDrawable();
|
||||
}
|
||||
if (source instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
Drawable drawable = findDrawable(group.getChildAt(i));
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ViewGroup.LayoutParams layoutParamsFor(ViewGroup host, Bounds bounds) {
|
||||
if (host instanceof FrameLayout) {
|
||||
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(bounds.width, bounds.height);
|
||||
lp.leftMargin = bounds.left;
|
||||
lp.topMargin = bounds.top;
|
||||
return lp;
|
||||
}
|
||||
ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(bounds.width, bounds.height);
|
||||
lp.leftMargin = bounds.left;
|
||||
lp.topMargin = bounds.top;
|
||||
return lp;
|
||||
}
|
||||
|
||||
private boolean applyLayoutParamsIfChanged(ViewGroup host, ImageView proxy, Bounds bounds) {
|
||||
ViewGroup.LayoutParams current = proxy.getLayoutParams();
|
||||
if (current != null
|
||||
&& current.width == bounds.width
|
||||
&& current.height == bounds.height
|
||||
&& current instanceof ViewGroup.MarginLayoutParams marginParams
|
||||
&& marginParams.leftMargin == bounds.left
|
||||
&& marginParams.topMargin == bounds.top) {
|
||||
return false;
|
||||
}
|
||||
proxy.setLayoutParams(layoutParamsFor(host, bounds));
|
||||
return true;
|
||||
}
|
||||
|
||||
private Bounds visualBoundsRelativeTo(View root, View view) {
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
View current = view;
|
||||
while (current != null && current != root) {
|
||||
left += current.getLeft() - current.getScrollX() + Math.round(current.getTranslationX());
|
||||
top += current.getTop() - current.getScrollY() + Math.round(current.getTranslationY());
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
if (current != root) {
|
||||
return null;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
return new Bounds(left, top, Math.max(0, width), Math.max(0, height));
|
||||
}
|
||||
|
||||
private static String resolveIdName(View view) {
|
||||
if (view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String reflectString(Object target, String methodName, String... fieldNames) {
|
||||
Object methodValue = ReflectionSupport.invokeMethod(target, methodName, new Class<?>[0]);
|
||||
if (methodValue instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
for (String fieldName : fieldNames) {
|
||||
Object fieldValue = ReflectionSupport.getFieldValue(target, fieldName);
|
||||
if (fieldValue instanceof String string && !string.isEmpty()) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static void detachFromParent(View view) {
|
||||
if (view != null && view.getParent() instanceof ViewGroup parent) {
|
||||
parent.removeView(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class UnlockedChipPlacementController {
|
||||
private final SnapshotRenderer renderer;
|
||||
private final UnlockedVerticalOffsetScaler offsetScaler;
|
||||
private ChipHoldState heldPlacements;
|
||||
private Bounds lastCollisionBounds;
|
||||
private int lastRootWidth;
|
||||
private int lastRootHeight;
|
||||
|
||||
UnlockedChipPlacementController(SnapshotRenderer renderer, UnlockedVerticalOffsetScaler offsetScaler) {
|
||||
this.renderer = renderer;
|
||||
this.offsetScaler = offsetScaler;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
heldPlacements = null;
|
||||
lastCollisionBounds = null;
|
||||
lastRootWidth = 0;
|
||||
lastRootHeight = 0;
|
||||
}
|
||||
|
||||
ArrayList<SnapshotPlacement> placements(
|
||||
View root,
|
||||
ArrayList<SnapshotSource> visibleSources,
|
||||
ArrayList<SnapshotSource> metricSources,
|
||||
SbtSettings settings
|
||||
) {
|
||||
ArrayList<SnapshotPlacement> rawPlacements = renderer.rawPlacements(root, visibleSources);
|
||||
if (!rawPlacements.isEmpty()) {
|
||||
rememberMetrics(root, rawPlacements);
|
||||
ArrayList<SnapshotPlacement> placements = offsetPlacements(
|
||||
rawPlacements,
|
||||
offsetScaler.chip(
|
||||
settings.layoutChipVerticalOffsetPx,
|
||||
representativeChipHeight(rawPlacements)));
|
||||
heldPlacements = new ChipHoldState(
|
||||
copyAsHeldPlacements(placements),
|
||||
root.getWidth(),
|
||||
root.getHeight(),
|
||||
0);
|
||||
return placements;
|
||||
}
|
||||
|
||||
ArrayList<SnapshotPlacement> placements = isAnyChipHidingEnabled(settings)
|
||||
? new ArrayList<>()
|
||||
: heldPlacements(root);
|
||||
if (placements.isEmpty()) {
|
||||
placements = emptyPlacement(root, metricSources, settings);
|
||||
}
|
||||
return placements;
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> heldPlacements(View root) {
|
||||
ChipHoldState state = heldPlacements;
|
||||
if (state == null || state.placements.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (state.rootWidth != root.getWidth() || state.rootHeight != root.getHeight()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (state.missingFrames > 0) {
|
||||
heldPlacements = null;
|
||||
return new ArrayList<>();
|
||||
}
|
||||
state = new ChipHoldState(
|
||||
state.placements,
|
||||
state.rootWidth,
|
||||
state.rootHeight,
|
||||
state.missingFrames + 1);
|
||||
heldPlacements = state;
|
||||
return new ArrayList<>(state.placements);
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> emptyPlacement(
|
||||
View root,
|
||||
ArrayList<SnapshotSource> sources,
|
||||
SbtSettings settings
|
||||
) {
|
||||
ArrayList<SnapshotPlacement> result = new ArrayList<>();
|
||||
Bounds bounds = emptyBounds(root, sources);
|
||||
if (bounds == null || bounds.height <= 0) {
|
||||
return result;
|
||||
}
|
||||
int verticalOffset = offsetScaler.chip(
|
||||
settings != null ? settings.layoutChipVerticalOffsetPx : 0,
|
||||
bounds.height);
|
||||
result.add(new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(0, bounds.top - verticalOffset, 1, bounds.height),
|
||||
SnapshotKeys.EMPTY_CHIP,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
new Bounds(0, 0, 0, bounds.height),
|
||||
0));
|
||||
return result;
|
||||
}
|
||||
|
||||
private Bounds emptyBounds(View root, ArrayList<SnapshotSource> sources) {
|
||||
if (root != null
|
||||
&& lastCollisionBounds != null
|
||||
&& lastRootWidth == root.getWidth()
|
||||
&& lastRootHeight == root.getHeight()) {
|
||||
return new Bounds(0, lastCollisionBounds.top, 0, lastCollisionBounds.height);
|
||||
}
|
||||
return metricBoundsFromSource(root, sources);
|
||||
}
|
||||
|
||||
private Bounds metricBoundsFromSource(View root, ArrayList<SnapshotSource> sources) {
|
||||
if (root == null || sources == null || sources.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
for (SnapshotSource source : sources) {
|
||||
Bounds bounds = ViewGeometry.boundsRelativeTo(root, source.view());
|
||||
if (bounds == null || bounds.width <= 0 || bounds.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
Bounds content = renderer.snapshotContentBounds(source, bounds);
|
||||
if (content != null && content.height > 0) {
|
||||
return new Bounds(0, bounds.top + content.top, 0, content.height);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void rememberMetrics(View root, ArrayList<SnapshotPlacement> placements) {
|
||||
Bounds bounds = collisionUnion(placements);
|
||||
if (root != null && bounds != null && bounds.height > 0) {
|
||||
lastCollisionBounds = bounds;
|
||||
lastRootWidth = root.getWidth();
|
||||
lastRootHeight = root.getHeight();
|
||||
}
|
||||
}
|
||||
|
||||
private Bounds collisionUnion(ArrayList<SnapshotPlacement> placements) {
|
||||
Bounds union = null;
|
||||
if (placements == null) {
|
||||
return null;
|
||||
}
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds collision = collisionBounds(placement);
|
||||
if (collision == null || collision.height <= 0) {
|
||||
continue;
|
||||
}
|
||||
union = union == null ? collision : union.union(collision);
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
private Bounds collisionBounds(SnapshotPlacement placement) {
|
||||
if (placement == null || placement.bounds == null) {
|
||||
return null;
|
||||
}
|
||||
Bounds relative = placement.contentBounds;
|
||||
if (relative == null || relative.height <= 0 || relative.width < 0) {
|
||||
relative = new Bounds(0, 0, placement.bounds.width, placement.bounds.height);
|
||||
}
|
||||
return new Bounds(
|
||||
placement.bounds.left + relative.left,
|
||||
placement.bounds.top + relative.top,
|
||||
relative.width,
|
||||
relative.height);
|
||||
}
|
||||
|
||||
private boolean isAnyChipHidingEnabled(SbtSettings settings) {
|
||||
return settings != null
|
||||
&& (settings.statusChipsHideAll
|
||||
|| settings.statusChipsHideMediaUnlocked
|
||||
|| settings.statusChipsHideNavUnlocked
|
||||
|| settings.statusChipsHideCallUnlocked);
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> copyAsHeldPlacements(ArrayList<SnapshotPlacement> placements) {
|
||||
ArrayList<SnapshotPlacement> held = new ArrayList<>();
|
||||
if (placements == null) {
|
||||
return held;
|
||||
}
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
held.add(new SnapshotPlacement(
|
||||
null,
|
||||
placement.bounds,
|
||||
placement.key,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
placement.contentBounds,
|
||||
placement.sourceOffsetX));
|
||||
}
|
||||
return held;
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> offsetPlacements(ArrayList<SnapshotPlacement> placements, int verticalOffsetPx) {
|
||||
if (placements == null || placements.isEmpty() || verticalOffsetPx == 0) {
|
||||
return placements;
|
||||
}
|
||||
ArrayList<SnapshotPlacement> result = new ArrayList<>();
|
||||
int dy = -verticalOffsetPx;
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds bounds = placement.bounds;
|
||||
result.add(new SnapshotPlacement(
|
||||
placement.source,
|
||||
new Bounds(bounds.left, bounds.top + dy, bounds.width, bounds.height),
|
||||
placement.key,
|
||||
placement.signal,
|
||||
placement.overflowDot,
|
||||
placement.heldSignal,
|
||||
placement.contentBounds,
|
||||
placement.sourceOffsetX));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int representativeChipHeight(ArrayList<SnapshotPlacement> placements) {
|
||||
Bounds bounds = collisionUnion(placements);
|
||||
return bounds != null ? Math.max(0, bounds.height) : representativeHeight(placements);
|
||||
}
|
||||
|
||||
private int representativeHeight(ArrayList<SnapshotPlacement> placements) {
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, placements.get(0).bounds.height);
|
||||
}
|
||||
|
||||
private record ChipHoldState(
|
||||
ArrayList<SnapshotPlacement> placements,
|
||||
int rootWidth,
|
||||
int rootHeight,
|
||||
int missingFrames
|
||||
) {
|
||||
}
|
||||
}
|
||||
+545
@@ -0,0 +1,545 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.features.clock.ClockLayoutTextFactory;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
/**
|
||||
* First unlocked renderer that does not mutate stock icon views or drawables.
|
||||
*
|
||||
* It snapshots visible stock icon views into owned ImageViews, then packs them
|
||||
* into owned unlocked rows instead of copying each stock X position.
|
||||
*/
|
||||
public final class UnlockedIconSnapshotRenderController {
|
||||
private final SnapshotRenderer statusRenderer;
|
||||
private final SnapshotRenderer notificationRenderer;
|
||||
private final SnapshotRenderer chipRenderer;
|
||||
private final ClockProxyRenderer clockRenderer = new ClockProxyRenderer();
|
||||
private final UnlockedStockSourceCollector sourceCollector = new UnlockedStockSourceCollector();
|
||||
private final UnlockedVerticalOffsetScaler offsetScaler = new UnlockedVerticalOffsetScaler();
|
||||
private final UnlockedChipPlacementController chipPlacementController;
|
||||
private final UnlockedLayoutPlanner layoutPlanner;
|
||||
private final WeakHashMap<View, Integer> clockBaselineDeltaByRoot = new WeakHashMap<>();
|
||||
private final WeakHashMap<View, LayoutInputSignature> lastLayoutSignatureByRoot = new WeakHashMap<>();
|
||||
private final WeakHashMap<View, CollectedIcons> lastCollectedIconsByRoot = new WeakHashMap<>();
|
||||
private final WeakHashMap<View, LayoutPlan> lastLayoutPlanByRoot = new WeakHashMap<>();
|
||||
private final WeakHashMap<View, Integer> lastClockGeometryByRoot = new WeakHashMap<>();
|
||||
private final WeakHashMap<View, Integer> lastClockSourceGeometryByRoot = new WeakHashMap<>();
|
||||
|
||||
public UnlockedIconSnapshotRenderController() {
|
||||
statusRenderer = new SnapshotRenderer(true, true, false);
|
||||
notificationRenderer = new SnapshotRenderer(false, false, false);
|
||||
chipRenderer = new SnapshotRenderer(false, false, true);
|
||||
chipPlacementController = new UnlockedChipPlacementController(chipRenderer, offsetScaler);
|
||||
layoutPlanner = new UnlockedLayoutPlanner(
|
||||
statusRenderer,
|
||||
notificationRenderer,
|
||||
chipRenderer,
|
||||
chipPlacementController,
|
||||
offsetScaler);
|
||||
}
|
||||
|
||||
public RenderResult renderUnlocked(
|
||||
View root,
|
||||
ViewGroup clockHost,
|
||||
ViewGroup chipHost,
|
||||
ViewGroup statusHost,
|
||||
ViewGroup notificationHost,
|
||||
SceneKey scene
|
||||
) {
|
||||
if (scene == null || !scene.isUnlocked() || !root.isAttachedToWindow()) {
|
||||
clearAll();
|
||||
return new RenderResult(true, true);
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
layoutHostToRoot(root, clockHost);
|
||||
layoutHostToRoot(root, chipHost);
|
||||
layoutHostToRoot(root, statusHost);
|
||||
layoutHostToRoot(root, notificationHost);
|
||||
RootAnchors anchors = sourceCollector.anchorsFor(root);
|
||||
CollectedIcons previousIcons = lastCollectedIconsByRoot.get(root);
|
||||
LayoutInputSignature signature = sourceCollector.layoutInputSignature(
|
||||
root,
|
||||
settings,
|
||||
scene,
|
||||
ViewGeometry.middleCutout(root),
|
||||
anchors,
|
||||
previousIcons);
|
||||
LayoutInputSignature previousSignature = lastLayoutSignatureByRoot.get(root);
|
||||
boolean stockSourcesChanged = previousSignature == null
|
||||
|| signature.stockSurfaceSignature() != previousSignature.stockSurfaceSignature();
|
||||
boolean chipSourcesChanged = previousSignature == null
|
||||
|| signature.chipSources != previousSignature.chipSources;
|
||||
if (signature.equals(previousSignature)) {
|
||||
return new RenderResult(false, false);
|
||||
}
|
||||
lastLayoutSignatureByRoot.put(root, signature);
|
||||
Integer currentClockSourceGeometry = clockSourceGeometrySignature(
|
||||
anchors != null ? anchors.clockView : null);
|
||||
ClockLayout currentClockLayout = null;
|
||||
Integer currentClockGeometry = null;
|
||||
if (isClockOnlySignatureChange(previousSignature, signature)
|
||||
&& settings.clockEnabledUnlocked
|
||||
&& previousIcons != null) {
|
||||
LayoutPlan previousPlan = lastLayoutPlanByRoot.get(root);
|
||||
if (previousPlan != null
|
||||
&& java.util.Objects.equals(
|
||||
lastClockSourceGeometryByRoot.get(root),
|
||||
currentClockSourceGeometry)) {
|
||||
ClockPlacement updatedClock = clockPlacementWithTextFromModel(
|
||||
previousPlan.clockPlacement,
|
||||
anchors != null ? anchors.clockView : null,
|
||||
settings);
|
||||
if (updatedClock != null) {
|
||||
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
||||
clockRenderer.render(clockHost, updatedClock);
|
||||
return new RenderResult(false, false);
|
||||
}
|
||||
}
|
||||
currentClockLayout = buildClockLayout(root, anchors, settings);
|
||||
currentClockGeometry = clockGeometrySignature(
|
||||
root,
|
||||
currentClockLayout,
|
||||
settings,
|
||||
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings.clockMiddleCutoutGapPx));
|
||||
ClockPlacement textPlacement = clockTextPlacementForPrevious(
|
||||
root,
|
||||
currentClockLayout,
|
||||
settings,
|
||||
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings.clockMiddleCutoutGapPx),
|
||||
previousPlan != null ? previousPlan.clockPlacement : null);
|
||||
ClockPlacement updatedClock = clockPlacementWithUpdatedText(
|
||||
previousPlan != null ? previousPlan.clockPlacement : null,
|
||||
textPlacement);
|
||||
Integer previousClockGeometry = lastClockGeometryByRoot.get(root);
|
||||
String missReason = clockFastPathMissReason(
|
||||
previousPlan,
|
||||
previousClockGeometry,
|
||||
currentClockGeometry,
|
||||
updatedClock);
|
||||
if (missReason == null) {
|
||||
LayoutPlan updatedPlan = previousPlan.withClockPlacement(updatedClock);
|
||||
lastLayoutPlanByRoot.put(root, updatedPlan);
|
||||
clockRenderer.render(clockHost, updatedClock);
|
||||
return new RenderResult(false, false);
|
||||
}
|
||||
}
|
||||
CollectedIcons collectedIcons = previousIcons;
|
||||
if (stockSourcesChanged || collectedIcons == null) {
|
||||
collectedIcons = sourceCollector.collect(root, anchors);
|
||||
lastCollectedIconsByRoot.put(root, collectedIcons);
|
||||
} else if (chipSourcesChanged) {
|
||||
collectedIcons = sourceCollector.collectChips(root, anchors, collectedIcons);
|
||||
lastCollectedIconsByRoot.put(root, collectedIcons);
|
||||
}
|
||||
if (currentClockLayout == null && settings.clockEnabledUnlocked) {
|
||||
currentClockLayout = buildClockLayout(root, anchors, settings);
|
||||
currentClockGeometry = clockGeometrySignature(
|
||||
root,
|
||||
currentClockLayout,
|
||||
settings,
|
||||
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings.clockMiddleCutoutGapPx));
|
||||
}
|
||||
LayoutPlan layoutPlan = layoutPlanner.solve(
|
||||
root,
|
||||
clockHost,
|
||||
chipHost,
|
||||
statusHost,
|
||||
notificationHost,
|
||||
anchors,
|
||||
collectedIcons,
|
||||
settings,
|
||||
currentClockLayout);
|
||||
lastLayoutPlanByRoot.put(root, layoutPlan);
|
||||
lastClockGeometryByRoot.put(root, currentClockGeometry != null ? currentClockGeometry : 0);
|
||||
lastClockSourceGeometryByRoot.put(root, currentClockSourceGeometry);
|
||||
if (settings.clockEnabledUnlocked && layoutPlan.clockPlacement != null) {
|
||||
clockRenderer.render(clockHost, layoutPlan.clockPlacement);
|
||||
} else {
|
||||
clockRenderer.clear(clockHost);
|
||||
}
|
||||
if (settings.layoutChipEnabledUnlocked && layoutPlan.chipPlacements != null) {
|
||||
chipRenderer.renderPlacements(chipHost, layoutPlan.chipPlacements);
|
||||
} else {
|
||||
chipRenderer.clear(chipHost);
|
||||
}
|
||||
if (settings.layoutStatusEnabledUnlocked && layoutPlan.statusPlacements != null) {
|
||||
statusRenderer.renderPlacements(statusHost, layoutPlan.statusPlacements);
|
||||
} else {
|
||||
statusRenderer.clear(statusHost);
|
||||
}
|
||||
if (settings.layoutNotifEnabledUnlocked && layoutPlan.notificationPlacements != null) {
|
||||
notificationRenderer.renderPlacements(notificationHost, layoutPlan.notificationPlacements);
|
||||
} else {
|
||||
notificationRenderer.clear(notificationHost);
|
||||
}
|
||||
return new RenderResult(true, stockSourcesChanged, chipSourcesChanged);
|
||||
}
|
||||
|
||||
private void layoutHostToRoot(View root, ViewGroup host) {
|
||||
if (root == null || host == null || root.getWidth() <= 0 || root.getHeight() <= 0) {
|
||||
return;
|
||||
}
|
||||
if (host.getLeft() == 0
|
||||
&& host.getTop() == 0
|
||||
&& host.getRight() == root.getWidth()
|
||||
&& host.getBottom() == root.getHeight()
|
||||
&& host.getMeasuredWidth() == root.getWidth()
|
||||
&& host.getMeasuredHeight() == root.getHeight()) {
|
||||
return;
|
||||
}
|
||||
int widthSpec = View.MeasureSpec.makeMeasureSpec(root.getWidth(), View.MeasureSpec.EXACTLY);
|
||||
int heightSpec = View.MeasureSpec.makeMeasureSpec(root.getHeight(), View.MeasureSpec.EXACTLY);
|
||||
host.measure(widthSpec, heightSpec);
|
||||
host.layout(0, 0, root.getWidth(), root.getHeight());
|
||||
}
|
||||
|
||||
public void clearAll() {
|
||||
clockRenderer.clearAll();
|
||||
statusRenderer.clearAll();
|
||||
statusRenderer.clearHeldSignals();
|
||||
notificationRenderer.clearAll();
|
||||
chipRenderer.clearAll();
|
||||
sourceCollector.clear();
|
||||
clockBaselineDeltaByRoot.clear();
|
||||
lastLayoutSignatureByRoot.clear();
|
||||
lastCollectedIconsByRoot.clear();
|
||||
lastLayoutPlanByRoot.clear();
|
||||
lastClockGeometryByRoot.clear();
|
||||
lastClockSourceGeometryByRoot.clear();
|
||||
chipPlacementController.clear();
|
||||
}
|
||||
|
||||
public void beginRootTransition() {
|
||||
statusRenderer.beginTransitionHold();
|
||||
sourceCollector.clear();
|
||||
lastLayoutSignatureByRoot.clear();
|
||||
lastCollectedIconsByRoot.clear();
|
||||
lastLayoutPlanByRoot.clear();
|
||||
lastClockGeometryByRoot.clear();
|
||||
}
|
||||
|
||||
private boolean isClockOnlySignatureChange(
|
||||
LayoutInputSignature previousSignature,
|
||||
LayoutInputSignature signature
|
||||
) {
|
||||
return previousSignature != null
|
||||
&& signature != null
|
||||
&& signature.nonClockLayoutSignature() == previousSignature.nonClockLayoutSignature();
|
||||
}
|
||||
|
||||
private ClockLayout buildClockLayout(
|
||||
View root,
|
||||
RootAnchors anchors,
|
||||
SbtSettings settings
|
||||
) {
|
||||
return buildClockLayout(
|
||||
root,
|
||||
anchors != null ? anchors.clockView : null,
|
||||
anchors != null ? anchors.clockContainer : null,
|
||||
settings,
|
||||
UnlockedLayoutPlanner.cutoutBounds(ViewGeometry.middleCutout(root), settings != null ? settings.clockMiddleCutoutGapPx : 0));
|
||||
}
|
||||
|
||||
private ClockLayout buildClockLayout(
|
||||
View root,
|
||||
TextView source,
|
||||
View clockContainer,
|
||||
SbtSettings settings,
|
||||
Bounds cutoutBounds
|
||||
) {
|
||||
if (root == null || source == null || settings == null) {
|
||||
return null;
|
||||
}
|
||||
Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, source);
|
||||
if (sourceBounds == null || sourceBounds.width <= 0 || sourceBounds.height <= 0) {
|
||||
return null;
|
||||
}
|
||||
ClockLayoutTextFactory.Model model = ClockLayoutTextFactory.build(source, settings);
|
||||
if (model == null || model.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return ClockLayout.measure(
|
||||
source,
|
||||
model,
|
||||
UnlockedLayoutPlanner.positionFor(settings.clockPosition),
|
||||
stableClockBaselineY(root, source, clockContainer)
|
||||
- offsetScaler.clock(settings.clockVerticalOffsetPx, source),
|
||||
root.getWidth(),
|
||||
cutoutBounds,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
UnlockedLayoutPlanner.sideFor(settings.clockMiddleCutoutSide));
|
||||
}
|
||||
|
||||
private Integer clockGeometrySignature(
|
||||
View root,
|
||||
ClockLayout clockLayout,
|
||||
SbtSettings settings,
|
||||
Bounds clockCutoutBounds
|
||||
) {
|
||||
ClockPlacement placement = clockTextPlacement(root, clockLayout, settings, clockCutoutBounds);
|
||||
if (placement == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + boundsGeometrySignature(placement.bounds);
|
||||
result = 31 * result + placement.rows.size();
|
||||
for (ClockRow row : placement.rows) {
|
||||
result = 31 * result + row.x;
|
||||
result = 31 * result + row.y;
|
||||
result = 31 * result + row.width;
|
||||
result = 31 * result + row.height;
|
||||
result = 31 * result + row.clipLeft;
|
||||
result = 31 * result + row.rowIndex;
|
||||
result = 31 * result + row.segment.ordinal();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ClockPlacement clockTextPlacement(
|
||||
View root,
|
||||
ClockLayout clockLayout,
|
||||
SbtSettings settings,
|
||||
Bounds clockCutoutBounds
|
||||
) {
|
||||
if (clockLayout == null) {
|
||||
return null;
|
||||
}
|
||||
if (settings != null
|
||||
&& root != null
|
||||
&& clockLayout.canSplitAroundCutout(root.getWidth(), clockCutoutBounds)) {
|
||||
ArrayList<ClockPlacement> placements = new ArrayList<>();
|
||||
ArrayList<ClockLayout> splitLayouts = clockLayout.splitAroundCutout(
|
||||
root.getWidth(),
|
||||
clockCutoutBounds,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
UnlockedLayoutPlanner.sideFor(settings.clockMiddleCutoutSide));
|
||||
if (splitLayouts.size() < 2) {
|
||||
return clockLayout.placement();
|
||||
}
|
||||
for (ClockLayout split : splitLayouts) {
|
||||
placements.add(split.placement());
|
||||
}
|
||||
return UnlockedLayoutPlanner.mergeClockPlacements(placements);
|
||||
}
|
||||
return clockLayout.placement();
|
||||
}
|
||||
|
||||
private ClockPlacement clockTextPlacementForPrevious(
|
||||
View root,
|
||||
ClockLayout clockLayout,
|
||||
SbtSettings settings,
|
||||
Bounds clockCutoutBounds,
|
||||
ClockPlacement previousPlacement
|
||||
) {
|
||||
if (previousPlacement == null || previousPlacement.rows == null) {
|
||||
return clockTextPlacement(root, clockLayout, settings, clockCutoutBounds);
|
||||
}
|
||||
if (!hasSplitClockRows(previousPlacement)) {
|
||||
return clockLayout != null ? clockLayout.placement() : null;
|
||||
}
|
||||
return clockTextPlacement(root, clockLayout, settings, clockCutoutBounds);
|
||||
}
|
||||
|
||||
private boolean hasSplitClockRows(ClockPlacement placement) {
|
||||
if (placement == null || placement.rows == null) {
|
||||
return false;
|
||||
}
|
||||
for (ClockRow row : placement.rows) {
|
||||
if (row.segment != ClockRowSegment.FULL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ClockPlacement clockPlacementWithTextFromModel(
|
||||
ClockPlacement previousPlacement,
|
||||
TextView source,
|
||||
SbtSettings settings
|
||||
) {
|
||||
if (previousPlacement == null
|
||||
|| previousPlacement.rows == null
|
||||
|| source == null
|
||||
|| settings == null) {
|
||||
return null;
|
||||
}
|
||||
ClockLayoutTextFactory.Model model = ClockLayoutTextFactory.build(source, settings);
|
||||
if (model == null || model.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<ClockRow> rows = new ArrayList<>();
|
||||
for (ClockRow row : previousPlacement.rows) {
|
||||
if (row.rowIndex < 0 || row.rowIndex >= model.rows.size()) {
|
||||
return null;
|
||||
}
|
||||
ClockLayoutTextFactory.Row modelRow = model.rows.get(row.rowIndex);
|
||||
CharSequence text = switch (row.segment) {
|
||||
case LEFT -> modelRow.leftText;
|
||||
case RIGHT -> modelRow.rightText;
|
||||
case FULL -> modelRow.text;
|
||||
};
|
||||
rows.add(new ClockRow(
|
||||
text,
|
||||
row.x,
|
||||
row.y,
|
||||
row.width,
|
||||
row.height,
|
||||
row.clipLeft,
|
||||
row.rowIndex,
|
||||
row.segment));
|
||||
}
|
||||
return new ClockPlacement(
|
||||
source,
|
||||
model.contentDescription,
|
||||
rows,
|
||||
previousPlacement.bounds);
|
||||
}
|
||||
|
||||
private Integer clockSourceGeometrySignature(TextView source) {
|
||||
if (source == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(source);
|
||||
result = 31 * result + source.getWidth();
|
||||
result = 31 * result + source.getHeight();
|
||||
result = 31 * result + source.getMeasuredWidth();
|
||||
result = 31 * result + source.getMeasuredHeight();
|
||||
result = 31 * result + source.getBaseline();
|
||||
result = 31 * result + source.getLineHeight();
|
||||
result = 31 * result + Float.floatToIntBits(source.getTextSize());
|
||||
result = 31 * result + source.getGravity();
|
||||
result = 31 * result + (source.getIncludeFontPadding() ? 1 : 0);
|
||||
result = 31 * result + (source.getTypeface() != null
|
||||
? System.identityHashCode(source.getTypeface())
|
||||
: 0);
|
||||
result = 31 * result + source.getPaddingLeft();
|
||||
result = 31 * result + source.getPaddingTop();
|
||||
result = 31 * result + source.getPaddingRight();
|
||||
result = 31 * result + source.getPaddingBottom();
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
|
||||
result = 31 * result + Float.floatToIntBits(source.getLetterSpacing());
|
||||
String fontFeatures = source.getFontFeatureSettings();
|
||||
result = 31 * result + (fontFeatures != null ? fontFeatures.hashCode() : 0);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private ClockPlacement clockPlacementWithUpdatedText(
|
||||
ClockPlacement previous,
|
||||
ClockPlacement currentText
|
||||
) {
|
||||
if (previous == null || currentText == null) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<ClockRow> rows = new ArrayList<>();
|
||||
for (int i = 0; i < previous.rows.size(); i++) {
|
||||
ClockRow previousRow = previous.rows.get(i);
|
||||
ClockRow currentRow = matchingClockRow(previousRow, currentText.rows);
|
||||
if (currentRow == null) {
|
||||
return null;
|
||||
}
|
||||
rows.add(new ClockRow(
|
||||
currentRow.text,
|
||||
previousRow.x,
|
||||
previousRow.y,
|
||||
previousRow.width,
|
||||
previousRow.height,
|
||||
previousRow.clipLeft,
|
||||
previousRow.rowIndex,
|
||||
previousRow.segment));
|
||||
}
|
||||
return new ClockPlacement(previous.source, currentText.contentDescription, rows, previous.bounds);
|
||||
}
|
||||
|
||||
private ClockRow matchingClockRow(ClockRow previousRow, ArrayList<ClockRow> currentRows) {
|
||||
if (previousRow == null || currentRows == null) {
|
||||
return null;
|
||||
}
|
||||
for (ClockRow row : currentRows) {
|
||||
if (row.rowIndex == previousRow.rowIndex && row.segment == previousRow.segment) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String clockFastPathMissReason(
|
||||
LayoutPlan previousPlan,
|
||||
Integer previousGeometry,
|
||||
Integer currentGeometry,
|
||||
ClockPlacement updatedClock
|
||||
) {
|
||||
if (previousPlan == null) {
|
||||
return "noPlan";
|
||||
}
|
||||
if (currentGeometry == null) {
|
||||
return "noGeometry";
|
||||
}
|
||||
if (!currentGeometry.equals(previousGeometry)) {
|
||||
return "geometryChanged";
|
||||
}
|
||||
if (previousPlan.clockPlacement != null && updatedClock == null) {
|
||||
return "rowMismatch";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int boundsGeometrySignature(Bounds bounds) {
|
||||
if (bounds == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + bounds.left;
|
||||
result = 31 * result + bounds.top;
|
||||
result = 31 * result + bounds.width;
|
||||
result = 31 * result + bounds.height;
|
||||
return result;
|
||||
}
|
||||
|
||||
private int stableClockBaselineY(View root, TextView source, View clockContainer) {
|
||||
Bounds sourceBounds = ViewGeometry.boundsRelativeTo(root, source);
|
||||
int sourceBaseline = source.getBaseline() > 0
|
||||
? source.getBaseline()
|
||||
: (sourceBounds != null ? Math.max(0, (sourceBounds.height * 3) / 4) : 0);
|
||||
Bounds containerBounds = ViewGeometry.boundsRelativeTo(root, clockContainer);
|
||||
if (containerBounds != null
|
||||
&& containerBounds.width > 0
|
||||
&& containerBounds.height > 0
|
||||
&& sourceBounds != null
|
||||
&& sourceBounds.height > 0) {
|
||||
int delta = sourceBounds.top + sourceBaseline - containerBounds.top;
|
||||
Integer previousDelta = clockBaselineDeltaByRoot.get(root);
|
||||
int tolerance = Math.max(4, containerBounds.height / 10);
|
||||
if (previousDelta != null && Math.abs(delta - previousDelta) > tolerance) {
|
||||
return containerBounds.top + previousDelta;
|
||||
}
|
||||
clockBaselineDeltaByRoot.put(root, delta);
|
||||
return containerBounds.top + delta;
|
||||
}
|
||||
return sourceBounds != null ? sourceBounds.top + sourceBaseline : 0;
|
||||
}
|
||||
|
||||
public record RenderResult(
|
||||
boolean layoutChanged,
|
||||
boolean stockSourcesChanged,
|
||||
boolean chipSourcesChanged
|
||||
) {
|
||||
public RenderResult(boolean layoutChanged, boolean stockSourcesChanged) {
|
||||
this(layoutChanged, stockSourcesChanged, stockSourcesChanged);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutItemKind;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.SplitRegion;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class UnlockedLayoutBand extends StatusBarLayoutSolver.LayoutBand {
|
||||
final ViewGroup host;
|
||||
final SnapshotRenderer renderer;
|
||||
final ArrayList<SnapshotPlacement> rawPlacements;
|
||||
final boolean showDotIfTruncated;
|
||||
final Bounds reservedBounds;
|
||||
final ClockLayout clockLayout;
|
||||
|
||||
ArrayList<SnapshotPlacement> placements;
|
||||
boolean reverseForPlacement;
|
||||
Bounds placedBounds;
|
||||
int clipStartPx;
|
||||
int clipEndPx;
|
||||
|
||||
private UnlockedLayoutBand(
|
||||
LayoutItemKind kind,
|
||||
ViewGroup host,
|
||||
SnapshotRenderer renderer,
|
||||
ArrayList<SnapshotPlacement> rawPlacements,
|
||||
LayoutPosition position,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide middleSide,
|
||||
boolean showDotIfTruncated,
|
||||
Bounds reservedBounds,
|
||||
ClockLayout clockLayout,
|
||||
SplitRegion splitRegion
|
||||
) {
|
||||
super(kind, position, middleAutoNearestSide, middleSide, splitRegion);
|
||||
this.host = host;
|
||||
this.renderer = renderer;
|
||||
this.rawPlacements = rawPlacements;
|
||||
this.showDotIfTruncated = showDotIfTruncated;
|
||||
this.reservedBounds = reservedBounds;
|
||||
this.clockLayout = clockLayout;
|
||||
}
|
||||
|
||||
static UnlockedLayoutBand clock(
|
||||
ViewGroup host,
|
||||
ClockLayout clockLayout,
|
||||
LayoutPosition position,
|
||||
SbtSettings settings
|
||||
) {
|
||||
return new UnlockedLayoutBand(
|
||||
LayoutItemKind.CLOCK,
|
||||
host,
|
||||
null,
|
||||
null,
|
||||
position,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
sideFor(settings.clockMiddleCutoutSide),
|
||||
false,
|
||||
null,
|
||||
clockLayout,
|
||||
SplitRegion.NONE);
|
||||
}
|
||||
|
||||
static UnlockedLayoutBand fixedClock(
|
||||
ViewGroup host,
|
||||
ClockLayout clockLayout,
|
||||
SplitRegion splitRegion,
|
||||
SbtSettings settings
|
||||
) {
|
||||
return new UnlockedLayoutBand(
|
||||
LayoutItemKind.CLOCK,
|
||||
host,
|
||||
null,
|
||||
null,
|
||||
LayoutPosition.MIDDLE,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
sideFor(settings.clockMiddleCutoutSide),
|
||||
false,
|
||||
null,
|
||||
clockLayout,
|
||||
splitRegion);
|
||||
}
|
||||
|
||||
static UnlockedLayoutBand icons(
|
||||
LayoutItemKind kind,
|
||||
ViewGroup host,
|
||||
SnapshotRenderer renderer,
|
||||
ArrayList<SnapshotPlacement> rawPlacements,
|
||||
LayoutPosition position,
|
||||
boolean middleAutoNearestSide,
|
||||
AnchorSide middleSide,
|
||||
boolean showDotIfTruncated
|
||||
) {
|
||||
return new UnlockedLayoutBand(
|
||||
kind,
|
||||
host,
|
||||
renderer,
|
||||
rawPlacements,
|
||||
position,
|
||||
middleAutoNearestSide,
|
||||
middleSide,
|
||||
showDotIfTruncated,
|
||||
null,
|
||||
null,
|
||||
SplitRegion.NONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void prepareForSide(AnchorSide side, String sortOrder) {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
placements = null;
|
||||
placedBounds = clockLayout != null ? clockLayout.bounds() : reservedBounds;
|
||||
return;
|
||||
}
|
||||
placements = new ArrayList<>(activePlacements());
|
||||
reverseForPlacement = "reversed".equals(sortOrder)
|
||||
|| ("automatic".equals(sortOrder)
|
||||
&& ((side == AnchorSide.LEFT && kind == LayoutItemKind.NOTIFICATIONS)
|
||||
|| (side == AnchorSide.RIGHT && kind == LayoutItemKind.STATUS)));
|
||||
placedBounds = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int width() {
|
||||
return Math.max(0, naturalWidth() - clipStartPx - clipEndPx);
|
||||
}
|
||||
|
||||
int naturalWidth() {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
if (clockLayout != null) {
|
||||
return clockLayout.width();
|
||||
}
|
||||
return reservedBounds != null ? reservedBounds.width : 0;
|
||||
}
|
||||
return sumWidths(activePlacements());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canClipContinuously() {
|
||||
return (kind == LayoutItemKind.CHIP || kind == LayoutItemKind.CLOCK)
|
||||
&& naturalWidth() > clipStartPx + clipEndPx;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean clipContinuous(AnchorSide wallSide, int amountPx) {
|
||||
int remaining = naturalWidth() - clipStartPx - clipEndPx;
|
||||
if (amountPx <= 0 || remaining <= 0 || !canClipContinuously()) {
|
||||
return false;
|
||||
}
|
||||
int amount = Math.min(amountPx, remaining);
|
||||
if (wallSide == AnchorSide.RIGHT) {
|
||||
clipEndPx += amount;
|
||||
} else {
|
||||
clipStartPx += amount;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int wallDotSlack(AnchorSide side) {
|
||||
SnapshotPlacement placement = wallPlacement(side);
|
||||
if (placement == null || !placement.overflowDot) {
|
||||
return 0;
|
||||
}
|
||||
int collisionWidth = relativeCollisionBounds(placement).width;
|
||||
int dotWidth = overflowDotVisualWidth(placement);
|
||||
return Math.max(0, (collisionWidth - dotWidth) / 2);
|
||||
}
|
||||
|
||||
int height() {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
if (clockLayout != null) {
|
||||
return clockLayout.height();
|
||||
}
|
||||
return reservedBounds != null ? reservedBounds.height : 0;
|
||||
}
|
||||
int height = 0;
|
||||
for (SnapshotPlacement placement : activePlacements()) {
|
||||
height = Math.max(height, collisionBounds(placement).height);
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
int top() {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
if (clockLayout != null) {
|
||||
return clockLayout.top();
|
||||
}
|
||||
return reservedBounds != null ? reservedBounds.top : 0;
|
||||
}
|
||||
int top = Integer.MAX_VALUE;
|
||||
for (SnapshotPlacement placement : activePlacements()) {
|
||||
top = Math.min(top, collisionBounds(placement).top);
|
||||
}
|
||||
return top == Integer.MAX_VALUE ? 0 : top;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shrinkOneIcon() {
|
||||
if (kind == LayoutItemKind.NOTIFICATIONS) {
|
||||
return shrinkIcon(false);
|
||||
}
|
||||
if (kind == LayoutItemKind.STATUS) {
|
||||
return shrinkIcon(true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean removeOverflowDot() {
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
ArrayList<SnapshotPlacement> withoutDot = new ArrayList<>();
|
||||
boolean removed = false;
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
if (placement.overflowDot) {
|
||||
removed = true;
|
||||
} else {
|
||||
withoutDot.add(placement);
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
return false;
|
||||
}
|
||||
if (withoutDot.isEmpty()) {
|
||||
withoutDot.add(emptyIconContainerPlacement());
|
||||
}
|
||||
placements = withoutDot;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object captureState() {
|
||||
return new LayoutBandState(placements == null, new ArrayList<>(activePlacements()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void restoreState(Object state) {
|
||||
if (!(state instanceof LayoutBandState bandState)) {
|
||||
return;
|
||||
}
|
||||
placements = bandState.placementsWereNull ? null : new ArrayList<>(bandState.placements);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean createdOverflowDotSince(Object state) {
|
||||
if (!(state instanceof LayoutBandState bandState)) {
|
||||
return hasOverflowDot(activePlacements());
|
||||
}
|
||||
return !hasOverflowDot(bandState.placements) && hasOverflowDot(activePlacements());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void place(int left) {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
placedBounds = clockLayout != null
|
||||
? clockLayout.place(left)
|
||||
: new Bounds(left, top(), width(), height());
|
||||
if (placedBounds != null && width() != naturalWidth()) {
|
||||
placedBounds = new Bounds(
|
||||
placedBounds.left + clipStartPx,
|
||||
placedBounds.top,
|
||||
width(),
|
||||
placedBounds.height);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (reverseForPlacement) {
|
||||
Collections.reverse(placements);
|
||||
}
|
||||
int cursor = left;
|
||||
int rowTop = top();
|
||||
int clipLeft = left + clipStartPx;
|
||||
int clipRight = left + naturalWidth() - clipEndPx;
|
||||
ArrayList<SnapshotPlacement> positioned = new ArrayList<>();
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds original = placement.bounds;
|
||||
Bounds collision = relativeCollisionBounds(placement);
|
||||
int collisionLeft = cursor;
|
||||
int collisionRight = cursor + collision.width;
|
||||
Bounds bounds = new Bounds(
|
||||
cursor - collision.left,
|
||||
rowTop - collision.top,
|
||||
original.width,
|
||||
original.height);
|
||||
SnapshotPlacement clipped = clipPlacement(
|
||||
placement,
|
||||
bounds,
|
||||
collision,
|
||||
Math.max(collisionLeft, clipLeft),
|
||||
Math.min(collisionRight, clipRight));
|
||||
if (clipped != null) {
|
||||
positioned.add(clipped);
|
||||
}
|
||||
cursor += collision.width;
|
||||
}
|
||||
if (positioned.isEmpty() && kind == LayoutItemKind.CHIP && height() > 0) {
|
||||
positioned.add(emptyClippedPlacement(left + clipStartPx, rowTop, height()));
|
||||
}
|
||||
placements = positioned;
|
||||
placedBounds = bounds();
|
||||
}
|
||||
|
||||
Bounds bounds() {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
return placedBounds;
|
||||
}
|
||||
Bounds union = null;
|
||||
if (placements == null) {
|
||||
return null;
|
||||
}
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds bounds = collisionBounds(placement);
|
||||
union = union == null ? bounds : union.union(bounds);
|
||||
}
|
||||
return union;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ArrayList<Bounds> collisionBoxes() {
|
||||
ArrayList<Bounds> boxes = new ArrayList<>();
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
if (clockLayout != null) {
|
||||
int clipLeft = clipStartPx;
|
||||
int clipRight = naturalWidth() - clipEndPx;
|
||||
for (Bounds box : clockLayout.collisionBoxes()) {
|
||||
int left = Math.max(box.left, clipLeft);
|
||||
int right = Math.min(box.left + box.width, clipRight);
|
||||
if (right >= left) {
|
||||
boxes.add(new Bounds(left - clipStartPx, box.top, right - left, box.height));
|
||||
}
|
||||
}
|
||||
} else if (reservedBounds != null) {
|
||||
boxes.add(new Bounds(0, reservedBounds.top, width(), reservedBounds.height));
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
Bounds bounds = new Bounds(0, top(), width(), height());
|
||||
if (bounds.height > 0) {
|
||||
boxes.add(bounds);
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ArrayList<Bounds> placedCollisionBounds() {
|
||||
ArrayList<Bounds> boxes = new ArrayList<>();
|
||||
int left = kind == LayoutItemKind.CLOCK && placedBounds != null
|
||||
? placedBounds.left
|
||||
: 0;
|
||||
for (Bounds box : collisionBoxes()) {
|
||||
boxes.add(new Bounds(left + box.left, box.top, box.width, box.height));
|
||||
}
|
||||
if (kind != LayoutItemKind.CLOCK && placedBounds != null) {
|
||||
boxes.clear();
|
||||
boxes.add(placedBounds);
|
||||
}
|
||||
return boxes;
|
||||
}
|
||||
|
||||
private SnapshotPlacement wallPlacement(AnchorSide side) {
|
||||
if (kind == LayoutItemKind.CLOCK) {
|
||||
return null;
|
||||
}
|
||||
ArrayList<SnapshotPlacement> icons = activePlacements();
|
||||
if (icons.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
boolean wallAtStart = side == AnchorSide.RIGHT;
|
||||
int index;
|
||||
if (wallAtStart) {
|
||||
index = reverseForPlacement ? icons.size() - 1 : 0;
|
||||
} else {
|
||||
index = reverseForPlacement ? 0 : icons.size() - 1;
|
||||
}
|
||||
return icons.get(index);
|
||||
}
|
||||
|
||||
private int overflowDotVisualWidth(SnapshotPlacement placement) {
|
||||
Bounds bounds = placement.bounds;
|
||||
int base = Math.max(1, Math.min(bounds.width, bounds.height));
|
||||
int radius = Math.max(1, Math.round(base * 0.16f));
|
||||
return Math.min(bounds.width, radius * 2 + 2);
|
||||
}
|
||||
|
||||
private Bounds collisionBounds(SnapshotPlacement placement) {
|
||||
Bounds relative = relativeCollisionBounds(placement);
|
||||
Bounds bounds = placement.bounds;
|
||||
return new Bounds(
|
||||
bounds.left + relative.left,
|
||||
bounds.top + relative.top,
|
||||
relative.width,
|
||||
relative.height);
|
||||
}
|
||||
|
||||
private Bounds relativeCollisionBounds(SnapshotPlacement placement) {
|
||||
if (placement.contentBounds != null
|
||||
&& placement.contentBounds.width >= 0
|
||||
&& placement.contentBounds.height > 0) {
|
||||
return placement.contentBounds;
|
||||
}
|
||||
Bounds bounds = placement.bounds;
|
||||
return new Bounds(0, 0, bounds.width, bounds.height);
|
||||
}
|
||||
|
||||
private boolean shrinkIcon(boolean fromStart) {
|
||||
ArrayList<SnapshotPlacement> icons = new ArrayList<>(activePlacements());
|
||||
int removeIndex = nonDotIndex(icons, fromStart);
|
||||
if (removeIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
boolean hadDot = hasOverflowDot(icons);
|
||||
icons.remove(removeIndex);
|
||||
if (showDotIfTruncated && !hadDot) {
|
||||
SnapshotPlacement dot = dotPlacement(dotWidth(icons), dotHeight(icons));
|
||||
if (fromStart) {
|
||||
icons.add(0, dot);
|
||||
} else {
|
||||
icons.add(dot);
|
||||
}
|
||||
}
|
||||
placements = icons;
|
||||
return true;
|
||||
}
|
||||
|
||||
private int nonDotIndex(ArrayList<SnapshotPlacement> icons, boolean fromStart) {
|
||||
int start = fromStart ? 0 : icons.size() - 1;
|
||||
int end = fromStart ? icons.size() : -1;
|
||||
int step = fromStart ? 1 : -1;
|
||||
for (int i = start; i != end; i += step) {
|
||||
if (isShrinkableIcon(icons.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private boolean isShrinkableIcon(SnapshotPlacement placement) {
|
||||
return placement != null
|
||||
&& !placement.overflowDot
|
||||
&& (placement.key == null || !placement.key.startsWith(SnapshotKeys.EMPTY_ICON_CONTAINER));
|
||||
}
|
||||
|
||||
private boolean hasOverflowDot(ArrayList<SnapshotPlacement> icons) {
|
||||
for (SnapshotPlacement icon : icons) {
|
||||
if (icon.overflowDot) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> activePlacements() {
|
||||
if (placements != null) {
|
||||
return placements;
|
||||
}
|
||||
return rawPlacements != null ? rawPlacements : new ArrayList<>();
|
||||
}
|
||||
|
||||
private int dotWidth(ArrayList<SnapshotPlacement> icons) {
|
||||
if (!icons.isEmpty()) {
|
||||
return icons.get(0).bounds.width;
|
||||
}
|
||||
if (rawPlacements != null && !rawPlacements.isEmpty()) {
|
||||
return rawPlacements.get(0).bounds.width;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private int dotHeight(ArrayList<SnapshotPlacement> icons) {
|
||||
if (!icons.isEmpty()) {
|
||||
return icons.get(0).bounds.height;
|
||||
}
|
||||
if (rawPlacements != null && !rawPlacements.isEmpty()) {
|
||||
return rawPlacements.get(0).bounds.height;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private SnapshotPlacement dotPlacement(int width, int height) {
|
||||
return new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(0, top(), Math.max(1, width), Math.max(1, height)),
|
||||
SnapshotKeys.OVERFLOW_DOT + ":" + kind.name().toLowerCase(java.util.Locale.ROOT),
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
new Bounds(0, 0, Math.max(1, width), Math.max(1, height)),
|
||||
0);
|
||||
}
|
||||
|
||||
private SnapshotPlacement emptyIconContainerPlacement() {
|
||||
int height = Math.max(1, dotHeight(activePlacements()));
|
||||
return new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(0, top(), 1, height),
|
||||
SnapshotKeys.EMPTY_ICON_CONTAINER + ":" + kind.name().toLowerCase(java.util.Locale.ROOT),
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
new Bounds(0, 0, 0, height),
|
||||
0);
|
||||
}
|
||||
|
||||
private SnapshotPlacement clipPlacement(
|
||||
SnapshotPlacement placement,
|
||||
Bounds bounds,
|
||||
Bounds collision,
|
||||
int clippedCollisionLeft,
|
||||
int clippedCollisionRight
|
||||
) {
|
||||
int collisionWidth = Math.max(0, clippedCollisionRight - clippedCollisionLeft);
|
||||
int removeLeft = Math.max(0, clippedCollisionLeft - (bounds.left + collision.left));
|
||||
int removeRight = Math.max(0, bounds.left + collision.left + collision.width - clippedCollisionRight);
|
||||
if (collisionWidth <= 0) {
|
||||
if (kind == LayoutItemKind.CHIP) {
|
||||
return emptyClippedPlacement(
|
||||
bounds.left + removeLeft,
|
||||
bounds.top + collision.top,
|
||||
collision.height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Bounds clippedBounds = new Bounds(
|
||||
bounds.left + removeLeft,
|
||||
bounds.top,
|
||||
Math.max(1, bounds.width - removeLeft - removeRight),
|
||||
bounds.height);
|
||||
Bounds clippedContent = placement.contentBounds != null
|
||||
? new Bounds(
|
||||
placement.contentBounds.left,
|
||||
placement.contentBounds.top,
|
||||
collisionWidth,
|
||||
placement.contentBounds.height)
|
||||
: null;
|
||||
return new SnapshotPlacement(
|
||||
placement.source,
|
||||
clippedBounds,
|
||||
placement.key,
|
||||
placement.signal,
|
||||
placement.overflowDot,
|
||||
placement.heldSignal,
|
||||
clippedContent,
|
||||
placement.sourceOffsetX + removeLeft);
|
||||
}
|
||||
|
||||
private int sumWidths(ArrayList<SnapshotPlacement> snapshotPlacements) {
|
||||
int width = 0;
|
||||
if (snapshotPlacements != null) {
|
||||
for (SnapshotPlacement placement : snapshotPlacements) {
|
||||
width += collisionBounds(placement).width;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
private SnapshotPlacement emptyClippedPlacement(int left, int top, int height) {
|
||||
return new SnapshotPlacement(
|
||||
null,
|
||||
new Bounds(left, top, 1, Math.max(1, height)),
|
||||
SnapshotKeys.EMPTY_CHIP,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
new Bounds(0, 0, 0, Math.max(1, height)),
|
||||
0);
|
||||
}
|
||||
|
||||
ClockPlacement clockPlacement() {
|
||||
if (clockLayout == null) {
|
||||
return null;
|
||||
}
|
||||
ClockPlacement placement = clockLayout.placement();
|
||||
if (placement == null || placement.bounds == null || clipStartPx + clipEndPx <= 0) {
|
||||
return placement;
|
||||
}
|
||||
int clipLeft = placement.bounds.left + clipStartPx;
|
||||
int clipRight = placement.bounds.left + naturalWidth() - clipEndPx;
|
||||
ArrayList<ClockRow> rows = new ArrayList<>();
|
||||
for (ClockRow row : placement.rows) {
|
||||
int rowLeft = placement.bounds.left + row.x;
|
||||
int rowRight = rowLeft + row.width;
|
||||
int left = Math.max(rowLeft, clipLeft);
|
||||
int right = Math.min(rowRight, clipRight);
|
||||
if (right <= left) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new ClockRow(
|
||||
row.text,
|
||||
left - clipLeft,
|
||||
row.y,
|
||||
right - left,
|
||||
row.height,
|
||||
row.clipLeft + left - rowLeft,
|
||||
row.rowIndex,
|
||||
row.segment));
|
||||
}
|
||||
return new ClockPlacement(
|
||||
placement.source,
|
||||
placement.contentDescription,
|
||||
rows,
|
||||
new Bounds(clipLeft, placement.bounds.top, Math.max(0, clipRight - clipLeft), placement.bounds.height));
|
||||
}
|
||||
|
||||
private static AnchorSide sideFor(String value) {
|
||||
return "right".equals(value) ? AnchorSide.RIGHT : AnchorSide.LEFT;
|
||||
}
|
||||
|
||||
private record LayoutBandState(boolean placementsWereNull, ArrayList<SnapshotPlacement> placements) {
|
||||
}
|
||||
}
|
||||
+459
@@ -0,0 +1,459 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Anchor;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorKind;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.AnchorSide;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutEdges;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutItemKind;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.LayoutPosition;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.PlacedBand;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Region;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.RegionPlan;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.SplitRegion;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class UnlockedLayoutPlanner {
|
||||
private final SnapshotRenderer statusRenderer;
|
||||
private final SnapshotRenderer notificationRenderer;
|
||||
private final SnapshotRenderer chipRenderer;
|
||||
private final UnlockedChipPlacementController chipPlacementController;
|
||||
private final UnlockedVerticalOffsetScaler offsetScaler;
|
||||
|
||||
UnlockedLayoutPlanner(
|
||||
SnapshotRenderer statusRenderer,
|
||||
SnapshotRenderer notificationRenderer,
|
||||
SnapshotRenderer chipRenderer,
|
||||
UnlockedChipPlacementController chipPlacementController,
|
||||
UnlockedVerticalOffsetScaler offsetScaler
|
||||
) {
|
||||
this.statusRenderer = statusRenderer;
|
||||
this.notificationRenderer = notificationRenderer;
|
||||
this.chipRenderer = chipRenderer;
|
||||
this.chipPlacementController = chipPlacementController;
|
||||
this.offsetScaler = offsetScaler;
|
||||
}
|
||||
|
||||
LayoutPlan solve(
|
||||
View root,
|
||||
ViewGroup clockHost,
|
||||
ViewGroup chipHost,
|
||||
ViewGroup statusHost,
|
||||
ViewGroup notificationHost,
|
||||
RootAnchors anchors,
|
||||
CollectedIcons collectedIcons,
|
||||
SbtSettings settings,
|
||||
ClockLayout clockLayout
|
||||
) {
|
||||
ArrayList<UnlockedLayoutBand> bands = new ArrayList<>();
|
||||
Rect cutout = ViewGeometry.middleCutout(root);
|
||||
Bounds layoutCutoutBounds = cutoutBounds(cutout, settings.layoutPaddingCutoutPx);
|
||||
Bounds clockCutoutBounds = cutoutBounds(cutout, settings.clockMiddleCutoutGapPx);
|
||||
if (settings.clockEnabledUnlocked && clockLayout != null && clockLayout.width() > 0) {
|
||||
bands.add(UnlockedLayoutBand.clock(
|
||||
clockHost,
|
||||
clockLayout,
|
||||
positionFor(settings.clockPosition),
|
||||
settings));
|
||||
}
|
||||
if (settings.layoutChipEnabledUnlocked) {
|
||||
ArrayList<SnapshotPlacement> placements = chipPlacementController.placements(
|
||||
root,
|
||||
collectedIcons.statusChips,
|
||||
collectedIcons.statusChipMetrics,
|
||||
settings);
|
||||
if (!placements.isEmpty()) {
|
||||
bands.add(UnlockedLayoutBand.icons(
|
||||
LayoutItemKind.CHIP,
|
||||
chipHost,
|
||||
chipRenderer,
|
||||
placements,
|
||||
positionFor(settings.layoutChipPosition),
|
||||
settings.layoutChipMiddleAutoNearestSide,
|
||||
sideFor(settings.layoutChipMiddleSide),
|
||||
false));
|
||||
}
|
||||
}
|
||||
if (settings.layoutStatusEnabledUnlocked) {
|
||||
ArrayList<SnapshotPlacement> rawPlacements =
|
||||
statusRenderer.rawPlacements(root, collectedIcons.statusIcons);
|
||||
ArrayList<SnapshotPlacement> placements =
|
||||
offsetPlacements(
|
||||
rawPlacements,
|
||||
offsetScaler.status(
|
||||
settings.layoutStatusVerticalOffsetPx,
|
||||
representativeHeight(rawPlacements)));
|
||||
if (!placements.isEmpty()) {
|
||||
bands.add(UnlockedLayoutBand.icons(
|
||||
LayoutItemKind.STATUS,
|
||||
statusHost,
|
||||
statusRenderer,
|
||||
placements,
|
||||
positionFor(settings.layoutStatusPosition),
|
||||
settings.layoutStatusMiddleAutoNearestSide,
|
||||
sideFor(settings.layoutStatusMiddleSide),
|
||||
settings.layoutStatusShowDotIfTruncated));
|
||||
}
|
||||
}
|
||||
if (settings.layoutNotifEnabledUnlocked) {
|
||||
ArrayList<SnapshotPlacement> rawPlacements =
|
||||
notificationRenderer.rawPlacements(root, collectedIcons.notificationIcons);
|
||||
ArrayList<SnapshotPlacement> placements =
|
||||
offsetPlacements(
|
||||
rawPlacements,
|
||||
offsetScaler.notification(
|
||||
settings.layoutNotifVerticalOffsetPx,
|
||||
representativeHeight(rawPlacements)));
|
||||
if (!placements.isEmpty()) {
|
||||
bands.add(UnlockedLayoutBand.icons(
|
||||
LayoutItemKind.NOTIFICATIONS,
|
||||
notificationHost,
|
||||
notificationRenderer,
|
||||
placements,
|
||||
positionFor(settings.layoutNotifPosition),
|
||||
settings.layoutNotifMiddleAutoNearestSide,
|
||||
sideFor(settings.layoutNotifMiddleSide),
|
||||
settings.layoutNotifShowDotIfTruncated));
|
||||
}
|
||||
}
|
||||
if (bands.isEmpty()) {
|
||||
return new LayoutPlan(null, new ArrayList<>(), new ArrayList<>(), new ArrayList<>());
|
||||
}
|
||||
ArrayList<PlacedBand> placedBands = new ArrayList<>();
|
||||
ArrayList<LayoutItemKind> itemOrder = orderedKinds(settings.layoutItemOrder);
|
||||
splitClockIfNeeded(root, bands, itemOrder, clockCutoutBounds, settings);
|
||||
LayoutEdges edges = layoutEdges(root, settings, anchors);
|
||||
int itemPadding = Math.max(0, settings.layoutItemPaddingPx);
|
||||
RegionPlan<UnlockedLayoutBand> regionPlan = StatusBarLayoutSolver.planRegions(
|
||||
root.getWidth(),
|
||||
edges,
|
||||
bands,
|
||||
itemOrder,
|
||||
layoutCutoutBounds,
|
||||
itemPadding);
|
||||
for (Region<UnlockedLayoutBand> region : regionPlan.regions()) {
|
||||
StatusBarLayoutSolver.truncateToFit(
|
||||
region.bands,
|
||||
region.width(),
|
||||
regionPlan.middleSide(),
|
||||
settings.layoutNotifSortOrder,
|
||||
itemPadding);
|
||||
}
|
||||
|
||||
StatusBarLayoutSolver.placeStackedBands(
|
||||
StatusBarLayoutSolver.bandsForPosition(bands, LayoutPosition.LEFT, itemOrder),
|
||||
new Anchor(edges.left(), AnchorSide.RIGHT, AnchorKind.WALL),
|
||||
placedBands,
|
||||
settings.layoutNotifSortOrder,
|
||||
false,
|
||||
itemPadding);
|
||||
StatusBarLayoutSolver.placeStackedBands(
|
||||
StatusBarLayoutSolver.bandsForPosition(bands, LayoutPosition.RIGHT, itemOrder),
|
||||
new Anchor(edges.right(), AnchorSide.LEFT, AnchorKind.WALL),
|
||||
placedBands,
|
||||
settings.layoutNotifSortOrder,
|
||||
false,
|
||||
itemPadding);
|
||||
ArrayList<UnlockedLayoutBand> middleBands = StatusBarLayoutSolver.middleBandsForPlacement(
|
||||
bands,
|
||||
itemOrder,
|
||||
regionPlan.middleSide());
|
||||
if (!middleBands.isEmpty()) {
|
||||
Anchor anchor = StatusBarLayoutSolver.anchorForMiddle(
|
||||
root.getWidth(),
|
||||
middleBands,
|
||||
placedBands,
|
||||
layoutCutoutBounds,
|
||||
regionPlan.middleSide(),
|
||||
itemPadding);
|
||||
StatusBarLayoutSolver.placeStackedBands(
|
||||
middleBands,
|
||||
anchor,
|
||||
placedBands,
|
||||
settings.layoutNotifSortOrder,
|
||||
true,
|
||||
itemPadding);
|
||||
}
|
||||
|
||||
ArrayList<SnapshotPlacement> statusPlacements = new ArrayList<>();
|
||||
ArrayList<SnapshotPlacement> notificationPlacements = new ArrayList<>();
|
||||
ArrayList<SnapshotPlacement> chipPlacements = new ArrayList<>();
|
||||
ArrayList<ClockPlacement> clockPlacements = new ArrayList<>();
|
||||
for (UnlockedLayoutBand band : bands) {
|
||||
if (band.kind == LayoutItemKind.CLOCK && band.clockLayout != null) {
|
||||
clockPlacements.add(band.clockPlacement());
|
||||
} else if (band.kind == LayoutItemKind.CHIP && band.placements != null) {
|
||||
chipPlacements.addAll(band.placements);
|
||||
} else if (band.kind == LayoutItemKind.STATUS && band.placements != null) {
|
||||
statusPlacements.addAll(band.placements);
|
||||
} else if (band.kind == LayoutItemKind.NOTIFICATIONS && band.placements != null) {
|
||||
notificationPlacements.addAll(band.placements);
|
||||
}
|
||||
}
|
||||
return new LayoutPlan(
|
||||
mergeClockPlacements(clockPlacements),
|
||||
chipPlacements,
|
||||
statusPlacements,
|
||||
notificationPlacements);
|
||||
}
|
||||
|
||||
private ArrayList<SnapshotPlacement> offsetPlacements(ArrayList<SnapshotPlacement> placements, int verticalOffsetPx) {
|
||||
if (placements == null || placements.isEmpty() || verticalOffsetPx == 0) {
|
||||
return placements;
|
||||
}
|
||||
ArrayList<SnapshotPlacement> result = new ArrayList<>();
|
||||
int dy = -verticalOffsetPx;
|
||||
for (SnapshotPlacement placement : placements) {
|
||||
Bounds bounds = placement.bounds;
|
||||
result.add(new SnapshotPlacement(
|
||||
placement.source,
|
||||
new Bounds(bounds.left, bounds.top + dy, bounds.width, bounds.height),
|
||||
placement.key,
|
||||
placement.signal,
|
||||
placement.overflowDot,
|
||||
placement.heldSignal,
|
||||
placement.contentBounds,
|
||||
placement.sourceOffsetX));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int representativeHeight(ArrayList<SnapshotPlacement> placements) {
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, placements.get(0).bounds.height);
|
||||
}
|
||||
|
||||
private void splitClockIfNeeded(
|
||||
View root,
|
||||
ArrayList<UnlockedLayoutBand> bands,
|
||||
ArrayList<LayoutItemKind> itemOrder,
|
||||
Bounds cutoutBounds,
|
||||
SbtSettings settings
|
||||
) {
|
||||
if (root == null || cutoutBounds == null || bands == null || settings == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<UnlockedLayoutBand> middleBands = StatusBarLayoutSolver.bandsForPosition(
|
||||
bands,
|
||||
LayoutPosition.MIDDLE,
|
||||
itemOrder);
|
||||
if (middleBands.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
UnlockedLayoutBand clockBand = middleBands.get(0);
|
||||
if (clockBand.kind != LayoutItemKind.CLOCK
|
||||
|| clockBand.clockLayout == null
|
||||
|| !clockBand.clockLayout.canSplitAroundCutout(root.getWidth(), cutoutBounds)) {
|
||||
return;
|
||||
}
|
||||
ArrayList<ClockLayout> splitLayouts = clockBand.clockLayout.splitAroundCutout(
|
||||
root.getWidth(),
|
||||
cutoutBounds,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
sideFor(settings.clockMiddleCutoutSide));
|
||||
if (splitLayouts.size() < 2) {
|
||||
return;
|
||||
}
|
||||
int index = bands.indexOf(clockBand);
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
bands.remove(index);
|
||||
bands.add(index, UnlockedLayoutBand.fixedClock(clockBand.host, splitLayouts.get(1), SplitRegion.RIGHT, settings));
|
||||
bands.add(index, UnlockedLayoutBand.fixedClock(clockBand.host, splitLayouts.get(0), SplitRegion.LEFT, settings));
|
||||
}
|
||||
|
||||
private LayoutEdges layoutEdges(
|
||||
View root,
|
||||
SbtSettings settings,
|
||||
RootAnchors anchors
|
||||
) {
|
||||
int rootWidth = root != null ? root.getWidth() : 0;
|
||||
StockInsets stockInsets = stockInsets(root, rootWidth, anchors);
|
||||
int left = settings.layoutPaddingLeftPx;
|
||||
int right = rootWidth - settings.layoutPaddingRightPx;
|
||||
if (settings.layoutPaddingLeftAddStock) {
|
||||
left += stockInsets.left;
|
||||
}
|
||||
if (settings.layoutPaddingRightAddStock) {
|
||||
right -= stockInsets.right;
|
||||
}
|
||||
return new LayoutEdges(left, right);
|
||||
}
|
||||
|
||||
private StockInsets stockInsets(
|
||||
View root,
|
||||
int rootWidth,
|
||||
RootAnchors anchors
|
||||
) {
|
||||
int left = firstUsableInset(
|
||||
rootWidth,
|
||||
leftInset(root, anchors != null ? anchors.clockContainer : null),
|
||||
leftInset(root, anchors != null ? anchors.notificationRoot : null),
|
||||
root != null ? root.getPaddingLeft() : 0);
|
||||
|
||||
int right = firstUsableInset(
|
||||
rootWidth,
|
||||
rightInset(rootWidth, statusUnion(root, anchors)),
|
||||
rightPadding(anchors != null ? anchors.statusEndContent : null),
|
||||
rightPadding(anchors != null ? anchors.statusRoot : null),
|
||||
root != null ? root.getPaddingRight() : 0);
|
||||
|
||||
return new StockInsets(left, right);
|
||||
}
|
||||
|
||||
private int firstUsableInset(int rootWidth, Integer... values) {
|
||||
int maxPlausible = Math.max(0, rootWidth / 4);
|
||||
if (values == null) {
|
||||
return 0;
|
||||
}
|
||||
for (Integer value : values) {
|
||||
if (value != null && value > 0 && value <= maxPlausible) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private Integer leftInset(View root, View view) {
|
||||
Bounds bounds = ViewGeometry.boundsRelativeTo(root, view);
|
||||
if (bounds == null || bounds.width <= 0) {
|
||||
return null;
|
||||
}
|
||||
return bounds.left;
|
||||
}
|
||||
|
||||
private Integer rightInset(int rootWidth, Bounds bounds) {
|
||||
if (bounds == null || bounds.width <= 0) {
|
||||
return null;
|
||||
}
|
||||
return rootWidth - (bounds.left + bounds.width);
|
||||
}
|
||||
|
||||
private Integer rightPadding(View view) {
|
||||
return view != null ? view.getPaddingRight() : null;
|
||||
}
|
||||
|
||||
private Bounds statusUnion(View root, RootAnchors anchors) {
|
||||
if (root == null || anchors == null) {
|
||||
return null;
|
||||
}
|
||||
Bounds statusBounds = ViewGeometry.boundsRelativeTo(root, anchors.statusIcons);
|
||||
Bounds batteryBounds = ViewGeometry.boundsRelativeTo(root, anchors.battery);
|
||||
if (statusBounds == null) {
|
||||
return batteryBounds;
|
||||
}
|
||||
if (batteryBounds == null) {
|
||||
return statusBounds;
|
||||
}
|
||||
return statusBounds.union(batteryBounds);
|
||||
}
|
||||
|
||||
static ClockPlacement mergeClockPlacements(ArrayList<ClockPlacement> placements) {
|
||||
if (placements == null || placements.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
ClockPlacement first = placements.get(0);
|
||||
Bounds union = null;
|
||||
for (ClockPlacement placement : placements) {
|
||||
if (placement != null && placement.bounds != null) {
|
||||
union = union == null ? placement.bounds : union.union(placement.bounds);
|
||||
}
|
||||
}
|
||||
if (union == null) {
|
||||
return first;
|
||||
}
|
||||
ArrayList<ClockRow> rows = new ArrayList<>();
|
||||
for (ClockPlacement placement : placements) {
|
||||
if (placement == null || placement.rows == null || placement.bounds == null) {
|
||||
continue;
|
||||
}
|
||||
for (ClockRow row : placement.rows) {
|
||||
rows.add(new ClockRow(
|
||||
row.text,
|
||||
placement.bounds.left + row.x - union.left,
|
||||
placement.bounds.top + row.y - union.top,
|
||||
row.width,
|
||||
row.height,
|
||||
row.clipLeft,
|
||||
row.rowIndex,
|
||||
row.segment));
|
||||
}
|
||||
}
|
||||
return new ClockPlacement(first.source, first.contentDescription, rows, union);
|
||||
}
|
||||
|
||||
static Bounds cutoutBounds(Rect cutout, int cutoutGap) {
|
||||
if (cutout == null) {
|
||||
return null;
|
||||
}
|
||||
int left = cutout.left - cutoutGap;
|
||||
int right = cutout.right + cutoutGap;
|
||||
return new Bounds(left, cutout.top, Math.max(0, right - left), cutout.height());
|
||||
}
|
||||
|
||||
static LayoutPosition positionFor(String value) {
|
||||
if ("right".equals(value)) {
|
||||
return LayoutPosition.RIGHT;
|
||||
}
|
||||
if ("middle".equals(value)) {
|
||||
return LayoutPosition.MIDDLE;
|
||||
}
|
||||
return LayoutPosition.LEFT;
|
||||
}
|
||||
|
||||
static AnchorSide sideFor(String value) {
|
||||
return "right".equals(value) ? AnchorSide.RIGHT : AnchorSide.LEFT;
|
||||
}
|
||||
|
||||
private ArrayList<LayoutItemKind> orderedKinds(String encodedOrder) {
|
||||
ArrayList<LayoutItemKind> result = new ArrayList<>();
|
||||
if (encodedOrder != null) {
|
||||
for (String token : encodedOrder.split(",")) {
|
||||
LayoutItemKind kind = kindFor(token != null ? token.trim() : "");
|
||||
if (kind != null && !result.contains(kind)) {
|
||||
result.add(kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
addKindIfMissing(result, LayoutItemKind.CLOCK);
|
||||
addKindIfMissing(result, LayoutItemKind.CHIP);
|
||||
addKindIfMissing(result, LayoutItemKind.STATUS);
|
||||
addKindIfMissing(result, LayoutItemKind.NOTIFICATIONS);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addKindIfMissing(ArrayList<LayoutItemKind> kinds, LayoutItemKind kind) {
|
||||
if (!kinds.contains(kind)) {
|
||||
kinds.add(kind);
|
||||
}
|
||||
}
|
||||
|
||||
private LayoutItemKind kindFor(String token) {
|
||||
if ("clock".equals(token)) {
|
||||
return LayoutItemKind.CLOCK;
|
||||
}
|
||||
if ("chip".equals(token)) {
|
||||
return LayoutItemKind.CHIP;
|
||||
}
|
||||
if ("notifications".equals(token)) {
|
||||
return LayoutItemKind.NOTIFICATIONS;
|
||||
}
|
||||
if ("status".equals(token)) {
|
||||
return LayoutItemKind.STATUS;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private record StockInsets(int left, int right) {
|
||||
}
|
||||
}
|
||||
+739
@@ -0,0 +1,739 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.WeakHashMap;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.hooks.ReflectionSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
import se.ajpanton.statusbartweak.runtime.mode.SceneKey;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class UnlockedStockSourceCollector {
|
||||
private final WeakHashMap<View, RootAnchors> anchorsByRoot = new WeakHashMap<>();
|
||||
|
||||
void clear() {
|
||||
anchorsByRoot.clear();
|
||||
}
|
||||
|
||||
RootAnchors anchorsFor(View root) {
|
||||
RootAnchors cached = anchorsByRoot.get(root);
|
||||
if (cached != null && cached.isUsable()) {
|
||||
return cached;
|
||||
}
|
||||
RootAnchors anchors = discoverAnchors(root);
|
||||
anchorsByRoot.put(root, anchors);
|
||||
return anchors;
|
||||
}
|
||||
|
||||
CollectedIcons collect(View root, RootAnchors anchors) {
|
||||
ArrayList<SnapshotSource> statusIcons = new ArrayList<>();
|
||||
ArrayList<SnapshotSource> notificationIcons = new ArrayList<>();
|
||||
ArrayList<SnapshotSource> statusChips = new ArrayList<>();
|
||||
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
|
||||
if (anchors.statusRoot != null) {
|
||||
collectStatusSnapshotViews(anchors.statusRoot, statusIcons);
|
||||
}
|
||||
if (anchors.battery != null && !isDescendantOf(anchors.battery, anchors.statusRoot)) {
|
||||
collectStatusSnapshotViews(anchors.battery, statusIcons);
|
||||
}
|
||||
if (anchors.notificationRoot != null) {
|
||||
collectNotificationSnapshotViews(anchors.notificationRoot, notificationIcons);
|
||||
}
|
||||
collectStatusChipSnapshotViews(root, statusChips, statusChipMetrics);
|
||||
return new CollectedIcons(
|
||||
anchors.clockView,
|
||||
statusIcons,
|
||||
notificationIcons,
|
||||
statusChips,
|
||||
statusChipMetrics);
|
||||
}
|
||||
|
||||
CollectedIcons collectChips(
|
||||
View root,
|
||||
RootAnchors anchors,
|
||||
CollectedIcons previousIcons
|
||||
) {
|
||||
ArrayList<SnapshotSource> statusChips = new ArrayList<>();
|
||||
ArrayList<SnapshotSource> statusChipMetrics = new ArrayList<>();
|
||||
collectKnownStatusChipSnapshotViews(root, previousIcons, statusChips, statusChipMetrics);
|
||||
if (statusChips.isEmpty() && statusChipMetrics.isEmpty()) {
|
||||
collectStatusChipSnapshotViews(root, statusChips, statusChipMetrics);
|
||||
}
|
||||
return new CollectedIcons(
|
||||
anchors != null ? anchors.clockView : null,
|
||||
previousIcons != null ? previousIcons.statusIcons : new ArrayList<>(),
|
||||
previousIcons != null ? previousIcons.notificationIcons : new ArrayList<>(),
|
||||
statusChips,
|
||||
statusChipMetrics);
|
||||
}
|
||||
|
||||
LayoutInputSignature layoutInputSignature(
|
||||
View root,
|
||||
SbtSettings settings,
|
||||
SceneKey scene,
|
||||
Rect cutout,
|
||||
RootAnchors anchors,
|
||||
CollectedIcons previousIcons
|
||||
) {
|
||||
int chipSourceSignature = settings.layoutChipEnabledUnlocked
|
||||
? chipSourceSignature(root, previousIcons)
|
||||
: 0;
|
||||
return new LayoutInputSignature(
|
||||
scene != null ? scene.toString() : "",
|
||||
root.getWidth(),
|
||||
root.getHeight(),
|
||||
root.getPaddingLeft(),
|
||||
root.getPaddingRight(),
|
||||
cutout != null ? cutout.left : Integer.MIN_VALUE,
|
||||
cutout != null ? cutout.top : Integer.MIN_VALUE,
|
||||
cutout != null ? cutout.right : Integer.MIN_VALUE,
|
||||
cutout != null ? cutout.bottom : Integer.MIN_VALUE,
|
||||
layoutSettingsSignature(settings),
|
||||
clockTimeSignature(settings),
|
||||
viewSignatureOrZero(anchors != null ? anchors.clockView : null),
|
||||
shallowTreeSignature(anchors != null ? anchors.clockContainer : null),
|
||||
shallowTreeSignatureExcluding(
|
||||
anchors != null ? anchors.statusRoot : null,
|
||||
anchors != null ? anchors.clockContainer : null),
|
||||
shallowTreeSignatureExcluding(
|
||||
anchors != null ? anchors.statusEndContent : null,
|
||||
anchors != null ? anchors.clockContainer : null),
|
||||
shallowTreeSignatureExcluding(
|
||||
anchors != null ? anchors.statusIcons : null,
|
||||
anchors != null ? anchors.clockContainer : null),
|
||||
shallowTreeSignatureExcluding(
|
||||
anchors != null ? anchors.battery : null,
|
||||
anchors != null ? anchors.clockContainer : null),
|
||||
notificationRootSignature(anchors != null ? anchors.notificationRoot : null),
|
||||
sourceListSignature(previousIcons != null ? previousIcons.statusIcons : null),
|
||||
sourceListSignature(previousIcons != null ? previousIcons.notificationIcons : null),
|
||||
chipSourceSignature);
|
||||
}
|
||||
|
||||
private void collectKnownStatusChipSnapshotViews(
|
||||
View root,
|
||||
CollectedIcons previousIcons,
|
||||
ArrayList<SnapshotSource> visibleOut,
|
||||
ArrayList<SnapshotSource> metricOut
|
||||
) {
|
||||
if (root == null || previousIcons == null || visibleOut == null || metricOut == null) {
|
||||
return;
|
||||
}
|
||||
if (previousIcons.statusChips != null) {
|
||||
for (SnapshotSource source : previousIcons.statusChips) {
|
||||
View view = source.view();
|
||||
if (isStatusChipSnapshotCandidate(root, view)) {
|
||||
visibleOut.add(source);
|
||||
metricOut.add(source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (previousIcons.statusChipMetrics != null) {
|
||||
for (SnapshotSource source : previousIcons.statusChipMetrics) {
|
||||
if (isStatusChipMetricCandidate(root, source.view())) {
|
||||
metricOut.add(source);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RootAnchors discoverAnchors(View root) {
|
||||
View clockContainer = null;
|
||||
TextView clockView = null;
|
||||
View statusRoot = null;
|
||||
int statusRootPriority = Integer.MAX_VALUE;
|
||||
View statusEndContent = null;
|
||||
View battery = null;
|
||||
View statusIcons = null;
|
||||
View notificationRoot = null;
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
String idName = resolveIdName(view);
|
||||
if (clockContainer == null && idName.equals("left_clock_container")) {
|
||||
clockContainer = view;
|
||||
}
|
||||
if (clockView == null
|
||||
&& view instanceof TextView textView
|
||||
&& view.getClass().getName().contains("QSClockIndicatorView")) {
|
||||
clockView = textView;
|
||||
}
|
||||
if (statusEndContent == null && idName.equals("status_bar_end_side_content")) {
|
||||
statusEndContent = view;
|
||||
}
|
||||
int priority = statusRootPriority(idName);
|
||||
if (priority < statusRootPriority) {
|
||||
statusRoot = view;
|
||||
statusRootPriority = priority;
|
||||
}
|
||||
if (statusIcons == null && idName.equals("statusIcons")) {
|
||||
statusIcons = view;
|
||||
}
|
||||
if (battery == null && idName.equals("battery")) {
|
||||
battery = view;
|
||||
}
|
||||
if (notificationRoot == null && idName.equals("notificationIcons")) {
|
||||
notificationRoot = view;
|
||||
}
|
||||
if (clockContainer != null
|
||||
&& clockView != null
|
||||
&& statusRootPriority == 0
|
||||
&& statusEndContent != null
|
||||
&& statusIcons != null
|
||||
&& battery != null
|
||||
&& notificationRoot != null) {
|
||||
break;
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.addLast(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new RootAnchors(
|
||||
clockContainer,
|
||||
clockView,
|
||||
statusRoot,
|
||||
statusEndContent,
|
||||
statusIcons,
|
||||
battery,
|
||||
notificationRoot);
|
||||
}
|
||||
|
||||
private int statusRootPriority(String idName) {
|
||||
if (idName.equals("system_icon_area")) {
|
||||
return 0;
|
||||
}
|
||||
if (idName.equals("status_bar_end_side_content")) {
|
||||
return 1;
|
||||
}
|
||||
if (idName.equals("statusIcons")) {
|
||||
return 2;
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
private void collectStatusSnapshotViews(View source, ArrayList<SnapshotSource> out) {
|
||||
boolean candidate = isStatusSnapshotCandidate(source);
|
||||
if (isModernSignalContainer(source)) {
|
||||
if (candidate) {
|
||||
out.add(new SnapshotSource(source, SnapshotMode.VIEW));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (candidate) {
|
||||
out.add(new SnapshotSource(source, SnapshotMode.VIEW));
|
||||
if (isAtomicStatusSource(source)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (source instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
collectStatusSnapshotViews(group.getChildAt(i), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isModernSignalContainer(View view) {
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("ModernStatusBarWifiView")
|
||||
|| className.contains("ModernStatusBarMobileView");
|
||||
}
|
||||
|
||||
private void collectNotificationSnapshotViews(View source, ArrayList<SnapshotSource> out) {
|
||||
if (isNotificationSnapshotCandidate(source)) {
|
||||
out.add(new SnapshotSource(source, SnapshotMode.NOTIFICATION_DRAWABLE));
|
||||
return;
|
||||
}
|
||||
if (source instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
collectNotificationSnapshotViews(group.getChildAt(i), out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void collectStatusChipSnapshotViews(
|
||||
View root,
|
||||
ArrayList<SnapshotSource> visibleOut,
|
||||
ArrayList<SnapshotSource> metricOut
|
||||
) {
|
||||
if (root == null || visibleOut == null || metricOut == null) {
|
||||
return;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (isStatusChipSnapshotCandidate(root, view)) {
|
||||
visibleOut.add(new SnapshotSource(view, SnapshotMode.VIEW));
|
||||
metricOut.add(new SnapshotSource(view, SnapshotMode.VIEW));
|
||||
return;
|
||||
}
|
||||
if (metricOut.isEmpty() && isStatusChipMetricCandidate(root, view)) {
|
||||
metricOut.add(new SnapshotSource(view, SnapshotMode.VIEW));
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
queue.addLast(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isStatusChipSnapshotCandidate(View root, View view) {
|
||||
if (view == null || view == root || view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
if (ReflectionSupport.getBooleanField(view, "sbtStatusChipForcedHidden", false)) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0 || root == null || width >= root.getWidth() / 2) {
|
||||
return false;
|
||||
}
|
||||
Bounds bounds = ViewGeometry.boundsRelativeTo(root, view);
|
||||
if (bounds == null || bounds.top < 0 || bounds.top > root.getHeight()) {
|
||||
return false;
|
||||
}
|
||||
if (!isStatusChipCandidate(view)) {
|
||||
return false;
|
||||
}
|
||||
Object parent = view.getParent();
|
||||
while (parent instanceof View parentView && parentView != root) {
|
||||
if (isStatusChipCandidate(parentView)) {
|
||||
return false;
|
||||
}
|
||||
parent = parentView.getParent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isStatusChipMetricCandidate(View root, View view) {
|
||||
if (view == null || view == root || root == null) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0 || width >= root.getWidth() / 2) {
|
||||
return false;
|
||||
}
|
||||
Bounds bounds = ViewGeometry.boundsRelativeTo(root, view);
|
||||
if (bounds == null || bounds.top < 0 || bounds.top > root.getHeight()) {
|
||||
return false;
|
||||
}
|
||||
if (!isStatusChipCandidate(view)) {
|
||||
return false;
|
||||
}
|
||||
Object parent = view.getParent();
|
||||
while (parent instanceof View parentView && parentView != root) {
|
||||
if (isStatusChipCandidate(parentView)) {
|
||||
return false;
|
||||
}
|
||||
parent = parentView.getParent();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isStatusChipCandidate(View view) {
|
||||
String className = view.getClass().getName().toLowerCase(java.util.Locale.ROOT);
|
||||
String idName = resolveIdName(view).toLowerCase(java.util.Locale.ROOT);
|
||||
if (className.contains("privacy") || idName.contains("privacy")) {
|
||||
return false;
|
||||
}
|
||||
return className.contains("touchinterceptframelayout")
|
||||
|| className.contains("ongoingactivity")
|
||||
|| "ongoing_activity_capsule".equals(idName);
|
||||
}
|
||||
|
||||
private boolean isStatusSnapshotCandidate(View view) {
|
||||
if (view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0) {
|
||||
return false;
|
||||
}
|
||||
String idName = resolveIdName(view);
|
||||
if (idName.equals("battery") || containsAny(idName, "wifi", "mobile", "signal")) {
|
||||
return true;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
if (className.contains("StatusBarIcon")
|
||||
|| className.contains("StatusBarWifi")
|
||||
|| className.contains("StatusBarMobile")
|
||||
|| className.contains("StatusBarSignal")
|
||||
|| className.contains("BatteryMeterView")) {
|
||||
return true;
|
||||
}
|
||||
return view instanceof ImageView && idName.isEmpty();
|
||||
}
|
||||
|
||||
private boolean isAtomicStatusSource(View view) {
|
||||
if (view instanceof ImageView) {
|
||||
return true;
|
||||
}
|
||||
String idName = resolveIdName(view);
|
||||
String className = view.getClass().getName();
|
||||
return idName.equals("battery")
|
||||
|| containsAny(idName, "wifi", "mobile", "signal")
|
||||
|| className.contains("StatusBarWifi")
|
||||
|| className.contains("StatusBarMobile")
|
||||
|| className.contains("StatusBarSignal")
|
||||
|| className.contains("BatteryMeterView");
|
||||
}
|
||||
|
||||
private boolean isNotificationSnapshotCandidate(View view) {
|
||||
if (view instanceof TextView) {
|
||||
return false;
|
||||
}
|
||||
if (view.getVisibility() != View.VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
if (width <= 0 || height <= 0 || resolveIdName(view).equals("status_bar_dot")) {
|
||||
return false;
|
||||
}
|
||||
String className = view.getClass().getName();
|
||||
return className.contains("StatusBarIconView")
|
||||
|| className.contains("IconView")
|
||||
|| view instanceof ImageView;
|
||||
}
|
||||
|
||||
private boolean containsAny(String value, String... needles) {
|
||||
String lowerValue = value.toLowerCase(java.util.Locale.ROOT);
|
||||
for (String needle : needles) {
|
||||
if (lowerValue.contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isDescendantOf(View child, View ancestor) {
|
||||
if (child == null || ancestor == null) {
|
||||
return false;
|
||||
}
|
||||
View current = child;
|
||||
while (current != null) {
|
||||
if (current == ancestor) {
|
||||
return true;
|
||||
}
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int viewTreeSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = viewSignature(view);
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
result = 31 * result + viewTreeSignature(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int viewSignatureOrZero(View view) {
|
||||
return view != null ? viewSignature(view) : 0;
|
||||
}
|
||||
|
||||
private static int shallowTreeSignature(View view) {
|
||||
return shallowTreeSignatureExcluding(view, null);
|
||||
}
|
||||
|
||||
private static int shallowTreeSignatureExcluding(View view, View excluded) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
if (view == excluded) {
|
||||
return 0;
|
||||
}
|
||||
int result = viewSignature(view);
|
||||
if (view instanceof ViewGroup group) {
|
||||
int includedChildren = 0;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child == excluded) {
|
||||
continue;
|
||||
}
|
||||
includedChildren++;
|
||||
result = 31 * result + viewSignature(child);
|
||||
}
|
||||
result = 31 * result + includedChildren;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int notificationRootSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(view);
|
||||
result = 31 * result + view.getClass().getName().hashCode();
|
||||
result = 31 * result + view.getId();
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(view.getAlpha());
|
||||
if (view instanceof ViewGroup group) {
|
||||
result = 31 * result + group.getChildCount();
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
result = 31 * result + System.identityHashCode(child);
|
||||
result = 31 * result + child.getClass().getName().hashCode();
|
||||
result = 31 * result + child.getId();
|
||||
result = 31 * result + child.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(child.getAlpha());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int sourceListSignature(ArrayList<SnapshotSource> sources) {
|
||||
if (sources == null || sources.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + sources.size();
|
||||
for (SnapshotSource source : sources) {
|
||||
result = 31 * result + source.mode().ordinal();
|
||||
result = 31 * result + viewTreeSignature(source.view());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int chipSourceSignature(View root, CollectedIcons previousIcons) {
|
||||
int result = 17;
|
||||
boolean hasKnownChipSource = false;
|
||||
if (previousIcons != null) {
|
||||
if (previousIcons.statusChips != null && !previousIcons.statusChips.isEmpty()) {
|
||||
hasKnownChipSource = true;
|
||||
result = 31 * result + chipSourceListSignature(previousIcons.statusChips);
|
||||
}
|
||||
if (previousIcons.statusChipMetrics != null && !previousIcons.statusChipMetrics.isEmpty()) {
|
||||
hasKnownChipSource = true;
|
||||
result = 31 * result + chipSourceListSignature(previousIcons.statusChipMetrics);
|
||||
}
|
||||
}
|
||||
if (!hasKnownChipSource) {
|
||||
result = 31 * result + statusChipCandidateSignature(root);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int statusChipCandidateSignature(View root) {
|
||||
if (root == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(root);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (view != root && isStatusChipCandidate(view)) {
|
||||
result = 31 * result + chipViewLayoutSignature(view);
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
queue.addLast(group.getChildAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int chipSourceListSignature(ArrayList<SnapshotSource> sources) {
|
||||
if (sources == null || sources.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + sources.size();
|
||||
for (SnapshotSource source : sources) {
|
||||
result = 31 * result + source.mode().ordinal();
|
||||
result = 31 * result + chipViewLayoutSignature(source.view());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int chipViewLayoutSignature(View view) {
|
||||
if (view == null) {
|
||||
return 0;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(view);
|
||||
result = 31 * result + view.getClass().getName().hashCode();
|
||||
result = 31 * result + view.getId();
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(view.getAlpha());
|
||||
result = 31 * result + view.getTop();
|
||||
result = 31 * result + width;
|
||||
result = 31 * result + height;
|
||||
result = 31 * result + Math.round(view.getTranslationY());
|
||||
result = 31 * result + view.getPaddingLeft();
|
||||
result = 31 * result + view.getPaddingRight();
|
||||
result = 31 * result + view.getPaddingTop();
|
||||
result = 31 * result + view.getPaddingBottom();
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int viewSignature(View view) {
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(view);
|
||||
result = 31 * result + view.getClass().getName().hashCode();
|
||||
result = 31 * result + view.getId();
|
||||
result = 31 * result + view.getVisibility();
|
||||
result = 31 * result + Float.floatToIntBits(view.getAlpha());
|
||||
result = 31 * result + view.getLeft();
|
||||
result = 31 * result + view.getTop();
|
||||
result = 31 * result + view.getRight();
|
||||
result = 31 * result + view.getBottom();
|
||||
result = 31 * result + view.getMeasuredWidth();
|
||||
result = 31 * result + view.getMeasuredHeight();
|
||||
result = 31 * result + view.getScrollX();
|
||||
result = 31 * result + view.getScrollY();
|
||||
result = 31 * result + Math.round(view.getTranslationX());
|
||||
result = 31 * result + Math.round(view.getTranslationY());
|
||||
result = 31 * result + view.getPaddingLeft();
|
||||
result = 31 * result + view.getPaddingRight();
|
||||
result = 31 * result + view.getPaddingTop();
|
||||
result = 31 * result + view.getPaddingBottom();
|
||||
CharSequence contentDescription = view.getContentDescription();
|
||||
result = 31 * result + (contentDescription != null ? contentDescription.toString().hashCode() : 0);
|
||||
result = 31 * result + java.util.Arrays.hashCode(view.getDrawableState());
|
||||
if (view instanceof TextView textView) {
|
||||
CharSequence text = textView.getText();
|
||||
result = 31 * result + (text != null ? text.toString().hashCode() : 0);
|
||||
result = 31 * result + textView.getCurrentTextColor();
|
||||
result = 31 * result + Float.floatToIntBits(textView.getTextSize());
|
||||
result = 31 * result + textView.getGravity();
|
||||
result = 31 * result + textView.getBaseline();
|
||||
result = 31 * result + (textView.getTypeface() != null
|
||||
? System.identityHashCode(textView.getTypeface())
|
||||
: 0);
|
||||
}
|
||||
if (view instanceof ImageView imageView) {
|
||||
result = 31 * result + drawableSignature(imageView.getDrawable());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int drawableSignature(Drawable drawable) {
|
||||
if (drawable == null) {
|
||||
return 0;
|
||||
}
|
||||
int result = 17;
|
||||
result = 31 * result + System.identityHashCode(drawable);
|
||||
result = 31 * result + drawable.getLevel();
|
||||
result = 31 * result + drawable.getAlpha();
|
||||
result = 31 * result + java.util.Arrays.hashCode(drawable.getState());
|
||||
Rect bounds = drawable.getBounds();
|
||||
result = 31 * result + bounds.left;
|
||||
result = 31 * result + bounds.top;
|
||||
result = 31 * result + bounds.right;
|
||||
result = 31 * result + bounds.bottom;
|
||||
return result;
|
||||
}
|
||||
|
||||
private String resolveIdName(View view) {
|
||||
if (view.getId() == View.NO_ID) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
return view.getResources().getResourceEntryName(view.getId());
|
||||
} catch (Resources.NotFoundException ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private int clockTimeSignature(SbtSettings settings) {
|
||||
if (settings == null || !settings.clockEnabledUnlocked) {
|
||||
return 0;
|
||||
}
|
||||
if (!settings.clockShowSeconds
|
||||
&& !settings.clockShowDatePrefix
|
||||
&& !settings.clockCustomFormatEnabled) {
|
||||
return 0;
|
||||
}
|
||||
long now = System.currentTimeMillis();
|
||||
boolean seconds = settings.clockShowSeconds
|
||||
|| (settings.clockCustomFormatEnabled
|
||||
&& settings.clockCustomFormat != null
|
||||
&& settings.clockCustomFormat.contains("s"));
|
||||
long divisor = seconds ? 1_000L : 60_000L;
|
||||
return Long.hashCode(now / divisor);
|
||||
}
|
||||
|
||||
private int layoutSettingsSignature(SbtSettings settings) {
|
||||
if (settings == null) {
|
||||
return 0;
|
||||
}
|
||||
return java.util.Objects.hash(
|
||||
settings.clockEnabled,
|
||||
settings.clockEnabledUnlocked,
|
||||
settings.clockPosition,
|
||||
settings.clockHorizontalOffsetPx,
|
||||
settings.clockMiddleCutoutSide,
|
||||
settings.clockMiddleAutoNearestSide,
|
||||
settings.clockMiddleCutoutGapPx,
|
||||
settings.clockVerticalOffsetPx,
|
||||
settings.clockShowSeconds,
|
||||
settings.clockShowDatePrefix,
|
||||
settings.clockCustomFormatEnabled,
|
||||
settings.clockCustomFormat,
|
||||
settings.clockIgnoreUnderDisplayCameras,
|
||||
settings.layoutPaddingCutoutPx,
|
||||
settings.layoutPaddingLeftPx,
|
||||
settings.layoutPaddingRightPx,
|
||||
settings.layoutItemPaddingPx,
|
||||
settings.layoutPaddingLeftAddStock,
|
||||
settings.layoutPaddingRightAddStock,
|
||||
settings.layoutIgnoreUnderDisplayCameras,
|
||||
settings.layoutItemOrder,
|
||||
settings.layoutChipPosition,
|
||||
settings.layoutChipEnabledUnlocked,
|
||||
settings.layoutChipMiddleSide,
|
||||
settings.layoutChipMiddleAutoNearestSide,
|
||||
settings.layoutChipVerticalOffsetPx,
|
||||
settings.layoutNotifPosition,
|
||||
settings.layoutNotifSortOrder,
|
||||
settings.layoutNotifShowDotIfTruncated,
|
||||
settings.layoutNotifEnabledUnlocked,
|
||||
settings.layoutNotifMiddleSide,
|
||||
settings.layoutNotifMiddleAutoNearestSide,
|
||||
settings.layoutNotifVerticalOffsetPx,
|
||||
settings.layoutStatusPosition,
|
||||
settings.layoutStatusEnabledUnlocked,
|
||||
settings.layoutStatusShowDotIfTruncated,
|
||||
settings.layoutStatusMiddleSide,
|
||||
settings.layoutStatusMiddleAutoNearestSide,
|
||||
settings.layoutStatusVerticalOffsetPx,
|
||||
settings.statusChipsHideAll,
|
||||
settings.statusChipsHideMediaUnlocked,
|
||||
settings.statusChipsHideNavUnlocked,
|
||||
settings.statusChipsHideCallUnlocked);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.widget.TextView;
|
||||
|
||||
final class UnlockedVerticalOffsetScaler {
|
||||
private int baselineNotificationIconSizePx;
|
||||
private int baselineStatusIconHeightPx;
|
||||
private int baselineChipHeightPx;
|
||||
private float baselineClockTextSizePx;
|
||||
|
||||
int notification(int offsetPx, int currentIconHeightPx) {
|
||||
baselineNotificationIconSizePx = updateBaseline(
|
||||
baselineNotificationIconSizePx,
|
||||
currentIconHeightPx);
|
||||
return scaledOffset(offsetPx, currentIconHeightPx, baselineNotificationIconSizePx);
|
||||
}
|
||||
|
||||
int status(int offsetPx, int currentIconHeightPx) {
|
||||
baselineStatusIconHeightPx = updateBaseline(
|
||||
baselineStatusIconHeightPx,
|
||||
currentIconHeightPx);
|
||||
return scaledOffset(offsetPx, currentIconHeightPx, baselineStatusIconHeightPx);
|
||||
}
|
||||
|
||||
int chip(int offsetPx, int currentChipHeightPx) {
|
||||
baselineChipHeightPx = updateBaseline(baselineChipHeightPx, currentChipHeightPx);
|
||||
return scaledOffset(offsetPx, currentChipHeightPx, baselineChipHeightPx);
|
||||
}
|
||||
|
||||
int clock(int offsetPx, TextView source) {
|
||||
if (offsetPx == 0 || source == null || source.getTextSize() <= 0f) {
|
||||
return offsetPx;
|
||||
}
|
||||
float textSize = source.getTextSize();
|
||||
if (baselineClockTextSizePx <= 0f || textSize < baselineClockTextSizePx) {
|
||||
baselineClockTextSizePx = textSize;
|
||||
}
|
||||
return Math.round(offsetPx * textSize / baselineClockTextSizePx);
|
||||
}
|
||||
|
||||
private int updateBaseline(int baselinePx, int currentPx) {
|
||||
if (currentPx <= 0) {
|
||||
return baselinePx;
|
||||
}
|
||||
return baselinePx <= 0 || currentPx < baselinePx ? currentPx : baselinePx;
|
||||
}
|
||||
|
||||
private int scaledOffset(int offsetPx, int currentPx, int baselinePx) {
|
||||
if (offsetPx == 0 || currentPx <= 0 || baselinePx <= 0) {
|
||||
return offsetPx;
|
||||
}
|
||||
return Math.round((float) offsetPx * currentPx / baselinePx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package se.ajpanton.statusbartweak.runtime.render;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import se.ajpanton.statusbartweak.runtime.layoutsolver.StatusBarLayoutSolver.Bounds;
|
||||
|
||||
final class ViewGeometry {
|
||||
private ViewGeometry() {
|
||||
}
|
||||
|
||||
static Bounds boundsRelativeTo(View root, View view) {
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
int left = 0;
|
||||
int top = 0;
|
||||
View current = view;
|
||||
while (current != null && current != root) {
|
||||
left += current.getLeft() - current.getScrollX() + Math.round(current.getTranslationX());
|
||||
top += current.getTop() - current.getScrollY() + Math.round(current.getTranslationY());
|
||||
Object parent = current.getParent();
|
||||
current = parent instanceof View ? (View) parent : null;
|
||||
}
|
||||
if (current != root) {
|
||||
return null;
|
||||
}
|
||||
int width = view.getWidth() > 0 ? view.getWidth() : view.getMeasuredWidth();
|
||||
int height = view.getHeight() > 0 ? view.getHeight() : view.getMeasuredHeight();
|
||||
return new Bounds(left, top, Math.max(0, width), Math.max(0, height));
|
||||
}
|
||||
|
||||
static Rect middleCutout(View root) {
|
||||
if (root == null || root.getWidth() <= 0) {
|
||||
return null;
|
||||
}
|
||||
WindowInsets insets = root.getRootWindowInsets();
|
||||
DisplayCutout cutout = insets != null ? insets.getDisplayCutout() : null;
|
||||
List<Rect> rects = cutout != null ? cutout.getBoundingRects() : null;
|
||||
if (rects == null || rects.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int rootWidth = root.getWidth();
|
||||
for (Rect rect : rects) {
|
||||
Rect localRect = localCutoutRect(root, rect);
|
||||
if (localRect == null || localRect.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (localRect.left <= 0 || localRect.right >= rootWidth) {
|
||||
continue;
|
||||
}
|
||||
return new Rect(
|
||||
Math.max(0, localRect.left),
|
||||
Math.max(0, localRect.top),
|
||||
Math.min(rootWidth, localRect.right),
|
||||
Math.min(root.getHeight(), localRect.bottom));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Rect localCutoutRect(View root, Rect rect) {
|
||||
if (root == null || rect == null || rect.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
int[] location = new int[2];
|
||||
root.getLocationOnScreen(location);
|
||||
Rect local = new Rect(
|
||||
rect.left - location[0],
|
||||
rect.top - location[1],
|
||||
rect.right - location[0],
|
||||
rect.bottom - location[1]);
|
||||
if (intersectsRoot(root, local)) {
|
||||
return local;
|
||||
}
|
||||
if (intersectsRoot(root, rect)) {
|
||||
return new Rect(rect);
|
||||
}
|
||||
return local;
|
||||
}
|
||||
|
||||
private static boolean intersectsRoot(View root, Rect rect) {
|
||||
return rect != null
|
||||
&& rect.right > 0
|
||||
&& rect.bottom > 0
|
||||
&& rect.left < root.getWidth()
|
||||
&& rect.top < root.getHeight();
|
||||
}
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package se.ajpanton.statusbartweak.runtime.settings;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Build;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
/**
|
||||
* Small process-local cache for hot SystemUI hooks.
|
||||
*
|
||||
* Runtime hooks can run multiple times per frame. Reading settings through the
|
||||
* provider in those paths is unnecessarily expensive; settings changes already
|
||||
* send ACTION_SETTINGS_CHANGED, so we invalidate and reload on demand.
|
||||
*/
|
||||
public final class RuntimeSettingsCache {
|
||||
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private static SbtSettings cachedSettings;
|
||||
private static boolean receiverRegistered;
|
||||
|
||||
private RuntimeSettingsCache() {
|
||||
}
|
||||
|
||||
public static SbtSettings get(Context context) {
|
||||
if (context == null) {
|
||||
return cachedSettings != null ? cachedSettings : SbtSettings.defaults();
|
||||
}
|
||||
registerReceiverIfNeeded(context);
|
||||
SbtSettings settings = cachedSettings;
|
||||
if (settings != null) {
|
||||
return settings;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
if (cachedSettings == null) {
|
||||
cachedSettings = SbtSettings.from(context);
|
||||
}
|
||||
return cachedSettings != null ? cachedSettings : SbtSettings.defaults();
|
||||
}
|
||||
}
|
||||
|
||||
public static void invalidate() {
|
||||
synchronized (LOCK) {
|
||||
cachedSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerReceiverIfNeeded(Context context) {
|
||||
if (receiverRegistered || context == null) {
|
||||
return;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
if (receiverRegistered) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
if (appContext == null) {
|
||||
appContext = context;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
invalidate();
|
||||
}
|
||||
};
|
||||
IntentFilter filter = new IntentFilter(SbtSettings.ACTION_SETTINGS_CHANGED);
|
||||
filter.addAction(Intent.ACTION_USER_UNLOCKED);
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
appContext.registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
appContext.registerReceiver(receiver, filter);
|
||||
}
|
||||
receiverRegistered = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package se.ajpanton.statusbartweak.shell.debug;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public final class DebugBootReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action)
|
||||
|| Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)) {
|
||||
DebugNotifications.applyPending(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package se.ajpanton.statusbartweak.shell.debug;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
public final class DebugNotificationDeleteReceiver extends BroadcastReceiver {
|
||||
private static final String ACTION = "se.ajpanton.statusbartweak.DEBUG_DELETE";
|
||||
private static final String EXTRA_NUMBER = "number";
|
||||
|
||||
static Intent newIntent(Context context, int number) {
|
||||
Intent intent = new Intent(context, DebugNotificationDeleteReceiver.class);
|
||||
intent.setAction(ACTION);
|
||||
intent.putExtra(EXTRA_NUMBER, number);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (!ACTION.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
int number = intent.getIntExtra(EXTRA_NUMBER, 0);
|
||||
if (number > 0 && number <= DebugNotifications.getDesiredCount(context)) {
|
||||
DebugNotifications.applyPending(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
package se.ajpanton.statusbartweak.shell.debug;
|
||||
|
||||
import android.Manifest;
|
||||
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.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import se.ajpanton.statusbartweak.shell.ui.MainActivity;
|
||||
|
||||
public final class DebugNotifications {
|
||||
private static final String PREFS = "sbt_debug_notifications";
|
||||
private static final String KEY_DESIRED = "debug_icon_count";
|
||||
private static final String KEY_CYCLE_ENABLED = "debug_icon_cycle_enabled";
|
||||
private static final String KEY_CYCLE_CURRENT = "debug_icon_cycle_current";
|
||||
private static final String KEY_CYCLE_DIRECTION = "debug_icon_cycle_direction";
|
||||
private static final String KEY_BOOT_EPOCH = "debug_boot_epoch";
|
||||
private static final String KEY_RECOVERY_USED = "debug_autogroup_recovery_used";
|
||||
|
||||
private static final int MIN_COUNT = 0;
|
||||
private static final int MAX_COUNT = 50;
|
||||
private static final int DEBUG_ID_BASE = 10_000;
|
||||
private static final long AUTO_GROUP_CHECK_DELAY_MS = 3_500L;
|
||||
private static final long BOOT_EPOCH_TOLERANCE_MS = 5_000L;
|
||||
private static final long CYCLE_STEP_MS = 2_000L;
|
||||
|
||||
private static final String CHANNEL_ID = "sbt_debug_icons";
|
||||
private static final String CHANNEL_NAME = "Debug Icons";
|
||||
private static final String GROUP_PREFIX = "sbt_debug_";
|
||||
private static final String SHORTCUT_PREFIX = "sbt_debug_shortcut_";
|
||||
private static final String EXTRA_DEBUG_NOTIFICATION = "sbt_debug_notification";
|
||||
private static final String EXTRA_DEBUG_NUMBER = "sbt_debug_number";
|
||||
|
||||
private static final Handler RECOVERY_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private static final Handler CYCLE_HANDLER = new Handler(Looper.getMainLooper());
|
||||
private static Runnable recoveryRunnable;
|
||||
private static Runnable cycleRunnable;
|
||||
private static PowerManager.WakeLock cycleWakeLock;
|
||||
private static boolean recoveryInProgress;
|
||||
|
||||
private DebugNotifications() {
|
||||
}
|
||||
|
||||
public static int getDesiredCount(Context context) {
|
||||
return prefs(context).getInt(KEY_DESIRED, 0);
|
||||
}
|
||||
|
||||
public static int clampCount(int value) {
|
||||
return Math.min(MAX_COUNT, Math.max(MIN_COUNT, value));
|
||||
}
|
||||
|
||||
public static boolean isCycleEnabled(Context context) {
|
||||
return prefs(context).getBoolean(KEY_CYCLE_ENABLED, false);
|
||||
}
|
||||
|
||||
public static void setCycleEnabled(Context context, boolean enabled) {
|
||||
SharedPreferences prefs = prefs(context);
|
||||
SharedPreferences.Editor edit = prefs.edit().putBoolean(KEY_CYCLE_ENABLED, enabled);
|
||||
if (enabled) {
|
||||
edit.putInt(KEY_CYCLE_CURRENT, 0);
|
||||
edit.putInt(KEY_CYCLE_DIRECTION, 1);
|
||||
}
|
||||
edit.putBoolean(KEY_RECOVERY_USED, false);
|
||||
edit.apply();
|
||||
applyPending(context);
|
||||
}
|
||||
|
||||
public static void setDesiredCount(Context context, int desiredCount) {
|
||||
prefs(context)
|
||||
.edit()
|
||||
.putInt(KEY_DESIRED, clampCount(desiredCount))
|
||||
.putBoolean(KEY_RECOVERY_USED, false)
|
||||
.apply();
|
||||
applyPending(context);
|
||||
}
|
||||
|
||||
public static boolean hasPostNotificationsPermission(Context context) {
|
||||
return context.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public static void applyPending(Context context) {
|
||||
if (!hasPostNotificationsPermission(context)) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
SharedPreferences prefs = prefs(appContext);
|
||||
ensureBootEpoch(prefs);
|
||||
int configured = clampCount(prefs.getInt(KEY_DESIRED, 0));
|
||||
int desired = resolveDesiredVisibleCount(prefs, configured);
|
||||
NotificationManager nm = appContext.getSystemService(NotificationManager.class);
|
||||
ensureChannel(nm);
|
||||
cancelDebugNotificationsAbove(appContext, nm, desired);
|
||||
cleanupAutoGroupSummaries(appContext, nm);
|
||||
|
||||
for (int i = 1; i <= desired; i++) {
|
||||
nm.notify(DEBUG_ID_BASE + i, buildDebugNotification(appContext, i));
|
||||
}
|
||||
if (desired > 0) {
|
||||
scheduleAutoGroupRecovery(appContext);
|
||||
} else {
|
||||
cancelAutoGroupRecovery();
|
||||
}
|
||||
syncCycleScheduler(appContext, prefs, configured);
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(Context context) {
|
||||
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static void ensureChannel(NotificationManager nm) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
CHANNEL_NAME,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
);
|
||||
channel.setDescription("Debug notification icons for StatusBarTweak");
|
||||
channel.setShowBadge(false);
|
||||
channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private static Notification buildDebugNotification(Context context, int number) {
|
||||
Intent openIntent = MainActivity.newOpenDebugIntent(context);
|
||||
Intent deleteIntent = DebugNotificationDeleteReceiver.newIntent(context, number);
|
||||
int flags = PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE;
|
||||
Bundle extras = new Bundle();
|
||||
extras.putBoolean(EXTRA_DEBUG_NOTIFICATION, true);
|
||||
extras.putInt(EXTRA_DEBUG_NUMBER, number);
|
||||
|
||||
Notification notification = new Notification.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(createNumberIcon(context, number))
|
||||
.setContentTitle("Debug icon " + number)
|
||||
.setContentText("StatusBarTweak")
|
||||
.setShowWhen(false)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setCategory(Notification.CATEGORY_STATUS)
|
||||
.setGroup(GROUP_PREFIX + number)
|
||||
.setSortKey(String.format(Locale.ROOT, "%02d", number))
|
||||
.setShortcutId(SHORTCUT_PREFIX + number)
|
||||
.setContentIntent(PendingIntent.getActivity(context, 0, openIntent, flags))
|
||||
.setDeleteIntent(PendingIntent.getBroadcast(context, number, deleteIntent, flags))
|
||||
.addExtras(extras)
|
||||
.build();
|
||||
notification.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
|
||||
return notification;
|
||||
}
|
||||
|
||||
private static Icon createNumberIcon(Context context, int number) {
|
||||
int sizePx = dpToPx(context, 48);
|
||||
Bitmap bitmap = Bitmap.createBitmap(sizePx, sizePx, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
Paint bg = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
float hue = (number * 37f) % 360f;
|
||||
bg.setColor(Color.HSVToColor(new float[]{hue, 0.6f, 0.9f}));
|
||||
canvas.drawRect(0, 0, sizePx, sizePx, bg);
|
||||
|
||||
Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
text.setColor(Color.WHITE);
|
||||
text.setTextAlign(Paint.Align.CENTER);
|
||||
text.setFakeBoldText(true);
|
||||
String label = String.valueOf(number);
|
||||
text.setTextSize(sizePx * (label.length() >= 2 ? 0.45f : 0.6f));
|
||||
|
||||
Rect bounds = new Rect();
|
||||
text.getTextBounds(label, 0, label.length(), bounds);
|
||||
canvas.drawText(label, sizePx / 2f, sizePx / 2f - bounds.exactCenterY(), text);
|
||||
return Icon.createWithBitmap(bitmap);
|
||||
}
|
||||
|
||||
private static void cancelDebugNotificationsAbove(
|
||||
Context context,
|
||||
NotificationManager nm,
|
||||
int desired
|
||||
) {
|
||||
for (StatusBarNotification sbn : nm.getActiveNotifications()) {
|
||||
if (!context.getPackageName().equals(sbn.getPackageName())) {
|
||||
continue;
|
||||
}
|
||||
int debugNumber = sbn.getId() - DEBUG_ID_BASE;
|
||||
if (debugNumber > desired && debugNumber >= 1 && debugNumber <= MAX_COUNT) {
|
||||
cancel(nm, sbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void cleanupAutoGroupSummaries(Context context, NotificationManager nm) {
|
||||
for (StatusBarNotification sbn : nm.getActiveNotifications()) {
|
||||
if (!context.getPackageName().equals(sbn.getPackageName()) || isDebugId(sbn.getId())) {
|
||||
continue;
|
||||
}
|
||||
Notification notification = sbn.getNotification();
|
||||
if (notification == null || isDebugNotification(notification)) {
|
||||
continue;
|
||||
}
|
||||
String group = notification.getGroup();
|
||||
if (group == null || !group.startsWith(GROUP_PREFIX)) {
|
||||
cancel(nm, sbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void scheduleAutoGroupRecovery(Context context) {
|
||||
SharedPreferences prefs = prefs(context);
|
||||
ensureBootEpoch(prefs);
|
||||
if (prefs.getBoolean(KEY_RECOVERY_USED, false) || recoveryInProgress) {
|
||||
return;
|
||||
}
|
||||
Context appContext = context.getApplicationContext();
|
||||
cancelAutoGroupRecovery();
|
||||
recoveryRunnable = () -> runAutoGroupRecovery(appContext);
|
||||
RECOVERY_HANDLER.postDelayed(recoveryRunnable, AUTO_GROUP_CHECK_DELAY_MS);
|
||||
}
|
||||
|
||||
private static void runAutoGroupRecovery(Context appContext) {
|
||||
if (recoveryInProgress || appContext == null || !hasPostNotificationsPermission(appContext)) {
|
||||
return;
|
||||
}
|
||||
SharedPreferences prefs = prefs(appContext);
|
||||
ensureBootEpoch(prefs);
|
||||
if (prefs.getBoolean(KEY_RECOVERY_USED, false)) {
|
||||
return;
|
||||
}
|
||||
int configured = clampCount(prefs.getInt(KEY_DESIRED, 0));
|
||||
int desired = resolveDesiredVisibleCount(prefs, configured);
|
||||
if (desired <= 0) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm = appContext.getSystemService(NotificationManager.class);
|
||||
if (nm == null || !hasAutoGroupSummary(appContext, nm)) {
|
||||
return;
|
||||
}
|
||||
recoveryInProgress = true;
|
||||
try {
|
||||
prefs.edit().putBoolean(KEY_RECOVERY_USED, true).apply();
|
||||
cancelDebugNotificationsAndSummaries(appContext, nm);
|
||||
for (int i = 1; i <= desired; i++) {
|
||||
nm.notify(DEBUG_ID_BASE + i, buildDebugNotification(appContext, i));
|
||||
}
|
||||
} finally {
|
||||
recoveryInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void cancelAutoGroupRecovery() {
|
||||
if (recoveryRunnable != null) {
|
||||
RECOVERY_HANDLER.removeCallbacks(recoveryRunnable);
|
||||
recoveryRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasAutoGroupSummary(Context context, NotificationManager nm) {
|
||||
for (StatusBarNotification sbn : nm.getActiveNotifications()) {
|
||||
if (!context.getPackageName().equals(sbn.getPackageName()) || isDebugId(sbn.getId())) {
|
||||
continue;
|
||||
}
|
||||
Notification notification = sbn.getNotification();
|
||||
if (notification == null || isDebugNotification(notification)) {
|
||||
continue;
|
||||
}
|
||||
String group = notification.getGroup();
|
||||
if (group == null || !group.startsWith(GROUP_PREFIX)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void cancelDebugNotificationsAndSummaries(Context context, NotificationManager nm) {
|
||||
for (StatusBarNotification sbn : nm.getActiveNotifications()) {
|
||||
if (!context.getPackageName().equals(sbn.getPackageName())) {
|
||||
continue;
|
||||
}
|
||||
Notification notification = sbn.getNotification();
|
||||
boolean debugSummary = notification != null
|
||||
&& !isDebugNotification(notification)
|
||||
&& (notification.getGroup() == null
|
||||
|| !notification.getGroup().startsWith(GROUP_PREFIX));
|
||||
if (isDebugId(sbn.getId()) || debugSummary) {
|
||||
cancel(nm, sbn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ensureBootEpoch(SharedPreferences prefs) {
|
||||
long bootEpoch = System.currentTimeMillis() - SystemClock.elapsedRealtime();
|
||||
long stored = prefs.getLong(KEY_BOOT_EPOCH, -1L);
|
||||
if (stored <= 0L || Math.abs(stored - bootEpoch) > BOOT_EPOCH_TOLERANCE_MS) {
|
||||
prefs.edit()
|
||||
.putLong(KEY_BOOT_EPOCH, bootEpoch)
|
||||
.putBoolean(KEY_RECOVERY_USED, false)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isDebugNotification(Notification notification) {
|
||||
Bundle extras = notification.extras;
|
||||
return extras != null && extras.getBoolean(EXTRA_DEBUG_NOTIFICATION, false);
|
||||
}
|
||||
|
||||
private static boolean isDebugId(int id) {
|
||||
int debugNumber = id - DEBUG_ID_BASE;
|
||||
return debugNumber >= 1 && debugNumber <= MAX_COUNT;
|
||||
}
|
||||
|
||||
private static void cancel(NotificationManager nm, StatusBarNotification sbn) {
|
||||
if (sbn.getTag() != null) {
|
||||
nm.cancel(sbn.getTag(), sbn.getId());
|
||||
} else {
|
||||
nm.cancel(sbn.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private static int resolveDesiredVisibleCount(SharedPreferences prefs, int configuredCount) {
|
||||
if (!prefs.getBoolean(KEY_CYCLE_ENABLED, false)) {
|
||||
return configuredCount;
|
||||
}
|
||||
int current = clampCount(prefs.getInt(KEY_CYCLE_CURRENT, 0));
|
||||
if (current > configuredCount) {
|
||||
current = configuredCount;
|
||||
prefs.edit().putInt(KEY_CYCLE_CURRENT, current).apply();
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private static void syncCycleScheduler(
|
||||
Context appContext,
|
||||
SharedPreferences prefs,
|
||||
int configuredCount
|
||||
) {
|
||||
if (!prefs.getBoolean(KEY_CYCLE_ENABLED, false) || configuredCount <= 0) {
|
||||
stopCycleScheduler();
|
||||
return;
|
||||
}
|
||||
startCycleScheduler(appContext);
|
||||
}
|
||||
|
||||
private static void startCycleScheduler(Context appContext) {
|
||||
if (cycleRunnable != null) {
|
||||
return;
|
||||
}
|
||||
acquireCycleWakeLock(appContext);
|
||||
cycleRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!advanceCycleAndApply(appContext)) {
|
||||
stopCycleScheduler();
|
||||
return;
|
||||
}
|
||||
CYCLE_HANDLER.postDelayed(this, CYCLE_STEP_MS);
|
||||
}
|
||||
};
|
||||
CYCLE_HANDLER.postDelayed(cycleRunnable, CYCLE_STEP_MS);
|
||||
}
|
||||
|
||||
private static void stopCycleScheduler() {
|
||||
if (cycleRunnable != null) {
|
||||
CYCLE_HANDLER.removeCallbacks(cycleRunnable);
|
||||
cycleRunnable = null;
|
||||
}
|
||||
releaseCycleWakeLock();
|
||||
}
|
||||
|
||||
private static boolean advanceCycleAndApply(Context appContext) {
|
||||
SharedPreferences prefs = prefs(appContext);
|
||||
if (!prefs.getBoolean(KEY_CYCLE_ENABLED, false)) {
|
||||
return false;
|
||||
}
|
||||
int configured = clampCount(prefs.getInt(KEY_DESIRED, 0));
|
||||
if (configured <= 0) {
|
||||
prefs.edit()
|
||||
.putInt(KEY_CYCLE_CURRENT, 0)
|
||||
.putInt(KEY_CYCLE_DIRECTION, 1)
|
||||
.apply();
|
||||
applyPending(appContext);
|
||||
return false;
|
||||
}
|
||||
|
||||
int current = Math.min(clampCount(prefs.getInt(KEY_CYCLE_CURRENT, 0)), configured);
|
||||
int direction = prefs.getInt(KEY_CYCLE_DIRECTION, 1);
|
||||
direction = direction == -1 ? -1 : 1;
|
||||
current += direction;
|
||||
if (current >= configured) {
|
||||
current = configured;
|
||||
direction = -1;
|
||||
} else if (current <= 0) {
|
||||
current = 0;
|
||||
direction = 1;
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putInt(KEY_CYCLE_CURRENT, current)
|
||||
.putInt(KEY_CYCLE_DIRECTION, direction)
|
||||
.apply();
|
||||
applyPending(appContext);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void acquireCycleWakeLock(Context context) {
|
||||
if (cycleWakeLock == null) {
|
||||
PowerManager pm = context.getSystemService(PowerManager.class);
|
||||
cycleWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||
"StatusBarTweak:DebugCycle");
|
||||
cycleWakeLock.setReferenceCounted(false);
|
||||
}
|
||||
if (!cycleWakeLock.isHeld()) {
|
||||
cycleWakeLock.acquire();
|
||||
}
|
||||
}
|
||||
|
||||
private static void releaseCycleWakeLock() {
|
||||
if (cycleWakeLock != null && cycleWakeLock.isHeld()) {
|
||||
cycleWakeLock.release();
|
||||
}
|
||||
}
|
||||
|
||||
private static int dpToPx(Context context, int dp) {
|
||||
return (int) (dp * context.getResources().getDisplayMetrics().density + 0.5f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,478 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Base64;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class AppIconRules {
|
||||
private static final Pattern PACKAGE_NAME_PATTERN =
|
||||
Pattern.compile("^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$");
|
||||
|
||||
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_SEEN_SESSION = "app_icon_seen_session";
|
||||
|
||||
public static final String EXTRA_PACKAGE = "package";
|
||||
public static final String EXTRA_FIRST_SEEN_MS = "first_seen_ms";
|
||||
|
||||
public static final int MODE_AOD = 1;
|
||||
public static final int MODE_LOCK = 1 << 1;
|
||||
public static final int MODE_UNLOCK = 1 << 2;
|
||||
private static final String MODE_NAME_AOD = "AOD";
|
||||
private static final String MODE_NAME_LOCK = "LOCK";
|
||||
private static final String MODE_NAME_UNLOCK = "UNLOCK";
|
||||
private static final String MODE_NAME_UNKNOWN = "UNKNOWN";
|
||||
|
||||
private AppIconRules() {
|
||||
}
|
||||
|
||||
public static int modeMask(String mode) {
|
||||
String normalized = normalizeModeName(mode);
|
||||
if (MODE_NAME_AOD.equals(normalized)) {
|
||||
return MODE_AOD;
|
||||
}
|
||||
if (MODE_NAME_LOCK.equals(normalized)) {
|
||||
return MODE_LOCK;
|
||||
}
|
||||
if (MODE_NAME_UNLOCK.equals(normalized)) {
|
||||
return MODE_UNLOCK;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static boolean isValidPackageName(String pkg) {
|
||||
if (pkg == null || pkg.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return PACKAGE_NAME_PATTERN.matcher(pkg).matches();
|
||||
}
|
||||
|
||||
public static Map<String, Integer> loadBlockedModes(SharedPreferences prefs) {
|
||||
if (prefs == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Set<String> raw = prefs.getStringSet(PREF_KEY_BLOCKED_MODES, Collections.emptySet());
|
||||
return parseBlockedModes(raw);
|
||||
}
|
||||
|
||||
public static void saveBlockedModes(SharedPreferences prefs, Map<String, Integer> map) {
|
||||
if (prefs == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> encoded = encodeBlockedModes(map);
|
||||
prefs.edit().putStringSet(PREF_KEY_BLOCKED_MODES, encoded).apply();
|
||||
}
|
||||
|
||||
public static Map<String, Long> loadSeenSession(SharedPreferences prefs) {
|
||||
if (prefs == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Set<String> raw = prefs.getStringSet(PREF_KEY_SEEN_SESSION, Collections.emptySet());
|
||||
return parseSeenSession(raw);
|
||||
}
|
||||
|
||||
public static Map<String, ArrayList<ExceptionRule>> loadExceptionRules(SharedPreferences prefs) {
|
||||
if (prefs == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
Set<String> raw = prefs.getStringSet(PREF_KEY_EXCEPTION_RULES, Collections.emptySet());
|
||||
return parseExceptionRules(raw);
|
||||
}
|
||||
|
||||
public static void saveExceptionRules(SharedPreferences prefs, Map<String, ArrayList<ExceptionRule>> map) {
|
||||
if (prefs == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> encoded = encodeExceptionRules(map);
|
||||
prefs.edit().putStringSet(PREF_KEY_EXCEPTION_RULES, encoded).apply();
|
||||
}
|
||||
|
||||
public static void saveSeenSession(SharedPreferences prefs, Map<String, Long> map) {
|
||||
if (prefs == null) {
|
||||
return;
|
||||
}
|
||||
Set<String> encoded = encodeSeenSession(map);
|
||||
prefs.edit().putStringSet(PREF_KEY_SEEN_SESSION, encoded).apply();
|
||||
}
|
||||
|
||||
public static void clearSeenSession(SharedPreferences prefs) {
|
||||
if (prefs == null) {
|
||||
return;
|
||||
}
|
||||
prefs.edit().remove(PREF_KEY_SEEN_SESSION).apply();
|
||||
}
|
||||
|
||||
public static void noteSeen(Map<String, Long> seen, String pkg, long firstSeenMs) {
|
||||
if (seen == null || !isValidPackageName(pkg)) {
|
||||
return;
|
||||
}
|
||||
Long existing = seen.get(pkg);
|
||||
if (existing == null) {
|
||||
seen.put(pkg, firstSeenMs);
|
||||
return;
|
||||
}
|
||||
if (firstSeenMs > 0 && firstSeenMs < existing) {
|
||||
seen.put(pkg, firstSeenMs);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isBlocked(Map<String, Integer> masks, String pkg, String mode) {
|
||||
if (masks == null || !isValidPackageName(pkg)) {
|
||||
return false;
|
||||
}
|
||||
Integer mask = masks.get(pkg);
|
||||
if (mask == null) {
|
||||
return false;
|
||||
}
|
||||
int modeMask = modeMask(mode);
|
||||
if (modeMask == 0) {
|
||||
return false;
|
||||
}
|
||||
return (mask & modeMask) != 0;
|
||||
}
|
||||
|
||||
public static void setModeBlocked(Map<String, Integer> masks,
|
||||
String pkg,
|
||||
boolean blockAod,
|
||||
boolean blockLock,
|
||||
boolean blockUnlock) {
|
||||
if (masks == null || !isValidPackageName(pkg)) {
|
||||
return;
|
||||
}
|
||||
int mask = 0;
|
||||
if (blockAod) {
|
||||
mask |= MODE_AOD;
|
||||
}
|
||||
if (blockLock) {
|
||||
mask |= MODE_LOCK;
|
||||
}
|
||||
if (blockUnlock) {
|
||||
mask |= MODE_UNLOCK;
|
||||
}
|
||||
if (mask == 0) {
|
||||
masks.remove(pkg);
|
||||
return;
|
||||
}
|
||||
masks.put(pkg, mask);
|
||||
}
|
||||
|
||||
public static ArrayList<ExceptionRule> copyExceptionRules(List<ExceptionRule> source) {
|
||||
ArrayList<ExceptionRule> out = new ArrayList<>();
|
||||
if (source == null) {
|
||||
return out;
|
||||
}
|
||||
for (ExceptionRule rule : source) {
|
||||
if (rule == null || rule.regex == null) {
|
||||
continue;
|
||||
}
|
||||
String regex = rule.regex.trim();
|
||||
if (regex.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
out.add(new ExceptionRule(regex, sanitizeModeMask(rule.modeMask)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static void setExceptionRules(Map<String, ArrayList<ExceptionRule>> rulesByPackage,
|
||||
String pkg,
|
||||
List<ExceptionRule> rules) {
|
||||
if (rulesByPackage == null || !isValidPackageName(pkg)) {
|
||||
return;
|
||||
}
|
||||
ArrayList<ExceptionRule> copy = copyExceptionRules(rules);
|
||||
if (copy.isEmpty()) {
|
||||
rulesByPackage.remove(pkg);
|
||||
return;
|
||||
}
|
||||
rulesByPackage.put(pkg, copy);
|
||||
}
|
||||
|
||||
public static Map<String, Integer> parseBlockedModes(Set<String> rawSet) {
|
||||
Map<String, Integer> out = new HashMap<>();
|
||||
if (rawSet == null || rawSet.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (String entry : new HashSet<>(rawSet)) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = entry.split("\\|", -1);
|
||||
if (parts.length != 4) {
|
||||
continue;
|
||||
}
|
||||
String pkg = parts[0];
|
||||
if (!isValidPackageName(pkg)) {
|
||||
continue;
|
||||
}
|
||||
int mask = 0;
|
||||
if ("1".equals(parts[1])) {
|
||||
mask |= MODE_AOD;
|
||||
}
|
||||
if ("1".equals(parts[2])) {
|
||||
mask |= MODE_LOCK;
|
||||
}
|
||||
if ("1".equals(parts[3])) {
|
||||
mask |= MODE_UNLOCK;
|
||||
}
|
||||
if (mask != 0) {
|
||||
out.put(pkg, mask);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Set<String> encodeBlockedModes(Map<String, Integer> map) {
|
||||
Set<String> out = new HashSet<>();
|
||||
if (map == null || map.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (Map.Entry<String, Integer> e : map.entrySet()) {
|
||||
String pkg = e.getKey();
|
||||
Integer maskObj = e.getValue();
|
||||
if (!isValidPackageName(pkg) || maskObj == null) {
|
||||
continue;
|
||||
}
|
||||
int mask = maskObj;
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
}
|
||||
String entry = pkg + "|"
|
||||
+ (((mask & MODE_AOD) != 0) ? "1" : "0") + "|"
|
||||
+ (((mask & MODE_LOCK) != 0) ? "1" : "0") + "|"
|
||||
+ (((mask & MODE_UNLOCK) != 0) ? "1" : "0");
|
||||
out.add(entry);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Map<String, ArrayList<ExceptionRule>> parseExceptionRules(Set<String> rawSet) {
|
||||
Map<String, ArrayList<IndexedExceptionRule>> staged = new HashMap<>();
|
||||
if (rawSet == null || rawSet.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
for (String entry : new HashSet<>(rawSet)) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = entry.split("\\|", 4);
|
||||
if (parts.length != 4) {
|
||||
continue;
|
||||
}
|
||||
String pkg = parts[0];
|
||||
if (!isValidPackageName(pkg)) {
|
||||
continue;
|
||||
}
|
||||
int index;
|
||||
int modeMask;
|
||||
try {
|
||||
index = Integer.parseInt(parts[1]);
|
||||
modeMask = Integer.parseInt(parts[2]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
continue;
|
||||
}
|
||||
if (index < 0) {
|
||||
continue;
|
||||
}
|
||||
modeMask = sanitizeModeMask(modeMask);
|
||||
String regex;
|
||||
try {
|
||||
byte[] decoded = Base64.decode(parts[3], Base64.NO_WRAP);
|
||||
regex = new String(decoded, StandardCharsets.UTF_8).trim();
|
||||
} catch (Throwable ignored) {
|
||||
continue;
|
||||
}
|
||||
if (regex.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
staged.computeIfAbsent(pkg, key -> new ArrayList<>())
|
||||
.add(new IndexedExceptionRule(index, new ExceptionRule(regex, modeMask)));
|
||||
}
|
||||
Map<String, ArrayList<ExceptionRule>> out = new HashMap<>();
|
||||
for (Map.Entry<String, ArrayList<IndexedExceptionRule>> e : staged.entrySet()) {
|
||||
ArrayList<IndexedExceptionRule> stagedList = e.getValue();
|
||||
stagedList.sort((a, b) -> Integer.compare(a.index, b.index));
|
||||
ArrayList<ExceptionRule> rules = new ArrayList<>(stagedList.size());
|
||||
for (IndexedExceptionRule indexed : stagedList) {
|
||||
rules.add(indexed.rule);
|
||||
}
|
||||
if (!rules.isEmpty()) {
|
||||
out.put(e.getKey(), rules);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Set<String> encodeExceptionRules(Map<String, ArrayList<ExceptionRule>> map) {
|
||||
Set<String> out = new HashSet<>();
|
||||
if (map == null || map.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (Map.Entry<String, ArrayList<ExceptionRule>> e : map.entrySet()) {
|
||||
String pkg = e.getKey();
|
||||
if (!isValidPackageName(pkg) || e.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
ArrayList<ExceptionRule> rules = e.getValue();
|
||||
for (int i = 0; i < rules.size(); i++) {
|
||||
ExceptionRule rule = rules.get(i);
|
||||
if (rule == null || rule.regex == null) {
|
||||
continue;
|
||||
}
|
||||
String regex = rule.regex.trim();
|
||||
if (regex.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String encodedRegex = Base64.encodeToString(
|
||||
regex.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);
|
||||
int modeMask = sanitizeModeMask(rule.modeMask);
|
||||
out.add(pkg + "|" + i + "|" + modeMask + "|" + encodedRegex);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static int sanitizeModeMask(int mask) {
|
||||
int allowed = MODE_AOD | MODE_LOCK | MODE_UNLOCK;
|
||||
return mask & allowed;
|
||||
}
|
||||
|
||||
private static String normalizeModeName(String mode) {
|
||||
if (mode == null) {
|
||||
return MODE_NAME_UNKNOWN;
|
||||
}
|
||||
String normalized = mode.trim().toUpperCase(Locale.ROOT);
|
||||
if (normalized.isEmpty()) {
|
||||
return MODE_NAME_UNKNOWN;
|
||||
}
|
||||
if (MODE_NAME_AOD.equals(normalized)) {
|
||||
return MODE_NAME_AOD;
|
||||
}
|
||||
if (MODE_NAME_LOCK.equals(normalized) || "LOCKSCREEN".equals(normalized)) {
|
||||
return MODE_NAME_LOCK;
|
||||
}
|
||||
if (MODE_NAME_UNLOCK.equals(normalized) || "UNLOCKED".equals(normalized)) {
|
||||
return MODE_NAME_UNLOCK;
|
||||
}
|
||||
return MODE_NAME_UNKNOWN;
|
||||
}
|
||||
|
||||
public static Map<String, Long> parseSeenSession(Set<String> rawSet) {
|
||||
Map<String, Long> out = new HashMap<>();
|
||||
if (rawSet == null || rawSet.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (String entry : new HashSet<>(rawSet)) {
|
||||
if (entry == null || entry.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = entry.split("\\|", -1);
|
||||
if (parts.length != 2) {
|
||||
continue;
|
||||
}
|
||||
String pkg = parts[0];
|
||||
if (!isValidPackageName(pkg)) {
|
||||
continue;
|
||||
}
|
||||
long firstSeen;
|
||||
try {
|
||||
firstSeen = Long.parseLong(parts[1]);
|
||||
} catch (NumberFormatException ignored) {
|
||||
continue;
|
||||
}
|
||||
if (firstSeen <= 0L) {
|
||||
continue;
|
||||
}
|
||||
Long existing = out.get(pkg);
|
||||
if (existing == null || firstSeen < existing) {
|
||||
out.put(pkg, firstSeen);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static Set<String> encodeSeenSession(Map<String, Long> map) {
|
||||
Set<String> out = new HashSet<>();
|
||||
if (map == null || map.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (Map.Entry<String, Long> e : map.entrySet()) {
|
||||
String pkg = e.getKey();
|
||||
Long firstSeenObj = e.getValue();
|
||||
if (!isValidPackageName(pkg) || firstSeenObj == null) {
|
||||
continue;
|
||||
}
|
||||
long firstSeen = firstSeenObj;
|
||||
if (firstSeen <= 0L) {
|
||||
continue;
|
||||
}
|
||||
out.add(pkg + "|" + firstSeen);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public static final class SeenApp {
|
||||
public final String packageName;
|
||||
public final long firstSeenMs;
|
||||
|
||||
public SeenApp(String packageName, long firstSeenMs) {
|
||||
this.packageName = packageName;
|
||||
this.firstSeenMs = firstSeenMs;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ExceptionRule {
|
||||
public final String regex;
|
||||
public final int modeMask;
|
||||
|
||||
public ExceptionRule(String regex, int modeMask) {
|
||||
this.regex = regex;
|
||||
this.modeMask = sanitizeModeMask(modeMask);
|
||||
}
|
||||
|
||||
public boolean blocksMode(String mode) {
|
||||
int modeBit = modeMask(mode);
|
||||
if (modeBit == 0) {
|
||||
return false;
|
||||
}
|
||||
return (modeMask & modeBit) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class IndexedExceptionRule {
|
||||
final int index;
|
||||
final ExceptionRule rule;
|
||||
|
||||
IndexedExceptionRule(int index, ExceptionRule rule) {
|
||||
this.index = index;
|
||||
this.rule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
public static List<SeenApp> sortSeenOldestFirst(Map<String, Long> seen) {
|
||||
List<SeenApp> out = new ArrayList<>();
|
||||
if (seen == null || seen.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
for (Map.Entry<String, Long> e : seen.entrySet()) {
|
||||
String pkg = e.getKey();
|
||||
Long firstSeen = e.getValue();
|
||||
if (!isValidPackageName(pkg) || firstSeen == null || firstSeen <= 0L) {
|
||||
continue;
|
||||
}
|
||||
out.add(new SeenApp(pkg, firstSeen));
|
||||
}
|
||||
out.sort((a, b) -> Long.compare(a.firstSeenMs, b.firstSeenMs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class BatteryBarStyle {
|
||||
public static final String THRESHOLD_CHARGE_USE_DEFAULT = "@default_charge";
|
||||
public static final class Threshold {
|
||||
public final int percent;
|
||||
public final String dischargeColor;
|
||||
public final String chargeColor;
|
||||
|
||||
public Threshold(int percent, String dischargeColor, String chargeColor) {
|
||||
this.percent = clampPercent(percent);
|
||||
this.dischargeColor = normalizeColor(dischargeColor);
|
||||
this.chargeColor = normalizeThresholdChargeColor(chargeColor);
|
||||
}
|
||||
}
|
||||
|
||||
private BatteryBarStyle() {
|
||||
}
|
||||
|
||||
public static String normalizeColor(String raw) {
|
||||
return ColourExpressionSupport.normaliseExpression(raw, true);
|
||||
}
|
||||
|
||||
public static int parseColorOrDefault(String raw, int fallback) {
|
||||
return ColourExpressionSupport.resolveColor(raw, fallback, fallback, true);
|
||||
}
|
||||
|
||||
public static String normalizeThresholdChargeColor(String raw) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String value = raw.trim();
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (THRESHOLD_CHARGE_USE_DEFAULT.equals(value)) {
|
||||
return THRESHOLD_CHARGE_USE_DEFAULT;
|
||||
}
|
||||
return normalizeColor(value);
|
||||
}
|
||||
|
||||
public static boolean isThresholdChargeDefault(String raw) {
|
||||
return THRESHOLD_CHARGE_USE_DEFAULT.equals(raw);
|
||||
}
|
||||
|
||||
public static String resolveCurrentColorHex(Context context, String fallbackRaw) {
|
||||
int fallback = parseColorOrDefault(fallbackRaw, Color.WHITE);
|
||||
return resolveCurrentColorHex(context, fallback, fallbackRaw);
|
||||
}
|
||||
|
||||
public static String resolveCurrentColorHex(Context context, int referenceColor, String fallbackRaw) {
|
||||
int fallback = parseColorOrDefault(fallbackRaw, Color.WHITE);
|
||||
int resolved = resolveCurrentColor(context, referenceColor, fallback);
|
||||
int alpha = Color.alpha(resolved);
|
||||
if (alpha >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
public static ArrayList<Threshold> decodeThresholds(Collection<String> encoded) {
|
||||
ArrayList<Threshold> result = new ArrayList<>();
|
||||
if (encoded == null) {
|
||||
return result;
|
||||
}
|
||||
for (String entry : encoded) {
|
||||
if (entry == null) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = entry.split("\\|", -1);
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
int percent;
|
||||
try {
|
||||
percent = Integer.parseInt(parts[0].trim());
|
||||
} catch (NumberFormatException ignored) {
|
||||
continue;
|
||||
}
|
||||
String discharge = parts.length > 1 ? Uri.decode(parts[1]) : "";
|
||||
String charge = parts.length > 2 ? Uri.decode(parts[2]) : "";
|
||||
String normalizedDischarge = normalizeColor(discharge);
|
||||
if (normalizedDischarge.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
result.add(new Threshold(percent, normalizedDischarge, charge));
|
||||
}
|
||||
sortThresholds(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ArrayList<String> encodeThresholds(List<Threshold> thresholds) {
|
||||
ArrayList<String> result = new ArrayList<>();
|
||||
if (thresholds == null) {
|
||||
return result;
|
||||
}
|
||||
for (Threshold threshold : thresholds) {
|
||||
if (threshold == null) {
|
||||
continue;
|
||||
}
|
||||
String discharge = normalizeColor(threshold.dischargeColor);
|
||||
if (discharge.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String charge = normalizeThresholdChargeColor(threshold.chargeColor);
|
||||
result.add(clampPercent(threshold.percent)
|
||||
+ "|" + Uri.encode(discharge)
|
||||
+ "|" + Uri.encode(charge));
|
||||
}
|
||||
Collections.sort(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void sortThresholds(List<Threshold> thresholds) {
|
||||
if (thresholds == null) {
|
||||
return;
|
||||
}
|
||||
Collections.sort(thresholds, Comparator.comparingInt(t -> t.percent));
|
||||
}
|
||||
|
||||
public static int resolveColor(int batteryLevel,
|
||||
boolean charging,
|
||||
String defaultDischarge,
|
||||
String defaultCharge,
|
||||
List<Threshold> thresholds,
|
||||
int referenceColor,
|
||||
int fallback) {
|
||||
String chosenDischarge = normalizeColor(defaultDischarge);
|
||||
String chosenCharge = normalizeColor(defaultCharge);
|
||||
if (thresholds != null && batteryLevel >= 0) {
|
||||
for (Threshold threshold : thresholds) {
|
||||
if (threshold == null) {
|
||||
continue;
|
||||
}
|
||||
if (batteryLevel <= threshold.percent) {
|
||||
chosenDischarge = normalizeColor(threshold.dischargeColor);
|
||||
String thresholdCharge = normalizeThresholdChargeColor(threshold.chargeColor);
|
||||
chosenCharge = isThresholdChargeDefault(thresholdCharge)
|
||||
? normalizeColor(defaultCharge)
|
||||
: thresholdCharge;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (charging) {
|
||||
if (!chosenCharge.isEmpty()) {
|
||||
return ColourExpressionSupport.resolveColor(chosenCharge, referenceColor, fallback, true);
|
||||
}
|
||||
if (!chosenDischarge.isEmpty()) {
|
||||
return ColourExpressionSupport.resolveColor(chosenDischarge, referenceColor, fallback, true);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
if (!chosenDischarge.isEmpty()) {
|
||||
return ColourExpressionSupport.resolveColor(chosenDischarge, referenceColor, fallback, true);
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
public static int resolveCurrentColor(Context context, int fallback) {
|
||||
return resolveCurrentColor(context, fallback, fallback);
|
||||
}
|
||||
|
||||
public static int resolveCurrentColor(Context context, int referenceColor, int fallback) {
|
||||
if (context == null) {
|
||||
return fallback;
|
||||
}
|
||||
SbtSettings settings = SbtSettings.from(context);
|
||||
BatterySnapshot snapshot = resolveBatterySnapshot(context);
|
||||
return resolveColor(
|
||||
snapshot.level,
|
||||
snapshot.charging,
|
||||
settings.batteryBarDefaultDischargeColor,
|
||||
settings.batteryBarDefaultChargeColor,
|
||||
settings.batteryBarThresholds,
|
||||
referenceColor,
|
||||
fallback);
|
||||
}
|
||||
|
||||
private static BatterySnapshot resolveBatterySnapshot(Context context) {
|
||||
IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
|
||||
Intent sticky = null;
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= 33) {
|
||||
sticky = context.registerReceiver(null, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
sticky = context.registerReceiver(null, filter);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (sticky != null) {
|
||||
int level = sticky.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
|
||||
int scale = sticky.getIntExtra(BatteryManager.EXTRA_SCALE, 100);
|
||||
int percent = (level >= 0 && scale > 0)
|
||||
? Math.round((level * 100f) / scale)
|
||||
: -1;
|
||||
int status = sticky.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
|
||||
boolean charging = sticky.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0
|
||||
|| status == BatteryManager.BATTERY_STATUS_CHARGING
|
||||
|| status == BatteryManager.BATTERY_STATUS_FULL;
|
||||
return new BatterySnapshot(percent, charging);
|
||||
}
|
||||
BatteryManager batteryManager = context.getSystemService(BatteryManager.class);
|
||||
int level = batteryManager != null
|
||||
? batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
|
||||
: -1;
|
||||
boolean charging = batteryManager != null && batteryManager.isCharging();
|
||||
return new BatterySnapshot(level, charging);
|
||||
}
|
||||
|
||||
private static final class BatterySnapshot {
|
||||
final int level;
|
||||
final boolean charging;
|
||||
|
||||
BatterySnapshot(int level, boolean charging) {
|
||||
this.level = level;
|
||||
this.charging = charging;
|
||||
}
|
||||
}
|
||||
|
||||
public static int clampPercent(int percent) {
|
||||
if (percent < 1) {
|
||||
return 1;
|
||||
}
|
||||
if (percent > 100) {
|
||||
return 100;
|
||||
}
|
||||
return percent;
|
||||
}
|
||||
}
|
||||
+437
@@ -0,0 +1,437 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Rect;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Display;
|
||||
import android.view.DisplayCutout;
|
||||
import android.view.View;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ClockCameraTypeSupport {
|
||||
public static final String TYPE_BLOCKING = "blocking";
|
||||
public static final String TYPE_UDC = "under_display";
|
||||
|
||||
private static final char ENTRY_SEPARATOR = '\u001f';
|
||||
|
||||
public static final class CameraInfo {
|
||||
public int semDisplayDeviceType = Integer.MIN_VALUE;
|
||||
public int fillUdcDisplayCutout = Integer.MIN_VALUE;
|
||||
public boolean isUdcMainDisplay;
|
||||
public String cutoutType;
|
||||
public String cutoutString;
|
||||
public Rect excludeRect;
|
||||
public String cameraId;
|
||||
public String automaticType = TYPE_BLOCKING;
|
||||
public String effectiveType = TYPE_BLOCKING;
|
||||
public boolean overridden;
|
||||
}
|
||||
|
||||
private ClockCameraTypeSupport() {
|
||||
}
|
||||
|
||||
public static String normalizeType(String value) {
|
||||
if (TYPE_UDC.equals(value)) {
|
||||
return TYPE_UDC;
|
||||
}
|
||||
return TYPE_BLOCKING;
|
||||
}
|
||||
|
||||
public static String oppositeType(String value) {
|
||||
return TYPE_UDC.equals(normalizeType(value)) ? TYPE_BLOCKING : TYPE_UDC;
|
||||
}
|
||||
|
||||
public static String formatType(String value) {
|
||||
return TYPE_UDC.equals(normalizeType(value))
|
||||
? "under-display camera"
|
||||
: "blocking cutout";
|
||||
}
|
||||
|
||||
public static String buildCameraId(int semDisplayDeviceType, String cutoutType, String cutoutString) {
|
||||
String descriptor = semDisplayDeviceType + "|"
|
||||
+ safe(cutoutType) + "|"
|
||||
+ safe(cutoutString);
|
||||
return "cam_" + shortSha1(descriptor);
|
||||
}
|
||||
|
||||
public static CameraInfo resolveCameraInfo(View root,
|
||||
DisplayCutout displayCutout,
|
||||
SbtSettings settings) {
|
||||
CameraInfo info = new CameraInfo();
|
||||
if (root == null) {
|
||||
return info;
|
||||
}
|
||||
Context context = root.getContext();
|
||||
if (context == null) {
|
||||
finalizeCameraInfo(info, settings);
|
||||
return info;
|
||||
}
|
||||
info.semDisplayDeviceType = readSemDisplayDeviceType(context);
|
||||
info.fillUdcDisplayCutout = readGlobalInt(context, "fill_udc_display_cutout");
|
||||
Object displayLifecycle = resolveDisplayLifecycle(context);
|
||||
try {
|
||||
ClassLoader classLoader = context.getClassLoader();
|
||||
Class<?> inputClass = findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.IndicatorGardenInputProperties",
|
||||
classLoader);
|
||||
Class<?> utilClass = findClassIfExists(
|
||||
"com.android.systemui.statusbar.phone.IndicatorCutoutUtil",
|
||||
classLoader);
|
||||
if (inputClass == null || utilClass == null || displayLifecycle == null) {
|
||||
finalizeCameraInfo(info, settings);
|
||||
return info;
|
||||
}
|
||||
|
||||
Object input = newInstance(inputClass, context);
|
||||
Display display = root.getDisplay();
|
||||
if (display != null) {
|
||||
setIntField(input, "rotation", display.getRotation());
|
||||
}
|
||||
int width = root.getWidth();
|
||||
if (width <= 0 && root.getRootView() != null) {
|
||||
width = root.getRootView().getWidth();
|
||||
}
|
||||
if (width > 0) {
|
||||
setIntField(input, "statusBarWidth", width);
|
||||
}
|
||||
|
||||
Object util = newInstance(utilClass, context, input, displayLifecycle);
|
||||
info.isUdcMainDisplay = asBoolean(callMethod(util, "isUDCMainDisplay"));
|
||||
DisplayCutout presenterCutout = displayCutout;
|
||||
if (info.isUdcMainDisplay && info.fillUdcDisplayCutout == 0) {
|
||||
presenterCutout = null;
|
||||
}
|
||||
callMethod(input, "onGardenApplyWindowInsets", presenterCutout);
|
||||
Object cutoutType = readObjectField(util, "cutoutType");
|
||||
Object cutoutString = readObjectField(util, "cutoutString");
|
||||
info.cutoutType = cutoutType != null ? String.valueOf(cutoutType) : null;
|
||||
info.cutoutString = cutoutString != null ? String.valueOf(cutoutString) : null;
|
||||
info.excludeRect = (Rect) callMethod(util, "getDisplayCutoutAreaToExclude");
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
finalizeCameraInfo(info, settings);
|
||||
return info;
|
||||
}
|
||||
|
||||
public static Map<String, String> parseOverrides(Set<String> encoded) {
|
||||
if (encoded == null || encoded.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
HashMap<String, String> out = new HashMap<>();
|
||||
for (String item : encoded) {
|
||||
if (TextUtils.isEmpty(item)) {
|
||||
continue;
|
||||
}
|
||||
int split = item.indexOf(ENTRY_SEPARATOR);
|
||||
if (split <= 0 || split >= item.length() - 1) {
|
||||
continue;
|
||||
}
|
||||
String cameraId = item.substring(0, split);
|
||||
String type = normalizeType(item.substring(split + 1));
|
||||
if (!TextUtils.isEmpty(cameraId)) {
|
||||
out.put(cameraId, type);
|
||||
}
|
||||
}
|
||||
return out.isEmpty() ? Collections.emptyMap() : out;
|
||||
}
|
||||
|
||||
public static Set<String> encodeOverrides(Map<String, String> overrides) {
|
||||
if (overrides == null || overrides.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
HashSet<String> out = new HashSet<>();
|
||||
for (Map.Entry<String, String> entry : overrides.entrySet()) {
|
||||
String cameraId = entry.getKey();
|
||||
if (TextUtils.isEmpty(cameraId)) {
|
||||
continue;
|
||||
}
|
||||
out.add(cameraId + ENTRY_SEPARATOR + normalizeType(entry.getValue()));
|
||||
}
|
||||
return out.isEmpty() ? Collections.emptySet() : out;
|
||||
}
|
||||
|
||||
public static void saveOverrides(SharedPreferences prefs, String key, Map<String, String> overrides) {
|
||||
if (prefs == null || TextUtils.isEmpty(key)) {
|
||||
return;
|
||||
}
|
||||
prefs.edit().putStringSet(key, encodeOverrides(overrides)).apply();
|
||||
}
|
||||
|
||||
public static String findReportValue(String report, String key) {
|
||||
if (TextUtils.isEmpty(report) || TextUtils.isEmpty(key)) {
|
||||
return "";
|
||||
}
|
||||
String prefix = key + "=";
|
||||
String[] lines = report.split("\\r?\\n");
|
||||
for (String line : lines) {
|
||||
if (line != null && line.startsWith(prefix)) {
|
||||
return line.substring(prefix.length()).trim();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static void finalizeCameraInfo(CameraInfo info, SbtSettings settings) {
|
||||
if (info == null) {
|
||||
return;
|
||||
}
|
||||
info.cameraId = buildCameraId(
|
||||
info.semDisplayDeviceType,
|
||||
info.cutoutType,
|
||||
info.cutoutString);
|
||||
boolean explicitUdc = info.isUdcMainDisplay || "UDC".equals(info.cutoutType);
|
||||
info.automaticType = explicitUdc ? TYPE_UDC : TYPE_BLOCKING;
|
||||
String override = settings != null && settings.clockCameraTypeOverrides != null
|
||||
? settings.clockCameraTypeOverrides.get(info.cameraId)
|
||||
: null;
|
||||
info.overridden = !TextUtils.isEmpty(override);
|
||||
info.effectiveType = info.overridden
|
||||
? normalizeType(override)
|
||||
: info.automaticType;
|
||||
}
|
||||
|
||||
private static Object resolveDisplayLifecycle(Context context) {
|
||||
if (context == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ClassLoader classLoader = context.getClassLoader();
|
||||
Class<?> dependencyClass = findClassIfExists(
|
||||
"com.android.systemui.Dependency",
|
||||
classLoader);
|
||||
Class<?> displayLifecycleClass = findClassIfExists(
|
||||
"com.android.systemui.keyguard.DisplayLifecycle",
|
||||
classLoader);
|
||||
if (dependencyClass == null || displayLifecycleClass == null) {
|
||||
return null;
|
||||
}
|
||||
Object dependency = getStaticObjectField(dependencyClass, "sDependency");
|
||||
if (dependency == null) {
|
||||
return null;
|
||||
}
|
||||
return callMethod(dependency, "getDependencyInner", displayLifecycleClass);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int readSemDisplayDeviceType(Context context) {
|
||||
if (context == null) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
try {
|
||||
return getIntField(
|
||||
context.getResources().getConfiguration(),
|
||||
"semDisplayDeviceType");
|
||||
} catch (Throwable ignored) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private static int readGlobalInt(Context context, String name) {
|
||||
if (context == null || TextUtils.isEmpty(name)) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
try {
|
||||
return Settings.Global.getInt(context.getContentResolver(), name, 0);
|
||||
} catch (Throwable ignored) {
|
||||
return Integer.MIN_VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
private static Object readObjectField(Object target, String fieldName) {
|
||||
if (target == null || TextUtils.isEmpty(fieldName)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return getObjectField(target, fieldName);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> findClassIfExists(String className, ClassLoader classLoader) {
|
||||
try {
|
||||
return Class.forName(className, false, classLoader);
|
||||
} catch (Throwable ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Object newInstance(Class<?> cls, Object... args) throws Exception {
|
||||
Constructor<?>[] constructors = cls.getConstructors();
|
||||
for (Constructor<?> constructor : constructors) {
|
||||
Class<?>[] parameterTypes = constructor.getParameterTypes();
|
||||
if (parameterTypes.length != args.length) {
|
||||
continue;
|
||||
}
|
||||
if (!areArgumentsCompatible(parameterTypes, args)) {
|
||||
continue;
|
||||
}
|
||||
return constructor.newInstance(args);
|
||||
}
|
||||
throw new NoSuchMethodException("No matching constructor for " + cls.getName());
|
||||
}
|
||||
|
||||
private static Object callMethod(Object target, String methodName, Object... args) throws Exception {
|
||||
Method method = findCompatibleMethod(target.getClass(), methodName, args);
|
||||
if (method == null) {
|
||||
throw new NoSuchMethodException(methodName);
|
||||
}
|
||||
method.setAccessible(true);
|
||||
return method.invoke(target, args);
|
||||
}
|
||||
|
||||
private static Method findCompatibleMethod(Class<?> cls, String methodName, Object... args) {
|
||||
for (Method method : cls.getMethods()) {
|
||||
if (!method.getName().equals(methodName)) {
|
||||
continue;
|
||||
}
|
||||
if (areArgumentsCompatible(method.getParameterTypes(), args)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
for (Method method : cls.getDeclaredMethods()) {
|
||||
if (!method.getName().equals(methodName)) {
|
||||
continue;
|
||||
}
|
||||
if (areArgumentsCompatible(method.getParameterTypes(), args)) {
|
||||
return method;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static boolean areArgumentsCompatible(Class<?>[] parameterTypes, Object[] args) {
|
||||
if (parameterTypes.length != args.length) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < parameterTypes.length; i++) {
|
||||
Object arg = args[i];
|
||||
if (arg == null) {
|
||||
if (parameterTypes[i].isPrimitive()) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Class<?> boxed = boxType(parameterTypes[i]);
|
||||
if (!boxed.isInstance(arg)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void setIntField(Object target, String fieldName, int value) throws Exception {
|
||||
Field field = findField(target.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException(fieldName);
|
||||
}
|
||||
field.setAccessible(true);
|
||||
field.setInt(target, value);
|
||||
}
|
||||
|
||||
private static int getIntField(Object target, String fieldName) throws Exception {
|
||||
Field field = findField(target.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException(fieldName);
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return field.getInt(target);
|
||||
}
|
||||
|
||||
private static Object getObjectField(Object target, String fieldName) throws Exception {
|
||||
Field field = findField(target.getClass(), fieldName);
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException(fieldName);
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return field.get(target);
|
||||
}
|
||||
|
||||
private static Object getStaticObjectField(Class<?> cls, String fieldName) throws Exception {
|
||||
Field field = findField(cls, fieldName);
|
||||
if (field == null) {
|
||||
throw new NoSuchFieldException(fieldName);
|
||||
}
|
||||
field.setAccessible(true);
|
||||
return field.get(null);
|
||||
}
|
||||
|
||||
private static Field findField(Class<?> cls, String fieldName) {
|
||||
Class<?> current = cls;
|
||||
while (current != null) {
|
||||
try {
|
||||
return current.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Class<?> boxType(Class<?> cls) {
|
||||
if (!cls.isPrimitive()) {
|
||||
return cls;
|
||||
}
|
||||
if (cls == int.class) {
|
||||
return Integer.class;
|
||||
}
|
||||
if (cls == boolean.class) {
|
||||
return Boolean.class;
|
||||
}
|
||||
if (cls == long.class) {
|
||||
return Long.class;
|
||||
}
|
||||
if (cls == float.class) {
|
||||
return Float.class;
|
||||
}
|
||||
if (cls == double.class) {
|
||||
return Double.class;
|
||||
}
|
||||
if (cls == short.class) {
|
||||
return Short.class;
|
||||
}
|
||||
if (cls == byte.class) {
|
||||
return Byte.class;
|
||||
}
|
||||
if (cls == char.class) {
|
||||
return Character.class;
|
||||
}
|
||||
return cls;
|
||||
}
|
||||
|
||||
private static boolean asBoolean(Object value) {
|
||||
return value instanceof Boolean && (Boolean) value;
|
||||
}
|
||||
|
||||
private static String safe(String value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
private static String shortSha1(String source) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
byte[] hash = digest.digest(source.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
StringBuilder out = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(hash.length, 5); i++) {
|
||||
out.append(String.format(Locale.US, "%02x", hash[i] & 0xff));
|
||||
}
|
||||
return out.toString();
|
||||
} catch (Throwable ignored) {
|
||||
return Integer.toHexString(source.hashCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
+394
@@ -0,0 +1,394 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class ColourExpressionSupport {
|
||||
private static final Pattern OPERATION_PATTERN =
|
||||
Pattern.compile("([+\\-*/])\\s*(" + hexOperandPattern() + "|\\d+(?:\\.\\d+)?|\\.\\d+)");
|
||||
private static final Pattern LEADING_HEX_PATTERN =
|
||||
Pattern.compile("(?i)^#?(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})");
|
||||
|
||||
private ColourExpressionSupport() {
|
||||
}
|
||||
|
||||
public static String normaliseExpression(String raw, boolean assumeHashPrefix) {
|
||||
if (raw == null) {
|
||||
return "";
|
||||
}
|
||||
String value = raw.trim();
|
||||
if (value.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int variantSplit = findVariantSplit(value);
|
||||
String left = value;
|
||||
String right = null;
|
||||
if (variantSplit >= 0) {
|
||||
left = value.substring(0, variantSplit).trim();
|
||||
right = value.substring(variantSplit + 1).trim();
|
||||
}
|
||||
if (!isValidSegment(left, assumeHashPrefix, false, Color.WHITE)) {
|
||||
return "";
|
||||
}
|
||||
if (right != null && !isValidSegment(right, assumeHashPrefix, true, Color.WHITE)) {
|
||||
return "";
|
||||
}
|
||||
if (right == null) {
|
||||
return left;
|
||||
}
|
||||
return left + "|" + right;
|
||||
}
|
||||
|
||||
public static int resolveColor(String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix) {
|
||||
String value = normaliseExpression(raw, assumeHashPrefix);
|
||||
if (value.isEmpty()) {
|
||||
return fallbackColor;
|
||||
}
|
||||
int variantSplit = findVariantSplit(value);
|
||||
if (variantSplit < 0) {
|
||||
return resolveSegment(value, fallbackColor, assumeHashPrefix, false);
|
||||
}
|
||||
String left = value.substring(0, variantSplit).trim();
|
||||
String right = value.substring(variantSplit + 1).trim();
|
||||
int brightVariant = resolveSegment(left, fallbackColor, assumeHashPrefix, false);
|
||||
if (!isDark(referenceColor)) {
|
||||
return brightVariant;
|
||||
}
|
||||
return resolveSegment(right, brightVariant, assumeHashPrefix, true);
|
||||
}
|
||||
|
||||
public static String resolveColorHex(String raw,
|
||||
int referenceColor,
|
||||
int fallbackColor,
|
||||
boolean assumeHashPrefix) {
|
||||
int resolved = resolveColor(raw, referenceColor, fallbackColor, assumeHashPrefix);
|
||||
int alpha = Color.alpha(resolved);
|
||||
if (alpha >= 255) {
|
||||
return String.format(Locale.ROOT, "#%06X", resolved & 0xFFFFFF);
|
||||
}
|
||||
return String.format(Locale.ROOT, "#%08X", resolved);
|
||||
}
|
||||
|
||||
private static boolean isValidSegment(String segment,
|
||||
boolean assumeHashPrefix,
|
||||
boolean allowInheritedBase,
|
||||
int baseColor) {
|
||||
if (TextUtils.isEmpty(segment)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
resolveSegment(segment, baseColor, assumeHashPrefix, allowInheritedBase);
|
||||
return true;
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int resolveSegment(String segment,
|
||||
int baseColor,
|
||||
boolean assumeHashPrefix,
|
||||
boolean allowInheritedBase) {
|
||||
String value = segment != null ? segment.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour segment");
|
||||
}
|
||||
int index = 0;
|
||||
boolean invert = false;
|
||||
if (value.regionMatches(true, 0, "inv", 0, 3)) {
|
||||
invert = true;
|
||||
index = 3;
|
||||
}
|
||||
double[] channels;
|
||||
double alpha;
|
||||
String remainder;
|
||||
if (startsWithOperator(value, index)) {
|
||||
if (!allowInheritedBase) {
|
||||
throw new IllegalArgumentException("Missing starting colour");
|
||||
}
|
||||
channels = channelsFor(baseColor);
|
||||
alpha = Color.alpha(baseColor);
|
||||
remainder = value.substring(index).trim();
|
||||
} else {
|
||||
LeadingColour leading = parseLeadingColour(value, index, assumeHashPrefix);
|
||||
channels = channelsFor(leading.color);
|
||||
alpha = Color.alpha(leading.color);
|
||||
remainder = value.substring(leading.endIndex).trim();
|
||||
}
|
||||
if (invert) {
|
||||
invertChannels(channels);
|
||||
}
|
||||
alpha = applyOperations(channels, alpha, remainder);
|
||||
return colorFromChannels(channels, alpha);
|
||||
}
|
||||
|
||||
private static LeadingColour parseLeadingColour(String value,
|
||||
int start,
|
||||
boolean assumeHashPrefix) {
|
||||
String remainder = value.substring(start).trim();
|
||||
Matcher matcher = LEADING_HEX_PATTERN.matcher(remainder);
|
||||
if (!matcher.find()) {
|
||||
throw new IllegalArgumentException("Missing starting colour");
|
||||
}
|
||||
String token = matcher.group();
|
||||
if (assumeHashPrefix && token.charAt(0) != '#') {
|
||||
token = "#" + token;
|
||||
}
|
||||
int color = parseHexColor(token, assumeHashPrefix);
|
||||
return new LeadingColour(color, start + matcher.end());
|
||||
}
|
||||
|
||||
private static double applyOperations(double[] channels, double alpha, String remainder) {
|
||||
int index = 0;
|
||||
while (index < remainder.length()) {
|
||||
while (index < remainder.length() && Character.isWhitespace(remainder.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= remainder.length()) {
|
||||
break;
|
||||
}
|
||||
Matcher matcher = OPERATION_PATTERN.matcher(remainder);
|
||||
matcher.region(index, remainder.length());
|
||||
if (!matcher.lookingAt()) {
|
||||
throw new IllegalArgumentException("Invalid colour operation");
|
||||
}
|
||||
char operator = matcher.group(1).charAt(0);
|
||||
String operandText = matcher.group(2);
|
||||
alpha = applyOperation(channels, alpha, operator, operandText);
|
||||
index = matcher.end();
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
private static double applyOperation(double[] channels, double alpha, char operator, String operandText) {
|
||||
Operand operand = parseOperand(operandText);
|
||||
for (int i = 0; i < 3; i++) {
|
||||
double value = channels[i];
|
||||
double rhs = operand.rgb[i];
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
switch (operator) {
|
||||
case '+':
|
||||
channels[i] = value + rhs;
|
||||
break;
|
||||
case '-':
|
||||
channels[i] = value - rhs;
|
||||
break;
|
||||
case '*':
|
||||
channels[i] = value * rhs;
|
||||
break;
|
||||
case '/':
|
||||
channels[i] = rhs == 0d ? value : value / rhs;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown operator");
|
||||
}
|
||||
}
|
||||
if (operand.hasAlpha) {
|
||||
double rhs = operand.alpha;
|
||||
if (operand.isHex && (operator == '*' || operator == '/')) {
|
||||
rhs = rhs / 255d;
|
||||
}
|
||||
switch (operator) {
|
||||
case '+':
|
||||
alpha = alpha + rhs;
|
||||
break;
|
||||
case '-':
|
||||
alpha = alpha - rhs;
|
||||
break;
|
||||
case '*':
|
||||
alpha = alpha * rhs;
|
||||
break;
|
||||
case '/':
|
||||
alpha = rhs == 0d ? alpha : alpha / rhs;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown operator");
|
||||
}
|
||||
}
|
||||
return alpha;
|
||||
}
|
||||
|
||||
private static Operand parseOperand(String operandText) {
|
||||
String value = operandText != null ? operandText.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing operand");
|
||||
}
|
||||
if (value.charAt(0) == '#') {
|
||||
return parseHexOperand(value);
|
||||
}
|
||||
double scalar;
|
||||
try {
|
||||
scalar = Double.parseDouble(value);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid operand", e);
|
||||
}
|
||||
return new Operand(new double[] { scalar, scalar, scalar }, 0d, false, false);
|
||||
}
|
||||
|
||||
private static int parseHexColor(String raw, boolean assumeHashPrefix) {
|
||||
String value = raw != null ? raw.trim() : "";
|
||||
if (value.isEmpty()) {
|
||||
throw new IllegalArgumentException("Empty colour");
|
||||
}
|
||||
if (assumeHashPrefix && value.charAt(0) != '#') {
|
||||
value = "#" + value;
|
||||
}
|
||||
if (value.charAt(0) != '#') {
|
||||
throw new IllegalArgumentException("Expected colour literal");
|
||||
}
|
||||
String hex = value.substring(1);
|
||||
if (hex.length() == 3) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() == 4) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Unsupported colour length");
|
||||
}
|
||||
try {
|
||||
return Color.parseColor("#" + hex);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException("Invalid colour", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String expandShortHex(String hex) {
|
||||
StringBuilder out = new StringBuilder(hex.length() * 2);
|
||||
for (int i = 0; i < hex.length(); i++) {
|
||||
char c = hex.charAt(i);
|
||||
out.append(c).append(c);
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static double[] channelsFor(int color) {
|
||||
return new double[] {
|
||||
Color.red(color),
|
||||
Color.green(color),
|
||||
Color.blue(color)
|
||||
};
|
||||
}
|
||||
|
||||
private static void invertChannels(double[] channels) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
channels[i] = 255d - channels[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static int colorFromChannels(double[] channels, double alpha) {
|
||||
int red = clampChannel(channels[0]);
|
||||
int green = clampChannel(channels[1]);
|
||||
int blue = clampChannel(channels[2]);
|
||||
return Color.argb(clampChannel(alpha), red, green, blue);
|
||||
}
|
||||
|
||||
private static Operand parseHexOperand(String raw) {
|
||||
String value = raw != null ? raw.trim() : "";
|
||||
if (value.isEmpty() || value.charAt(0) != '#') {
|
||||
throw new IllegalArgumentException("Expected colour literal");
|
||||
}
|
||||
String hex = value.substring(1);
|
||||
if (hex.length() == 3 || hex.length() == 4) {
|
||||
hex = expandShortHex(hex);
|
||||
} else if (hex.length() != 6 && hex.length() != 8) {
|
||||
throw new IllegalArgumentException("Unsupported colour length");
|
||||
}
|
||||
boolean hasAlpha = hex.length() == 8;
|
||||
int offset = hasAlpha ? 2 : 0;
|
||||
double alpha = 0d;
|
||||
if (hasAlpha) {
|
||||
alpha = parseHexByte(hex.substring(0, 2));
|
||||
}
|
||||
double[] rgb = new double[] {
|
||||
parseHexByte(hex.substring(offset, offset + 2)),
|
||||
parseHexByte(hex.substring(offset + 2, offset + 4)),
|
||||
parseHexByte(hex.substring(offset + 4, offset + 6))
|
||||
};
|
||||
return new Operand(rgb, alpha, hasAlpha, true);
|
||||
}
|
||||
|
||||
private static double parseHexByte(String value) {
|
||||
return Integer.parseInt(value, 16);
|
||||
}
|
||||
|
||||
private static int clampChannel(double value) {
|
||||
if (Double.isNaN(value) || Double.isInfinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
if (value <= 0d) {
|
||||
return 0;
|
||||
}
|
||||
if (value >= 255d) {
|
||||
return 255;
|
||||
}
|
||||
return (int) Math.round(value);
|
||||
}
|
||||
|
||||
private static boolean startsWithOperator(String value, int start) {
|
||||
if (start < 0 || start >= value.length()) {
|
||||
return false;
|
||||
}
|
||||
char c = value.charAt(start);
|
||||
return c == '+' || c == '-' || c == '*' || c == '/';
|
||||
}
|
||||
|
||||
private static int findVariantSplit(String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return -1;
|
||||
}
|
||||
boolean quoted = false;
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char c = value.charAt(i);
|
||||
if (c == '\'') {
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
if (!quoted && c == '|') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static boolean isDark(int color) {
|
||||
double brightness = (Color.red(color) * 299d
|
||||
+ Color.green(color) * 587d
|
||||
+ Color.blue(color) * 114d) / 1000d;
|
||||
return brightness < 128d;
|
||||
}
|
||||
|
||||
private static String hexOperandPattern() {
|
||||
return "#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})";
|
||||
}
|
||||
|
||||
private static final class LeadingColour {
|
||||
final int color;
|
||||
final int endIndex;
|
||||
|
||||
LeadingColour(int color, int endIndex) {
|
||||
this.color = color;
|
||||
this.endIndex = endIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Operand {
|
||||
final double[] rgb;
|
||||
final double alpha;
|
||||
final boolean hasAlpha;
|
||||
final boolean isHex;
|
||||
|
||||
Operand(double[] rgb, double alpha, boolean hasAlpha, boolean isHex) {
|
||||
this.rgb = rgb;
|
||||
this.alpha = alpha;
|
||||
this.hasAlpha = hasAlpha;
|
||||
this.isHex = isHex;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
public final class SbtDefaults {
|
||||
public static final int LOCKSCREEN_DOT_Y_OFFSET_DP = 3;
|
||||
|
||||
public static final boolean ICONS_AOD_CUTOUT_LIMIT_DEFAULT = true;
|
||||
public static final boolean ICONS_LOCK_CUTOUT_LIMIT_DEFAULT = true;
|
||||
|
||||
// Unlocked slider defaults and bounds.
|
||||
public static final int UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT = 3;
|
||||
public static final int UNLOCKED_MAX_ICONS_PER_ROW_MIN = 3;
|
||||
public static final int UNLOCKED_MAX_ICONS_PER_ROW_MAX = 50;
|
||||
|
||||
// Icons mode (AOD).
|
||||
public static final int ICONS_AOD_MAX_ICONS_PER_ROW_DEFAULT = 8;
|
||||
public static final int ICONS_AOD_MAX_ICONS_PER_ROW_MIN = 1;
|
||||
public static final int ICONS_AOD_MAX_ICONS_PER_ROW_MAX = 50;
|
||||
public static final int ICONS_AOD_MAX_ROWS_DEFAULT = 1;
|
||||
public static final int ICONS_AOD_MAX_ROWS_MIN = 1;
|
||||
public static final int ICONS_AOD_MAX_ROWS_MAX = 5;
|
||||
public static final boolean ICONS_AOD_EVEN_DISTRIBUTION_DEFAULT = false;
|
||||
|
||||
// Icons mode (Lockscreen).
|
||||
public static final int ICONS_LOCK_MAX_ICONS_PER_ROW_DEFAULT = 8;
|
||||
public static final int ICONS_LOCK_MAX_ICONS_PER_ROW_MIN = 1;
|
||||
public static final int ICONS_LOCK_MAX_ICONS_PER_ROW_MAX = 50;
|
||||
public static final int ICONS_LOCK_MAX_ROWS_DEFAULT = 1;
|
||||
public static final int ICONS_LOCK_MAX_ROWS_MIN = 1;
|
||||
public static final int ICONS_LOCK_MAX_ROWS_MAX = 5;
|
||||
public static final boolean ICONS_LOCK_EVEN_DISTRIBUTION_DEFAULT = false;
|
||||
|
||||
// Cards mode (AOD).
|
||||
public static final int CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT = 8;
|
||||
public static final int CARDS_AOD_MAX_ICONS_PER_ROW_MIN = 1;
|
||||
public static final int CARDS_AOD_MAX_ICONS_PER_ROW_MAX = 15;
|
||||
public static final int CARDS_AOD_MAX_ROWS_DEFAULT = 3;
|
||||
public static final int CARDS_AOD_MAX_ROWS_MIN = 1;
|
||||
public static final int CARDS_AOD_MAX_ROWS_MAX = 5;
|
||||
public static final boolean CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT = true;
|
||||
public static final boolean CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT = false;
|
||||
|
||||
// Cards mode (Lockscreen).
|
||||
public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT = 8;
|
||||
public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_MIN = 1;
|
||||
public static final int CARDS_LOCK_MAX_ICONS_PER_ROW_MAX = 15;
|
||||
public static final int CARDS_LOCK_MAX_ROWS_DEFAULT = 3;
|
||||
public static final int CARDS_LOCK_MAX_ROWS_MIN = 1;
|
||||
public static final int CARDS_LOCK_MAX_ROWS_MAX = 5;
|
||||
public static final boolean CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT = true;
|
||||
public static final boolean CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT = false;
|
||||
public static final boolean AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT = false;
|
||||
public static final boolean BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT = true;
|
||||
public static final boolean BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT = true;
|
||||
public static final boolean BATTERY_BAR_ENABLED_DEFAULT = false;
|
||||
public static final String BATTERY_BAR_POSITION_DEFAULT = "top";
|
||||
public static final String BATTERY_BAR_ALIGNMENT_DEFAULT = "ltr";
|
||||
public static final int BATTERY_BAR_THICKNESS_DP_DEFAULT = 2;
|
||||
public static final int BATTERY_BAR_THICKNESS_DP_MIN = 1;
|
||||
public static final int BATTERY_BAR_THICKNESS_DP_MAX = 12;
|
||||
public static final int BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT = 0;
|
||||
public static final int BATTERY_BAR_EDGE_OFFSET_DP_MIN = 0;
|
||||
public static final int BATTERY_BAR_EDGE_OFFSET_DP_MAX = 64;
|
||||
public static final int BATTERY_BAR_MIN_LEVEL_DEFAULT = 0;
|
||||
public static final int BATTERY_BAR_MIN_LEVEL_MIN = 0;
|
||||
public static final int BATTERY_BAR_MIN_LEVEL_MAX = 99;
|
||||
public static final int BATTERY_BAR_MAX_LEVEL_DEFAULT = 100;
|
||||
public static final int BATTERY_BAR_MAX_LEVEL_MIN = 1;
|
||||
public static final int BATTERY_BAR_MAX_LEVEL_MAX = 100;
|
||||
public static final String BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT = "";
|
||||
public static final String BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT = "";
|
||||
public static final boolean CLOCK_ENABLED_DEFAULT = false;
|
||||
public static final boolean CLOCK_ENABLED_LOCKSCREEN_DEFAULT = false;
|
||||
public static final boolean CLOCK_ENABLED_UNLOCKED_DEFAULT = false;
|
||||
public static final String CLOCK_POSITION_DEFAULT = "middle";
|
||||
public static final int CLOCK_HORIZONTAL_OFFSET_PX_DEFAULT = 0;
|
||||
public static final String CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT = "left";
|
||||
public static final boolean CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true;
|
||||
public static final int CLOCK_MIDDLE_CUTOUT_GAP_PX_DEFAULT = 0;
|
||||
public static final int CLOCK_MIDDLE_CUTOUT_GAP_PX_MIN = -5;
|
||||
public static final int CLOCK_MIDDLE_CUTOUT_GAP_PX_MAX = 20;
|
||||
public static final int CLOCK_VERTICAL_OFFSET_PX_DEFAULT = 0;
|
||||
public static final int CLOCK_VERTICAL_OFFSET_PX_MIN = -99;
|
||||
public static final int CLOCK_VERTICAL_OFFSET_PX_MAX = 99;
|
||||
public static final boolean CLOCK_SHOW_SECONDS_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 String CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
||||
public static final boolean CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT = true;
|
||||
public static final boolean DEBUG_WRITE_LOGS_DEFAULT = true;
|
||||
public static final boolean DEBUG_VISUALIZE_NOTIFICATION_CONTAINER_DEFAULT = false;
|
||||
public static final boolean DEBUG_VISUALIZE_STATUS_CONTAINER_DEFAULT = false;
|
||||
public static final boolean DEBUG_VISUALIZE_CLOCK_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 int LAYOUT_PADDING_CUTOUT_PX_DEFAULT = 0;
|
||||
public static final int LAYOUT_PADDING_LEFT_PX_DEFAULT = 0;
|
||||
public static final int LAYOUT_PADDING_RIGHT_PX_DEFAULT = 0;
|
||||
public static final int LAYOUT_ITEM_PADDING_PX_DEFAULT = 11;
|
||||
public static final int LAYOUT_ITEM_PADDING_PX_MIN = 0;
|
||||
public static final int LAYOUT_ITEM_PADDING_PX_MAX = 999;
|
||||
public static final boolean LAYOUT_PADDING_LEFT_ADD_STOCK_DEFAULT = true;
|
||||
public static final boolean LAYOUT_PADDING_RIGHT_ADD_STOCK_DEFAULT = true;
|
||||
public static final boolean LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT = true;
|
||||
public static final String LAYOUT_ITEM_ORDER_DEFAULT = "clock,chip,status,notifications";
|
||||
public static final String LAYOUT_CHIP_POSITION_DEFAULT = "left";
|
||||
public static final boolean LAYOUT_CHIP_ENABLED_LOCKSCREEN_DEFAULT = true;
|
||||
public static final boolean LAYOUT_CHIP_ENABLED_UNLOCKED_DEFAULT = true;
|
||||
public static final String LAYOUT_CHIP_MIDDLE_SIDE_DEFAULT = "left";
|
||||
public static final boolean LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true;
|
||||
public static final int LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT = 0;
|
||||
public static final String LAYOUT_NOTIF_POSITION_DEFAULT = "left";
|
||||
public static final String LAYOUT_NOTIF_SORT_ORDER_DEFAULT = "automatic";
|
||||
public static final boolean LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT = true;
|
||||
public static final boolean LAYOUT_NOTIF_ENABLED_AOD_DEFAULT = true;
|
||||
public static final boolean LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT = true;
|
||||
public static final boolean LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT = true;
|
||||
public static final String LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT = "left";
|
||||
public static final boolean LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true;
|
||||
public static final int LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT = 0;
|
||||
public static final String LAYOUT_STATUS_POSITION_DEFAULT = "right";
|
||||
public static final boolean LAYOUT_STATUS_ENABLED_AOD_DEFAULT = true;
|
||||
public static final boolean LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT = true;
|
||||
public static final boolean LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT = true;
|
||||
public static final boolean LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT = true;
|
||||
public static final String LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT = "left";
|
||||
public static final boolean LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT = true;
|
||||
public static final int LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT = 0;
|
||||
|
||||
private SbtDefaults() {
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,334 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
public class SbtSettingsProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "se.ajpanton.statusbartweak.settings";
|
||||
public static final String METHOD_GET_UNLOCKED = "get_unlocked";
|
||||
public static final String METHOD_GET_ALL = "get_all";
|
||||
public static final String METHOD_SET_CLOCK_CAMERA_DEBUG_STATE = "set_clock_camera_debug_state";
|
||||
public static final String METHOD_NOTE_SEEN_ICON_APP = "note_seen_icon_app";
|
||||
public static final String EXTRA_VALUE = "value";
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Bundle call(String method, String arg, Bundle extras) {
|
||||
Context ctx = getContext();
|
||||
if (ctx == null) {
|
||||
return null;
|
||||
}
|
||||
SbtSettings.restoreCredentialProtectedPreferences(ctx);
|
||||
SharedPreferences prefs = ctx.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
if (METHOD_GET_UNLOCKED.equals(method)) {
|
||||
int value = prefs.getInt(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT);
|
||||
Bundle out = new Bundle();
|
||||
out.putInt(EXTRA_VALUE, value);
|
||||
return out;
|
||||
}
|
||||
if (METHOD_GET_ALL.equals(method)) {
|
||||
Bundle out = new Bundle();
|
||||
out.putBoolean(SbtSettings.KEY_ICONS_AOD_CUTOUT_LIMIT,
|
||||
prefs.getBoolean(SbtSettings.KEY_ICONS_AOD_CUTOUT_LIMIT, SbtDefaults.ICONS_AOD_CUTOUT_LIMIT_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_ICONS_LOCK_CUTOUT_LIMIT,
|
||||
prefs.getBoolean(SbtSettings.KEY_ICONS_LOCK_CUTOUT_LIMIT, SbtDefaults.ICONS_LOCK_CUTOUT_LIMIT_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW,
|
||||
prefs.getInt(SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW, SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_ICONS_AOD_MAX_ICONS_PER_ROW,
|
||||
prefs.getInt(SbtSettings.KEY_ICONS_AOD_MAX_ICONS_PER_ROW, SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_ICONS_AOD_MAX_ROWS,
|
||||
prefs.getInt(SbtSettings.KEY_ICONS_AOD_MAX_ROWS, SbtDefaults.ICONS_AOD_MAX_ROWS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_ICONS_AOD_EVEN_DISTRIBUTION,
|
||||
prefs.getBoolean(SbtSettings.KEY_ICONS_AOD_EVEN_DISTRIBUTION, SbtDefaults.ICONS_AOD_EVEN_DISTRIBUTION_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_ICONS_LOCK_MAX_ICONS_PER_ROW,
|
||||
prefs.getInt(SbtSettings.KEY_ICONS_LOCK_MAX_ICONS_PER_ROW, SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_ICONS_LOCK_MAX_ROWS,
|
||||
prefs.getInt(SbtSettings.KEY_ICONS_LOCK_MAX_ROWS, SbtDefaults.ICONS_LOCK_MAX_ROWS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_ICONS_LOCK_EVEN_DISTRIBUTION,
|
||||
prefs.getBoolean(SbtSettings.KEY_ICONS_LOCK_EVEN_DISTRIBUTION, SbtDefaults.ICONS_LOCK_EVEN_DISTRIBUTION_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW,
|
||||
prefs.getInt(SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW, SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CARDS_AOD_MAX_ROWS,
|
||||
prefs.getInt(SbtSettings.KEY_CARDS_AOD_MAX_ROWS, SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION,
|
||||
prefs.getBoolean(SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION, SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH,
|
||||
prefs.getBoolean(SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH, SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW,
|
||||
prefs.getInt(SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW, SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CARDS_LOCK_MAX_ROWS,
|
||||
prefs.getInt(SbtSettings.KEY_CARDS_LOCK_MAX_ROWS, SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION,
|
||||
prefs.getBoolean(SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION, SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH,
|
||||
prefs.getBoolean(SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH, SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_AOD_KEEP_VISIBLE_DURING_CALL,
|
||||
prefs.getBoolean(SbtSettings.KEY_AOD_KEEP_VISIBLE_DURING_CALL, SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT,
|
||||
prefs.getBoolean(SbtSettings.KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_BATTERY_AOD_UNPLUGGED_PERCENT,
|
||||
prefs.getBoolean(SbtSettings.KEY_BATTERY_AOD_UNPLUGGED_PERCENT, SbtDefaults.BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED,
|
||||
prefs.getBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED, SbtDefaults.BATTERY_BAR_ENABLED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_BATTERY_BAR_POSITION,
|
||||
prefs.getString(SbtSettings.KEY_BATTERY_BAR_POSITION, SbtDefaults.BATTERY_BAR_POSITION_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_BATTERY_BAR_ALIGNMENT,
|
||||
prefs.getString(SbtSettings.KEY_BATTERY_BAR_ALIGNMENT, SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP,
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP, SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP,
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP, SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL,
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL,
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL, SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR,
|
||||
prefs.getString(SbtSettings.KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
|
||||
prefs.getString(SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_BATTERY_BAR_THRESHOLDS,
|
||||
new ArrayList<>(prefs.getStringSet(
|
||||
SbtSettings.KEY_BATTERY_BAR_THRESHOLDS,
|
||||
java.util.Collections.emptySet())));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_ENABLED,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_ENABLED, SbtDefaults.CLOCK_ENABLED_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN, SbtDefaults.CLOCK_ENABLED_LOCKSCREEN_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED, SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_CLOCK_POSITION,
|
||||
prefs.getString(SbtSettings.KEY_CLOCK_POSITION, SbtDefaults.CLOCK_POSITION_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CLOCK_HORIZONTAL_OFFSET_PX,
|
||||
prefs.getInt(SbtSettings.KEY_CLOCK_HORIZONTAL_OFFSET_PX, SbtDefaults.CLOCK_HORIZONTAL_OFFSET_PX_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_SIDE,
|
||||
prefs.getString(SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_SIDE, SbtDefaults.CLOCK_MIDDLE_CUTOUT_SIDE_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_MIDDLE_AUTO_NEAREST_SIDE, SbtDefaults.CLOCK_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_GAP_PX,
|
||||
prefs.getInt(SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_GAP_PX, SbtDefaults.CLOCK_MIDDLE_CUTOUT_GAP_PX_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX,
|
||||
prefs.getInt(SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX, SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_SHOW_SECONDS,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_SHOW_SECONDS, SbtDefaults.CLOCK_SHOW_SECONDS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_SHOW_DATE_PREFIX,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_SHOW_DATE_PREFIX, SbtDefaults.CLOCK_SHOW_DATE_PREFIX_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||
prefs.getString(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS,
|
||||
prefs.getBoolean(SbtSettings.KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS,
|
||||
SbtDefaults.CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_WRITE_LOGS,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_WRITE_LOGS,
|
||||
SbtDefaults.DEBUG_WRITE_LOGS_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_NOTIFICATION_CONTAINER,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_NOTIFICATION_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_NOTIFICATION_CONTAINER_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_STATUS_CONTAINER,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_STATUS_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_STATUS_CONTAINER_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CLOCK_CONTAINER,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CLOCK_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CHIP_CONTAINER,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CHIP_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT,
|
||||
prefs.getBoolean(SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_LEFT_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_LEFT_PX,
|
||||
SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_PADDING_RIGHT_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_PADDING_RIGHT_PX,
|
||||
SbtDefaults.LAYOUT_PADDING_RIGHT_PX_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_ITEM_PADDING_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_ITEM_PADDING_PX,
|
||||
SbtDefaults.LAYOUT_ITEM_PADDING_PX_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_PADDING_LEFT_ADD_STOCK,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_PADDING_LEFT_ADD_STOCK,
|
||||
SbtDefaults.LAYOUT_PADDING_LEFT_ADD_STOCK_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_PADDING_RIGHT_ADD_STOCK,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_PADDING_RIGHT_ADD_STOCK,
|
||||
SbtDefaults.LAYOUT_PADDING_RIGHT_ADD_STOCK_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS,
|
||||
SbtDefaults.LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_ITEM_ORDER,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_ITEM_ORDER,
|
||||
SbtDefaults.LAYOUT_ITEM_ORDER_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_CHIP_POSITION,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_CHIP_POSITION,
|
||||
SbtDefaults.LAYOUT_CHIP_POSITION_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_CHIP_ENABLED_LOCKSCREEN,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_CHIP_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_CHIP_ENABLED_LOCKSCREEN_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_CHIP_ENABLED_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_CHIP_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_CHIP_ENABLED_UNLOCKED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_SIDE,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_SIDE,
|
||||
SbtDefaults.LAYOUT_CHIP_MIDDLE_SIDE_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtDefaults.LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX,
|
||||
SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_NOTIF_POSITION,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_POSITION,
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER,
|
||||
SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED,
|
||||
SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_SIDE_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtDefaults.LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX,
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_STATUS_POSITION,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_POSITION,
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED,
|
||||
SbtDefaults.LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_SIDE_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
prefs.getBoolean(SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtDefaults.LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE_DEFAULT));
|
||||
out.putInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX,
|
||||
prefs.getInt(SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX,
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES,
|
||||
new ArrayList<>(prefs.getStringSet(
|
||||
SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES,
|
||||
java.util.Collections.emptySet())));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES,
|
||||
new ArrayList<>(prefs.getStringSet(
|
||||
SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES,
|
||||
java.util.Collections.emptySet())));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_SYSTEM_ICON_HIDE_SLOTS,
|
||||
new ArrayList<>(prefs.getStringSet(
|
||||
SbtSettings.KEY_SYSTEM_ICON_HIDE_SLOTS,
|
||||
java.util.Collections.emptySet())));
|
||||
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL,
|
||||
prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL, false));
|
||||
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED, false));
|
||||
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED,
|
||||
prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED, false));
|
||||
out.putBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_CALL,
|
||||
prefs.getBoolean(SbtSettings.KEY_STATUS_CHIPS_HIDE_CALL, false));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_APP_ICON_BLOCKED_MODES,
|
||||
new ArrayList<>(AppIconRules.encodeBlockedModes(AppIconRules.loadBlockedModes(prefs))));
|
||||
out.putStringArrayList(
|
||||
SbtSettings.KEY_APP_ICON_EXCEPTION_RULES,
|
||||
new ArrayList<>(AppIconRules.encodeExceptionRules(AppIconRules.loadExceptionRules(prefs))));
|
||||
return out;
|
||||
}
|
||||
if (METHOD_NOTE_SEEN_ICON_APP.equals(method)) {
|
||||
String pkg = extras != null ? extras.getString(AppIconRules.EXTRA_PACKAGE) : null;
|
||||
long firstSeenMs = extras != null
|
||||
? extras.getLong(AppIconRules.EXTRA_FIRST_SEEN_MS, System.currentTimeMillis())
|
||||
: System.currentTimeMillis();
|
||||
if (!AppIconRules.isValidPackageName(pkg)) {
|
||||
return Bundle.EMPTY;
|
||||
}
|
||||
synchronized (LOCK) {
|
||||
Map<String, Long> seen = AppIconRules.loadSeenSession(prefs);
|
||||
AppIconRules.noteSeen(seen, pkg, firstSeenMs);
|
||||
AppIconRules.saveSeenSession(prefs, seen);
|
||||
}
|
||||
return Bundle.EMPTY;
|
||||
}
|
||||
if (METHOD_SET_CLOCK_CAMERA_DEBUG_STATE.equals(method)) {
|
||||
String cameraId = extras != null ? extras.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_ID, "") : "";
|
||||
String autoType = extras != null ? extras.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_AUTO_TYPE, "") : "";
|
||||
String effectiveType = extras != null ? extras.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_EFFECTIVE_TYPE, "") : "";
|
||||
prefs.edit()
|
||||
.putString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_ID, cameraId)
|
||||
.putString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_AUTO_TYPE, autoType)
|
||||
.putString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_EFFECTIVE_TYPE, effectiveType)
|
||||
.apply();
|
||||
return Bundle.EMPTY;
|
||||
}
|
||||
return super.call(method, arg, extras);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package se.ajpanton.statusbartweak.shell.settings;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class SystemIconRules {
|
||||
public static final String PREF_KEY_BLOCKED_MODES = "system_icon_blocked_modes";
|
||||
|
||||
public static final int MODE_AOD = 1;
|
||||
public static final int MODE_LOCK = 1 << 1;
|
||||
public static final int MODE_UNLOCK = 1 << 2;
|
||||
private static final String MODE_NAME_AOD = "AOD";
|
||||
private static final String MODE_NAME_LOCK = "LOCK";
|
||||
private static final String MODE_NAME_UNLOCK = "UNLOCK";
|
||||
|
||||
private static final String ENTRY_SEPARATOR = "|";
|
||||
|
||||
private SystemIconRules() {
|
||||
}
|
||||
|
||||
public static Map<String, Integer> parseBlockedModes(Set<String> encoded) {
|
||||
if (encoded == null || encoded.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
HashMap<String, Integer> out = new HashMap<>();
|
||||
for (String entry : encoded) {
|
||||
if (entry == null || entry.trim().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = entry.split("\\|", 2);
|
||||
if (parts.length != 2) {
|
||||
continue;
|
||||
}
|
||||
String slot = normalizeSlot(parts[0]);
|
||||
if (slot == null) {
|
||||
continue;
|
||||
}
|
||||
int mask;
|
||||
try {
|
||||
mask = Integer.parseInt(parts[1].trim());
|
||||
} catch (Throwable ignored) {
|
||||
continue;
|
||||
}
|
||||
mask &= (MODE_AOD | MODE_LOCK | MODE_UNLOCK);
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
}
|
||||
Integer existing = out.get(slot);
|
||||
out.put(slot, existing == null ? mask : (existing | mask));
|
||||
}
|
||||
return out.isEmpty() ? Collections.emptyMap() : out;
|
||||
}
|
||||
|
||||
public static Set<String> encodeBlockedModes(Map<String, Integer> blockedModes) {
|
||||
if (blockedModes == null || blockedModes.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
HashSet<String> out = new HashSet<>();
|
||||
for (Map.Entry<String, Integer> entry : blockedModes.entrySet()) {
|
||||
String slot = normalizeSlot(entry.getKey());
|
||||
if (slot == null) {
|
||||
continue;
|
||||
}
|
||||
Integer maskObj = entry.getValue();
|
||||
if (maskObj == null) {
|
||||
continue;
|
||||
}
|
||||
int mask = maskObj & (MODE_AOD | MODE_LOCK | MODE_UNLOCK);
|
||||
if (mask == 0) {
|
||||
continue;
|
||||
}
|
||||
out.add(slot + ENTRY_SEPARATOR + mask);
|
||||
}
|
||||
return out.isEmpty() ? Collections.emptySet() : out;
|
||||
}
|
||||
|
||||
public static boolean isBlocked(Map<String, Integer> blockedModes, String slot, String mode) {
|
||||
if (blockedModes == null || blockedModes.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String normalizedSlot = normalizeSlot(slot);
|
||||
if (normalizedSlot == null) {
|
||||
return false;
|
||||
}
|
||||
int modeMask = modeMaskFromName(mode);
|
||||
if (modeMask == 0) {
|
||||
return false;
|
||||
}
|
||||
Integer maskObj = blockedModes.get(normalizedSlot);
|
||||
int mask = maskObj == null ? 0 : maskObj;
|
||||
return (mask & modeMask) != 0;
|
||||
}
|
||||
|
||||
public static void setModeBlocked(Map<String, Integer> blockedModes,
|
||||
String slot,
|
||||
int modeMask,
|
||||
boolean blocked) {
|
||||
if (blockedModes == null) {
|
||||
return;
|
||||
}
|
||||
String normalizedSlot = normalizeSlot(slot);
|
||||
if (normalizedSlot == null) {
|
||||
return;
|
||||
}
|
||||
int validModeMask = modeMask & (MODE_AOD | MODE_LOCK | MODE_UNLOCK);
|
||||
if (validModeMask == 0) {
|
||||
return;
|
||||
}
|
||||
Integer currentObj = blockedModes.get(normalizedSlot);
|
||||
int current = currentObj == null ? 0 : currentObj;
|
||||
int next = blocked ? (current | validModeMask) : (current & ~validModeMask);
|
||||
if (next == 0) {
|
||||
blockedModes.remove(normalizedSlot);
|
||||
} else {
|
||||
blockedModes.put(normalizedSlot, next);
|
||||
}
|
||||
}
|
||||
|
||||
public static int modeMaskFromName(String mode) {
|
||||
if (mode == null) {
|
||||
return 0;
|
||||
}
|
||||
String normalized = mode.trim().toUpperCase(Locale.ROOT);
|
||||
if (MODE_NAME_AOD.equals(normalized)) {
|
||||
return MODE_AOD;
|
||||
}
|
||||
if (MODE_NAME_LOCK.equals(normalized) || "LOCKSCREEN".equals(normalized)) {
|
||||
return MODE_LOCK;
|
||||
}
|
||||
if (MODE_NAME_UNLOCK.equals(normalized) || "UNLOCKED".equals(normalized)) {
|
||||
return MODE_UNLOCK;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static String normalizeSlot(String slot) {
|
||||
if (slot == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = slot.trim();
|
||||
if (normalized.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,872 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.RangeSlider;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.BatteryBarStyle;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
public final class BatteryBarFragment extends Fragment {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_battery_bar, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
SwitchMaterial enabled = root.findViewById(R.id.battery_bar_enabled_switch);
|
||||
RadioButton positionTop = root.findViewById(R.id.battery_bar_position_top);
|
||||
RadioButton positionBottom = root.findViewById(R.id.battery_bar_position_bottom);
|
||||
RadioGroup positionGroup = root.findViewById(R.id.battery_bar_position_group);
|
||||
View rtlCell = root.findViewById(R.id.battery_bar_alignment_cell_rtl);
|
||||
View centreCell = root.findViewById(R.id.battery_bar_alignment_cell_center);
|
||||
View ltrCell = root.findViewById(R.id.battery_bar_alignment_cell_ltr);
|
||||
RadioButton alignLtr = root.findViewById(R.id.battery_bar_alignment_ltr);
|
||||
RadioButton alignRtl = root.findViewById(R.id.battery_bar_alignment_rtl);
|
||||
RadioButton alignCentre = root.findViewById(R.id.battery_bar_alignment_center);
|
||||
EditText thicknessInput = root.findViewById(R.id.battery_bar_thickness_input);
|
||||
MaterialButton thicknessMinus = root.findViewById(R.id.battery_bar_thickness_minus);
|
||||
MaterialButton thicknessPlus = root.findViewById(R.id.battery_bar_thickness_plus);
|
||||
EditText edgeOffsetInput = root.findViewById(R.id.battery_bar_edge_offset_input);
|
||||
MaterialButton edgeOffsetMinus = root.findViewById(R.id.battery_bar_edge_offset_minus);
|
||||
MaterialButton edgeOffsetPlus = root.findViewById(R.id.battery_bar_edge_offset_plus);
|
||||
MaterialButton minLevelMinus = root.findViewById(R.id.battery_bar_min_level_minus);
|
||||
MaterialButton minLevelPlus = root.findViewById(R.id.battery_bar_min_level_plus);
|
||||
MaterialButton maxLevelMinus = root.findViewById(R.id.battery_bar_max_level_minus);
|
||||
MaterialButton maxLevelPlus = root.findViewById(R.id.battery_bar_max_level_plus);
|
||||
RangeSlider mappingSlider = root.findViewById(R.id.battery_bar_mapping_slider);
|
||||
TextView minLevelLabel = root.findViewById(R.id.battery_bar_min_level_label);
|
||||
TextView maxLevelLabel = root.findViewById(R.id.battery_bar_max_level_label);
|
||||
MaterialButton defaultDischargeButton = root.findViewById(R.id.battery_bar_default_discharge_button);
|
||||
MaterialButton defaultChargeButton = root.findViewById(R.id.battery_bar_default_charge_button);
|
||||
MaterialButton addThresholdButton = root.findViewById(R.id.battery_bar_add_threshold_button);
|
||||
LinearLayout thresholdsContainer = root.findViewById(R.id.battery_bar_thresholds_container);
|
||||
View geometrySection = root.findViewById(R.id.battery_bar_geometry_section);
|
||||
mappingSlider.setValueFrom(0f);
|
||||
mappingSlider.setValueTo(100f);
|
||||
mappingSlider.setStepSize(1f);
|
||||
mappingSlider.setMinSeparationValue(1f);
|
||||
trimMappingStepButton(minLevelMinus);
|
||||
trimMappingStepButton(minLevelPlus);
|
||||
trimMappingStepButton(maxLevelMinus);
|
||||
trimMappingStepButton(maxLevelPlus);
|
||||
|
||||
enabled.setChecked(prefs.getBoolean(
|
||||
SbtSettings.KEY_BATTERY_BAR_ENABLED,
|
||||
SbtDefaults.BATTERY_BAR_ENABLED_DEFAULT));
|
||||
applyPositionSelection(
|
||||
positionTop,
|
||||
positionBottom,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_BATTERY_BAR_POSITION,
|
||||
SbtDefaults.BATTERY_BAR_POSITION_DEFAULT));
|
||||
applyAlignmentSelection(
|
||||
alignLtr,
|
||||
alignRtl,
|
||||
alignCentre,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_BATTERY_BAR_ALIGNMENT,
|
||||
SbtDefaults.BATTERY_BAR_ALIGNMENT_DEFAULT));
|
||||
setIntInput(thicknessInput, prefs.getInt(
|
||||
SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP,
|
||||
SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT));
|
||||
setIntInput(edgeOffsetInput, prefs.getInt(
|
||||
SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP,
|
||||
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT));
|
||||
setMapping(
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, SbtDefaults.BATTERY_BAR_MIN_LEVEL_DEFAULT),
|
||||
prefs.getInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL, SbtDefaults.BATTERY_BAR_MAX_LEVEL_DEFAULT),
|
||||
mappingSlider,
|
||||
minLevelLabel,
|
||||
maxLevelLabel,
|
||||
false);
|
||||
setSectionEnabled(geometrySection, enabled.isChecked());
|
||||
updateColourButtonLabel(
|
||||
defaultDischargeButton,
|
||||
R.string.battery_bar_default_discharge_colour,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT),
|
||||
R.string.battery_bar_colour_auto);
|
||||
updateColourButtonLabel(
|
||||
defaultChargeButton,
|
||||
R.string.battery_bar_default_charge_colour,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_CHARGE_COLOR_DEFAULT),
|
||||
R.string.battery_bar_colour_same_as_default);
|
||||
|
||||
enabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_BATTERY_BAR_ENABLED, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
setSectionEnabled(geometrySection, isChecked);
|
||||
});
|
||||
|
||||
positionGroup.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
prefs.edit()
|
||||
.putString(SbtSettings.KEY_BATTERY_BAR_POSITION, positionValue(checkedId))
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
bindAlignmentCell(rtlCell, alignRtl, alignCentre, alignLtr, "rtl", prefs);
|
||||
bindAlignmentCell(centreCell, alignRtl, alignCentre, alignLtr, "center", prefs);
|
||||
bindAlignmentCell(ltrCell, alignRtl, alignCentre, alignLtr, "ltr", prefs);
|
||||
bindAlignmentCell(alignRtl, alignRtl, alignCentre, alignLtr, "rtl", prefs);
|
||||
bindAlignmentCell(alignCentre, alignRtl, alignCentre, alignLtr, "center", prefs);
|
||||
bindAlignmentCell(alignLtr, alignRtl, alignCentre, alignLtr, "ltr", prefs);
|
||||
|
||||
bindNumberControl(
|
||||
prefs,
|
||||
thicknessInput,
|
||||
thicknessMinus,
|
||||
thicknessPlus,
|
||||
SbtSettings.KEY_BATTERY_BAR_THICKNESS_DP,
|
||||
SbtDefaults.BATTERY_BAR_THICKNESS_DP_MIN,
|
||||
SbtDefaults.BATTERY_BAR_THICKNESS_DP_MAX,
|
||||
SbtDefaults.BATTERY_BAR_THICKNESS_DP_DEFAULT);
|
||||
bindNumberControl(
|
||||
prefs,
|
||||
edgeOffsetInput,
|
||||
edgeOffsetMinus,
|
||||
edgeOffsetPlus,
|
||||
SbtSettings.KEY_BATTERY_BAR_EDGE_OFFSET_DP,
|
||||
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MIN,
|
||||
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_MAX,
|
||||
SbtDefaults.BATTERY_BAR_EDGE_OFFSET_DP_DEFAULT);
|
||||
|
||||
mappingSlider.addOnChangeListener((slider, value, fromUser) -> {
|
||||
if (!fromUser) {
|
||||
return;
|
||||
}
|
||||
List<Float> values = slider.getValues();
|
||||
if (values.size() < 2) {
|
||||
return;
|
||||
}
|
||||
persistMapping(prefs, Math.round(values.get(0)), Math.round(values.get(1)),
|
||||
mappingSlider, minLevelLabel, maxLevelLabel);
|
||||
});
|
||||
minLevelPlus.setOnClickListener(v -> adjustMapping(prefs, mappingSlider, minLevelLabel, maxLevelLabel, true, 1));
|
||||
minLevelMinus.setOnClickListener(v -> adjustMapping(prefs, mappingSlider, minLevelLabel, maxLevelLabel, true, -1));
|
||||
maxLevelPlus.setOnClickListener(v -> adjustMapping(prefs, mappingSlider, minLevelLabel, maxLevelLabel, false, 1));
|
||||
maxLevelMinus.setOnClickListener(v -> adjustMapping(prefs, mappingSlider, minLevelLabel, maxLevelLabel, false, -1));
|
||||
|
||||
defaultDischargeButton.setOnClickListener(v -> showColourDialog(
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR,
|
||||
R.string.battery_bar_default_discharge_colour,
|
||||
R.string.battery_bar_colour_auto,
|
||||
prefs,
|
||||
defaultDischargeButton));
|
||||
defaultChargeButton.setOnClickListener(v -> showColourDialog(
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_CHARGE_COLOR,
|
||||
R.string.battery_bar_default_charge_colour,
|
||||
R.string.battery_bar_colour_same_as_discharge,
|
||||
prefs,
|
||||
defaultChargeButton));
|
||||
|
||||
addThresholdButton.setOnClickListener(v -> showThresholdPercentDialog(
|
||||
null,
|
||||
prefs,
|
||||
thresholdsContainer,
|
||||
percent -> {
|
||||
ArrayList<BatteryBarStyle.Threshold> thresholds = loadThresholds(prefs);
|
||||
thresholds.add(new BatteryBarStyle.Threshold(
|
||||
percent,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_BATTERY_BAR_DEFAULT_DISCHARGE_COLOR,
|
||||
SbtDefaults.BATTERY_BAR_DEFAULT_DISCHARGE_COLOR_DEFAULT),
|
||||
BatteryBarStyle.THRESHOLD_CHARGE_USE_DEFAULT));
|
||||
persistThresholds(prefs, thresholds);
|
||||
renderThresholdRows(thresholdsContainer, prefs);
|
||||
}));
|
||||
|
||||
renderThresholdRows(thresholdsContainer, prefs);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void bindAlignmentCell(View clickTarget,
|
||||
RadioButton alignRtl,
|
||||
RadioButton alignCentre,
|
||||
RadioButton alignLtr,
|
||||
String value,
|
||||
SharedPreferences prefs) {
|
||||
if (clickTarget == null) {
|
||||
return;
|
||||
}
|
||||
clickTarget.setOnClickListener(v -> {
|
||||
applyAlignmentSelection(alignLtr, alignRtl, alignCentre, value);
|
||||
prefs.edit().putString(SbtSettings.KEY_BATTERY_BAR_ALIGNMENT, value).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void updateColourButtonLabel(MaterialButton button,
|
||||
int labelRes,
|
||||
String colour,
|
||||
int emptyLabelRes) {
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
String normalised = BatteryBarStyle.normalizeColor(colour);
|
||||
String value;
|
||||
if (BatteryBarStyle.isThresholdChargeDefault(colour)) {
|
||||
value = getString(R.string.battery_bar_colour_same_as_default);
|
||||
} else if (normalised.isEmpty()) {
|
||||
value = getString(emptyLabelRes);
|
||||
} else {
|
||||
value = normalised;
|
||||
}
|
||||
button.setText(getString(labelRes) + ": " + value);
|
||||
}
|
||||
|
||||
private void trimMappingStepButton(MaterialButton button) {
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
button.setMinHeight(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setPadding(button.getPaddingLeft(), 0, button.getPaddingRight(), 0);
|
||||
tryInvokeIntMethod(button, "setInsetTop", 0);
|
||||
tryInvokeIntMethod(button, "setInsetBottom", 0);
|
||||
}
|
||||
|
||||
private void tryInvokeIntMethod(Object target, String methodName, int value) {
|
||||
if (target == null || methodName == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
java.lang.reflect.Method method = target.getClass().getMethod(methodName, int.class);
|
||||
method.invoke(target, value);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private void setSectionEnabled(View view, boolean enabled) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setAlpha(enabled ? 1f : 0.82f);
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setSectionEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
} else {
|
||||
if (view.getClass() == TextView.class) {
|
||||
view.setEnabled(true);
|
||||
} else {
|
||||
view.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bindNumberControl(SharedPreferences prefs,
|
||||
EditText input,
|
||||
MaterialButton minus,
|
||||
MaterialButton plus,
|
||||
String key,
|
||||
int min,
|
||||
int max,
|
||||
int fallback) {
|
||||
final boolean[] updating = new boolean[]{false};
|
||||
if (minus != null) {
|
||||
minus.setOnClickListener(v -> {
|
||||
int current = parseIntInput(input, prefs.getInt(key, fallback));
|
||||
persistInt(prefs, key, clampInt(current - 1, min, max), input, updating);
|
||||
});
|
||||
}
|
||||
if (plus != null) {
|
||||
plus.setOnClickListener(v -> {
|
||||
int current = parseIntInput(input, prefs.getInt(key, fallback));
|
||||
persistInt(prefs, key, clampInt(current + 1, min, max), input, updating);
|
||||
});
|
||||
}
|
||||
if (input != null) {
|
||||
input.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) {
|
||||
if (updating[0] || s == null) {
|
||||
return;
|
||||
}
|
||||
String text = s.toString().trim();
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int current = parseIntInput(input, prefs.getInt(key, fallback));
|
||||
persistInt(prefs, key, clampInt(current, min, max), input, updating);
|
||||
}
|
||||
});
|
||||
input.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
return;
|
||||
}
|
||||
int current = parseIntInput(input, prefs.getInt(key, fallback));
|
||||
persistInt(prefs, key, clampInt(current, min, max), input, updating);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void adjustMapping(SharedPreferences prefs,
|
||||
RangeSlider slider,
|
||||
TextView minLabel,
|
||||
TextView maxLabel,
|
||||
boolean lower,
|
||||
int delta) {
|
||||
List<Float> values = slider.getValues();
|
||||
if (values.size() < 2) {
|
||||
return;
|
||||
}
|
||||
int min = Math.round(values.get(0));
|
||||
int max = Math.round(values.get(1));
|
||||
if (lower) {
|
||||
min = clampInt(min + delta, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, max - 1);
|
||||
} else {
|
||||
max = clampInt(max + delta, min + 1, SbtDefaults.BATTERY_BAR_MAX_LEVEL_MAX);
|
||||
}
|
||||
persistMapping(prefs, min, max, slider, minLabel, maxLabel);
|
||||
}
|
||||
|
||||
private void persistMapping(SharedPreferences prefs,
|
||||
int min,
|
||||
int max,
|
||||
RangeSlider slider,
|
||||
TextView minLabel,
|
||||
TextView maxLabel) {
|
||||
min = clampInt(min, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MIN_LEVEL_MAX);
|
||||
max = clampInt(max, SbtDefaults.BATTERY_BAR_MAX_LEVEL_MIN, SbtDefaults.BATTERY_BAR_MAX_LEVEL_MAX);
|
||||
if (max <= min) {
|
||||
max = Math.min(SbtDefaults.BATTERY_BAR_MAX_LEVEL_MAX, min + 1);
|
||||
if (max <= min) {
|
||||
min = Math.max(SbtDefaults.BATTERY_BAR_MIN_LEVEL_MIN, max - 1);
|
||||
}
|
||||
}
|
||||
prefs.edit()
|
||||
.putInt(SbtSettings.KEY_BATTERY_BAR_MIN_LEVEL, min)
|
||||
.putInt(SbtSettings.KEY_BATTERY_BAR_MAX_LEVEL, max)
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
setMapping(min, max, slider, minLabel, maxLabel, true);
|
||||
}
|
||||
|
||||
private void setMapping(int min,
|
||||
int max,
|
||||
RangeSlider slider,
|
||||
TextView minLabel,
|
||||
TextView maxLabel,
|
||||
boolean forceSlider) {
|
||||
if (slider != null) {
|
||||
List<Float> current = slider.getValues();
|
||||
boolean different = current.size() < 2
|
||||
|| Math.round(current.get(0)) != min
|
||||
|| Math.round(current.get(1)) != max;
|
||||
if (forceSlider || different) {
|
||||
slider.setValues((float) min, (float) max);
|
||||
}
|
||||
}
|
||||
if (minLabel != null) {
|
||||
minLabel.setText(getString(R.string.battery_bar_mapping_min_label, min));
|
||||
}
|
||||
if (maxLabel != null) {
|
||||
maxLabel.setText(getString(R.string.battery_bar_mapping_max_label, max));
|
||||
}
|
||||
}
|
||||
|
||||
private void persistInt(SharedPreferences prefs,
|
||||
String key,
|
||||
int value,
|
||||
EditText input,
|
||||
boolean[] updating) {
|
||||
prefs.edit().putInt(key, value).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
if (updating != null) {
|
||||
updating[0] = true;
|
||||
}
|
||||
try {
|
||||
setIntInput(input, value);
|
||||
} finally {
|
||||
if (updating != null) {
|
||||
updating[0] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistThresholds(SharedPreferences prefs,
|
||||
ArrayList<BatteryBarStyle.Threshold> thresholds) {
|
||||
prefs.edit()
|
||||
.putStringSet(
|
||||
SbtSettings.KEY_BATTERY_BAR_THRESHOLDS,
|
||||
new LinkedHashSet<>(BatteryBarStyle.encodeThresholds(thresholds)))
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
|
||||
private ArrayList<BatteryBarStyle.Threshold> loadThresholds(SharedPreferences prefs) {
|
||||
return BatteryBarStyle.decodeThresholds(prefs.getStringSet(
|
||||
SbtSettings.KEY_BATTERY_BAR_THRESHOLDS,
|
||||
java.util.Collections.emptySet()));
|
||||
}
|
||||
|
||||
private ArrayList<BatteryBarStyle.Threshold> sortedThresholdsForUi(SharedPreferences prefs) {
|
||||
ArrayList<BatteryBarStyle.Threshold> thresholds = loadThresholds(prefs);
|
||||
thresholds.sort((a, b) -> Integer.compare(b.percent, a.percent));
|
||||
return thresholds;
|
||||
}
|
||||
|
||||
private void renderThresholdRows(LinearLayout container, SharedPreferences prefs) {
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
Context context = container.getContext();
|
||||
container.removeAllViews();
|
||||
ArrayList<BatteryBarStyle.Threshold> thresholds = sortedThresholdsForUi(prefs);
|
||||
for (BatteryBarStyle.Threshold threshold : thresholds) {
|
||||
final int originalPercent = threshold.percent;
|
||||
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
row.setOrientation(LinearLayout.VERTICAL);
|
||||
row.setBackgroundResource(R.drawable.sbt_threshold_card);
|
||||
int padding = dpToPx(context, 12);
|
||||
row.setPadding(padding, padding, padding, padding);
|
||||
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
rowParams.topMargin = dpToPx(context, 12);
|
||||
row.setLayoutParams(rowParams);
|
||||
|
||||
TextView title = new TextView(context);
|
||||
title.setText(getString(R.string.battery_bar_threshold_label, threshold.percent));
|
||||
row.addView(title);
|
||||
|
||||
MaterialButton percentButton = createRowButton(context);
|
||||
percentButton.setText(getString(R.string.battery_bar_threshold_change_percent));
|
||||
percentButton.setOnClickListener(v -> showThresholdPercentDialog(
|
||||
threshold,
|
||||
prefs,
|
||||
container,
|
||||
newPercent -> {
|
||||
ArrayList<BatteryBarStyle.Threshold> updated = loadThresholds(prefs);
|
||||
if (containsThreshold(updated, newPercent, originalPercent)) {
|
||||
showMessageDialog(R.string.battery_bar_threshold_duplicate);
|
||||
return;
|
||||
}
|
||||
int oldIndex = indexOfPercent(sortedThresholdsForUi(prefs), originalPercent);
|
||||
replaceThresholdPercent(updated, originalPercent, newPercent);
|
||||
persistThresholds(prefs, updated);
|
||||
ArrayList<BatteryBarStyle.Threshold> sortedUpdated = sortedThresholdsForUi(prefs);
|
||||
int newIndex = indexOfPercent(sortedUpdated, newPercent);
|
||||
renderThresholdRows(container, prefs);
|
||||
if (oldIndex != -1 && newIndex != -1 && oldIndex != newIndex) {
|
||||
showMessageDialog(R.string.battery_bar_threshold_reordered);
|
||||
}
|
||||
}));
|
||||
row.addView(percentButton);
|
||||
|
||||
MaterialButton dischargeButton = createRowButton(context);
|
||||
updateColourButtonLabel(
|
||||
dischargeButton,
|
||||
R.string.battery_bar_threshold_discharge_colour,
|
||||
threshold.dischargeColor,
|
||||
R.string.battery_bar_colour_auto);
|
||||
dischargeButton.setOnClickListener(v -> showThresholdColourDialog(
|
||||
threshold,
|
||||
true,
|
||||
prefs,
|
||||
container));
|
||||
row.addView(dischargeButton);
|
||||
|
||||
MaterialButton chargeButton = createRowButton(context);
|
||||
updateColourButtonLabel(
|
||||
chargeButton,
|
||||
R.string.battery_bar_threshold_charge_colour,
|
||||
threshold.chargeColor,
|
||||
R.string.battery_bar_colour_same_as_default);
|
||||
chargeButton.setOnClickListener(v -> showThresholdColourDialog(
|
||||
threshold,
|
||||
false,
|
||||
prefs,
|
||||
container));
|
||||
row.addView(chargeButton);
|
||||
|
||||
MaterialButton removeButton = createRowButton(context);
|
||||
removeButton.setText(getString(R.string.battery_bar_threshold_remove));
|
||||
removeButton.setOnClickListener(v -> {
|
||||
ArrayList<BatteryBarStyle.Threshold> updated = loadThresholds(prefs);
|
||||
removeThreshold(updated, originalPercent);
|
||||
persistThresholds(prefs, updated);
|
||||
renderThresholdRows(container, prefs);
|
||||
});
|
||||
row.addView(removeButton);
|
||||
|
||||
container.addView(row);
|
||||
}
|
||||
}
|
||||
|
||||
private MaterialButton createRowButton(Context context) {
|
||||
MaterialButton button = new MaterialButton(
|
||||
context,
|
||||
null,
|
||||
com.google.android.material.R.attr.materialButtonOutlinedStyle);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.topMargin = dpToPx(context, 8);
|
||||
button.setLayoutParams(params);
|
||||
return button;
|
||||
}
|
||||
|
||||
private boolean containsThreshold(ArrayList<BatteryBarStyle.Threshold> thresholds,
|
||||
int percent,
|
||||
int ignorePercent) {
|
||||
for (BatteryBarStyle.Threshold threshold : thresholds) {
|
||||
if (threshold.percent == percent && threshold.percent != ignorePercent) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private int indexOfPercent(ArrayList<BatteryBarStyle.Threshold> thresholds, int percent) {
|
||||
for (int i = 0; i < thresholds.size(); i++) {
|
||||
if (thresholds.get(i).percent == percent) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void replaceThresholdPercent(ArrayList<BatteryBarStyle.Threshold> thresholds,
|
||||
int oldPercent,
|
||||
int newPercent) {
|
||||
for (int i = 0; i < thresholds.size(); i++) {
|
||||
BatteryBarStyle.Threshold threshold = thresholds.get(i);
|
||||
if (threshold.percent == oldPercent) {
|
||||
thresholds.set(i, new BatteryBarStyle.Threshold(
|
||||
newPercent,
|
||||
threshold.dischargeColor,
|
||||
threshold.chargeColor));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeThreshold(ArrayList<BatteryBarStyle.Threshold> thresholds, int percent) {
|
||||
for (int i = 0; i < thresholds.size(); i++) {
|
||||
if (thresholds.get(i).percent == percent) {
|
||||
thresholds.remove(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showThresholdColourDialog(BatteryBarStyle.Threshold threshold,
|
||||
boolean discharge,
|
||||
SharedPreferences prefs,
|
||||
LinearLayout container) {
|
||||
showColourDialog(
|
||||
discharge
|
||||
? R.string.battery_bar_threshold_discharge_colour
|
||||
: R.string.battery_bar_threshold_charge_colour,
|
||||
discharge ? threshold.dischargeColor : threshold.chargeColor,
|
||||
discharge
|
||||
? R.string.battery_bar_colour_auto
|
||||
: R.string.battery_bar_colour_same_as_discharge,
|
||||
!discharge,
|
||||
colour -> {
|
||||
ArrayList<BatteryBarStyle.Threshold> updated = loadThresholds(prefs);
|
||||
for (int i = 0; i < updated.size(); i++) {
|
||||
BatteryBarStyle.Threshold current = updated.get(i);
|
||||
if (current.percent != threshold.percent) {
|
||||
continue;
|
||||
}
|
||||
updated.set(i, new BatteryBarStyle.Threshold(
|
||||
current.percent,
|
||||
discharge ? colour : current.dischargeColor,
|
||||
discharge ? current.chargeColor : colour));
|
||||
break;
|
||||
}
|
||||
persistThresholds(prefs, updated);
|
||||
renderThresholdRows(container, prefs);
|
||||
});
|
||||
}
|
||||
|
||||
private void showColourDialog(String key,
|
||||
int titleRes,
|
||||
int emptyLabelRes,
|
||||
SharedPreferences prefs,
|
||||
MaterialButton button) {
|
||||
showColourDialog(
|
||||
titleRes,
|
||||
prefs.getString(key, ""),
|
||||
emptyLabelRes,
|
||||
false,
|
||||
colour -> {
|
||||
prefs.edit().putString(key, colour).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
updateColourButtonLabel(button, titleRes, colour, emptyLabelRes);
|
||||
});
|
||||
}
|
||||
|
||||
private void showColourDialog(int titleRes,
|
||||
String initialValue,
|
||||
int emptyLabelRes,
|
||||
boolean allowSameAsDefault,
|
||||
ColourConsumer consumer) {
|
||||
Context context = requireContext();
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setPadding(
|
||||
dpToPx(context, 24),
|
||||
dpToPx(context, 8),
|
||||
dpToPx(context, 24),
|
||||
0);
|
||||
EditText input = new EditText(context);
|
||||
input.setInputType(InputType.TYPE_CLASS_TEXT);
|
||||
input.setSingleLine(true);
|
||||
input.setHint(getString(R.string.battery_bar_colour_dialog_hint));
|
||||
String normalised = BatteryBarStyle.normalizeColor(initialValue);
|
||||
input.setText(normalised);
|
||||
layout.addView(input, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
MaterialButton sameAsDischargeButton = null;
|
||||
MaterialButton sameAsDefaultButton = null;
|
||||
if (allowSameAsDefault) {
|
||||
TextView note = new TextView(context);
|
||||
note.setText(R.string.battery_bar_threshold_charge_note);
|
||||
LinearLayout.LayoutParams noteParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
noteParams.topMargin = dpToPx(context, 8);
|
||||
note.setLayoutParams(noteParams);
|
||||
note.setGravity(Gravity.START);
|
||||
layout.addView(note);
|
||||
|
||||
sameAsDischargeButton = new MaterialButton(
|
||||
context,
|
||||
null,
|
||||
com.google.android.material.R.attr.materialButtonOutlinedStyle);
|
||||
sameAsDischargeButton.setText(R.string.battery_bar_colour_same_as_discharge);
|
||||
LinearLayout.LayoutParams dischargeParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
dischargeParams.topMargin = dpToPx(context, 8);
|
||||
sameAsDischargeButton.setLayoutParams(dischargeParams);
|
||||
layout.addView(sameAsDischargeButton);
|
||||
|
||||
sameAsDefaultButton = new MaterialButton(
|
||||
context,
|
||||
null,
|
||||
com.google.android.material.R.attr.materialButtonOutlinedStyle);
|
||||
sameAsDefaultButton.setText(R.string.battery_bar_colour_same_as_default);
|
||||
LinearLayout.LayoutParams defaultParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
defaultParams.topMargin = dpToPx(context, 8);
|
||||
sameAsDefaultButton.setLayoutParams(defaultParams);
|
||||
layout.addView(sameAsDefaultButton);
|
||||
}
|
||||
androidx.appcompat.app.AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(titleRes)
|
||||
.setView(layout)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(allowSameAsDefault ? null : getString(emptyLabelRes),
|
||||
(dialogInterface, which) -> consumer.accept(""))
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, which) -> {
|
||||
String raw = input.getText() != null ? input.getText().toString() : "";
|
||||
consumer.accept(BatteryBarStyle.normalizeColor(raw));
|
||||
})
|
||||
.show();
|
||||
if (sameAsDischargeButton != null) {
|
||||
MaterialButton finalSameAsDischargeButton = sameAsDischargeButton;
|
||||
finalSameAsDischargeButton.setOnClickListener(v -> {
|
||||
consumer.accept("");
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
if (sameAsDefaultButton != null) {
|
||||
MaterialButton finalSameAsDefaultButton = sameAsDefaultButton;
|
||||
finalSameAsDefaultButton.setOnClickListener(v -> {
|
||||
consumer.accept(BatteryBarStyle.THRESHOLD_CHARGE_USE_DEFAULT);
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
focusInput(dialog, input);
|
||||
}
|
||||
|
||||
private void showThresholdPercentDialog(@Nullable BatteryBarStyle.Threshold existing,
|
||||
SharedPreferences prefs,
|
||||
LinearLayout container,
|
||||
IntConsumer consumer) {
|
||||
Context context = requireContext();
|
||||
EditText input = new EditText(context);
|
||||
input.setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||
input.setSingleLine(true);
|
||||
input.setHint(getString(R.string.battery_bar_threshold_percent_hint));
|
||||
if (existing != null) {
|
||||
input.setText(String.valueOf(existing.percent));
|
||||
}
|
||||
androidx.appcompat.app.AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.battery_bar_threshold_percent_title)
|
||||
.setView(input)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, which) -> {
|
||||
int percent = BatteryBarStyle.clampPercent(
|
||||
parseIntInput(input, existing != null ? existing.percent : 15));
|
||||
if (existing == null && containsThreshold(loadThresholds(prefs), percent, -1)) {
|
||||
showMessageDialog(R.string.battery_bar_threshold_duplicate);
|
||||
return;
|
||||
}
|
||||
consumer.accept(percent);
|
||||
})
|
||||
.show();
|
||||
focusInput(dialog, input);
|
||||
}
|
||||
|
||||
private void showMessageDialog(int messageRes) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setMessage(messageRes)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private int parseIntInput(EditText input, int fallback) {
|
||||
if (input == null) {
|
||||
return fallback;
|
||||
}
|
||||
String text = input.getText() != null ? input.getText().toString().trim() : "";
|
||||
if (text.isEmpty() || "-".equals(text)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(text);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private void setIntInput(EditText input, int value) {
|
||||
if (input != null) {
|
||||
input.setText(String.valueOf(value));
|
||||
}
|
||||
}
|
||||
|
||||
private int clampInt(int value, int min, int max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int dpToPx(Context context, int dp) {
|
||||
if (context == null) {
|
||||
return dp;
|
||||
}
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
return (int) (dp * density + 0.5f);
|
||||
}
|
||||
|
||||
private void applyPositionSelection(RadioButton top,
|
||||
RadioButton bottom,
|
||||
String position) {
|
||||
if (top != null) {
|
||||
top.setChecked(!"bottom".equals(position));
|
||||
}
|
||||
if (bottom != null) {
|
||||
bottom.setChecked("bottom".equals(position));
|
||||
}
|
||||
}
|
||||
|
||||
private String positionValue(int checkedId) {
|
||||
if (checkedId == R.id.battery_bar_position_bottom) {
|
||||
return "bottom";
|
||||
}
|
||||
return "top";
|
||||
}
|
||||
|
||||
private void applyAlignmentSelection(RadioButton ltr,
|
||||
RadioButton rtl,
|
||||
RadioButton centre,
|
||||
String alignment) {
|
||||
if (ltr != null) {
|
||||
ltr.setChecked("ltr".equals(alignment));
|
||||
}
|
||||
if (rtl != null) {
|
||||
rtl.setChecked("rtl".equals(alignment));
|
||||
}
|
||||
if (centre != null) {
|
||||
centre.setChecked("center".equals(alignment));
|
||||
}
|
||||
}
|
||||
|
||||
private interface ColourConsumer {
|
||||
void accept(String colour);
|
||||
}
|
||||
|
||||
private interface IntConsumer {
|
||||
void accept(int value);
|
||||
}
|
||||
|
||||
private void focusInput(androidx.appcompat.app.AlertDialog dialog, EditText input) {
|
||||
if (dialog == null || input == null) {
|
||||
return;
|
||||
}
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
|
||||
}
|
||||
input.post(() -> {
|
||||
input.requestFocus();
|
||||
if (input.getText() != null) {
|
||||
input.setSelection(input.getText().length());
|
||||
}
|
||||
InputMethodManager imm = requireContext().getSystemService(InputMethodManager.class);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
import se.ajpanton.statusbartweak.shell.ui.HiddenAppsUiSupport.HiddenAppsAdapter;
|
||||
import se.ajpanton.statusbartweak.shell.ui.HiddenAppsUiSupport.RowItem;
|
||||
|
||||
public class BlockAppNotificationIconsFragment extends Fragment {
|
||||
private final ArrayList<RowItem> rows = new ArrayList<>();
|
||||
private final Handler uiHandler = new Handler(Looper.getMainLooper());
|
||||
private HiddenAppsAdapter adapter;
|
||||
private SharedPreferences prefs;
|
||||
private PackageManager packageManager;
|
||||
private Drawable fallbackIcon;
|
||||
private TextView emptyView;
|
||||
private ListView listView;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_block_app_notification_icons, container, false);
|
||||
Context context = requireContext();
|
||||
prefs = context.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
packageManager = context.getPackageManager();
|
||||
fallbackIcon = packageManager.getDefaultActivityIcon();
|
||||
emptyView = root.findViewById(R.id.hidden_apps_empty);
|
||||
listView = root.findViewById(R.id.hidden_apps_list);
|
||||
SwipeRefreshLayout swipeRefresh = root.findViewById(R.id.hidden_apps_swipe);
|
||||
View clearSessionButton = root.findViewById(R.id.hidden_apps_clear_session);
|
||||
|
||||
adapter = new HiddenAppsAdapter(context, rows, (row, modeMask) -> {
|
||||
if (!HiddenAppsUiSupport.toggleRowMode(row, modeMask)) {
|
||||
return;
|
||||
}
|
||||
HiddenAppsUiSupport.persistRowState(requireContext(), prefs, row);
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
listView.setAdapter(adapter);
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (position < 0 || position >= rows.size()) {
|
||||
return;
|
||||
}
|
||||
listView.clearChoices();
|
||||
adapter.notifyDataSetChanged();
|
||||
HiddenAppsUiSupport.flashRowTapFeedback(view);
|
||||
HiddenAppsUiSupport.showModePopup(this, rows.get(position), changedRow -> {
|
||||
HiddenAppsUiSupport.persistRowState(requireContext(), prefs, changedRow);
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
});
|
||||
swipeRefresh.setOnRefreshListener(() -> {
|
||||
reloadRowsSnapshot();
|
||||
swipeRefresh.setRefreshing(false);
|
||||
});
|
||||
clearSessionButton.setOnClickListener(v -> {
|
||||
resetSeenSessionNow();
|
||||
reloadRowsSnapshot();
|
||||
scheduleAutoRefreshAfterClear();
|
||||
});
|
||||
reloadRowsSnapshot();
|
||||
applyLayoutAvailability(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void resetSeenSessionNow() {
|
||||
if (prefs == null) {
|
||||
return;
|
||||
}
|
||||
AppIconRules.clearSeenSession(prefs);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
|
||||
private void scheduleAutoRefreshAfterClear() {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
uiHandler.postDelayed(this::reloadRowsSnapshot, 150);
|
||||
uiHandler.postDelayed(this::reloadRowsSnapshot, 600);
|
||||
uiHandler.postDelayed(this::reloadRowsSnapshot, 1400);
|
||||
}
|
||||
|
||||
private void reloadRowsSnapshot() {
|
||||
HiddenAppsUiSupport.reloadRowsSnapshot(requireContext(), prefs, packageManager, fallbackIcon, rows);
|
||||
|
||||
if (adapter != null) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (emptyView == null) {
|
||||
return;
|
||||
}
|
||||
emptyView.setVisibility(rows.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void applyLayoutAvailability(View root) {
|
||||
if (root == null || prefs == null) {
|
||||
return;
|
||||
}
|
||||
boolean enabled = prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_ENABLED,
|
||||
SbtDefaults.CLOCK_ENABLED_DEFAULT);
|
||||
View content = root.findViewById(R.id.hidden_apps_content);
|
||||
TextView disabledHint = root.findViewById(R.id.hidden_apps_disabled_hint);
|
||||
if (content != null) {
|
||||
setViewTreeEnabled(content, enabled);
|
||||
content.setAlpha(enabled ? 1f : 0.45f);
|
||||
}
|
||||
if (disabledHint != null) {
|
||||
disabledHint.setVisibility(enabled ? View.GONE : View.VISIBLE);
|
||||
disabledHint.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void setViewTreeEnabled(View view, boolean enabled) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setEnabled(enabled);
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewTreeEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class ClockFragment extends Fragment {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_clock, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
CheckBox customFormatEnabled = root.findViewById(R.id.clock_custom_format_enabled);
|
||||
View customFormatContainer = root.findViewById(R.id.clock_custom_format_container);
|
||||
EditText customFormatInput = root.findViewById(R.id.clock_custom_format_input);
|
||||
MaterialButton fontPickerButton = root.findViewById(R.id.clock_font_picker_button);
|
||||
TextView customFormatHelpToggle = root.findViewById(R.id.clock_custom_format_help_toggle);
|
||||
View customFormatHelpContainer = root.findViewById(R.id.clock_custom_format_help_container);
|
||||
TextView customFormatHelpCommon = root.findViewById(R.id.clock_custom_format_help);
|
||||
TextView customFormatHelpStyling = root.findViewById(R.id.clock_custom_format_help_styling);
|
||||
TextView customFormatHelpExamples = root.findViewById(R.id.clock_custom_format_help_examples);
|
||||
TextView customFormatHelpLink = root.findViewById(R.id.clock_custom_format_help_link);
|
||||
|
||||
boolean customFormatEnabledValue = prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT);
|
||||
String customFormatValue = prefs.getString(
|
||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT);
|
||||
|
||||
customFormatEnabled.setChecked(customFormatEnabledValue);
|
||||
customFormatInput.setText(customFormatValue);
|
||||
updateCustomFormatVisibility(customFormatContainer, customFormatEnabledValue);
|
||||
updateCustomFormatHelpVisibility(customFormatHelpContainer, customFormatHelpToggle, false);
|
||||
applyBulletText(customFormatHelpCommon);
|
||||
applyBulletText(customFormatHelpStyling);
|
||||
applyBulletText(customFormatHelpExamples);
|
||||
if (customFormatHelpLink != null) {
|
||||
customFormatHelpLink.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
|
||||
customFormatEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
updateCustomFormatVisibility(customFormatContainer, isChecked);
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_CUSTOM_FORMAT_ENABLED, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
if (customFormatHelpToggle != null) {
|
||||
customFormatHelpToggle.setOnClickListener(v -> updateCustomFormatHelpVisibility(
|
||||
customFormatHelpContainer,
|
||||
customFormatHelpToggle,
|
||||
customFormatHelpContainer == null || customFormatHelpContainer.getVisibility() != View.VISIBLE));
|
||||
}
|
||||
|
||||
customFormatInput.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_CUSTOM_FORMAT, s != null ? s.toString() : "")
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
|
||||
if (fontPickerButton != null) {
|
||||
fontPickerButton.setOnClickListener(v -> showFontPickerDialog());
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void updateCustomFormatVisibility(View customFormatContainer, boolean enabled) {
|
||||
if (customFormatContainer == null) {
|
||||
return;
|
||||
}
|
||||
customFormatContainer.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void updateCustomFormatHelpVisibility(View helpContainer,
|
||||
TextView helpToggle,
|
||||
boolean visible) {
|
||||
if (helpContainer != null) {
|
||||
helpContainer.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (helpToggle != null) {
|
||||
helpToggle.setText(visible
|
||||
? R.string.clock_custom_format_help_hide
|
||||
: R.string.clock_custom_format_help_show);
|
||||
}
|
||||
}
|
||||
|
||||
private void showFontPickerDialog() {
|
||||
Context context = requireContext();
|
||||
List<String> allFamilies = FontFamilySupport.listAvailableFamilies();
|
||||
ArrayList<String> filteredFamilies = new ArrayList<>(allFamilies);
|
||||
|
||||
LinearLayout container = new LinearLayout(context);
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
int padding = dpToPx(context, 20);
|
||||
container.setPadding(padding, dpToPx(context, 8), padding, 0);
|
||||
|
||||
EditText searchInput = new EditText(context);
|
||||
searchInput.setHint(R.string.clock_font_picker_hint);
|
||||
searchInput.setSingleLine(true);
|
||||
searchInput.setImportantForAutofill(View.IMPORTANT_FOR_AUTOFILL_NO);
|
||||
container.addView(searchInput, new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
|
||||
ListView listView = new ListView(context);
|
||||
listView.setDividerHeight(0);
|
||||
LinearLayout.LayoutParams listParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
dpToPx(context, 360));
|
||||
listParams.topMargin = dpToPx(context, 12);
|
||||
container.addView(listView, listParams);
|
||||
|
||||
FontFamilyAdapter adapter = new FontFamilyAdapter(context, filteredFamilies);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
searchInput.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) {
|
||||
applyFontFilter(adapter, allFamilies, s != null ? s.toString() : "");
|
||||
}
|
||||
});
|
||||
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
if (position < 0 || position >= adapter.getCount()) {
|
||||
return;
|
||||
}
|
||||
String family = adapter.getItem(position);
|
||||
if (family == null || family.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
copyFontFamily(family);
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.clock_font_picker_copied, family),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
});
|
||||
|
||||
Runnable refreshSamples = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapter.notifyDataSetChanged();
|
||||
listView.postDelayed(this, 1000L);
|
||||
}
|
||||
};
|
||||
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.clock_font_picker_title)
|
||||
.setView(container)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
listView.post(refreshSamples);
|
||||
dialog.setOnDismissListener(d -> listView.removeCallbacks(refreshSamples));
|
||||
}
|
||||
|
||||
private void applyFontFilter(FontFamilyAdapter adapter, List<String> allFamilies, String query) {
|
||||
if (adapter == null || allFamilies == null) {
|
||||
return;
|
||||
}
|
||||
String needle = query != null ? query.trim().toLowerCase(Locale.ROOT) : "";
|
||||
adapter.replaceAll(filterFontFamilies(allFamilies, needle));
|
||||
}
|
||||
|
||||
private List<String> filterFontFamilies(List<String> allFamilies, String needle) {
|
||||
ArrayList<String> filtered = new ArrayList<>();
|
||||
for (String family : allFamilies) {
|
||||
if (family == null) {
|
||||
continue;
|
||||
}
|
||||
if (needle == null || needle.isEmpty() || family.toLowerCase(Locale.ROOT).contains(needle)) {
|
||||
filtered.add(family);
|
||||
}
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void copyFontFamily(String family) {
|
||||
ClipboardManager clipboard = requireContext().getSystemService(ClipboardManager.class);
|
||||
if (clipboard != null) {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText("font-family", family));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyBulletText(TextView view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
CharSequence raw = view.getText();
|
||||
if (raw == null || raw.length() == 0) {
|
||||
return;
|
||||
}
|
||||
String[] lines = raw.toString().split("\\n", -1);
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||
int bulletGap = dpToPx(view.getContext(), 8);
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
String line = lines[i];
|
||||
boolean bulleted = line.startsWith("\u2022");
|
||||
String text = bulleted ? line.substring(1).trim() : line;
|
||||
int start = builder.length();
|
||||
builder.append(text);
|
||||
if (bulleted) {
|
||||
builder.setSpan(
|
||||
new BulletSpan(bulletGap),
|
||||
start,
|
||||
builder.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
if (i < lines.length - 1) {
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
view.setText(builder);
|
||||
}
|
||||
|
||||
private int dpToPx(Context context, int dp) {
|
||||
if (context == null) {
|
||||
return dp;
|
||||
}
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
private static final class FontFamilyAdapter extends BaseAdapter {
|
||||
private static final String SAMPLE_PATTERN = "EEEE HH:mm:ss";
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
private final ArrayList<String> families;
|
||||
|
||||
FontFamilyAdapter(Context context, List<String> initialFamilies) {
|
||||
this.inflater = LayoutInflater.from(context);
|
||||
this.families = new ArrayList<>();
|
||||
replaceAll(initialFamilies);
|
||||
}
|
||||
|
||||
void replaceAll(List<String> updatedFamilies) {
|
||||
families.clear();
|
||||
if (updatedFamilies != null) {
|
||||
families.addAll(updatedFamilies);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return families.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getItem(int position) {
|
||||
if (position < 0 || position >= families.size()) {
|
||||
return null;
|
||||
}
|
||||
return families.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
private static String buildSampleText() {
|
||||
return new SimpleDateFormat(SAMPLE_PATTERN, Locale.getDefault()).format(new Date());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
View view = convertView != null
|
||||
? convertView
|
||||
: inflater.inflate(R.layout.item_clock_font_family, parent, false);
|
||||
TextView nameView = view.findViewById(R.id.font_family_name);
|
||||
TextView sampleView = view.findViewById(R.id.font_family_sample);
|
||||
String family = getItem(position);
|
||||
if (nameView != null) {
|
||||
nameView.setText(family);
|
||||
}
|
||||
if (sampleView != null) {
|
||||
sampleView.setText(buildSampleText());
|
||||
sampleView.setTypeface(Typeface.create(family, Typeface.NORMAL));
|
||||
}
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
final class FontFamilySupport {
|
||||
private static final Pattern FAMILY_NAME_PATTERN = Pattern.compile(
|
||||
"<(?:family|alias)\\b[^>]*\\bname\\s*=\\s*['\"]([^'\"]+)['\"]",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private static final String[] FONT_CONFIG_PATHS = new String[] {
|
||||
"/system/etc/fonts.xml",
|
||||
"/system/etc/system_fonts.xml",
|
||||
"/system_ext/etc/fonts.xml",
|
||||
"/product/etc/fonts.xml",
|
||||
"/vendor/etc/fonts.xml"
|
||||
};
|
||||
|
||||
private static final List<String> FALLBACK_FAMILIES = Arrays.asList(
|
||||
"sans-serif",
|
||||
"sans-serif-light",
|
||||
"sans-serif-medium",
|
||||
"sans-serif-thin",
|
||||
"sans-serif-condensed",
|
||||
"sans-serif-smallcaps",
|
||||
"serif",
|
||||
"monospace",
|
||||
"cursive",
|
||||
"casual"
|
||||
);
|
||||
|
||||
private FontFamilySupport() {
|
||||
}
|
||||
|
||||
static List<String> listAvailableFamilies() {
|
||||
Set<String> families = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||
for (String path : FONT_CONFIG_PATHS) {
|
||||
readFamiliesFromConfig(path, families);
|
||||
}
|
||||
families.addAll(FALLBACK_FAMILIES);
|
||||
return new ArrayList<>(families);
|
||||
}
|
||||
|
||||
private static void readFamiliesFromConfig(String path, Set<String> out) {
|
||||
if (out == null || path == null || path.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
File file = new File(path);
|
||||
if (!file.isFile() || !file.canRead()) {
|
||||
return;
|
||||
}
|
||||
StringBuilder content = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
content.append(line).append('\n');
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
return;
|
||||
}
|
||||
Matcher matcher = FAMILY_NAME_PATTERN.matcher(content);
|
||||
while (matcher.find()) {
|
||||
String family = matcher.group(1);
|
||||
if (family == null) {
|
||||
continue;
|
||||
}
|
||||
String normalized = family.trim();
|
||||
if (!normalized.isEmpty()) {
|
||||
out.add(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,839 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.AppIconRules;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.ViewParent;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
final class HiddenAppsUiSupport {
|
||||
interface RowChangedListener {
|
||||
void onChanged(RowItem row);
|
||||
}
|
||||
|
||||
private static final int MODE_INDEX_AOD = 0;
|
||||
private static final int MODE_INDEX_LOCK = 1;
|
||||
private static final int MODE_INDEX_UNLOCK = 2;
|
||||
|
||||
private static void bindModeIcon(ImageView icon, TextView cross, Drawable drawable, boolean blocked) {
|
||||
if (icon == null || cross == null) {
|
||||
return;
|
||||
}
|
||||
if (drawable != null && drawable.getConstantState() != null) {
|
||||
icon.setImageDrawable(drawable.getConstantState().newDrawable().mutate());
|
||||
} else {
|
||||
icon.setImageDrawable(drawable);
|
||||
}
|
||||
icon.setAlpha(blocked ? 0.25f : 1f);
|
||||
cross.setVisibility(blocked ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private HiddenAppsUiSupport() {
|
||||
}
|
||||
|
||||
static void reloadRowsSnapshot(Context context,
|
||||
SharedPreferences prefs,
|
||||
PackageManager packageManager,
|
||||
Drawable fallbackIcon,
|
||||
ArrayList<RowItem> rows) {
|
||||
if (rows == null) {
|
||||
return;
|
||||
}
|
||||
rows.clear();
|
||||
if (context == null || prefs == null || packageManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Integer> blocked = AppIconRules.loadBlockedModes(prefs);
|
||||
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptionRules =
|
||||
AppIconRules.loadExceptionRules(prefs);
|
||||
Map<String, Long> seen = AppIconRules.loadSeenSession(prefs);
|
||||
|
||||
ArrayList<RowItem> blockedItems = new ArrayList<>();
|
||||
for (Map.Entry<String, Integer> entry : blocked.entrySet()) {
|
||||
String pkg = entry.getKey();
|
||||
Integer maskObj = entry.getValue();
|
||||
if (!AppIconRules.isValidPackageName(pkg) || maskObj == null) {
|
||||
continue;
|
||||
}
|
||||
int mask = maskObj;
|
||||
RowItem item = buildRowItem(context, packageManager, fallbackIcon, pkg);
|
||||
item.blockAod = (mask & AppIconRules.MODE_AOD) != 0;
|
||||
item.blockLock = (mask & AppIconRules.MODE_LOCK) != 0;
|
||||
item.blockUnlock = (mask & AppIconRules.MODE_UNLOCK) != 0;
|
||||
item.exceptionRules = toEditableExceptionRules(exceptionRules.get(pkg));
|
||||
blockedItems.add(item);
|
||||
}
|
||||
blockedItems.sort((a, b) -> {
|
||||
int byLabel = a.label.toLowerCase(Locale.ROOT).compareTo(b.label.toLowerCase(Locale.ROOT));
|
||||
if (byLabel != 0) {
|
||||
return byLabel;
|
||||
}
|
||||
return a.packageName.compareTo(b.packageName);
|
||||
});
|
||||
|
||||
rows.addAll(blockedItems);
|
||||
Set<String> alreadyAdded = new HashSet<>();
|
||||
for (RowItem item : blockedItems) {
|
||||
alreadyAdded.add(item.packageName);
|
||||
}
|
||||
|
||||
List<AppIconRules.SeenApp> seenOldestFirst = AppIconRules.sortSeenOldestFirst(seen);
|
||||
for (AppIconRules.SeenApp seenApp : seenOldestFirst) {
|
||||
if (seenApp == null || !AppIconRules.isValidPackageName(seenApp.packageName)) {
|
||||
continue;
|
||||
}
|
||||
if (alreadyAdded.contains(seenApp.packageName)) {
|
||||
continue;
|
||||
}
|
||||
RowItem item = buildRowItem(context, packageManager, fallbackIcon, seenApp.packageName);
|
||||
item.exceptionRules = toEditableExceptionRules(exceptionRules.get(seenApp.packageName));
|
||||
rows.add(item);
|
||||
alreadyAdded.add(seenApp.packageName);
|
||||
}
|
||||
}
|
||||
|
||||
private static RowItem buildRowItem(Context context,
|
||||
PackageManager packageManager,
|
||||
Drawable fallbackIcon,
|
||||
String packageName) {
|
||||
String label = packageName;
|
||||
Drawable icon = fallbackIcon;
|
||||
try {
|
||||
ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
|
||||
CharSequence cs = packageManager.getApplicationLabel(appInfo);
|
||||
if (cs != null && cs.length() > 0) {
|
||||
label = cs.toString();
|
||||
}
|
||||
icon = packageManager.getApplicationIcon(appInfo);
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
// Keep fallback label/icon.
|
||||
}
|
||||
if (label == null || label.trim().isEmpty()) {
|
||||
label = context.getString(R.string.hidden_apps_package_unknown);
|
||||
}
|
||||
return new RowItem(packageName, label, icon);
|
||||
}
|
||||
|
||||
static void showModePopup(Fragment host, RowItem row, RowChangedListener rowChangedListener) {
|
||||
if (host == null || row == null) {
|
||||
return;
|
||||
}
|
||||
Context context = host.requireContext();
|
||||
View content = LayoutInflater.from(context).inflate(R.layout.dialog_hidden_app_config, null, false);
|
||||
TextView popupTitle = content.findViewById(R.id.hidden_app_config_title);
|
||||
popupTitle.setText(row.label);
|
||||
|
||||
View defaultAodCell = content.findViewById(R.id.hidden_app_default_aod_cell);
|
||||
View defaultLockCell = content.findViewById(R.id.hidden_app_default_lock_cell);
|
||||
View defaultUnlockCell = content.findViewById(R.id.hidden_app_default_unlock_cell);
|
||||
ImageView defaultAodIcon = content.findViewById(R.id.hidden_app_default_aod_icon);
|
||||
ImageView defaultLockIcon = content.findViewById(R.id.hidden_app_default_lock_icon);
|
||||
ImageView defaultUnlockIcon = content.findViewById(R.id.hidden_app_default_unlock_icon);
|
||||
TextView defaultAodCross = content.findViewById(R.id.hidden_app_default_aod_cross);
|
||||
TextView defaultLockCross = content.findViewById(R.id.hidden_app_default_lock_cross);
|
||||
TextView defaultUnlockCross = content.findViewById(R.id.hidden_app_default_unlock_cross);
|
||||
RecyclerView exceptionsList = content.findViewById(R.id.hidden_app_exceptions_list);
|
||||
View addException = content.findViewById(R.id.hidden_app_add_exception);
|
||||
|
||||
final boolean[] defaultModes = new boolean[] { row.blockAod, row.blockLock, row.blockUnlock };
|
||||
bindModeIconTriple(
|
||||
defaultAodIcon, defaultAodCross,
|
||||
defaultLockIcon, defaultLockCross,
|
||||
defaultUnlockIcon, defaultUnlockCross,
|
||||
row.icon,
|
||||
defaultModes[MODE_INDEX_AOD],
|
||||
defaultModes[MODE_INDEX_LOCK],
|
||||
defaultModes[MODE_INDEX_UNLOCK]);
|
||||
final boolean[] originalModes = defaultModes.clone();
|
||||
bindPopupDefaultModeToggle(defaultAodCell, defaultAodIcon, defaultAodCross,
|
||||
row.icon, defaultModes, MODE_INDEX_AOD, row, rowChangedListener);
|
||||
bindPopupDefaultModeToggle(defaultLockCell, defaultLockIcon, defaultLockCross,
|
||||
row.icon, defaultModes, MODE_INDEX_LOCK, row, rowChangedListener);
|
||||
bindPopupDefaultModeToggle(defaultUnlockCell, defaultUnlockIcon, defaultUnlockCross,
|
||||
row.icon, defaultModes, MODE_INDEX_UNLOCK, row, rowChangedListener);
|
||||
|
||||
ArrayList<EditableExceptionRule> workingRules = copyEditableExceptionRules(row.exceptionRules);
|
||||
ExceptionRulesAdapter exceptionsAdapter = new ExceptionRulesAdapter(
|
||||
context,
|
||||
row.icon,
|
||||
workingRules,
|
||||
() -> requestExceptionsRelayout(exceptionsList));
|
||||
exceptionsList.setLayoutManager(new LinearLayoutManager(context));
|
||||
exceptionsList.setAdapter(exceptionsAdapter);
|
||||
exceptionsList.setItemAnimator(null);
|
||||
exceptionsList.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
|
||||
exceptionsList.setNestedScrollingEnabled(false);
|
||||
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||
@Override
|
||||
public boolean onMove(RecyclerView recyclerView,
|
||||
RecyclerView.ViewHolder viewHolder,
|
||||
RecyclerView.ViewHolder target) {
|
||||
int from = viewHolder.getAdapterPosition();
|
||||
int to = target.getAdapterPosition();
|
||||
if (from < 0 || to < 0 || from >= workingRules.size() || to >= workingRules.size()) {
|
||||
return false;
|
||||
}
|
||||
if (from == to) {
|
||||
return true;
|
||||
}
|
||||
Collections.swap(workingRules, from, to);
|
||||
exceptionsAdapter.notifyDataSetChanged();
|
||||
requestExceptionsRelayout(exceptionsList);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
// Not used.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
itemTouchHelper.attachToRecyclerView(exceptionsList);
|
||||
exceptionsAdapter.setItemTouchHelper(itemTouchHelper);
|
||||
|
||||
addException.setOnClickListener(v -> {
|
||||
workingRules.add(new EditableExceptionRule());
|
||||
int pos = workingRules.size() - 1;
|
||||
exceptionsAdapter.notifyDataSetChanged();
|
||||
requestExceptionsRelayout(exceptionsList);
|
||||
exceptionsList.post(() -> exceptionsList.scrollToPosition(pos));
|
||||
});
|
||||
|
||||
androidx.appcompat.app.AlertDialog dialog = new MaterialAlertDialogBuilder(context)
|
||||
.setView(content)
|
||||
.setNegativeButton(android.R.string.cancel, (d, w) -> {
|
||||
if (row.blockAod != originalModes[MODE_INDEX_AOD]
|
||||
|| row.blockLock != originalModes[MODE_INDEX_LOCK]
|
||||
|| row.blockUnlock != originalModes[MODE_INDEX_UNLOCK]) {
|
||||
row.blockAod = originalModes[MODE_INDEX_AOD];
|
||||
row.blockLock = originalModes[MODE_INDEX_LOCK];
|
||||
row.blockUnlock = originalModes[MODE_INDEX_UNLOCK];
|
||||
notifyRowChanged(rowChangedListener, row);
|
||||
}
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dlg, which) -> {
|
||||
row.blockAod = defaultModes[MODE_INDEX_AOD];
|
||||
row.blockLock = defaultModes[MODE_INDEX_LOCK];
|
||||
row.blockUnlock = defaultModes[MODE_INDEX_UNLOCK];
|
||||
row.exceptionRules = copyEditableExceptionRules(workingRules);
|
||||
notifyRowChanged(rowChangedListener, row);
|
||||
})
|
||||
.show();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
if (dialog.getWindow() != null) {
|
||||
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
|
||||
dialog.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
|
||||
dialog.getWindow().setLayout(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
}
|
||||
compactDialogButtons(dialog, context);
|
||||
requestExceptionsRelayout(exceptionsList);
|
||||
}
|
||||
|
||||
static void persistRowState(Context context, SharedPreferences prefs, RowItem row) {
|
||||
if (context == null || prefs == null || row == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, Integer> blocked = AppIconRules.loadBlockedModes(prefs);
|
||||
Map<String, ArrayList<AppIconRules.ExceptionRule>> exceptionRules = AppIconRules.loadExceptionRules(prefs);
|
||||
AppIconRules.setModeBlocked(blocked, row.packageName, row.blockAod, row.blockLock, row.blockUnlock);
|
||||
AppIconRules.setExceptionRules(exceptionRules, row.packageName, toPersistedExceptionRules(row.exceptionRules));
|
||||
AppIconRules.saveBlockedModes(prefs, blocked);
|
||||
AppIconRules.saveExceptionRules(prefs, exceptionRules);
|
||||
SbtSettings.ensureReadable(context);
|
||||
}
|
||||
|
||||
static boolean toggleRowMode(RowItem row, int modeMask) {
|
||||
if (row == null) {
|
||||
return false;
|
||||
}
|
||||
if (modeMask == AppIconRules.MODE_AOD) {
|
||||
row.blockAod = !row.blockAod;
|
||||
return true;
|
||||
}
|
||||
if (modeMask == AppIconRules.MODE_LOCK) {
|
||||
row.blockLock = !row.blockLock;
|
||||
return true;
|
||||
}
|
||||
if (modeMask == AppIconRules.MODE_UNLOCK) {
|
||||
row.blockUnlock = !row.blockUnlock;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void flashRowTapFeedback(View rowView) {
|
||||
if (rowView == null) {
|
||||
return;
|
||||
}
|
||||
rowView.setPressed(true);
|
||||
rowView.postDelayed(() -> {
|
||||
rowView.setPressed(false);
|
||||
rowView.jumpDrawablesToCurrentState();
|
||||
}, 120L);
|
||||
}
|
||||
|
||||
private static void notifyRowChanged(RowChangedListener listener, RowItem row) {
|
||||
if (listener != null && row != null) {
|
||||
listener.onChanged(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void bindPopupDefaultModeToggle(View target,
|
||||
ImageView icon,
|
||||
TextView cross,
|
||||
Drawable drawable,
|
||||
boolean[] defaultModes,
|
||||
int modeIndex,
|
||||
RowItem row,
|
||||
RowChangedListener listener) {
|
||||
if (defaultModes == null || modeIndex < 0 || modeIndex >= defaultModes.length) {
|
||||
return;
|
||||
}
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
target.setOnClickListener(v -> {
|
||||
defaultModes[modeIndex] = !defaultModes[modeIndex];
|
||||
bindModeIcon(icon, cross, drawable, defaultModes[modeIndex]);
|
||||
applyModeIndexToRow(row, modeIndex, defaultModes[modeIndex]);
|
||||
notifyRowChanged(listener, row);
|
||||
});
|
||||
}
|
||||
|
||||
private static void applyModeIndexToRow(RowItem row, int modeIndex, boolean blocked) {
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
if (modeIndex == MODE_INDEX_AOD) {
|
||||
row.blockAod = blocked;
|
||||
} else if (modeIndex == MODE_INDEX_LOCK) {
|
||||
row.blockLock = blocked;
|
||||
} else if (modeIndex == MODE_INDEX_UNLOCK) {
|
||||
row.blockUnlock = blocked;
|
||||
}
|
||||
}
|
||||
|
||||
private static void bindModeIconTriple(ImageView aodIcon,
|
||||
TextView aodCross,
|
||||
ImageView lockIcon,
|
||||
TextView lockCross,
|
||||
ImageView unlockIcon,
|
||||
TextView unlockCross,
|
||||
Drawable drawable,
|
||||
boolean blockAod,
|
||||
boolean blockLock,
|
||||
boolean blockUnlock) {
|
||||
bindModeIcon(aodIcon, aodCross, drawable, blockAod);
|
||||
bindModeIcon(lockIcon, lockCross, drawable, blockLock);
|
||||
bindModeIcon(unlockIcon, unlockCross, drawable, blockUnlock);
|
||||
}
|
||||
|
||||
private static void requestExceptionsRelayout(RecyclerView list) {
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
list.post(() -> {
|
||||
list.requestLayout();
|
||||
ViewParent parent = list.getParent();
|
||||
if (parent instanceof View) {
|
||||
((View) parent).requestLayout();
|
||||
ViewParent gp = parent.getParent();
|
||||
if (gp instanceof View) {
|
||||
((View) gp).requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static ArrayList<EditableExceptionRule> toEditableExceptionRules(List<AppIconRules.ExceptionRule> rules) {
|
||||
ArrayList<EditableExceptionRule> out = new ArrayList<>();
|
||||
if (rules == null) {
|
||||
return out;
|
||||
}
|
||||
for (AppIconRules.ExceptionRule rule : rules) {
|
||||
if (rule == null) {
|
||||
continue;
|
||||
}
|
||||
EditableExceptionRule editable = new EditableExceptionRule();
|
||||
editable.regex = normalizeRegex(rule.regex);
|
||||
editable.blockAod = (rule.modeMask & AppIconRules.MODE_AOD) != 0;
|
||||
editable.blockLock = (rule.modeMask & AppIconRules.MODE_LOCK) != 0;
|
||||
editable.blockUnlock = (rule.modeMask & AppIconRules.MODE_UNLOCK) != 0;
|
||||
if (editable.regex != null) {
|
||||
out.add(editable);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static ArrayList<EditableExceptionRule> copyEditableExceptionRules(List<EditableExceptionRule> source) {
|
||||
ArrayList<EditableExceptionRule> out = new ArrayList<>();
|
||||
if (source == null) {
|
||||
return out;
|
||||
}
|
||||
for (EditableExceptionRule rule : source) {
|
||||
if (rule == null) {
|
||||
continue;
|
||||
}
|
||||
EditableExceptionRule copy = new EditableExceptionRule();
|
||||
copy.regex = normalizeRegex(rule.regex);
|
||||
copy.blockAod = rule.blockAod;
|
||||
copy.blockLock = rule.blockLock;
|
||||
copy.blockUnlock = rule.blockUnlock;
|
||||
out.add(copy);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static ArrayList<AppIconRules.ExceptionRule> toPersistedExceptionRules(List<EditableExceptionRule> source) {
|
||||
ArrayList<AppIconRules.ExceptionRule> out = new ArrayList<>();
|
||||
if (source == null) {
|
||||
return out;
|
||||
}
|
||||
for (EditableExceptionRule rule : source) {
|
||||
if (rule == null) {
|
||||
continue;
|
||||
}
|
||||
String regex = normalizeRegex(rule.regex);
|
||||
if (regex == null) {
|
||||
continue;
|
||||
}
|
||||
int mask = 0;
|
||||
if (rule.blockAod) {
|
||||
mask |= AppIconRules.MODE_AOD;
|
||||
}
|
||||
if (rule.blockLock) {
|
||||
mask |= AppIconRules.MODE_LOCK;
|
||||
}
|
||||
if (rule.blockUnlock) {
|
||||
mask |= AppIconRules.MODE_UNLOCK;
|
||||
}
|
||||
out.add(new AppIconRules.ExceptionRule(regex, mask));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String normalizeRegex(String raw) {
|
||||
if (raw == null) {
|
||||
return null;
|
||||
}
|
||||
String normalized = raw.trim();
|
||||
return normalized.isEmpty() ? null : normalized;
|
||||
}
|
||||
|
||||
static final class RowItem {
|
||||
final String packageName;
|
||||
final String label;
|
||||
final Drawable icon;
|
||||
boolean blockAod;
|
||||
boolean blockLock;
|
||||
boolean blockUnlock;
|
||||
ArrayList<EditableExceptionRule> exceptionRules = new ArrayList<>();
|
||||
|
||||
RowItem(String packageName, String label, Drawable icon) {
|
||||
this.packageName = packageName;
|
||||
this.label = label;
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
boolean hasAnyBlocked() {
|
||||
return blockAod || blockLock || blockUnlock;
|
||||
}
|
||||
|
||||
int exceptionCount() {
|
||||
int count = 0;
|
||||
for (EditableExceptionRule rule : exceptionRules) {
|
||||
if (rule != null && normalizeRegex(rule.regex) != null) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
static final class EditableExceptionRule {
|
||||
String regex;
|
||||
boolean blockAod;
|
||||
boolean blockLock;
|
||||
boolean blockUnlock;
|
||||
}
|
||||
|
||||
interface RowModeToggleListener {
|
||||
void onToggle(RowItem row, int modeMask);
|
||||
}
|
||||
|
||||
private static final class RowHolder {
|
||||
View root;
|
||||
View iconAodCell;
|
||||
View iconLockCell;
|
||||
View iconUnlockCell;
|
||||
ImageView iconAod;
|
||||
ImageView iconLock;
|
||||
ImageView iconUnlock;
|
||||
TextView crossAod;
|
||||
TextView crossLock;
|
||||
TextView crossUnlock;
|
||||
TextView label;
|
||||
TextView pkg;
|
||||
}
|
||||
|
||||
static final class HiddenAppsAdapter extends BaseAdapter {
|
||||
private final Context context;
|
||||
private final ArrayList<RowItem> rows;
|
||||
private final RowModeToggleListener rowModeToggleListener;
|
||||
|
||||
HiddenAppsAdapter(Context context,
|
||||
ArrayList<RowItem> rows,
|
||||
RowModeToggleListener rowModeToggleListener) {
|
||||
this.context = context;
|
||||
this.rows = rows;
|
||||
this.rowModeToggleListener = rowModeToggleListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return rows != null ? rows.size() : 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
if (rows == null || position < 0 || position >= rows.size()) {
|
||||
return null;
|
||||
}
|
||||
return rows.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
RowHolder holder;
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(context).inflate(R.layout.item_hidden_app, parent, false);
|
||||
holder = new RowHolder();
|
||||
holder.root = convertView.findViewById(R.id.hidden_item_root);
|
||||
holder.iconAodCell = convertView.findViewById(R.id.icon_mode_aod_cell);
|
||||
holder.iconLockCell = convertView.findViewById(R.id.icon_mode_lock_cell);
|
||||
holder.iconUnlockCell = convertView.findViewById(R.id.icon_mode_unlock_cell);
|
||||
holder.iconAod = convertView.findViewById(R.id.icon_mode_aod);
|
||||
holder.iconLock = convertView.findViewById(R.id.icon_mode_lock);
|
||||
holder.iconUnlock = convertView.findViewById(R.id.icon_mode_unlock);
|
||||
holder.crossAod = convertView.findViewById(R.id.icon_mode_aod_cross);
|
||||
holder.crossLock = convertView.findViewById(R.id.icon_mode_lock_cross);
|
||||
holder.crossUnlock = convertView.findViewById(R.id.icon_mode_unlock_cross);
|
||||
holder.label = convertView.findViewById(R.id.hidden_item_label);
|
||||
holder.pkg = convertView.findViewById(R.id.hidden_item_package);
|
||||
convertView.setTag(holder);
|
||||
} else {
|
||||
holder = (RowHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
RowItem row = rows.get(position);
|
||||
holder.label.setText(row.label);
|
||||
int exceptionCount = row.exceptionCount();
|
||||
if (exceptionCount > 0) {
|
||||
holder.pkg.setText(context.getString(
|
||||
R.string.hidden_apps_package_with_exceptions, row.packageName, exceptionCount));
|
||||
} else {
|
||||
holder.pkg.setText(row.packageName);
|
||||
}
|
||||
bindModeIconTriple(
|
||||
holder.iconAod, holder.crossAod,
|
||||
holder.iconLock, holder.crossLock,
|
||||
holder.iconUnlock, holder.crossUnlock,
|
||||
row.icon,
|
||||
row.blockAod, row.blockLock, row.blockUnlock);
|
||||
|
||||
int bg = row.hasAnyBlocked()
|
||||
? ContextCompat.getColor(context, R.color.sbt_hidden_item_active_bg)
|
||||
: Color.TRANSPARENT;
|
||||
holder.root.setBackgroundColor(bg);
|
||||
holder.root.setPressed(false);
|
||||
|
||||
bindModeToggle(holder.iconAodCell, row, AppIconRules.MODE_AOD);
|
||||
bindModeToggle(holder.iconLockCell, row, AppIconRules.MODE_LOCK);
|
||||
bindModeToggle(holder.iconUnlockCell, row, AppIconRules.MODE_UNLOCK);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private void bindModeToggle(View target, RowItem row, int modeMask) {
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
target.setOnClickListener(v -> {
|
||||
if (rowModeToggleListener != null) {
|
||||
rowModeToggleListener.onToggle(row, modeMask);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static final class ExceptionRuleHolder extends RecyclerView.ViewHolder {
|
||||
final View dragRoot;
|
||||
final TextView title;
|
||||
final TextView deleteButton;
|
||||
final EditText regexField;
|
||||
final View aodCell;
|
||||
final View lockCell;
|
||||
final View unlockCell;
|
||||
final ImageView aodIcon;
|
||||
final ImageView lockIcon;
|
||||
final ImageView unlockIcon;
|
||||
final TextView aodCross;
|
||||
final TextView lockCross;
|
||||
final TextView unlockCross;
|
||||
TextWatcher watcher;
|
||||
|
||||
ExceptionRuleHolder(View itemView) {
|
||||
super(itemView);
|
||||
dragRoot = itemView.findViewById(R.id.exception_rule_drag_root);
|
||||
title = itemView.findViewById(R.id.exception_rule_title);
|
||||
deleteButton = itemView.findViewById(R.id.exception_rule_delete);
|
||||
regexField = itemView.findViewById(R.id.exception_rule_regex);
|
||||
aodCell = itemView.findViewById(R.id.exception_rule_aod_cell);
|
||||
lockCell = itemView.findViewById(R.id.exception_rule_lock_cell);
|
||||
unlockCell = itemView.findViewById(R.id.exception_rule_unlock_cell);
|
||||
aodIcon = itemView.findViewById(R.id.exception_rule_aod_icon);
|
||||
lockIcon = itemView.findViewById(R.id.exception_rule_lock_icon);
|
||||
unlockIcon = itemView.findViewById(R.id.exception_rule_unlock_icon);
|
||||
aodCross = itemView.findViewById(R.id.exception_rule_aod_cross);
|
||||
lockCross = itemView.findViewById(R.id.exception_rule_lock_cross);
|
||||
unlockCross = itemView.findViewById(R.id.exception_rule_unlock_cross);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ExceptionRulesAdapter
|
||||
extends RecyclerView.Adapter<ExceptionRuleHolder> {
|
||||
private final Context context;
|
||||
private final Drawable appIcon;
|
||||
private final ArrayList<EditableExceptionRule> rules;
|
||||
private ItemTouchHelper itemTouchHelper;
|
||||
private final Runnable onRulesCountChanged;
|
||||
|
||||
ExceptionRulesAdapter(Context context,
|
||||
Drawable appIcon,
|
||||
ArrayList<EditableExceptionRule> rules,
|
||||
Runnable onRulesCountChanged) {
|
||||
this.context = context;
|
||||
this.appIcon = appIcon;
|
||||
this.rules = rules;
|
||||
this.onRulesCountChanged = onRulesCountChanged;
|
||||
}
|
||||
|
||||
void setItemTouchHelper(ItemTouchHelper itemTouchHelper) {
|
||||
this.itemTouchHelper = itemTouchHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExceptionRuleHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(context)
|
||||
.inflate(R.layout.item_hidden_app_exception_rule, parent, false);
|
||||
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
view.setLayoutParams(lp);
|
||||
return new ExceptionRuleHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ExceptionRuleHolder holder, int position) {
|
||||
EditableExceptionRule rule = rules.get(position);
|
||||
holder.title.setText(context.getString(R.string.hidden_apps_exception_label, position + 1));
|
||||
if (holder.watcher != null) {
|
||||
holder.regexField.removeTextChangedListener(holder.watcher);
|
||||
}
|
||||
holder.regexField.setText(rule.regex != null ? rule.regex : "");
|
||||
holder.watcher = new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
int pos = holder.getAdapterPosition();
|
||||
if (pos == RecyclerView.NO_POSITION || pos >= rules.size()) {
|
||||
return;
|
||||
}
|
||||
rules.get(pos).regex = s != null ? s.toString() : null;
|
||||
}
|
||||
};
|
||||
holder.regexField.addTextChangedListener(holder.watcher);
|
||||
holder.regexField.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
showKeyboard(holder.regexField);
|
||||
}
|
||||
});
|
||||
holder.regexField.setOnClickListener(v -> showKeyboard(holder.regexField));
|
||||
holder.regexField.setOnTouchListener((v, event) -> {
|
||||
v.getParent().requestDisallowInterceptTouchEvent(true);
|
||||
return false;
|
||||
});
|
||||
|
||||
bindModeIconTriple(
|
||||
holder.aodIcon, holder.aodCross,
|
||||
holder.lockIcon, holder.lockCross,
|
||||
holder.unlockIcon, holder.unlockCross,
|
||||
appIcon,
|
||||
rule.blockAod, rule.blockLock, rule.blockUnlock);
|
||||
bindExceptionModeToggle(holder, holder.aodCell, AppIconRules.MODE_AOD);
|
||||
bindExceptionModeToggle(holder, holder.lockCell, AppIconRules.MODE_LOCK);
|
||||
bindExceptionModeToggle(holder, holder.unlockCell, AppIconRules.MODE_UNLOCK);
|
||||
|
||||
holder.deleteButton.setOnClickListener(v -> {
|
||||
int pos = holder.getAdapterPosition();
|
||||
if (pos == RecyclerView.NO_POSITION || pos >= rules.size()) {
|
||||
return;
|
||||
}
|
||||
rules.remove(pos);
|
||||
notifyDataSetChanged();
|
||||
if (onRulesCountChanged != null) {
|
||||
onRulesCountChanged.run();
|
||||
}
|
||||
});
|
||||
|
||||
holder.dragRoot.setOnLongClickListener(v -> {
|
||||
if (itemTouchHelper != null) {
|
||||
itemTouchHelper.startDrag(holder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return rules != null ? rules.size() : 0;
|
||||
}
|
||||
|
||||
private void bindExceptionModeToggle(ExceptionRuleHolder holder, View target, int modeMask) {
|
||||
if (target == null) {
|
||||
return;
|
||||
}
|
||||
target.setOnClickListener(v -> {
|
||||
int pos = holder.getAdapterPosition();
|
||||
if (pos == RecyclerView.NO_POSITION || pos >= rules.size()) {
|
||||
return;
|
||||
}
|
||||
EditableExceptionRule r = rules.get(pos);
|
||||
if (modeMask == AppIconRules.MODE_AOD) {
|
||||
r.blockAod = !r.blockAod;
|
||||
} else if (modeMask == AppIconRules.MODE_LOCK) {
|
||||
r.blockLock = !r.blockLock;
|
||||
} else if (modeMask == AppIconRules.MODE_UNLOCK) {
|
||||
r.blockUnlock = !r.blockUnlock;
|
||||
}
|
||||
bindModeIconTriple(
|
||||
holder.aodIcon, holder.aodCross,
|
||||
holder.lockIcon, holder.lockCross,
|
||||
holder.unlockIcon, holder.unlockCross,
|
||||
appIcon,
|
||||
r.blockAod, r.blockLock, r.blockUnlock);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private abstract static class SimpleTextWatcher implements 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) {
|
||||
}
|
||||
}
|
||||
|
||||
private static void showKeyboard(EditText field) {
|
||||
if (field == null) {
|
||||
return;
|
||||
}
|
||||
field.post(() -> {
|
||||
field.requestFocus();
|
||||
InputMethodManager imm = (InputMethodManager) field.getContext()
|
||||
.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
field.requestFocusFromTouch();
|
||||
boolean shown = imm.showSoftInput(field, InputMethodManager.SHOW_IMPLICIT);
|
||||
if (!shown) {
|
||||
imm.showSoftInput(field, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void compactDialogButtons(androidx.appcompat.app.AlertDialog dialog, Context context) {
|
||||
if (dialog == null || context == null) {
|
||||
return;
|
||||
}
|
||||
int small = Math.round(2f * context.getResources().getDisplayMetrics().density);
|
||||
Button pos = dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE);
|
||||
Button neg = dialog.getButton(androidx.appcompat.app.AlertDialog.BUTTON_NEGATIVE);
|
||||
if (pos != null) {
|
||||
pos.setMinHeight(0);
|
||||
pos.setMinimumHeight(0);
|
||||
pos.setPadding(pos.getPaddingLeft(), small, pos.getPaddingRight(), small);
|
||||
}
|
||||
if (neg != null) {
|
||||
neg.setMinHeight(0);
|
||||
neg.setMinimumHeight(0);
|
||||
neg.setPadding(neg.getPaddingLeft(), small, neg.getPaddingRight(), small);
|
||||
}
|
||||
if (pos != null) {
|
||||
ViewParent p = pos.getParent();
|
||||
if (p instanceof ViewGroup) {
|
||||
ViewGroup panel = (ViewGroup) p;
|
||||
panel.setPadding(panel.getPaddingLeft(), small, panel.getPaddingRight(), small);
|
||||
ViewParent gp = panel.getParent();
|
||||
if (gp instanceof ViewGroup) {
|
||||
ViewGroup gpGroup = (ViewGroup) gp;
|
||||
gpGroup.setPadding(gpGroup.getPaddingLeft(), small, gpGroup.getPaddingRight(), small);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.debug.DebugNotifications;
|
||||
import se.ajpanton.statusbartweak.shell.settings.ClockCameraTypeSupport;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
public class IconsDebugFragment extends Fragment {
|
||||
private static final int REQUEST_POST_NOTIFICATIONS = 1001;
|
||||
|
||||
private TextView countView;
|
||||
private int currentCount;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_icons_debug, container, false);
|
||||
|
||||
countView = root.findViewById(R.id.debug_count_value);
|
||||
MaterialButton minus10 = root.findViewById(R.id.btn_minus_10);
|
||||
MaterialButton minus1 = root.findViewById(R.id.btn_minus_1);
|
||||
MaterialButton plus1 = root.findViewById(R.id.btn_plus_1);
|
||||
MaterialButton plus10 = root.findViewById(R.id.btn_plus_10);
|
||||
MaterialButton reset = root.findViewById(R.id.btn_reset);
|
||||
SwitchMaterial writeLogsSwitch = root.findViewById(R.id.debug_write_logs_switch);
|
||||
SwitchMaterial visualizeNotificationContainerSwitch =
|
||||
root.findViewById(R.id.debug_visualize_notification_container_switch);
|
||||
SwitchMaterial visualizeStatusContainerSwitch =
|
||||
root.findViewById(R.id.debug_visualize_status_container_switch);
|
||||
SwitchMaterial visualizeClockContainerSwitch =
|
||||
root.findViewById(R.id.debug_visualize_clock_container_switch);
|
||||
SwitchMaterial visualizeChipContainerSwitch =
|
||||
root.findViewById(R.id.debug_visualize_chip_container_switch);
|
||||
SwitchMaterial visualizeCameraCutoutSwitch =
|
||||
root.findViewById(R.id.debug_visualize_camera_cutout_switch);
|
||||
MaterialButton restartSystemUiButton = root.findViewById(R.id.debug_restart_systemui_button);
|
||||
SwitchMaterial cycleIconsSwitch = root.findViewById(R.id.debug_cycle_icons_switch);
|
||||
TextView currentCameraIdentified = root.findViewById(R.id.debug_camera_current_identified);
|
||||
LinearLayout currentCameraOverrideRow = root.findViewById(R.id.debug_camera_current_override_row);
|
||||
TextView currentCameraOverride = root.findViewById(R.id.debug_camera_current_override);
|
||||
MaterialButton currentCameraAction = root.findViewById(R.id.debug_camera_current_action);
|
||||
View otherCameraSection = root.findViewById(R.id.debug_camera_other_section);
|
||||
LinearLayout otherCameraList = root.findViewById(R.id.debug_camera_other_list);
|
||||
|
||||
currentCount = DebugNotifications.getDesiredCount(requireContext());
|
||||
updateCountLabel();
|
||||
bindCycleSwitch(cycleIconsSwitch);
|
||||
|
||||
minus10.setOnClickListener(v -> adjustCount(-10));
|
||||
minus1.setOnClickListener(v -> adjustCount(-1));
|
||||
plus1.setOnClickListener(v -> adjustCount(1));
|
||||
plus10.setOnClickListener(v -> adjustCount(10));
|
||||
reset.setOnClickListener(v -> setCount(0));
|
||||
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
bindWriteLogsSwitch(prefs, writeLogsSwitch);
|
||||
bindBooleanSwitch(
|
||||
prefs,
|
||||
visualizeNotificationContainerSwitch,
|
||||
SbtSettings.KEY_DEBUG_VISUALIZE_NOTIFICATION_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_NOTIFICATION_CONTAINER_DEFAULT);
|
||||
bindBooleanSwitch(
|
||||
prefs,
|
||||
visualizeStatusContainerSwitch,
|
||||
SbtSettings.KEY_DEBUG_VISUALIZE_STATUS_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_STATUS_CONTAINER_DEFAULT);
|
||||
bindBooleanSwitch(
|
||||
prefs,
|
||||
visualizeClockContainerSwitch,
|
||||
SbtSettings.KEY_DEBUG_VISUALIZE_CLOCK_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CLOCK_CONTAINER_DEFAULT);
|
||||
bindBooleanSwitch(
|
||||
prefs,
|
||||
visualizeChipContainerSwitch,
|
||||
SbtSettings.KEY_DEBUG_VISUALIZE_CHIP_CONTAINER,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CHIP_CONTAINER_DEFAULT);
|
||||
bindBooleanSwitch(
|
||||
prefs,
|
||||
visualizeCameraCutoutSwitch,
|
||||
SbtSettings.KEY_DEBUG_VISUALIZE_CAMERA_CUTOUT,
|
||||
SbtDefaults.DEBUG_VISUALIZE_CAMERA_CUTOUT_DEFAULT);
|
||||
bindRestartSystemUiButton(restartSystemUiButton);
|
||||
updateVisualizerSwitchAvailability(
|
||||
prefs,
|
||||
visualizeNotificationContainerSwitch,
|
||||
visualizeStatusContainerSwitch,
|
||||
visualizeClockContainerSwitch,
|
||||
visualizeChipContainerSwitch);
|
||||
Runnable refreshCameraUi = () -> updateCameraUi(
|
||||
prefs,
|
||||
currentCameraIdentified,
|
||||
currentCameraOverrideRow,
|
||||
currentCameraOverride,
|
||||
currentCameraAction,
|
||||
otherCameraSection,
|
||||
otherCameraList);
|
||||
SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPrefs, key) -> {
|
||||
if (SbtSettings.KEY_DEBUG_CURRENT_CAMERA_ID.equals(key)
|
||||
|| SbtSettings.KEY_DEBUG_CURRENT_CAMERA_AUTO_TYPE.equals(key)
|
||||
|| SbtSettings.KEY_DEBUG_CURRENT_CAMERA_EFFECTIVE_TYPE.equals(key)
|
||||
|| SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES.equals(key)) {
|
||||
refreshCameraUi.run();
|
||||
}
|
||||
if (SbtSettings.KEY_CLOCK_ENABLED.equals(key)) {
|
||||
updateVisualizerSwitchAvailability(
|
||||
prefs,
|
||||
visualizeNotificationContainerSwitch,
|
||||
visualizeStatusContainerSwitch,
|
||||
visualizeClockContainerSwitch,
|
||||
visualizeChipContainerSwitch);
|
||||
}
|
||||
};
|
||||
prefs.registerOnSharedPreferenceChangeListener(listener);
|
||||
root.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
prefs.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
root.removeOnAttachStateChangeListener(this);
|
||||
}
|
||||
});
|
||||
refreshCameraUi.run();
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void bindWriteLogsSwitch(SharedPreferences prefs, SwitchMaterial writeLogsSwitch) {
|
||||
if (prefs == null || writeLogsSwitch == null) {
|
||||
return;
|
||||
}
|
||||
writeLogsSwitch.setChecked(prefs.getBoolean(
|
||||
SbtSettings.KEY_DEBUG_WRITE_LOGS,
|
||||
SbtDefaults.DEBUG_WRITE_LOGS_DEFAULT));
|
||||
writeLogsSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_DEBUG_WRITE_LOGS, isChecked).commit();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void bindBooleanSwitch(SharedPreferences prefs,
|
||||
SwitchMaterial switchView,
|
||||
String key,
|
||||
boolean defaultValue) {
|
||||
if (prefs == null || switchView == null || key == null) {
|
||||
return;
|
||||
}
|
||||
switchView.setChecked(prefs.getBoolean(key, defaultValue));
|
||||
switchView.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(key, isChecked).commit();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void bindRestartSystemUiButton(MaterialButton restartButton) {
|
||||
if (restartButton == null) {
|
||||
return;
|
||||
}
|
||||
restartButton.setOnClickListener(v -> restartSystemUi());
|
||||
}
|
||||
|
||||
private void updateVisualizerSwitchAvailability(SharedPreferences prefs,
|
||||
SwitchMaterial notificationSwitch,
|
||||
SwitchMaterial statusSwitch,
|
||||
SwitchMaterial clockSwitch,
|
||||
SwitchMaterial chipSwitch) {
|
||||
boolean layoutEnabled = prefs != null && prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_ENABLED,
|
||||
SbtDefaults.CLOCK_ENABLED_DEFAULT);
|
||||
if (notificationSwitch != null) {
|
||||
notificationSwitch.setEnabled(layoutEnabled);
|
||||
}
|
||||
if (statusSwitch != null) {
|
||||
statusSwitch.setEnabled(layoutEnabled);
|
||||
}
|
||||
if (clockSwitch != null) {
|
||||
clockSwitch.setEnabled(layoutEnabled);
|
||||
}
|
||||
if (chipSwitch != null) {
|
||||
chipSwitch.setEnabled(layoutEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void restartSystemUi() {
|
||||
Context appContext = requireContext().getApplicationContext();
|
||||
Toast.makeText(appContext, R.string.debug_restart_systemui_started, Toast.LENGTH_SHORT).show();
|
||||
new Thread(() -> {
|
||||
boolean success = false;
|
||||
try {
|
||||
Process process = new ProcessBuilder(
|
||||
"su",
|
||||
"-c",
|
||||
"killall com.android.systemui || pkill -f com.android.systemui")
|
||||
.redirectErrorStream(true)
|
||||
.start();
|
||||
success = process.waitFor() == 0;
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
if (!success && isAdded()) {
|
||||
requireActivity().runOnUiThread(() -> Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.debug_restart_systemui_failed,
|
||||
Toast.LENGTH_LONG).show());
|
||||
}
|
||||
}, "StatusBarTweak:SystemUIRestart").start();
|
||||
}
|
||||
|
||||
private void bindCycleSwitch(SwitchMaterial cycleSwitch) {
|
||||
if (cycleSwitch == null) {
|
||||
return;
|
||||
}
|
||||
Context context = requireContext();
|
||||
cycleSwitch.setChecked(DebugNotifications.isCycleEnabled(context));
|
||||
cycleSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
ensureNotificationPermission();
|
||||
}
|
||||
DebugNotifications.setCycleEnabled(context, isChecked);
|
||||
});
|
||||
}
|
||||
|
||||
private void adjustCount(int delta) {
|
||||
setCount(currentCount + delta);
|
||||
}
|
||||
|
||||
private void setCount(int value) {
|
||||
int clamped = DebugNotifications.clampCount(value);
|
||||
if (clamped == currentCount) {
|
||||
return;
|
||||
}
|
||||
currentCount = clamped;
|
||||
updateCountLabel();
|
||||
if (currentCount > 0) {
|
||||
ensureNotificationPermission();
|
||||
}
|
||||
DebugNotifications.setDesiredCount(requireContext(), currentCount);
|
||||
}
|
||||
|
||||
private void updateCountLabel() {
|
||||
if (countView != null) {
|
||||
countView.setText(String.valueOf(currentCount));
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNotificationPermission() {
|
||||
Context context = requireContext();
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
|
||||
== PackageManager.PERMISSION_GRANTED) {
|
||||
return;
|
||||
}
|
||||
requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATIONS);
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.debug_notification_permission_required,
|
||||
Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode,
|
||||
@NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (requestCode == REQUEST_POST_NOTIFICATIONS
|
||||
&& grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
DebugNotifications.applyPending(requireContext());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCameraUi(SharedPreferences prefs,
|
||||
TextView identifiedView,
|
||||
LinearLayout overrideRow,
|
||||
TextView overrideView,
|
||||
MaterialButton actionButton,
|
||||
View otherSection,
|
||||
LinearLayout otherList) {
|
||||
if (identifiedView == null) {
|
||||
return;
|
||||
}
|
||||
String cameraId = prefs != null
|
||||
? prefs.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_ID, "")
|
||||
: "";
|
||||
String autoType = prefs != null
|
||||
? prefs.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_AUTO_TYPE, "")
|
||||
: "";
|
||||
String effectiveTypeFromReport = prefs != null
|
||||
? prefs.getString(SbtSettings.KEY_DEBUG_CURRENT_CAMERA_EFFECTIVE_TYPE, "")
|
||||
: "";
|
||||
|
||||
Map<String, String> overrides = ClockCameraTypeSupport.parseOverrides(
|
||||
prefs != null
|
||||
? prefs.getStringSet(SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, java.util.Collections.emptySet())
|
||||
: java.util.Collections.emptySet());
|
||||
|
||||
if (TextUtils.isEmpty(cameraId)) {
|
||||
identifiedView.setText(getString(R.string.debug_camera_identified_as,
|
||||
getString(R.string.debug_camera_unknown)));
|
||||
if (overrideRow != null) {
|
||||
overrideRow.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
String currentOverride = overrides.get(cameraId);
|
||||
String currentAutoType = ClockCameraTypeSupport.normalizeType(autoType);
|
||||
String currentEffectiveType = !TextUtils.isEmpty(currentOverride)
|
||||
? currentOverride
|
||||
: (TextUtils.isEmpty(effectiveTypeFromReport)
|
||||
? currentAutoType
|
||||
: ClockCameraTypeSupport.normalizeType(effectiveTypeFromReport));
|
||||
identifiedView.setText(getString(
|
||||
R.string.debug_camera_identified_as,
|
||||
readableType(currentAutoType)));
|
||||
|
||||
if (overrideRow != null && overrideView != null && actionButton != null) {
|
||||
overrideRow.setVisibility(View.VISIBLE);
|
||||
if (TextUtils.isEmpty(currentOverride)) {
|
||||
overrideView.setText("");
|
||||
actionButton.setText(R.string.debug_camera_action_wrong);
|
||||
actionButton.setOnClickListener(v -> saveCameraOverride(
|
||||
prefs,
|
||||
cameraId,
|
||||
ClockCameraTypeSupport.oppositeType(currentAutoType)));
|
||||
} else {
|
||||
overrideView.setText(getString(
|
||||
R.string.debug_camera_overridden_as,
|
||||
readableType(currentEffectiveType)));
|
||||
actionButton.setText(R.string.debug_camera_action_undo);
|
||||
actionButton.setOnClickListener(v -> removeCameraOverride(prefs, cameraId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (otherSection == null || otherList == null) {
|
||||
return;
|
||||
}
|
||||
otherList.removeAllViews();
|
||||
ArrayList<String> keys = new ArrayList<>(overrides.keySet());
|
||||
java.util.Collections.sort(keys);
|
||||
int shown = 0;
|
||||
for (String key : keys) {
|
||||
if (!TextUtils.isEmpty(cameraId) && TextUtils.equals(key, cameraId)) {
|
||||
continue;
|
||||
}
|
||||
otherList.addView(createOtherCameraRow(key, overrides.get(key), prefs, shown > 0));
|
||||
shown++;
|
||||
}
|
||||
otherSection.setVisibility(shown > 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private View createOtherCameraRow(String cameraId,
|
||||
String overrideType,
|
||||
SharedPreferences prefs,
|
||||
boolean addTopMargin) {
|
||||
Context context = requireContext();
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
row.setGravity(android.view.Gravity.CENTER_VERTICAL);
|
||||
LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
if (addTopMargin) {
|
||||
rowParams.topMargin = dpToPx(context, 8);
|
||||
}
|
||||
row.setLayoutParams(rowParams);
|
||||
|
||||
TextView label = new TextView(context);
|
||||
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
labelParams.weight = 1f;
|
||||
label.setLayoutParams(labelParams);
|
||||
label.setText(getString(
|
||||
R.string.debug_camera_other_item,
|
||||
cameraId,
|
||||
readableType(overrideType)));
|
||||
otherListTextAppearance(label);
|
||||
row.addView(label);
|
||||
|
||||
MaterialButton undoButton = new MaterialButton(
|
||||
context,
|
||||
null,
|
||||
com.google.android.material.R.attr.materialButtonOutlinedStyle);
|
||||
undoButton.setMinWidth(0);
|
||||
undoButton.setText(R.string.debug_camera_action_undo);
|
||||
undoButton.setOnClickListener(v -> removeCameraOverride(prefs, cameraId));
|
||||
row.addView(undoButton);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void otherListTextAppearance(TextView view) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body2);
|
||||
}
|
||||
|
||||
private void saveCameraOverride(SharedPreferences prefs, String cameraId, String type) {
|
||||
if (prefs == null || TextUtils.isEmpty(cameraId)) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> overrides = new java.util.HashMap<>(
|
||||
ClockCameraTypeSupport.parseOverrides(
|
||||
prefs.getStringSet(SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, java.util.Collections.emptySet())));
|
||||
overrides.put(cameraId, ClockCameraTypeSupport.normalizeType(type));
|
||||
ClockCameraTypeSupport.saveOverrides(prefs, SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, overrides);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
|
||||
private void removeCameraOverride(SharedPreferences prefs, String cameraId) {
|
||||
if (prefs == null || TextUtils.isEmpty(cameraId)) {
|
||||
return;
|
||||
}
|
||||
Map<String, String> overrides = new java.util.HashMap<>(
|
||||
ClockCameraTypeSupport.parseOverrides(
|
||||
prefs.getStringSet(SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, java.util.Collections.emptySet())));
|
||||
overrides.remove(cameraId);
|
||||
ClockCameraTypeSupport.saveOverrides(prefs, SbtSettings.KEY_CLOCK_CAMERA_TYPE_OVERRIDES, overrides);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
private String readableType(String type) {
|
||||
return ClockCameraTypeSupport.formatType(type);
|
||||
}
|
||||
|
||||
private int dpToPx(Context context, int dp) {
|
||||
if (context == null) {
|
||||
return dp;
|
||||
}
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
return (int) (dp * density + 0.5f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,827 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.EditText;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class LayoutFragment extends Fragment {
|
||||
private static final int PADDING_MIN = -999;
|
||||
private static final int PADDING_MAX = 999;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_layout, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
SwitchMaterial enabledSwitch = root.findViewById(R.id.clock_enabled_switch);
|
||||
enabledSwitch.setChecked(prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_ENABLED,
|
||||
SbtDefaults.CLOCK_ENABLED_DEFAULT));
|
||||
enabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(SbtSettings.KEY_CLOCK_ENABLED, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
setupPositionSection(root,
|
||||
prefs,
|
||||
R.id.clock_position_group,
|
||||
R.id.clock_position_middle_options_container,
|
||||
R.id.clock_middle_auto_nearest,
|
||||
R.id.clock_middle_side_prompt,
|
||||
SbtSettings.KEY_CLOCK_POSITION,
|
||||
SbtDefaults.CLOCK_POSITION_DEFAULT,
|
||||
SbtSettings.KEY_CLOCK_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_CLOCK_MIDDLE_CUTOUT_SIDE);
|
||||
setupPositionSection(root,
|
||||
prefs,
|
||||
R.id.notif_position_group,
|
||||
R.id.notif_position_middle_options_container,
|
||||
R.id.notif_middle_auto_nearest,
|
||||
R.id.notif_middle_side_prompt,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_POSITION,
|
||||
SbtDefaults.LAYOUT_NOTIF_POSITION_DEFAULT,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_MIDDLE_SIDE);
|
||||
setupPositionSection(root,
|
||||
prefs,
|
||||
R.id.chip_position_group,
|
||||
R.id.chip_position_middle_options_container,
|
||||
R.id.chip_middle_auto_nearest,
|
||||
R.id.chip_middle_side_prompt,
|
||||
SbtSettings.KEY_LAYOUT_CHIP_POSITION,
|
||||
SbtDefaults.LAYOUT_CHIP_POSITION_DEFAULT,
|
||||
SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_CHIP_MIDDLE_SIDE);
|
||||
setupPositionSection(root,
|
||||
prefs,
|
||||
R.id.status_position_group,
|
||||
R.id.status_position_middle_options_container,
|
||||
R.id.status_middle_auto_nearest,
|
||||
R.id.status_middle_side_prompt,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_POSITION,
|
||||
SbtDefaults.LAYOUT_STATUS_POSITION_DEFAULT,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_AUTO_NEAREST_SIDE,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_MIDDLE_SIDE);
|
||||
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_PADDING_CUTOUT_PX,
|
||||
root.findViewById(R.id.layout_cutout_gap_input),
|
||||
root.findViewById(R.id.layout_cutout_gap_minus),
|
||||
root.findViewById(R.id.layout_cutout_gap_plus),
|
||||
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_PADDING_LEFT_PX,
|
||||
root.findViewById(R.id.layout_left_padding_input),
|
||||
root.findViewById(R.id.layout_left_padding_minus),
|
||||
root.findViewById(R.id.layout_left_padding_plus),
|
||||
SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_PADDING_RIGHT_PX,
|
||||
root.findViewById(R.id.layout_right_padding_input),
|
||||
root.findViewById(R.id.layout_right_padding_minus),
|
||||
root.findViewById(R.id.layout_right_padding_plus),
|
||||
SbtDefaults.LAYOUT_PADDING_RIGHT_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_ITEM_PADDING_PX,
|
||||
root.findViewById(R.id.layout_item_padding_input),
|
||||
root.findViewById(R.id.layout_item_padding_minus),
|
||||
root.findViewById(R.id.layout_item_padding_plus),
|
||||
SbtDefaults.LAYOUT_ITEM_PADDING_PX_DEFAULT,
|
||||
SbtDefaults.LAYOUT_ITEM_PADDING_PX_MIN,
|
||||
SbtDefaults.LAYOUT_ITEM_PADDING_PX_MAX);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_CLOCK_VERTICAL_OFFSET_PX,
|
||||
root.findViewById(R.id.clock_vertical_offset_input),
|
||||
root.findViewById(R.id.clock_vertical_offset_minus_fast),
|
||||
root.findViewById(R.id.clock_vertical_offset_minus),
|
||||
root.findViewById(R.id.clock_vertical_offset_plus),
|
||||
root.findViewById(R.id.clock_vertical_offset_plus_fast),
|
||||
root.findViewById(R.id.clock_vertical_offset_reset),
|
||||
SbtDefaults.CLOCK_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_VERTICAL_OFFSET_PX,
|
||||
root.findViewById(R.id.notif_vertical_offset_input),
|
||||
root.findViewById(R.id.notif_vertical_offset_minus_fast),
|
||||
root.findViewById(R.id.notif_vertical_offset_minus),
|
||||
root.findViewById(R.id.notif_vertical_offset_plus),
|
||||
root.findViewById(R.id.notif_vertical_offset_plus_fast),
|
||||
root.findViewById(R.id.notif_vertical_offset_reset),
|
||||
SbtDefaults.LAYOUT_NOTIF_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_CHIP_VERTICAL_OFFSET_PX,
|
||||
root.findViewById(R.id.chip_vertical_offset_input),
|
||||
root.findViewById(R.id.chip_vertical_offset_minus_fast),
|
||||
root.findViewById(R.id.chip_vertical_offset_minus),
|
||||
root.findViewById(R.id.chip_vertical_offset_plus),
|
||||
root.findViewById(R.id.chip_vertical_offset_plus_fast),
|
||||
root.findViewById(R.id.chip_vertical_offset_reset),
|
||||
SbtDefaults.LAYOUT_CHIP_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
bindStepper(prefs,
|
||||
SbtSettings.KEY_LAYOUT_STATUS_VERTICAL_OFFSET_PX,
|
||||
root.findViewById(R.id.status_vertical_offset_input),
|
||||
root.findViewById(R.id.status_vertical_offset_minus_fast),
|
||||
root.findViewById(R.id.status_vertical_offset_minus),
|
||||
root.findViewById(R.id.status_vertical_offset_plus),
|
||||
root.findViewById(R.id.status_vertical_offset_plus_fast),
|
||||
root.findViewById(R.id.status_vertical_offset_reset),
|
||||
SbtDefaults.LAYOUT_STATUS_VERTICAL_OFFSET_PX_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.layout_left_padding_add_stock),
|
||||
SbtSettings.KEY_LAYOUT_PADDING_LEFT_ADD_STOCK,
|
||||
SbtDefaults.LAYOUT_PADDING_LEFT_ADD_STOCK_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.layout_right_padding_add_stock),
|
||||
SbtSettings.KEY_LAYOUT_PADDING_RIGHT_ADD_STOCK,
|
||||
SbtDefaults.LAYOUT_PADDING_RIGHT_ADD_STOCK_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.layout_ignore_under_display_cameras),
|
||||
SbtSettings.KEY_LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS,
|
||||
SbtDefaults.LAYOUT_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT);
|
||||
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.clock_position_mode_lockscreen),
|
||||
SbtSettings.KEY_CLOCK_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.CLOCK_ENABLED_LOCKSCREEN_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.clock_position_mode_unlocked),
|
||||
SbtSettings.KEY_CLOCK_ENABLED_UNLOCKED,
|
||||
SbtDefaults.CLOCK_ENABLED_UNLOCKED_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_position_mode_aod),
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_AOD,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_AOD_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_position_mode_lockscreen),
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_LOCKSCREEN_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_position_mode_unlocked),
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_NOTIF_ENABLED_UNLOCKED_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.chip_position_mode_lockscreen),
|
||||
SbtSettings.KEY_LAYOUT_CHIP_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_CHIP_ENABLED_LOCKSCREEN_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.chip_position_mode_unlocked),
|
||||
SbtSettings.KEY_LAYOUT_CHIP_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_CHIP_ENABLED_UNLOCKED_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.status_position_mode_aod),
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_AOD,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_AOD_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.status_position_mode_lockscreen),
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_LOCKSCREEN,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_LOCKSCREEN_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.status_position_mode_unlocked),
|
||||
SbtSettings.KEY_LAYOUT_STATUS_ENABLED_UNLOCKED,
|
||||
SbtDefaults.LAYOUT_STATUS_ENABLED_UNLOCKED_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.notif_show_dot_if_truncated),
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED,
|
||||
SbtDefaults.LAYOUT_NOTIF_SHOW_DOT_IF_TRUNCATED_DEFAULT);
|
||||
bindCheckBoxPref(prefs, root.findViewById(R.id.status_show_dot_if_truncated),
|
||||
SbtSettings.KEY_LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED,
|
||||
SbtDefaults.LAYOUT_STATUS_SHOW_DOT_IF_TRUNCATED_DEFAULT);
|
||||
|
||||
bindSortOrderGroup(prefs, root.findViewById(R.id.layout_sort_icons_group));
|
||||
|
||||
setupOrderingList(root, prefs);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void setupPositionSection(View root,
|
||||
SharedPreferences prefs,
|
||||
int groupId,
|
||||
int middleOptionsId,
|
||||
int autoNearestId,
|
||||
int promptId,
|
||||
@Nullable String positionKey,
|
||||
@Nullable String positionDefault,
|
||||
@Nullable String autoNearestKey,
|
||||
@Nullable String middleSideKey) {
|
||||
RadioGroup group = root.findViewById(groupId);
|
||||
View middleOptions = root.findViewById(middleOptionsId);
|
||||
CheckBox autoNearest = root.findViewById(autoNearestId);
|
||||
TextView prompt = root.findViewById(promptId);
|
||||
if (group == null || middleOptions == null || autoNearest == null || prompt == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (positionKey != null && positionDefault != null) {
|
||||
group.check(positionIdForValue(groupId, prefs.getString(positionKey, positionDefault)));
|
||||
}
|
||||
if (autoNearestKey != null) {
|
||||
autoNearest.setChecked(prefs.getBoolean(autoNearestKey, true));
|
||||
}
|
||||
updateMiddleOptions(middleOptions, autoNearest, prompt, group.getCheckedRadioButtonId());
|
||||
|
||||
group.setOnCheckedChangeListener((radioGroup, id) -> {
|
||||
updateMiddleOptions(middleOptions, autoNearest, prompt, id);
|
||||
if (positionKey != null) {
|
||||
prefs.edit().putString(positionKey, positionValueForId(id)).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
|
||||
autoNearest.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
updateMiddlePrompt(prompt, isChecked);
|
||||
if (autoNearestKey != null) {
|
||||
prefs.edit().putBoolean(autoNearestKey, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
|
||||
if (middleSideKey != null) {
|
||||
RadioGroup sideGroup = resolveMiddleSideGroup(root, groupId);
|
||||
if (sideGroup != null) {
|
||||
sideGroup.check(sideIdForValue(sideGroup, prefs.getString(middleSideKey, "left")));
|
||||
sideGroup.setOnCheckedChangeListener((g, checkedId) -> {
|
||||
prefs.edit().putString(middleSideKey, checkedId == resolveRightSideId(g) ? "right" : "left").apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMiddleOptions(View middleOptions,
|
||||
CheckBox autoNearest,
|
||||
TextView prompt,
|
||||
int checkedId) {
|
||||
if (middleOptions == null || autoNearest == null || prompt == null) {
|
||||
return;
|
||||
}
|
||||
if (middleOptions.getVisibility() != View.VISIBLE) {
|
||||
middleOptions.setVisibility(View.VISIBLE);
|
||||
}
|
||||
boolean enabled = isMiddleSelection(checkedId);
|
||||
setViewTreeEnabled(middleOptions, enabled);
|
||||
middleOptions.setAlpha(enabled ? 1f : 0.45f);
|
||||
updateMiddlePrompt(prompt, autoNearest.isChecked());
|
||||
}
|
||||
|
||||
private boolean isMiddleSelection(int checkedId) {
|
||||
return checkedId == R.id.clock_position_middle
|
||||
|| checkedId == R.id.notif_position_middle
|
||||
|| checkedId == R.id.chip_position_middle
|
||||
|| checkedId == R.id.status_position_middle;
|
||||
}
|
||||
|
||||
private void updateMiddlePrompt(TextView prompt, boolean autoNearest) {
|
||||
if (prompt == null) {
|
||||
return;
|
||||
}
|
||||
prompt.setText(autoNearest
|
||||
? R.string.layout_middle_side_prompt_centred
|
||||
: R.string.layout_middle_side_prompt_attach_only);
|
||||
}
|
||||
|
||||
private void bindSortOrderGroup(SharedPreferences prefs, @Nullable RadioGroup group) {
|
||||
if (prefs == null || group == null) {
|
||||
return;
|
||||
}
|
||||
String current = prefs.getString(
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER,
|
||||
SbtDefaults.LAYOUT_NOTIF_SORT_ORDER_DEFAULT);
|
||||
group.check(sortOrderIdForValue(current));
|
||||
group.setOnCheckedChangeListener((radioGroup, checkedId) -> {
|
||||
prefs.edit().putString(
|
||||
SbtSettings.KEY_LAYOUT_NOTIF_SORT_ORDER,
|
||||
sortOrderValueForId(checkedId)).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void bindStepper(SharedPreferences prefs,
|
||||
@Nullable String key,
|
||||
@Nullable EditText input,
|
||||
@Nullable MaterialButton minusButton,
|
||||
@Nullable MaterialButton plusButton,
|
||||
int initialValue) {
|
||||
bindStepper(prefs, key, input, null, minusButton, plusButton, null, null, initialValue);
|
||||
}
|
||||
|
||||
private void bindStepper(SharedPreferences prefs,
|
||||
@Nullable String key,
|
||||
@Nullable EditText input,
|
||||
@Nullable MaterialButton minusButton,
|
||||
@Nullable MaterialButton plusButton,
|
||||
int initialValue,
|
||||
int minValue,
|
||||
int maxValue) {
|
||||
bindStepper(
|
||||
prefs,
|
||||
key,
|
||||
input,
|
||||
null,
|
||||
minusButton,
|
||||
plusButton,
|
||||
null,
|
||||
null,
|
||||
initialValue,
|
||||
minValue,
|
||||
maxValue);
|
||||
}
|
||||
|
||||
private void bindStepper(SharedPreferences prefs,
|
||||
@Nullable String key,
|
||||
@Nullable EditText input,
|
||||
@Nullable MaterialButton minusFastButton,
|
||||
@Nullable MaterialButton minusButton,
|
||||
@Nullable MaterialButton plusButton,
|
||||
@Nullable MaterialButton plusFastButton,
|
||||
@Nullable MaterialButton resetButton,
|
||||
int initialValue) {
|
||||
bindStepper(
|
||||
prefs,
|
||||
key,
|
||||
input,
|
||||
minusFastButton,
|
||||
minusButton,
|
||||
plusButton,
|
||||
plusFastButton,
|
||||
resetButton,
|
||||
initialValue,
|
||||
PADDING_MIN,
|
||||
PADDING_MAX);
|
||||
}
|
||||
|
||||
private void bindStepper(SharedPreferences prefs,
|
||||
@Nullable String key,
|
||||
@Nullable EditText input,
|
||||
@Nullable MaterialButton minusFastButton,
|
||||
@Nullable MaterialButton minusButton,
|
||||
@Nullable MaterialButton plusButton,
|
||||
@Nullable MaterialButton plusFastButton,
|
||||
@Nullable MaterialButton resetButton,
|
||||
int initialValue,
|
||||
int minValue,
|
||||
int maxValue) {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
int startValue = clampValue(key != null ? prefs.getInt(key, initialValue) : initialValue, minValue, maxValue);
|
||||
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(4)});
|
||||
setNumericText(input, startValue);
|
||||
final boolean[] updating = new boolean[]{false};
|
||||
|
||||
bindStepperButton(prefs, key, input, updating, minusFastButton, -5, minValue, maxValue);
|
||||
bindStepperButton(prefs, key, input, updating, minusButton, -1, minValue, maxValue);
|
||||
bindStepperButton(prefs, key, input, updating, plusButton, 1, minValue, maxValue);
|
||||
bindStepperButton(prefs, key, input, updating, plusFastButton, 5, minValue, maxValue);
|
||||
if (resetButton != null) {
|
||||
resetButton.setOnClickListener(v -> {
|
||||
updating[0] = true;
|
||||
int next = clampValue(initialValue, minValue, maxValue);
|
||||
setNumericText(input, next);
|
||||
updating[0] = false;
|
||||
persistIntPref(prefs, key, next);
|
||||
});
|
||||
}
|
||||
|
||||
input.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) {
|
||||
if (updating[0]) {
|
||||
return;
|
||||
}
|
||||
String text = s != null ? s.toString().trim() : "";
|
||||
if (text.isEmpty() || "-".equals(text)) {
|
||||
persistIntPref(prefs, key, clampValue(0, minValue, maxValue));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
int parsed = clampValue(Integer.parseInt(text), minValue, maxValue);
|
||||
if (parsed != Integer.parseInt(text)) {
|
||||
updating[0] = true;
|
||||
setNumericText(input, parsed);
|
||||
updating[0] = false;
|
||||
}
|
||||
persistIntPref(prefs, key, parsed);
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
input.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
int next = clampValue(parseInt(input, 0), minValue, maxValue);
|
||||
updating[0] = true;
|
||||
setNumericText(input, next);
|
||||
updating[0] = false;
|
||||
persistIntPref(prefs, key, next);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
input.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
return;
|
||||
}
|
||||
int next = clampValue(parseInt(input, 0), minValue, maxValue);
|
||||
updating[0] = true;
|
||||
setNumericText(input, next);
|
||||
updating[0] = false;
|
||||
persistIntPref(prefs, key, next);
|
||||
});
|
||||
}
|
||||
|
||||
private void bindStepperButton(SharedPreferences prefs,
|
||||
@Nullable String key,
|
||||
EditText input,
|
||||
boolean[] updating,
|
||||
@Nullable MaterialButton button,
|
||||
int delta,
|
||||
int minValue,
|
||||
int maxValue) {
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
button.setOnClickListener(v -> {
|
||||
int next = clampValue(parseInt(input, 0) + delta, minValue, maxValue);
|
||||
updating[0] = true;
|
||||
setNumericText(input, next);
|
||||
updating[0] = false;
|
||||
persistIntPref(prefs, key, next);
|
||||
});
|
||||
}
|
||||
|
||||
private void bindCheckBoxPref(SharedPreferences prefs,
|
||||
@Nullable CheckBox checkBox,
|
||||
String key,
|
||||
boolean defaultValue) {
|
||||
if (checkBox == null) {
|
||||
return;
|
||||
}
|
||||
checkBox.setChecked(prefs.getBoolean(key, defaultValue));
|
||||
checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(key, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private void persistIntPref(SharedPreferences prefs, @Nullable String key, int value) {
|
||||
if (key == null) {
|
||||
return;
|
||||
}
|
||||
prefs.edit().putInt(key, value).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
|
||||
private void setViewTreeEnabled(View view, boolean enabled) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setEnabled(enabled);
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewTreeEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int positionIdForValue(int groupId, @Nullable String value) {
|
||||
String normalized = value != null ? value : "left";
|
||||
if (groupId == R.id.clock_position_group) {
|
||||
if ("right".equals(normalized)) {
|
||||
return R.id.clock_position_right;
|
||||
}
|
||||
if ("middle".equals(normalized)) {
|
||||
return R.id.clock_position_middle;
|
||||
}
|
||||
return R.id.clock_position_left;
|
||||
}
|
||||
if (groupId == R.id.notif_position_group) {
|
||||
if ("right".equals(normalized)) {
|
||||
return R.id.notif_position_right;
|
||||
}
|
||||
if ("middle".equals(normalized)) {
|
||||
return R.id.notif_position_middle;
|
||||
}
|
||||
return R.id.notif_position_left;
|
||||
}
|
||||
if (groupId == R.id.chip_position_group) {
|
||||
if ("right".equals(normalized)) {
|
||||
return R.id.chip_position_right;
|
||||
}
|
||||
if ("middle".equals(normalized)) {
|
||||
return R.id.chip_position_middle;
|
||||
}
|
||||
return R.id.chip_position_left;
|
||||
}
|
||||
if ("right".equals(normalized)) {
|
||||
return R.id.status_position_right;
|
||||
}
|
||||
if ("middle".equals(normalized)) {
|
||||
return R.id.status_position_middle;
|
||||
}
|
||||
return R.id.status_position_left;
|
||||
}
|
||||
|
||||
private String positionValueForId(int checkedId) {
|
||||
if (checkedId == R.id.clock_position_middle
|
||||
|| checkedId == R.id.notif_position_middle
|
||||
|| checkedId == R.id.chip_position_middle
|
||||
|| checkedId == R.id.status_position_middle) {
|
||||
return "middle";
|
||||
}
|
||||
if (checkedId == R.id.clock_position_right
|
||||
|| checkedId == R.id.notif_position_right
|
||||
|| checkedId == R.id.chip_position_right
|
||||
|| checkedId == R.id.status_position_right) {
|
||||
return "right";
|
||||
}
|
||||
return "left";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RadioGroup resolveMiddleSideGroup(View root, int groupId) {
|
||||
if (groupId == R.id.clock_position_group) {
|
||||
return root.findViewById(R.id.clock_middle_side_group);
|
||||
}
|
||||
if (groupId == R.id.notif_position_group) {
|
||||
return root.findViewById(R.id.notif_middle_side_group);
|
||||
}
|
||||
if (groupId == R.id.chip_position_group) {
|
||||
return root.findViewById(R.id.chip_middle_side_group);
|
||||
}
|
||||
return root.findViewById(R.id.status_middle_side_group);
|
||||
}
|
||||
|
||||
private int sideIdForValue(RadioGroup group, @Nullable String value) {
|
||||
return "right".equals(value) ? resolveRightSideId(group) : resolveLeftSideId(group);
|
||||
}
|
||||
|
||||
private int resolveLeftSideId(RadioGroup group) {
|
||||
int id = group.getChildCount() > 0 ? group.getChildAt(0).getId() : View.NO_ID;
|
||||
return id != View.NO_ID ? id : 0;
|
||||
}
|
||||
|
||||
private int resolveRightSideId(RadioGroup group) {
|
||||
int id = group.getChildCount() > 1 ? group.getChildAt(1).getId() : View.NO_ID;
|
||||
return id != View.NO_ID ? id : resolveLeftSideId(group);
|
||||
}
|
||||
|
||||
private int parseInt(EditText input, int fallback) {
|
||||
if (input == null) {
|
||||
return fallback;
|
||||
}
|
||||
String text = input.getText() != null ? input.getText().toString().trim() : "";
|
||||
if (text.isEmpty() || "-".equals(text)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(text);
|
||||
} catch (NumberFormatException ignored) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
private int clampPadding(int value) {
|
||||
return clampValue(value, PADDING_MIN, PADDING_MAX);
|
||||
}
|
||||
|
||||
private int clampValue(int value, int minValue, int maxValue) {
|
||||
if (value < minValue) {
|
||||
return minValue;
|
||||
}
|
||||
if (value > maxValue) {
|
||||
return maxValue;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void setNumericText(EditText input, int value) {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
String text = String.valueOf(value);
|
||||
if (!text.contentEquals(input.getText())) {
|
||||
input.setText(text);
|
||||
input.setSelection(text.length());
|
||||
}
|
||||
}
|
||||
|
||||
private int sortOrderIdForValue(@Nullable String value) {
|
||||
if ("stock".equals(value)) {
|
||||
return R.id.layout_sort_icons_stock;
|
||||
}
|
||||
if ("reversed".equals(value)) {
|
||||
return R.id.layout_sort_icons_reversed;
|
||||
}
|
||||
return R.id.layout_sort_icons_automatic;
|
||||
}
|
||||
|
||||
private String sortOrderValueForId(int checkedId) {
|
||||
if (checkedId == R.id.layout_sort_icons_stock) {
|
||||
return "stock";
|
||||
}
|
||||
if (checkedId == R.id.layout_sort_icons_reversed) {
|
||||
return "reversed";
|
||||
}
|
||||
return "automatic";
|
||||
}
|
||||
|
||||
private void setupOrderingList(View root, SharedPreferences prefs) {
|
||||
RecyclerView list = root.findViewById(R.id.layout_ordering_list);
|
||||
if (list == null) {
|
||||
return;
|
||||
}
|
||||
ArrayList<OrderingItem> items = parseOrderingItems(
|
||||
prefs.getString(SbtSettings.KEY_LAYOUT_ITEM_ORDER, SbtDefaults.LAYOUT_ITEM_ORDER_DEFAULT));
|
||||
|
||||
list.setLayoutManager(new LinearLayoutManager(requireContext()));
|
||||
OrderingAdapter adapter = new OrderingAdapter(items, order -> {
|
||||
prefs.edit().putString(SbtSettings.KEY_LAYOUT_ITEM_ORDER, order).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
list.setAdapter(adapter);
|
||||
|
||||
final ItemTouchHelper[] itemTouchHelperHolder = new ItemTouchHelper[1];
|
||||
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback(
|
||||
ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0) {
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView,
|
||||
@NonNull RecyclerView.ViewHolder viewHolder,
|
||||
@NonNull RecyclerView.ViewHolder target) {
|
||||
int from = viewHolder.getAdapterPosition();
|
||||
int to = target.getAdapterPosition();
|
||||
if (from == RecyclerView.NO_POSITION || to == RecyclerView.NO_POSITION) {
|
||||
return false;
|
||||
}
|
||||
adapter.moveItem(from, to);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongPressDragEnabled() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
itemTouchHelperHolder[0] = itemTouchHelper;
|
||||
adapter.setDragStarter(holder -> {
|
||||
ItemTouchHelper helper = itemTouchHelperHolder[0];
|
||||
if (helper != null) {
|
||||
helper.startDrag(holder);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ArrayList<OrderingItem> parseOrderingItems(@Nullable String encoded) {
|
||||
if (encoded == null || encoded.isEmpty() || "clock,notifications,status".equals(encoded)) {
|
||||
encoded = SbtDefaults.LAYOUT_ITEM_ORDER_DEFAULT;
|
||||
}
|
||||
ArrayList<OrderingItem> items = new ArrayList<>();
|
||||
for (String token : encoded.split(",")) {
|
||||
addOrderingItemIfValid(items, token != null ? token.trim() : "");
|
||||
}
|
||||
addOrderingItemIfValid(items, "clock");
|
||||
addOrderingItemIfValid(items, "chip");
|
||||
addOrderingItemIfValid(items, "status");
|
||||
addOrderingItemIfValid(items, "notifications");
|
||||
return items;
|
||||
}
|
||||
|
||||
private void addOrderingItemIfValid(ArrayList<OrderingItem> items, String key) {
|
||||
if (key == null || key.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (OrderingItem item : items) {
|
||||
if (item.key.equals(key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
int labelRes;
|
||||
if ("clock".equals(key)) {
|
||||
labelRes = R.string.layout_ordering_clock;
|
||||
} else if ("chip".equals(key)) {
|
||||
labelRes = R.string.layout_ordering_chip;
|
||||
} else if ("notifications".equals(key)) {
|
||||
labelRes = R.string.layout_ordering_notif_icons;
|
||||
} else if ("status".equals(key)) {
|
||||
labelRes = R.string.layout_ordering_status_icons;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
items.add(new OrderingItem(key, getString(labelRes)));
|
||||
}
|
||||
|
||||
private record OrderingItem(String key, String label) {
|
||||
}
|
||||
|
||||
private static final class OrderingAdapter extends RecyclerView.Adapter<OrderingViewHolder> {
|
||||
interface DragStarter {
|
||||
void startDrag(OrderingViewHolder holder);
|
||||
}
|
||||
|
||||
interface OrderChangedListener {
|
||||
void onOrderChanged(String encodedOrder);
|
||||
}
|
||||
|
||||
private final List<OrderingItem> items;
|
||||
private final OrderChangedListener orderChangedListener;
|
||||
private DragStarter dragStarter;
|
||||
|
||||
OrderingAdapter(List<OrderingItem> items, OrderChangedListener orderChangedListener) {
|
||||
this.items = items;
|
||||
this.orderChangedListener = orderChangedListener;
|
||||
}
|
||||
|
||||
void setDragStarter(DragStarter dragStarter) {
|
||||
this.dragStarter = dragStarter;
|
||||
}
|
||||
|
||||
void moveItem(int from, int to) {
|
||||
if (from == to || from < 0 || to < 0 || from >= items.size() || to >= items.size()) {
|
||||
return;
|
||||
}
|
||||
Collections.swap(items, from, to);
|
||||
notifyItemMoved(from, to);
|
||||
if (orderChangedListener != null) {
|
||||
orderChangedListener.onOrderChanged(encodeOrder());
|
||||
}
|
||||
}
|
||||
|
||||
private String encodeOrder() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (OrderingItem item : items) {
|
||||
if (builder.length() > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(item.key);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public OrderingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_layout_ordering, parent, false);
|
||||
return new OrderingViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull OrderingViewHolder holder, int position) {
|
||||
holder.label.setText(items.get(position).label);
|
||||
holder.handle.setOnTouchListener((v, event) -> {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && dragStarter != null) {
|
||||
dragStarter.startDrag(holder);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return items.size();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OrderingViewHolder extends RecyclerView.ViewHolder {
|
||||
final TextView label;
|
||||
final TextView handle;
|
||||
|
||||
OrderingViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
label = itemView.findViewById(R.id.layout_ordering_item_label);
|
||||
handle = itemView.findViewById(R.id.layout_ordering_item_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
public class MainActivity extends AppCompatActivity implements NavigationView.OnNavigationItemSelectedListener {
|
||||
private static final String EXTRA_OPEN_NAV_ITEM = "open_nav_item";
|
||||
|
||||
private DrawerLayout drawerLayout;
|
||||
private NavigationView navigationView;
|
||||
|
||||
public static Intent newOpenDebugIntent(Context context) {
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
intent.putExtra(EXTRA_OPEN_NAV_ITEM, R.id.nav_icons_debug);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
SbtSettings.restoreCredentialProtectedPreferences(this);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
MaterialToolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
drawerLayout = findViewById(R.id.drawer_layout);
|
||||
navigationView = findViewById(R.id.nav_view);
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
navigationView.getMenu().setGroupCheckable(0, true, true);
|
||||
|
||||
ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(
|
||||
this,
|
||||
drawerLayout,
|
||||
toolbar,
|
||||
R.string.nav_open,
|
||||
R.string.nav_close
|
||||
);
|
||||
drawerLayout.addDrawerListener(toggle);
|
||||
toggle.syncState();
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
int initialItemId = resolveInitialItemId(getIntent());
|
||||
navigateTo(initialItemId);
|
||||
consumeOpenNavItemExtra(getIntent());
|
||||
} else {
|
||||
handleOpenNavIntent(getIntent());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
handleOpenNavIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onNavigationItemSelected(@NonNull android.view.MenuItem item) {
|
||||
navigateTo(item.getItemId());
|
||||
drawerLayout.closeDrawer(GravityCompat.START);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void navigateTo(int itemId) {
|
||||
syncDrawerSelection(itemId);
|
||||
Fragment fragment;
|
||||
if (itemId == R.id.nav_system_icons) {
|
||||
fragment = new SystemIconsFragment();
|
||||
} else if (itemId == R.id.nav_layout) {
|
||||
fragment = new LayoutFragment();
|
||||
} else if (itemId == R.id.nav_battery_bar) {
|
||||
fragment = new BatteryBarFragment();
|
||||
} else if (itemId == R.id.nav_clock) {
|
||||
fragment = new ClockFragment();
|
||||
} else if (itemId == R.id.nav_notification_icons) {
|
||||
fragment = new NotificationIconsFragment();
|
||||
} else if (itemId == R.id.nav_block_app_notification_icons) {
|
||||
fragment = new BlockAppNotificationIconsFragment();
|
||||
} else if (itemId == R.id.nav_status_chips) {
|
||||
fragment = new StatusChipsFragment();
|
||||
} else if (itemId == R.id.nav_misc) {
|
||||
fragment = new MiscFragment();
|
||||
} else {
|
||||
fragment = new IconsDebugFragment();
|
||||
}
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.content_frame, fragment)
|
||||
.commit();
|
||||
setTitle(resolveTitle(itemId));
|
||||
}
|
||||
|
||||
private int resolveTitle(int itemId) {
|
||||
if (itemId == R.id.nav_system_icons) {
|
||||
return R.string.nav_system_icons;
|
||||
} else if (itemId == R.id.nav_layout) {
|
||||
return R.string.nav_layout;
|
||||
} else if (itemId == R.id.nav_notification_icons) {
|
||||
return R.string.nav_notification_icons;
|
||||
} else if (itemId == R.id.nav_block_app_notification_icons) {
|
||||
return R.string.hidden_apps_page_title;
|
||||
} else if (itemId == R.id.nav_clock) {
|
||||
return R.string.nav_clock;
|
||||
} else if (itemId == R.id.nav_battery_bar) {
|
||||
return R.string.nav_battery_bar;
|
||||
} else if (itemId == R.id.nav_status_chips) {
|
||||
return R.string.nav_status_chips;
|
||||
} else if (itemId == R.id.nav_misc) {
|
||||
return R.string.nav_misc;
|
||||
}
|
||||
return R.string.nav_icons_debug;
|
||||
}
|
||||
|
||||
private void handleOpenNavIntent(Intent intent) {
|
||||
int itemId = resolveOpenItemId(intent);
|
||||
if (itemId == -1) {
|
||||
return;
|
||||
}
|
||||
navigateTo(itemId);
|
||||
consumeOpenNavItemExtra(intent);
|
||||
}
|
||||
|
||||
private void syncDrawerSelection(int itemId) {
|
||||
if (navigationView == null) {
|
||||
return;
|
||||
}
|
||||
Menu menu = navigationView.getMenu();
|
||||
if (menu == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < menu.size(); i++) {
|
||||
MenuItem item = menu.getItem(i);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
item.setCheckable(true);
|
||||
item.setChecked(item.getItemId() == itemId);
|
||||
}
|
||||
navigationView.invalidate();
|
||||
}
|
||||
|
||||
private int resolveInitialItemId(Intent intent) {
|
||||
int requested = resolveOpenItemId(intent);
|
||||
return requested != -1 ? requested : R.id.nav_layout;
|
||||
}
|
||||
|
||||
private int resolveOpenItemId(Intent intent) {
|
||||
if (intent == null || !intent.hasExtra(EXTRA_OPEN_NAV_ITEM)) {
|
||||
return -1;
|
||||
}
|
||||
return intent.getIntExtra(EXTRA_OPEN_NAV_ITEM, -1);
|
||||
}
|
||||
|
||||
private void consumeOpenNavItemExtra(Intent intent) {
|
||||
if (intent != null) {
|
||||
intent.removeExtra(EXTRA_OPEN_NAV_ITEM);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
|
||||
public class MiscFragment extends Fragment {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_misc, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
|
||||
bindSwitch(root, prefs,
|
||||
R.id.misc_aod_keep_visible_during_call_switch,
|
||||
SbtSettings.KEY_AOD_KEEP_VISIBLE_DURING_CALL,
|
||||
SbtDefaults.AOD_KEEP_VISIBLE_DURING_CALL_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.misc_battery_lockscreen_unplugged_percent_switch,
|
||||
SbtSettings.KEY_BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT,
|
||||
SbtDefaults.BATTERY_LOCKSCREEN_UNPLUGGED_PERCENT_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.misc_battery_aod_unplugged_percent_switch,
|
||||
SbtSettings.KEY_BATTERY_AOD_UNPLUGGED_PERCENT,
|
||||
SbtDefaults.BATTERY_AOD_UNPLUGGED_PERCENT_DEFAULT);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void bindSwitch(View root,
|
||||
SharedPreferences prefs,
|
||||
int switchId,
|
||||
String key,
|
||||
boolean defaultValue) {
|
||||
SwitchMaterial toggle = root.findViewById(switchId);
|
||||
toggle.setChecked(prefs.getBoolean(key, defaultValue));
|
||||
toggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(key, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,489 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import java.util.Arrays;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.slider.Slider;
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
|
||||
public class NotificationIconsFragment extends Fragment {
|
||||
private static final int INF_VALUE = Integer.MAX_VALUE;
|
||||
private static final String INF_LABEL = "\u221e";
|
||||
private static final int SLIDER_STEPS = 200;
|
||||
private static final int INF_GAP_STEPS = 20;
|
||||
private static final double SLIDER_EXPONENT = 2.0;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_notification_icons, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.unlocked_max_icons_slider,
|
||||
R.id.unlocked_max_icons_input,
|
||||
R.id.unlocked_max_icons_minus,
|
||||
R.id.unlocked_max_icons_plus,
|
||||
SbtSettings.KEY_UNLOCKED_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MIN,
|
||||
SbtDefaults.UNLOCKED_MAX_ICONS_PER_ROW_MAX,
|
||||
true);
|
||||
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.icons_aod_max_icons_slider,
|
||||
R.id.icons_aod_max_icons_input,
|
||||
R.id.icons_aod_max_icons_minus,
|
||||
R.id.icons_aod_max_icons_plus,
|
||||
SbtSettings.KEY_ICONS_AOD_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_MIN,
|
||||
SbtDefaults.ICONS_AOD_MAX_ICONS_PER_ROW_MAX,
|
||||
true);
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.icons_aod_max_rows_slider,
|
||||
R.id.icons_aod_max_rows_input,
|
||||
R.id.icons_aod_max_rows_minus,
|
||||
R.id.icons_aod_max_rows_plus,
|
||||
SbtSettings.KEY_ICONS_AOD_MAX_ROWS,
|
||||
SbtDefaults.ICONS_AOD_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.ICONS_AOD_MAX_ROWS_MIN,
|
||||
SbtDefaults.ICONS_AOD_MAX_ROWS_MAX,
|
||||
true);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.icons_aod_even_switch,
|
||||
SbtSettings.KEY_ICONS_AOD_EVEN_DISTRIBUTION,
|
||||
SbtDefaults.ICONS_AOD_EVEN_DISTRIBUTION_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.icons_aod_cutout_switch,
|
||||
SbtSettings.KEY_ICONS_AOD_CUTOUT_LIMIT,
|
||||
SbtDefaults.ICONS_AOD_CUTOUT_LIMIT_DEFAULT);
|
||||
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.icons_lock_max_icons_slider,
|
||||
R.id.icons_lock_max_icons_input,
|
||||
R.id.icons_lock_max_icons_minus,
|
||||
R.id.icons_lock_max_icons_plus,
|
||||
SbtSettings.KEY_ICONS_LOCK_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_MIN,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ICONS_PER_ROW_MAX,
|
||||
true);
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.icons_lock_max_rows_slider,
|
||||
R.id.icons_lock_max_rows_input,
|
||||
R.id.icons_lock_max_rows_minus,
|
||||
R.id.icons_lock_max_rows_plus,
|
||||
SbtSettings.KEY_ICONS_LOCK_MAX_ROWS,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ROWS_MIN,
|
||||
SbtDefaults.ICONS_LOCK_MAX_ROWS_MAX,
|
||||
true);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.icons_lock_even_switch,
|
||||
SbtSettings.KEY_ICONS_LOCK_EVEN_DISTRIBUTION,
|
||||
SbtDefaults.ICONS_LOCK_EVEN_DISTRIBUTION_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.icons_lock_cutout_switch,
|
||||
SbtSettings.KEY_ICONS_LOCK_CUTOUT_LIMIT,
|
||||
SbtDefaults.ICONS_LOCK_CUTOUT_LIMIT_DEFAULT);
|
||||
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.cards_aod_max_icons_slider,
|
||||
R.id.cards_aod_max_icons_input,
|
||||
R.id.cards_aod_max_icons_minus,
|
||||
R.id.cards_aod_max_icons_plus,
|
||||
SbtSettings.KEY_CARDS_AOD_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MIN,
|
||||
SbtDefaults.CARDS_AOD_MAX_ICONS_PER_ROW_MAX,
|
||||
true);
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.cards_aod_max_rows_slider,
|
||||
R.id.cards_aod_max_rows_input,
|
||||
R.id.cards_aod_max_rows_minus,
|
||||
R.id.cards_aod_max_rows_plus,
|
||||
SbtSettings.KEY_CARDS_AOD_MAX_ROWS,
|
||||
SbtDefaults.CARDS_AOD_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.CARDS_AOD_MAX_ROWS_MIN,
|
||||
SbtDefaults.CARDS_AOD_MAX_ROWS_MAX,
|
||||
true);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.cards_aod_even_switch,
|
||||
SbtSettings.KEY_CARDS_AOD_EVEN_DISTRIBUTION,
|
||||
SbtDefaults.CARDS_AOD_EVEN_DISTRIBUTION_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.cards_aod_limit_width_switch,
|
||||
SbtSettings.KEY_CARDS_AOD_LIMIT_TO_WIDTH,
|
||||
SbtDefaults.CARDS_AOD_LIMIT_TO_WIDTH_DEFAULT);
|
||||
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.cards_lock_max_icons_slider,
|
||||
R.id.cards_lock_max_icons_input,
|
||||
R.id.cards_lock_max_icons_minus,
|
||||
R.id.cards_lock_max_icons_plus,
|
||||
SbtSettings.KEY_CARDS_LOCK_MAX_ICONS_PER_ROW,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_DEFAULT,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MIN,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ICONS_PER_ROW_MAX,
|
||||
true);
|
||||
bindNumberControl(root, prefs,
|
||||
R.id.cards_lock_max_rows_slider,
|
||||
R.id.cards_lock_max_rows_input,
|
||||
R.id.cards_lock_max_rows_minus,
|
||||
R.id.cards_lock_max_rows_plus,
|
||||
SbtSettings.KEY_CARDS_LOCK_MAX_ROWS,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ROWS_DEFAULT,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ROWS_MIN,
|
||||
SbtDefaults.CARDS_LOCK_MAX_ROWS_MAX,
|
||||
true);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.cards_lock_even_switch,
|
||||
SbtSettings.KEY_CARDS_LOCK_EVEN_DISTRIBUTION,
|
||||
SbtDefaults.CARDS_LOCK_EVEN_DISTRIBUTION_DEFAULT);
|
||||
bindSwitch(root, prefs,
|
||||
R.id.cards_lock_limit_width_switch,
|
||||
SbtSettings.KEY_CARDS_LOCK_LIMIT_TO_WIDTH,
|
||||
SbtDefaults.CARDS_LOCK_LIMIT_TO_WIDTH_DEFAULT);
|
||||
applyLayoutAvailability(root, prefs);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void applyLayoutAvailability(View root, SharedPreferences prefs) {
|
||||
if (root == null || prefs == null) {
|
||||
return;
|
||||
}
|
||||
boolean layoutEnabled = prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_ENABLED,
|
||||
SbtDefaults.CLOCK_ENABLED_DEFAULT);
|
||||
View unlockedSection = root.findViewById(R.id.notification_icons_unlocked_section);
|
||||
View layoutSections = root.findViewById(R.id.notification_icons_layout_sections);
|
||||
View layoutRequiredHint = root.findViewById(R.id.notification_icons_layout_required_hint);
|
||||
View unlockedObsoleteHint = root.findViewById(R.id.notification_icons_unlocked_obsolete_hint);
|
||||
if (unlockedSection != null) {
|
||||
setViewTreeEnabled(unlockedSection, !layoutEnabled);
|
||||
unlockedSection.setAlpha(layoutEnabled ? 0.45f : 1f);
|
||||
}
|
||||
if (layoutSections != null) {
|
||||
setViewTreeEnabled(layoutSections, layoutEnabled);
|
||||
layoutSections.setAlpha(layoutEnabled ? 1f : 0.45f);
|
||||
}
|
||||
if (layoutRequiredHint != null) {
|
||||
layoutRequiredHint.setVisibility(layoutEnabled ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if (unlockedObsoleteHint != null) {
|
||||
unlockedObsoleteHint.setVisibility(layoutEnabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setViewTreeEnabled(View view, boolean enabled) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setEnabled(enabled);
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewTreeEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void bindNumberControl(View root,
|
||||
SharedPreferences prefs,
|
||||
int sliderId,
|
||||
int inputId,
|
||||
int minusId,
|
||||
int plusId,
|
||||
String key,
|
||||
int defaultValue,
|
||||
int min,
|
||||
int max,
|
||||
boolean allowInf) {
|
||||
Slider slider = root.findViewById(sliderId);
|
||||
EditText input = root.findViewById(inputId);
|
||||
MaterialButton minus = root.findViewById(minusId);
|
||||
MaterialButton plus = root.findViewById(plusId);
|
||||
|
||||
int[] positions = buildPositions(min, max);
|
||||
int maxPos = SLIDER_STEPS - INF_GAP_STEPS;
|
||||
|
||||
slider.setValueFrom(0);
|
||||
slider.setValueTo(SLIDER_STEPS);
|
||||
slider.setStepSize(1f);
|
||||
slider.setTickVisible(false);
|
||||
slider.setLabelFormatter(value -> labelForSliderValue(Math.round(value), min, max, allowInf, positions));
|
||||
|
||||
final boolean[] updating = new boolean[]{false};
|
||||
|
||||
int current = clampInt(prefs.getInt(key, defaultValue), min, Integer.MAX_VALUE);
|
||||
applyValueToUi(slider, input, min, max, allowInf, positions, current, updating);
|
||||
input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)});
|
||||
|
||||
slider.addOnChangeListener((s, v, fromUser) -> {
|
||||
if (updating[0]) {
|
||||
return;
|
||||
}
|
||||
int pos = Math.round(v);
|
||||
int snapped = snapPosition(pos, positions, allowInf, maxPos);
|
||||
if (snapped != pos) {
|
||||
updating[0] = true;
|
||||
slider.setValue(snapped);
|
||||
updating[0] = false;
|
||||
}
|
||||
int mapped = positionToValue(snapped, min, max, allowInf, positions, maxPos);
|
||||
int nextValue = mapped;
|
||||
updating[0] = true;
|
||||
if (mapped == INF_VALUE) {
|
||||
input.setText(INF_LABEL);
|
||||
} else {
|
||||
input.setText(String.valueOf(mapped));
|
||||
}
|
||||
updating[0] = false;
|
||||
if (fromUser) {
|
||||
prefs.edit().putInt(key, nextValue).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
|
||||
minus.setOnClickListener(v -> {
|
||||
int parsed = parseInput(input, current);
|
||||
int next;
|
||||
if (parsed == INF_VALUE) {
|
||||
next = max;
|
||||
} else {
|
||||
next = clampInt(parsed - 1, min, Integer.MAX_VALUE);
|
||||
}
|
||||
applyValueToUi(slider, input, min, max, allowInf, positions, next, updating);
|
||||
prefs.edit().putInt(key, next).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
plus.setOnClickListener(v -> {
|
||||
int parsed = parseInput(input, current);
|
||||
int next;
|
||||
if (parsed == INF_VALUE) {
|
||||
next = INF_VALUE;
|
||||
} else {
|
||||
next = clampInt(parsed + 1, min, Integer.MAX_VALUE);
|
||||
}
|
||||
applyValueToUi(slider, input, min, max, allowInf, positions, next, updating);
|
||||
prefs.edit().putInt(key, next).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
|
||||
input.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
int next = clampInt(parseInput(input, current), min, Integer.MAX_VALUE);
|
||||
applyValueToUi(slider, input, min, max, allowInf, positions, next, updating);
|
||||
prefs.edit().putInt(key, next).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
input.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) {
|
||||
if (updating[0]) {
|
||||
return;
|
||||
}
|
||||
String text = s != null ? s.toString().trim() : "";
|
||||
if (text.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int parsed = parseInput(input, current);
|
||||
int next = clampInt(parsed, min, Integer.MAX_VALUE);
|
||||
if (next != parsed) {
|
||||
applyValueToUi(slider, input, min, max, allowInf, positions, next, updating);
|
||||
} else {
|
||||
updateSliderValue(slider, min, max, allowInf, positions, next, updating);
|
||||
}
|
||||
prefs.edit().putInt(key, next).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindSwitch(View root,
|
||||
SharedPreferences prefs,
|
||||
int switchId,
|
||||
String key,
|
||||
boolean defaultValue) {
|
||||
SwitchMaterial toggle = root.findViewById(switchId);
|
||||
boolean current = prefs.getBoolean(key, defaultValue);
|
||||
toggle.setChecked(current);
|
||||
toggle.setOnCheckedChangeListener((buttonView, isChecked) ->
|
||||
{
|
||||
prefs.edit().putBoolean(key, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
|
||||
private int clampInt(int value, int min, int max) {
|
||||
if (value < min) {
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private int parseInput(EditText input, int fallback) {
|
||||
if (input == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
String text = input.getText().toString().trim();
|
||||
if (text.isEmpty()) {
|
||||
return fallback;
|
||||
}
|
||||
if (INF_LABEL.equals(text) || "inf".equalsIgnoreCase(text)) {
|
||||
return INF_VALUE;
|
||||
}
|
||||
return Integer.parseInt(text);
|
||||
}
|
||||
|
||||
private void applyValueToUi(Slider slider,
|
||||
EditText input,
|
||||
int min,
|
||||
int max,
|
||||
boolean allowInf,
|
||||
int[] positions,
|
||||
int value,
|
||||
boolean[] updating) {
|
||||
updateSliderValue(slider, min, max, allowInf, positions, value, updating);
|
||||
updating[0] = true;
|
||||
if (value == INF_VALUE) {
|
||||
input.setText(INF_LABEL);
|
||||
} else {
|
||||
input.setText(String.valueOf(value));
|
||||
}
|
||||
updating[0] = false;
|
||||
}
|
||||
|
||||
private void updateSliderValue(Slider slider,
|
||||
int min,
|
||||
int max,
|
||||
boolean allowInf,
|
||||
int[] positions,
|
||||
int value,
|
||||
boolean[] updating) {
|
||||
int idx = valueToPosition(value, min, max, allowInf, positions);
|
||||
updating[0] = true;
|
||||
slider.setValue(idx);
|
||||
updating[0] = false;
|
||||
}
|
||||
|
||||
private int[] buildPositions(int min, int max) {
|
||||
int count = max - min + 1;
|
||||
int maxPos = SLIDER_STEPS - INF_GAP_STEPS;
|
||||
int[] positions = new int[count];
|
||||
if (count <= 1) {
|
||||
positions[0] = 0;
|
||||
return positions;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
double t = (double) i / (double) (count - 1);
|
||||
double shaped = Math.pow(t, 1.0 / SLIDER_EXPONENT);
|
||||
positions[i] = (int) Math.round(shaped * maxPos);
|
||||
}
|
||||
positions[0] = 0;
|
||||
positions[count - 1] = maxPos;
|
||||
for (int i = 1; i < count; i++) {
|
||||
if (positions[i] <= positions[i - 1]) {
|
||||
positions[i] = positions[i - 1] + 1;
|
||||
}
|
||||
}
|
||||
positions[count - 1] = maxPos;
|
||||
for (int i = count - 2; i >= 0; i--) {
|
||||
if (positions[i] >= positions[i + 1]) {
|
||||
positions[i] = positions[i + 1] - 1;
|
||||
}
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
private int valueToPosition(int value, int min, int max, boolean allowInf, int[] positions) {
|
||||
if (allowInf && value == INF_VALUE) {
|
||||
return SLIDER_STEPS;
|
||||
}
|
||||
if (allowInf && value > max) {
|
||||
return SLIDER_STEPS;
|
||||
}
|
||||
int clamped = clampInt(value, min, max);
|
||||
return positions[clamped - min];
|
||||
}
|
||||
|
||||
private int positionToValue(int pos,
|
||||
int min,
|
||||
int max,
|
||||
boolean allowInf,
|
||||
int[] positions,
|
||||
int maxPos) {
|
||||
if (allowInf && pos >= maxPos + (INF_GAP_STEPS / 2)) {
|
||||
return INF_VALUE;
|
||||
}
|
||||
int idx = nearestIndex(pos, positions);
|
||||
int value = min + idx;
|
||||
return clampInt(value, min, max);
|
||||
}
|
||||
|
||||
private int nearestIndex(int pos, int[] positions) {
|
||||
int idx = Arrays.binarySearch(positions, pos);
|
||||
if (idx >= 0) {
|
||||
return idx;
|
||||
}
|
||||
int insert = -idx - 1;
|
||||
if (insert <= 0) {
|
||||
return 0;
|
||||
}
|
||||
if (insert >= positions.length) {
|
||||
return positions.length - 1;
|
||||
}
|
||||
int left = positions[insert - 1];
|
||||
int right = positions[insert];
|
||||
return (pos - left <= right - pos) ? (insert - 1) : insert;
|
||||
}
|
||||
|
||||
private int snapPosition(int pos, int[] positions, boolean allowInf, int maxPos) {
|
||||
if (allowInf && pos >= maxPos + (INF_GAP_STEPS / 2)) {
|
||||
return SLIDER_STEPS;
|
||||
}
|
||||
int idx = nearestIndex(pos, positions);
|
||||
return positions[idx];
|
||||
}
|
||||
|
||||
private String labelForSliderValue(int pos, int min, int max, boolean allowInf, int[] positions) {
|
||||
int maxPos = SLIDER_STEPS - INF_GAP_STEPS;
|
||||
int value = positionToValue(pos, min, max, allowInf, positions, maxPos);
|
||||
return value == INF_VALUE ? INF_LABEL : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial;
|
||||
|
||||
public class StatusChipsFragment extends Fragment {
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_status_chips, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
|
||||
bindSwitch(root, prefs, R.id.status_chips_hide_all_switch,
|
||||
SbtSettings.KEY_STATUS_CHIPS_HIDE_ALL, false);
|
||||
bindSwitch(root, prefs, R.id.status_chips_hide_media_unlocked_switch,
|
||||
SbtSettings.KEY_STATUS_CHIPS_HIDE_MEDIA_UNLOCKED, false);
|
||||
bindSwitch(root, prefs, R.id.status_chips_hide_navigation_unlocked_switch,
|
||||
SbtSettings.KEY_STATUS_CHIPS_HIDE_NAV_UNLOCKED, false);
|
||||
bindSwitch(root, prefs, R.id.status_chips_hide_call_unlocked_switch,
|
||||
SbtSettings.KEY_STATUS_CHIPS_HIDE_CALL, false);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void bindSwitch(View root,
|
||||
SharedPreferences prefs,
|
||||
int switchId,
|
||||
String key,
|
||||
boolean defaultValue) {
|
||||
SwitchMaterial toggle = root.findViewById(switchId);
|
||||
toggle.setChecked(prefs.getBoolean(key, defaultValue));
|
||||
toggle.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
prefs.edit().putBoolean(key, isChecked).commit();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,443 @@
|
||||
package se.ajpanton.statusbartweak.shell.ui;
|
||||
|
||||
import se.ajpanton.statusbartweak.R;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtDefaults;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SystemIconRules;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.NinePatchDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
|
||||
public class SystemIconsFragment extends Fragment {
|
||||
|
||||
private static final String QUICKSTAR_PACKAGE = "com.samsung.android.qstuner";
|
||||
private static final String SYSTEMUI_PACKAGE = "com.android.systemui";
|
||||
private static final int TOGGLE_ICON_DP = 24;
|
||||
|
||||
private static final List<SystemIconSection> SECTIONS = Arrays.asList(
|
||||
new SystemIconSection(
|
||||
R.string.system_icons_group_connectivity,
|
||||
Arrays.asList(
|
||||
new SystemIconEntry(R.string.system_icon_airplane,
|
||||
new String[]{"stat_sys_airplane_mode"},
|
||||
android.R.drawable.stat_sys_warning,
|
||||
"airplane"),
|
||||
new SystemIconEntry(R.string.system_icon_bluetooth,
|
||||
new String[]{"stat_sys_data_bluetooth"},
|
||||
android.R.drawable.stat_sys_data_bluetooth,
|
||||
"bluetooth", "bluetooth_connected"),
|
||||
new SystemIconEntry(R.string.system_icon_mobile,
|
||||
new String[]{"sbt_qs_mobile_signal"},
|
||||
R.drawable.sbt_qs_mobile_signal,
|
||||
false,
|
||||
"mobile"),
|
||||
new SystemIconEntry(R.string.system_icon_hotspot,
|
||||
new String[]{"stat_sys_mobile_hotspot"},
|
||||
R.drawable.sbt_ic_hotspot,
|
||||
"hotspot"),
|
||||
new SystemIconEntry(R.string.system_icon_nfc,
|
||||
new String[]{"stat_sys_nfc"},
|
||||
android.R.drawable.ic_menu_share,
|
||||
"nfc_on", "nfc"),
|
||||
new SystemIconEntry(R.string.system_icon_vpn,
|
||||
new String[]{"stat_sys_branded_vpn"},
|
||||
android.R.drawable.ic_secure,
|
||||
"vpn"),
|
||||
new SystemIconEntry(R.string.system_icon_wifi,
|
||||
new String[]{"stat_sys_wifi_signal_3", "stat_sys_wifi_signal_3_pure"},
|
||||
android.R.drawable.ic_menu_upload,
|
||||
"wifi")
|
||||
)
|
||||
),
|
||||
new SystemIconSection(
|
||||
R.string.system_icons_group_notifications,
|
||||
Arrays.asList(
|
||||
new SystemIconEntry(R.string.system_icon_alarm,
|
||||
new String[]{"stat_sys_alarm"},
|
||||
android.R.drawable.ic_lock_idle_alarm,
|
||||
"alarm_clock"),
|
||||
new SystemIconEntry(R.string.system_icon_do_not_disturb,
|
||||
new String[]{"stat_sys_dnd", "ic_notification_dnd_on", "ic_qs_dnd_on"},
|
||||
android.R.drawable.presence_busy,
|
||||
"donotdisturb", "zen", "dnd"),
|
||||
new SystemIconEntry(R.string.system_icon_volume,
|
||||
new String[]{"stat_sys_ringer_vibrate"},
|
||||
android.R.drawable.ic_lock_silent_mode_off,
|
||||
"volume", "mute")
|
||||
)
|
||||
),
|
||||
new SystemIconSection(
|
||||
R.string.system_icons_group_system,
|
||||
Arrays.asList(
|
||||
new SystemIconEntry(R.string.system_icon_battery_icon,
|
||||
new String[]{"stat_sys_battery_outline"},
|
||||
R.drawable.sbt_ic_battery,
|
||||
true,
|
||||
"battery", "battery_icon"),
|
||||
new SystemIconEntry(R.string.system_icon_data_saver,
|
||||
new String[]{"stat_sys_data_saver"},
|
||||
android.R.drawable.stat_notify_sync_noanim,
|
||||
"data_saver"),
|
||||
new SystemIconEntry(R.string.system_icon_location,
|
||||
new String[]{"stat_sys_location"},
|
||||
android.R.drawable.ic_menu_mylocation,
|
||||
"location")
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
private Context quickStarContext;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_system_icons, container, false);
|
||||
SharedPreferences prefs = requireContext()
|
||||
.getSharedPreferences(SbtSettings.PREFS_NAME, Context.MODE_PRIVATE);
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
Map<String, Integer> blockedModes = new HashMap<>(SbtSettings.from(requireContext()).systemIconBlockedModes);
|
||||
|
||||
Map<Integer, LinearLayout> sectionContainers = new HashMap<>();
|
||||
sectionContainers.put(R.string.system_icons_group_connectivity,
|
||||
root.findViewById(R.id.system_icons_connectivity_container));
|
||||
sectionContainers.put(R.string.system_icons_group_notifications,
|
||||
root.findViewById(R.id.system_icons_notifications_container));
|
||||
sectionContainers.put(R.string.system_icons_group_system,
|
||||
root.findViewById(R.id.system_icons_system_container));
|
||||
|
||||
for (SystemIconSection section : SECTIONS) {
|
||||
LinearLayout sectionContainer = sectionContainers.get(section.labelRes);
|
||||
if (sectionContainer == null) {
|
||||
continue;
|
||||
}
|
||||
for (SystemIconEntry entry : section.entries) {
|
||||
sectionContainer.addView(createRow(sectionContainer, prefs, blockedModes, entry));
|
||||
}
|
||||
}
|
||||
applyLayoutAvailability(root, prefs);
|
||||
return root;
|
||||
}
|
||||
|
||||
private void applyLayoutAvailability(View root, SharedPreferences prefs) {
|
||||
if (root == null || prefs == null) {
|
||||
return;
|
||||
}
|
||||
boolean enabled = prefs.getBoolean(
|
||||
SbtSettings.KEY_CLOCK_ENABLED,
|
||||
SbtDefaults.CLOCK_ENABLED_DEFAULT);
|
||||
View content = root.findViewById(R.id.system_icons_content);
|
||||
TextView disabledHint = root.findViewById(R.id.system_icons_disabled_hint);
|
||||
if (content != null) {
|
||||
setViewTreeEnabled(content, enabled);
|
||||
content.setAlpha(enabled ? 1f : 0.45f);
|
||||
}
|
||||
if (disabledHint != null) {
|
||||
disabledHint.setVisibility(enabled ? View.GONE : View.VISIBLE);
|
||||
disabledHint.setAlpha(1f);
|
||||
}
|
||||
}
|
||||
|
||||
private void setViewTreeEnabled(View view, boolean enabled) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setEnabled(enabled);
|
||||
if (view instanceof ViewGroup) {
|
||||
ViewGroup group = (ViewGroup) view;
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
setViewTreeEnabled(group.getChildAt(i), enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private View createRow(ViewGroup parent,
|
||||
SharedPreferences prefs,
|
||||
Map<String, Integer> blockedModes,
|
||||
SystemIconEntry entry) {
|
||||
Context context = parent.getContext();
|
||||
LinearLayout row = new LinearLayout(context);
|
||||
row.setOrientation(LinearLayout.HORIZONTAL);
|
||||
row.setGravity(Gravity.CENTER_VERTICAL);
|
||||
row.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
row.setPadding(0, dpToPx(context, 6), 0, dpToPx(context, 6));
|
||||
|
||||
LinearLayout modeStrip = new LinearLayout(context);
|
||||
modeStrip.setOrientation(LinearLayout.HORIZONTAL);
|
||||
LinearLayout.LayoutParams modeStripParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
modeStrip.setLayoutParams(modeStripParams);
|
||||
modeStrip.addView(createModeToggle(context, prefs, blockedModes, entry,
|
||||
SystemIconRules.MODE_AOD, R.string.hidden_apps_mode_aod));
|
||||
modeStrip.addView(createModeToggle(context, prefs, blockedModes, entry,
|
||||
SystemIconRules.MODE_LOCK, R.string.hidden_apps_mode_lock_short));
|
||||
modeStrip.addView(createModeToggle(context, prefs, blockedModes, entry,
|
||||
SystemIconRules.MODE_UNLOCK, R.string.hidden_apps_mode_unlock_short));
|
||||
row.addView(modeStrip);
|
||||
|
||||
TextView label = new TextView(context);
|
||||
LinearLayout.LayoutParams labelParams = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
labelParams.weight = 1f;
|
||||
labelParams.leftMargin = dpToPx(context, 8);
|
||||
label.setLayoutParams(labelParams);
|
||||
label.setText(context.getString(entry.labelRes));
|
||||
label.setTextAppearance(com.google.android.material.R.style.TextAppearance_MaterialComponents_Body1);
|
||||
row.addView(label);
|
||||
return row;
|
||||
}
|
||||
|
||||
private View createModeToggle(Context context,
|
||||
SharedPreferences prefs,
|
||||
Map<String, Integer> blockedModes,
|
||||
SystemIconEntry entry,
|
||||
int modeMask,
|
||||
int modeLabelRes) {
|
||||
LinearLayout cell = new LinearLayout(context);
|
||||
cell.setOrientation(LinearLayout.HORIZONTAL);
|
||||
cell.setGravity(Gravity.CENTER);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
dpToPx(context, 32), ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
cell.setLayoutParams(params);
|
||||
|
||||
FrameLayout iconCell = new FrameLayout(context);
|
||||
LinearLayout.LayoutParams iconCellParams = new LinearLayout.LayoutParams(
|
||||
dpToPx(context, TOGGLE_ICON_DP),
|
||||
dpToPx(context, TOGGLE_ICON_DP));
|
||||
iconCell.setLayoutParams(iconCellParams);
|
||||
|
||||
ImageView iconView = new ImageView(context);
|
||||
FrameLayout.LayoutParams iconParams = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
iconView.setLayoutParams(iconParams);
|
||||
iconView.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
Drawable iconDrawable = resolveIconDrawable(context, entry);
|
||||
if (iconDrawable != null) {
|
||||
iconView.setImageDrawable(applyBestEffortTint(context, iconDrawable, entry.tintBitmap));
|
||||
}
|
||||
iconCell.addView(iconView);
|
||||
|
||||
TextView cross = new TextView(context);
|
||||
FrameLayout.LayoutParams crossParams = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
cross.setLayoutParams(crossParams);
|
||||
cross.setGravity(Gravity.CENTER);
|
||||
cross.setText("\u2715");
|
||||
cross.setTextSize(14f);
|
||||
cross.setTextColor(ContextCompat.getColor(context, R.color.sbt_hidden_cross));
|
||||
iconCell.addView(cross);
|
||||
cell.addView(iconCell);
|
||||
cell.setContentDescription(context.getString(modeLabelRes));
|
||||
|
||||
boolean blocked = true;
|
||||
for (String slot : entry.slots) {
|
||||
if ((modeMask & SystemIconRules.MODE_AOD) != 0
|
||||
&& !SystemIconRules.isBlocked(blockedModes, slot, "AOD")) {
|
||||
blocked = false;
|
||||
break;
|
||||
}
|
||||
if ((modeMask & SystemIconRules.MODE_LOCK) != 0
|
||||
&& !SystemIconRules.isBlocked(blockedModes, slot, "LOCK")) {
|
||||
blocked = false;
|
||||
break;
|
||||
}
|
||||
if ((modeMask & SystemIconRules.MODE_UNLOCK) != 0
|
||||
&& !SystemIconRules.isBlocked(blockedModes, slot, "UNLOCK")) {
|
||||
blocked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
applyToggleCellState(iconCell, cross, blocked);
|
||||
cell.setOnClickListener(v -> {
|
||||
boolean nextBlocked = !(Boolean.TRUE.equals(iconCell.getTag()));
|
||||
for (String slot : entry.slots) {
|
||||
SystemIconRules.setModeBlocked(blockedModes, slot, modeMask, nextBlocked);
|
||||
}
|
||||
persistBlockedModes(prefs, blockedModes);
|
||||
applyToggleCellState(iconCell, cross, nextBlocked);
|
||||
});
|
||||
return cell;
|
||||
}
|
||||
|
||||
private void persistBlockedModes(SharedPreferences prefs, Map<String, Integer> blockedModes) {
|
||||
Set<String> encoded = SystemIconRules.encodeBlockedModes(blockedModes);
|
||||
prefs.edit()
|
||||
.putStringSet(SbtSettings.KEY_SYSTEM_ICON_BLOCKED_MODES, encoded)
|
||||
// Clear legacy key so migration does not keep re-forcing stale global hides.
|
||||
.putStringSet(SbtSettings.KEY_SYSTEM_ICON_HIDE_SLOTS, Collections.emptySet())
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
|
||||
private void applyToggleCellState(View iconCell, TextView cross, boolean blocked) {
|
||||
if (iconCell != null) {
|
||||
iconCell.setTag(blocked);
|
||||
iconCell.setAlpha(blocked ? 0.45f : 1f);
|
||||
}
|
||||
if (cross != null) {
|
||||
cross.setVisibility(blocked ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private Drawable resolveIconDrawable(Context context, SystemIconEntry entry) {
|
||||
if (entry == null || context == null) {
|
||||
return null;
|
||||
}
|
||||
if (entry.quickStarDrawableNames != null) {
|
||||
for (String name : entry.quickStarDrawableNames) {
|
||||
Drawable drawable = loadPackageDrawable(context, QUICKSTAR_PACKAGE, name);
|
||||
if (drawable == null) {
|
||||
drawable = loadPackageDrawable(context, SYSTEMUI_PACKAGE, name);
|
||||
}
|
||||
if (drawable != null) {
|
||||
return drawable;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (entry.fallbackDrawableRes != 0) {
|
||||
Drawable fallback = ContextCompat.getDrawable(context, entry.fallbackDrawableRes);
|
||||
return cloneDrawable(fallback, context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Drawable loadPackageDrawable(Context context, String packageName, String drawableName) {
|
||||
if (context == null || drawableName == null || drawableName.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
Context packageContext = getPackageContext(context, packageName);
|
||||
if (packageContext == null) {
|
||||
return null;
|
||||
}
|
||||
int id = packageContext.getResources().getIdentifier(drawableName, "drawable", packageName);
|
||||
if (id == 0) {
|
||||
return null;
|
||||
}
|
||||
Drawable drawable = ContextCompat.getDrawable(packageContext, id);
|
||||
return cloneDrawable(drawable, packageContext);
|
||||
}
|
||||
|
||||
private Context getPackageContext(Context context, String packageName) {
|
||||
if (context == null || packageName == null) {
|
||||
return null;
|
||||
}
|
||||
if (QUICKSTAR_PACKAGE.equals(packageName) && quickStarContext != null) {
|
||||
return quickStarContext;
|
||||
}
|
||||
try {
|
||||
Context pkgContext = context.createPackageContext(packageName, Context.CONTEXT_IGNORE_SECURITY);
|
||||
if (QUICKSTAR_PACKAGE.equals(packageName)) {
|
||||
quickStarContext = pkgContext;
|
||||
}
|
||||
return pkgContext;
|
||||
} catch (Throwable ignored) {
|
||||
if (QUICKSTAR_PACKAGE.equals(packageName)) {
|
||||
quickStarContext = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Drawable cloneDrawable(Drawable drawable, Context context) {
|
||||
if (drawable == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Drawable.ConstantState state = drawable.getConstantState();
|
||||
if (state != null && context != null) {
|
||||
Drawable copy = state.newDrawable(context.getResources(), context.getTheme());
|
||||
return copy != null ? copy.mutate() : drawable.mutate();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
return drawable.mutate();
|
||||
}
|
||||
|
||||
private Drawable applyBestEffortTint(Context context, Drawable drawable, boolean tintBitmap) {
|
||||
if (drawable == null) {
|
||||
return null;
|
||||
}
|
||||
if (!tintBitmap && (drawable instanceof BitmapDrawable || drawable instanceof NinePatchDrawable)) {
|
||||
// Many OEM bitmap assets become solid squares if tinted.
|
||||
return drawable;
|
||||
}
|
||||
int tint = MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorOnSurface,
|
||||
0xFFFFFFFF);
|
||||
drawable.setTintList(ColorStateList.valueOf(tint));
|
||||
return drawable;
|
||||
}
|
||||
|
||||
private int dpToPx(Context context, int dp) {
|
||||
float density = context.getResources().getDisplayMetrics().density;
|
||||
return Math.round(dp * density);
|
||||
}
|
||||
|
||||
private static final class SystemIconSection {
|
||||
final int labelRes;
|
||||
final List<SystemIconEntry> entries;
|
||||
|
||||
SystemIconSection(int labelRes, List<SystemIconEntry> entries) {
|
||||
this.labelRes = labelRes;
|
||||
this.entries = entries;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SystemIconEntry {
|
||||
final int labelRes;
|
||||
final String[] quickStarDrawableNames;
|
||||
final int fallbackDrawableRes;
|
||||
final boolean tintBitmap;
|
||||
final Set<String> slots;
|
||||
|
||||
SystemIconEntry(int labelRes, String[] quickStarDrawableNames, int fallbackDrawableRes, String... slots) {
|
||||
this(labelRes, quickStarDrawableNames, fallbackDrawableRes, false, slots);
|
||||
}
|
||||
|
||||
SystemIconEntry(int labelRes,
|
||||
String[] quickStarDrawableNames,
|
||||
int fallbackDrawableRes,
|
||||
boolean tintBitmap,
|
||||
String... slots) {
|
||||
this.labelRes = labelRes;
|
||||
this.quickStarDrawableNames = quickStarDrawableNames;
|
||||
this.fallbackDrawableRes = fallbackDrawableRes;
|
||||
this.tintBitmap = tintBitmap;
|
||||
this.slots = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(slots)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:color="@android:color/white" />
|
||||
<item android:color="?android:textColorPrimary" />
|
||||
</selector>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true">
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="12dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#FF03DAC5" />
|
||||
<solid android:color="#5503DAC5" />
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent" />
|
||||
</shape>
|
||||
</item>
|
||||
</selector>
|
||||
@@ -0,0 +1,170 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillColor="#3DDC84"
|
||||
android:pathData="M0,0h108v108h-108z" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M9,0L9,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,0L19,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,0L29,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,0L39,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,0L49,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,0L59,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,0L69,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,0L79,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M89,0L89,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M99,0L99,108"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,9L108,9"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,19L108,19"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,29L108,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,39L108,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,49L108,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,59L108,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,69L108,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,79L108,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,89L108,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M0,99L108,99"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,29L89,29"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,39L89,39"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,49L89,49"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,59L89,59"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,69L89,69"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M19,79L89,79"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M29,19L29,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M39,19L39,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M49,19L49,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M59,19L59,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M69,19L69,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
<path
|
||||
android:fillColor="#00000000"
|
||||
android:pathData="M79,19L79,89"
|
||||
android:strokeWidth="0.8"
|
||||
android:strokeColor="#33FFFFFF" />
|
||||
</vector>
|
||||
@@ -0,0 +1,30 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="85.84757"
|
||||
android:endY="92.4963"
|
||||
android:startX="42.9492"
|
||||
android:startY="49.59793"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</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="#FFFFFFFF"
|
||||
android:pathData="M8,7l-5,5l5,5l1.41,-1.41L6.83,13H17.17l-2.58,2.59L16,17l5,-5l-5,-5l-1.41,1.41L17.17,11H6.83l2.58,-2.59z" />
|
||||
</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="#FFFFFFFF"
|
||||
android:pathData="M10,7l5,5l-5,5l-1.41,-1.41L11.17,13H5v-2h6.17L8.59,8.41z" />
|
||||
</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="#FFFFFFFF"
|
||||
android:pathData="M14,7l-5,5l5,5l1.41,-1.41L12.83,13H19v-2H12.83l2.58,-2.59z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?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="#FFFFFFFF"
|
||||
android:pathData="M10,2h4c0.55,0 1,0.45 1,1v1h1c1.1,0 2,0.9 2,2v14c0,1.1 -0.9,2 -2,2H8c-1.1,0 -2,-0.9 -2,-2V6c0,-1.1 0.9,-2 2,-2h1V3c0,-0.55 0.45,-1 1,-1zM11,4h2V3h-2v1zM8,6v14h8V6H8z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M10,9h4v8h-4z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?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="#FFFFFFFF"
|
||||
android:pathData="M12,15.6a1.6,1.6 0,1 0,0 3.2a1.6,1.6 0,1 0,0 -3.2z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,11.4c-2.05,0 -3.93,0.77 -5.36,2.03l1.06,1.21c1.15,-1 2.65,-1.62 4.3,-1.62s3.15,0.62 4.3,1.62l1.06,-1.21C15.93,12.17 14.05,11.4 12,11.4z" />
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,7.2c-3.2,0 -6.12,1.2 -8.34,3.18l1.06,1.21C6.66,9.88 9.2,8.84 12,8.84s5.34,1.04 7.28,2.75l1.06,-1.21C18.12,8.4 15.2,7.2 12,7.2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="15dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="15"
|
||||
android:viewportHeight="14">
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:fillAlpha="0.3961"
|
||||
android:pathData="M12,1.719C11.632,1.719 11.333,2.018 11.333,2.386L11.333,2.447L11.333,11.679L11.333,12.351C11.333,12.72 11.632,13.018 12,13.018C12.368,13.018 12.667,12.72 12.667,12.351L12.667,11.679L12.667,2.447L12.667,2.386C12.667,2.018 12.368,1.719 12,1.719Z" />
|
||||
<path
|
||||
android:fillColor="#ff000000"
|
||||
android:pathData="M7.5,6.053C7.868,6.053 8.167,6.352 8.167,6.719L8.167,6.719L8.167,12.352C8.167,12.721 7.868,13.019 7.5,13.019C7.131,13.019 6.833,12.721 6.833,12.352L6.833,12.352L6.833,6.72C6.833,6.352 7.131,6.053 7.5,6.053ZM5.25,8.386C5.618,8.386 5.917,8.685 5.917,9.053L5.917,9.053L5.917,12.351C5.917,12.72 5.618,13.018 5.25,13.018C4.881,13.018 4.583,12.72 4.583,12.351L4.583,12.351L4.583,9.053C4.583,8.685 4.881,8.386 5.25,8.386ZM3,10.386C3.368,10.386 3.667,10.685 3.667,11.053L3.667,11.053L3.667,12.351C3.667,12.72 3.368,13.018 3,13.018C2.631,13.018 2.333,12.72 2.333,12.351L2.333,12.351L2.333,11.053C2.333,10.685 2.631,10.386 3,10.386ZM9.75,3.719C10.118,3.719 10.417,4.018 10.417,4.386L10.417,4.386L10.417,12.351C10.417,12.72 10.118,13.018 9.75,13.018C9.382,13.018 9.083,12.72 9.083,12.351L9.083,12.351L9.083,4.386C9.083,4.018 9.382,3.719 9.75,3.719Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="#12FFFFFF" />
|
||||
<corners android:radius="12dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@color/sbt_panel_outline" />
|
||||
</shape>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||
app:titleTextColor="@android:color/white" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/drawer_header"
|
||||
app:itemBackground="@drawable/drawer_item_background"
|
||||
app:itemTextColor="@color/drawer_item_text_color"
|
||||
app:menu="@menu/drawer_menu" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
@@ -0,0 +1,202 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="ifContentScrolls"
|
||||
android:padding="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_app_config_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:textStyle="bold"
|
||||
android:textSize="18sp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_app_config_default_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="2dp"
|
||||
android:text="@string/hidden_apps_default_modes_title"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/hidden_app_default_aod_cell"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="2dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/hidden_app_default_aod_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_app_default_aod_cross"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="✕"
|
||||
android:textColor="@color/sbt_hidden_cross"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/hidden_apps_mode_aod"
|
||||
android:textSize="11sp"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/hidden_app_default_lock_cell"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="2dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/hidden_app_default_lock_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_app_default_lock_cross"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="✕"
|
||||
android:textColor="@color/sbt_hidden_cross"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/hidden_apps_mode_lock"
|
||||
android:textSize="11sp"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/hidden_app_default_unlock_cell"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="2dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/hidden_app_default_unlock_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_app_default_unlock_cross"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="✕"
|
||||
android:textColor="@color/sbt_hidden_cross"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
</FrameLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:text="@string/hidden_apps_mode_unlock"
|
||||
android:textSize="11sp"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/hidden_app_exceptions_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="2dp"
|
||||
android:overScrollMode="ifContentScrolls" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/hidden_app_add_exception"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:text="@string/hidden_apps_add_exception"
|
||||
android:minHeight="40dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_name"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6"
|
||||
android:textColor="@android:color/white" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,548 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_master_toggle_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/battery_bar_enabled_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_enabled" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_geometry_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_position_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/battery_bar_position_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/battery_bar_position_top"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_position_top" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/battery_bar_position_bottom"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_position_bottom" />
|
||||
</RadioGroup>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/battery_bar_thickness_title"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_thickness_minus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="-" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/battery_bar_thickness_input"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ems="3"
|
||||
android:gravity="center"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_thickness_plus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/battery_bar_edge_offset_title"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_edge_offset_minus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="-" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/battery_bar_edge_offset_input"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:ems="3"
|
||||
android:gravity="center"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="number"
|
||||
android:maxLines="1"
|
||||
android:singleLine="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_edge_offset_plus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_alignment_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_alignment_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_alignment_cell_ltr"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/battery_bar_alignment_ltr"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:minHeight="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:padding="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:contentDescription="@null"
|
||||
app:srcCompat="@drawable/sbt_align_ltr" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_alignment_cell_center"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/battery_bar_alignment_center"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:minHeight="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:padding="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:contentDescription="@null"
|
||||
app:srcCompat="@drawable/sbt_align_center" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_alignment_cell_rtl"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/battery_bar_alignment_rtl"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:minHeight="0dp"
|
||||
android:minWidth="0dp"
|
||||
android:padding="0dp" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:contentDescription="@null"
|
||||
app:srcCompat="@drawable/sbt_align_rtl" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_mapping_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/battery_bar_mapping_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_mapping_left_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_min_level_plus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:text="+" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_min_level_minus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:text="-" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/battery_bar_mapping_middle_section"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/battery_bar_mapping_right_controls"
|
||||
app:layout_constraintStart_toEndOf="@id/battery_bar_mapping_left_controls"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_bar_mapping_slider_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/battery_bar_mapping_slider_label"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.slider.RangeSlider
|
||||
android:id="@+id/battery_bar_mapping_slider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:valueFrom="0"
|
||||
android:valueTo="100"
|
||||
app:labelBehavior="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.5" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_bar_min_level_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/battery_bar_max_level_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="end"
|
||||
android:textAppearance="?attr/textAppearanceCaption"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_mapping_right_controls"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_max_level_plus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:text="+" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_max_level_minus"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingTop="0dp"
|
||||
android:paddingBottom="0dp"
|
||||
android:text="-" />
|
||||
</LinearLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_style_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<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="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_colours_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_default_discharge_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_default_discharge_colour" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_default_charge_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_default_charge_colour" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/battery_bar_thresholds_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_thresholds_hint"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/battery_bar_thresholds_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/battery_bar_add_threshold_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/battery_bar_add_threshold" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,84 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_apps_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hidden_apps_page_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline6"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_apps_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/hidden_apps_dialog_subtitle"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_apps_disabled_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/layout_subpage_disabled_hint"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/hidden_apps_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_apps_page_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/hidden_apps_page_hint"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/hidden_apps_swipe"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_weight="1">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/hidden_apps_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:dividerHeight="0dp"
|
||||
android:listSelector="@android:color/transparent"
|
||||
android:paddingBottom="8dp"
|
||||
android:scrollbarStyle="insideInset" />
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/hidden_apps_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_horizontal"
|
||||
android:text="@string/hidden_apps_empty"
|
||||
android:textAppearance="?attr/textAppearanceBody2"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/hidden_apps_clear_session"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="@string/hidden_apps_clear_session" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clock_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/clock_custom_format_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/clock_custom_format_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/clock_custom_format_enabled" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/clock_custom_format_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/clock_custom_format_input"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="top|start"
|
||||
android:hint="@string/clock_custom_format_hint"
|
||||
android:importantForAutofill="no"
|
||||
android:inputType="textMultiLine|textNoSuggestions"
|
||||
android:maxLines="4"
|
||||
android:minLines="2"
|
||||
android:scrollHorizontally="false"
|
||||
android:singleLine="false" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/clock_font_picker_button"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/clock_font_picker_button" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help_toggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/clock_custom_format_help_show"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/clock_custom_format_help_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/clock_custom_format_help_common"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help_styling"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/clock_custom_format_help_styling"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help_examples"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/clock_custom_format_help_examples"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help_link"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:autoLink="web"
|
||||
android:linksClickable="true"
|
||||
android:text="@string/clock_custom_format_help_link"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -0,0 +1,342 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_title"
|
||||
android:textAppearance="?attr/textAppearanceHeadline5" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_section_visualizers"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_visualize_notification_container_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/debug_visualize_notification_container" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_visualize_status_container_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_visualize_status_container" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_visualize_clock_container_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_visualize_clock_container" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_visualize_chip_container_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_visualize_chip_container" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_visualize_camera_cutout_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_visualize_camera_cutout" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_section_systemui"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_restart_systemui_button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/debug_restart_systemui" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_section_camera"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_camera_current_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/debug_camera_current_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_camera_current_identified"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/debug_camera_current_override_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/debug_camera_current_action"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="0dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_camera_current_override"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/debug_camera_other_section"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_camera_other_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_camera_other_title"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/debug_camera_other_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_section_logging"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_write_logs_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/debug_write_logs_label" />
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
app:strokeColor="@color/sbt_card_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/debug_section_notification_icons"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle1"
|
||||
android:textAllCaps="true"
|
||||
android:letterSpacing="0.03"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/debug_cycle_icons_switch"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/debug_cycle_icons_label" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_cycle_icons_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/debug_cycle_icons_hint"
|
||||
android:textAppearance="?attr/textAppearanceBody2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_count_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/debug_count_label"
|
||||
android:textAppearance="?attr/textAppearanceSubtitle2" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_count_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="0"
|
||||
android:textAppearance="?attr/textAppearanceHeadline3" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_minus_10"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="-10" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_minus_1"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="-1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_plus_1"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="+1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_plus_10"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:text="+10" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_reset"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/debug_reset" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user