Customize drawer clock text
This commit is contained in:
@@ -14,6 +14,7 @@ import se.ajpanton.statusbartweak.runtime.features.RuntimeFeature;
|
||||
public final class ClockFeature implements RuntimeFeature {
|
||||
|
||||
private StockClockController stockClockController;
|
||||
private DrawerClockTextController drawerClockTextController;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
@@ -36,5 +37,9 @@ public final class ClockFeature implements RuntimeFeature {
|
||||
stockClockController = new StockClockController(context);
|
||||
}
|
||||
stockClockController.install(classLoader);
|
||||
if (drawerClockTextController == null) {
|
||||
drawerClockTextController = new DrawerClockTextController(context);
|
||||
}
|
||||
drawerClockTextController.install(classLoader);
|
||||
}
|
||||
}
|
||||
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
package se.ajpanton.statusbartweak.runtime.features.clock;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.WeakHashMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.github.libxposed.api.XposedInterface;
|
||||
import se.ajpanton.statusbartweak.runtime.ViewIdNames;
|
||||
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.BroadcastReceiverSupport;
|
||||
import se.ajpanton.statusbartweak.runtime.settings.RuntimeSettingsCache;
|
||||
import se.ajpanton.statusbartweak.shell.settings.SbtSettings;
|
||||
|
||||
final class DrawerClockTextController {
|
||||
private static final Pattern TIME_TEXT_PATTERN =
|
||||
Pattern.compile("\\b\\d{1,2}:\\d{2}(?::\\d{2})?\\b");
|
||||
|
||||
private final RuntimeContext runtimeContext;
|
||||
private final ClockPatternParser patternParser = new ClockPatternParser();
|
||||
private final ClockTextRenderer textRenderer = new ClockTextRenderer();
|
||||
private final Map<TextView, Role> trackedRoles = new WeakHashMap<>();
|
||||
private final Map<TextView, Snapshot> stockSnapshots = new WeakHashMap<>();
|
||||
private final Map<TextView, Runnable> tickers = new WeakHashMap<>();
|
||||
private final Set<TextView> ownedTexts = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
private boolean hooksInstalled;
|
||||
private boolean receiverRegistered;
|
||||
private int ownTextWriteDepth;
|
||||
|
||||
DrawerClockTextController(RuntimeContext runtimeContext) {
|
||||
this.runtimeContext = runtimeContext;
|
||||
}
|
||||
|
||||
void install(ClassLoader classLoader) {
|
||||
if (hooksInstalled || classLoader == null) {
|
||||
return;
|
||||
}
|
||||
XposedInterface framework = runtimeContext.getFramework();
|
||||
Class<?> viewRootClass = ReflectionSupport.requireClass("android.view.ViewRootImpl", classLoader);
|
||||
XposedHookSupport.hookAllMethods(framework, viewRootClass, "performTraversals", chain -> {
|
||||
Object result = chain.proceed();
|
||||
Object rootObj = ReflectionSupport.requireFieldValue(chain.getThisObject(), "mView");
|
||||
if (rootObj instanceof View root && isNotificationShadeRoot(root)) {
|
||||
scanAndApply(root);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
Class<?> textViewClass = ReflectionSupport.requireClass("android.widget.TextView", classLoader);
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, textViewClass, "setText", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (ownTextWriteDepth <= 0 && chain.getThisObject() instanceof TextView textView) {
|
||||
reapplyTrackedText(textView, true, false);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
XposedHookSupport.hookAllMethodsIfExists(framework, textViewClass, "setTextColor", chain -> {
|
||||
Object result = chain.proceed();
|
||||
if (ownTextWriteDepth <= 0 && chain.getThisObject() instanceof TextView textView) {
|
||||
reapplyTrackedText(textView, false, true);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
hooksInstalled = true;
|
||||
}
|
||||
|
||||
private void scanAndApply(View root) {
|
||||
SbtSettings settings = RuntimeSettingsCache.get(root.getContext());
|
||||
boolean clockEnabled = isEnabled(settings, Role.CLOCK);
|
||||
boolean dateEnabled = isEnabled(settings, Role.DATE);
|
||||
if (!clockEnabled && !dateEnabled) {
|
||||
for (TextView textView : new ArrayList<>(trackedRoles.keySet())) {
|
||||
restore(textView);
|
||||
untrack(textView);
|
||||
}
|
||||
return;
|
||||
}
|
||||
registerReceiverIfNeeded(root.getContext());
|
||||
|
||||
Candidate clock = null;
|
||||
Candidate date = null;
|
||||
for (TextView textView : textViewsIn(root)) {
|
||||
if (isStatusbarClock(textView)) {
|
||||
continue;
|
||||
}
|
||||
int clockScore = scoreClock(textView);
|
||||
if (clockScore > 0 && (clock == null || clockScore > clock.score)) {
|
||||
clock = new Candidate(textView, clockScore);
|
||||
}
|
||||
int dateScore = scoreDate(textView);
|
||||
if (dateScore > 0 && (date == null || dateScore > date.score)) {
|
||||
date = new Candidate(textView, dateScore);
|
||||
}
|
||||
}
|
||||
|
||||
Set<TextView> selected = Collections.newSetFromMap(new WeakHashMap<>());
|
||||
if (clock != null) {
|
||||
selected.add(clock.view);
|
||||
track(clock.view, Role.CLOCK);
|
||||
}
|
||||
if (date != null && (clock == null || date.view != clock.view)) {
|
||||
selected.add(date.view);
|
||||
track(date.view, Role.DATE);
|
||||
}
|
||||
|
||||
for (TextView textView : new ArrayList<>(trackedRoles.keySet())) {
|
||||
if (!selected.contains(textView)) {
|
||||
restore(textView);
|
||||
untrack(textView);
|
||||
}
|
||||
}
|
||||
for (TextView textView : new ArrayList<>(trackedRoles.keySet())) {
|
||||
reapplyTrackedText(textView, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void track(TextView textView, Role role) {
|
||||
if (textView == null || role == null) {
|
||||
return;
|
||||
}
|
||||
Role previous = trackedRoles.put(textView, role);
|
||||
if (previous == null || previous != role || !stockSnapshots.containsKey(textView)) {
|
||||
captureSnapshot(textView);
|
||||
}
|
||||
}
|
||||
|
||||
private void untrack(TextView textView) {
|
||||
trackedRoles.remove(textView);
|
||||
stockSnapshots.remove(textView);
|
||||
ownedTexts.remove(textView);
|
||||
stopTicker(textView);
|
||||
}
|
||||
|
||||
private void reapplyTrackedText(TextView textView, boolean stockTextJustChanged, boolean forceWrite) {
|
||||
Role role = trackedRoles.get(textView);
|
||||
if (role == null) {
|
||||
return;
|
||||
}
|
||||
if (stockTextJustChanged) {
|
||||
captureSnapshot(textView);
|
||||
}
|
||||
SbtSettings settings = RuntimeSettingsCache.get(textView.getContext());
|
||||
if (!isEnabled(settings, role)) {
|
||||
restore(textView);
|
||||
return;
|
||||
}
|
||||
RenderedText rendered = render(textView.getContext(), pattern(settings, role), textView.getCurrentTextColor());
|
||||
if (rendered == null) {
|
||||
return;
|
||||
}
|
||||
writeText(textView, rendered.text, forceWrite);
|
||||
if (!TextUtils.equals(textView.getContentDescription(), rendered.contentDescription)) {
|
||||
textView.setContentDescription(rendered.contentDescription);
|
||||
}
|
||||
ownedTexts.add(textView);
|
||||
if (ClockMarkupSupport.containsSecondsPattern(pattern(settings, role))) {
|
||||
startTicker(textView);
|
||||
} else {
|
||||
stopTicker(textView);
|
||||
}
|
||||
}
|
||||
|
||||
private RenderedText render(Context context, String pattern, int referenceColor) {
|
||||
if (context == null || TextUtils.isEmpty(pattern)) {
|
||||
return null;
|
||||
}
|
||||
ClockParsedPattern parsedPattern = patternParser.parse(pattern);
|
||||
ArrayList<ClockTextRenderer.RowResult> rows =
|
||||
textRenderer.renderRows(context, parsedPattern, referenceColor);
|
||||
if (rows.isEmpty()) {
|
||||
return new RenderedText("", "");
|
||||
}
|
||||
SpannableStringBuilder text = new SpannableStringBuilder();
|
||||
StringBuilder description = new StringBuilder();
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
if (i > 0) {
|
||||
text.append('\n');
|
||||
description.append('\n');
|
||||
}
|
||||
ClockTextRenderer.RowResult row = rows.get(i);
|
||||
text.append(row.text);
|
||||
description.append(row.contentDescription);
|
||||
}
|
||||
return new RenderedText(text, description.toString());
|
||||
}
|
||||
|
||||
private void restore(TextView textView) {
|
||||
if (textView == null || !ownedTexts.contains(textView)) {
|
||||
return;
|
||||
}
|
||||
Snapshot snapshot = stockSnapshots.get(textView);
|
||||
if (snapshot == null) {
|
||||
ownedTexts.remove(textView);
|
||||
return;
|
||||
}
|
||||
writeText(textView, snapshot.text, false);
|
||||
if (!TextUtils.equals(textView.getContentDescription(), snapshot.contentDescription)) {
|
||||
textView.setContentDescription(snapshot.contentDescription);
|
||||
}
|
||||
ownedTexts.remove(textView);
|
||||
stopTicker(textView);
|
||||
}
|
||||
|
||||
private void captureSnapshot(TextView textView) {
|
||||
if (textView == null || ownTextWriteDepth > 0) {
|
||||
return;
|
||||
}
|
||||
stockSnapshots.put(textView, new Snapshot(
|
||||
textView.getText(),
|
||||
textView.getContentDescription()));
|
||||
}
|
||||
|
||||
private void writeText(TextView textView, CharSequence text, boolean force) {
|
||||
if (!force && sameText(textView.getText(), text)) {
|
||||
return;
|
||||
}
|
||||
ownTextWriteDepth++;
|
||||
try {
|
||||
textView.setText(text);
|
||||
} finally {
|
||||
ownTextWriteDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
private void registerReceiverIfNeeded(Context context) {
|
||||
if (context == null || receiverRegistered) {
|
||||
return;
|
||||
}
|
||||
BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context ctx, Intent intent) {
|
||||
refreshTrackedTexts();
|
||||
}
|
||||
};
|
||||
BroadcastReceiverSupport.registerExportedOnApplicationContext(
|
||||
context,
|
||||
receiver,
|
||||
BroadcastReceiverSupport.filter(
|
||||
SbtSettings.ACTION_SETTINGS_CHANGED,
|
||||
Intent.ACTION_USER_UNLOCKED,
|
||||
Intent.ACTION_BATTERY_CHANGED,
|
||||
Intent.ACTION_TIME_TICK,
|
||||
Intent.ACTION_TIME_CHANGED,
|
||||
Intent.ACTION_TIMEZONE_CHANGED,
|
||||
Intent.ACTION_LOCALE_CHANGED,
|
||||
Intent.ACTION_CONFIGURATION_CHANGED));
|
||||
receiverRegistered = true;
|
||||
}
|
||||
|
||||
private void refreshTrackedTexts() {
|
||||
for (TextView textView : new ArrayList<>(trackedRoles.keySet())) {
|
||||
reapplyTrackedText(textView, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
private void startTicker(TextView textView) {
|
||||
Runnable ticker = tickers.get(textView);
|
||||
if (ticker == null) {
|
||||
ticker = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (textView.getHandler() == null || !textView.isAttachedToWindow()) {
|
||||
stopTicker(textView);
|
||||
return;
|
||||
}
|
||||
reapplyTrackedText(textView, false, false);
|
||||
scheduleNextTick(textView, this);
|
||||
}
|
||||
};
|
||||
tickers.put(textView, ticker);
|
||||
}
|
||||
textView.removeCallbacks(ticker);
|
||||
scheduleNextTick(textView, ticker);
|
||||
}
|
||||
|
||||
private void stopTicker(TextView textView) {
|
||||
Runnable ticker = tickers.remove(textView);
|
||||
if (ticker != null) {
|
||||
textView.removeCallbacks(ticker);
|
||||
}
|
||||
}
|
||||
|
||||
private void scheduleNextTick(TextView textView, Runnable ticker) {
|
||||
long now = System.currentTimeMillis();
|
||||
long next = now - (now % 1000L) + 1000L;
|
||||
textView.postDelayed(ticker, Math.max(1L, next - now));
|
||||
}
|
||||
|
||||
private boolean sameText(CharSequence left, CharSequence right) {
|
||||
return TextUtils.equals(left != null ? left.toString() : "", right != null ? right.toString() : "");
|
||||
}
|
||||
|
||||
private boolean isEnabled(SbtSettings settings, Role role) {
|
||||
String pattern = pattern(settings, role);
|
||||
if (TextUtils.isEmpty(pattern)) {
|
||||
return false;
|
||||
}
|
||||
return role == Role.CLOCK
|
||||
? settings.drawerClockCustomFormatEnabled
|
||||
: settings.drawerDateCustomFormatEnabled;
|
||||
}
|
||||
|
||||
private String pattern(SbtSettings settings, Role role) {
|
||||
if (settings == null || role == null) {
|
||||
return "";
|
||||
}
|
||||
return role == Role.CLOCK
|
||||
? settings.drawerClockCustomFormat
|
||||
: settings.drawerDateCustomFormat;
|
||||
}
|
||||
|
||||
private Iterable<TextView> textViewsIn(View root) {
|
||||
ArrayList<TextView> textViews = new ArrayList<>();
|
||||
if (!(root instanceof ViewGroup rootGroup)) {
|
||||
if (root instanceof TextView textView) {
|
||||
textViews.add(textView);
|
||||
}
|
||||
return textViews;
|
||||
}
|
||||
ArrayDeque<View> queue = new ArrayDeque<>();
|
||||
queue.add(rootGroup);
|
||||
while (!queue.isEmpty()) {
|
||||
View view = queue.removeFirst();
|
||||
if (view instanceof TextView textView) {
|
||||
textViews.add(textView);
|
||||
}
|
||||
if (view instanceof ViewGroup group) {
|
||||
for (int i = 0; i < group.getChildCount(); i++) {
|
||||
View child = group.getChildAt(i);
|
||||
if (child != null) {
|
||||
queue.add(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return textViews;
|
||||
}
|
||||
|
||||
private int scoreClock(TextView textView) {
|
||||
String metadata = metadata(textView);
|
||||
if (metadata.contains("date") || metadata.contains("day") || isKeyguardOrAodText(metadata)) {
|
||||
return 0;
|
||||
}
|
||||
int score = 0;
|
||||
if (metadata.contains("shade") || metadata.contains("qs") || metadata.contains("quick")) {
|
||||
score += 90;
|
||||
}
|
||||
if (metadata.contains("header_clock") || metadata.contains("clock_header")) {
|
||||
score += 120;
|
||||
}
|
||||
if (metadata.contains("clock")) {
|
||||
score += 70;
|
||||
}
|
||||
CharSequence text = textView.getText();
|
||||
if (text != null && TIME_TEXT_PATTERN.matcher(text).find()) {
|
||||
score += 40;
|
||||
}
|
||||
if (score <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return score + Math.round(textView.getTextSize());
|
||||
}
|
||||
|
||||
private int scoreDate(TextView textView) {
|
||||
String metadata = metadata(textView);
|
||||
if (metadata.contains("clock") || isKeyguardOrAodText(metadata)) {
|
||||
return 0;
|
||||
}
|
||||
int score = 0;
|
||||
if (metadata.contains("date")) {
|
||||
score += 100;
|
||||
}
|
||||
if (metadata.contains("day")) {
|
||||
score += 40;
|
||||
}
|
||||
CharSequence text = textView.getText();
|
||||
if (text != null && looksLikeDate(text.toString())) {
|
||||
score += 30;
|
||||
}
|
||||
if (score <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return score + Math.round(textView.getTextSize());
|
||||
}
|
||||
|
||||
private boolean looksLikeDate(String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return false;
|
||||
}
|
||||
String lower = text.toLowerCase(Locale.ROOT);
|
||||
return lower.contains(",")
|
||||
|| lower.matches(".*\\b(mon|tue|wed|thu|fri|sat|sun)\\b.*")
|
||||
|| lower.matches(".*\\b(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)\\b.*");
|
||||
}
|
||||
|
||||
private String metadata(TextView textView) {
|
||||
return (ViewIdNames.idName(textView) + ' ' + textView.getClass().getName())
|
||||
.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private boolean isStatusbarClock(TextView textView) {
|
||||
String className = textView.getClass().getName();
|
||||
return className.contains("QSClockIndicatorView");
|
||||
}
|
||||
|
||||
private boolean isKeyguardOrAodText(String metadata) {
|
||||
return metadata.contains("keyguard")
|
||||
|| metadata.contains("aod")
|
||||
|| metadata.contains("plugin")
|
||||
|| metadata.contains("facewidget");
|
||||
}
|
||||
|
||||
private boolean isNotificationShadeRoot(View root) {
|
||||
return root != null && root.getClass().getName().contains("NotificationShadeWindowView");
|
||||
}
|
||||
|
||||
private enum Role {
|
||||
CLOCK,
|
||||
DATE
|
||||
}
|
||||
|
||||
private record Candidate(TextView view, int score) {
|
||||
}
|
||||
|
||||
private record RenderedText(CharSequence text, String contentDescription) {
|
||||
}
|
||||
|
||||
private static final class Snapshot {
|
||||
final CharSequence text;
|
||||
final CharSequence contentDescription;
|
||||
|
||||
Snapshot(CharSequence text, CharSequence contentDescription) {
|
||||
this.text = text != null ? text : "";
|
||||
this.contentDescription = contentDescription != null ? contentDescription : "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,10 @@ public final class SbtDefaults {
|
||||
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 DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
||||
public static final String DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT = "";
|
||||
public static final boolean DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT = false;
|
||||
public static final String DRAWER_DATE_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;
|
||||
|
||||
@@ -98,6 +98,12 @@ public final class SbtSettings {
|
||||
public static final String KEY_CLOCK_SHOW_DATE_PREFIX = "clock_show_date_prefix";
|
||||
public static final String KEY_CLOCK_CUSTOM_FORMAT_ENABLED = "clock_custom_format_enabled";
|
||||
public static final String KEY_CLOCK_CUSTOM_FORMAT = "clock_custom_format";
|
||||
public static final String KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED =
|
||||
"drawer_clock_custom_format_enabled";
|
||||
public static final String KEY_DRAWER_CLOCK_CUSTOM_FORMAT = "drawer_clock_custom_format";
|
||||
public static final String KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED =
|
||||
"drawer_date_custom_format_enabled";
|
||||
public static final String KEY_DRAWER_DATE_CUSTOM_FORMAT = "drawer_date_custom_format";
|
||||
public static final String KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS = "clock_ignore_under_display_cameras";
|
||||
public static final String KEY_CLOCK_CAMERA_TYPE_OVERRIDES = "clock_camera_type_overrides";
|
||||
public static final String KEY_LAYOUT_PADDING_CUTOUT_PX = "layout_padding_cutout_px";
|
||||
@@ -326,6 +332,10 @@ public final class SbtSettings {
|
||||
public final boolean clockShowDatePrefix;
|
||||
public final boolean clockCustomFormatEnabled;
|
||||
public final String clockCustomFormat;
|
||||
public final boolean drawerClockCustomFormatEnabled;
|
||||
public final String drawerClockCustomFormat;
|
||||
public final boolean drawerDateCustomFormatEnabled;
|
||||
public final String drawerDateCustomFormat;
|
||||
public final boolean clockIgnoreUnderDisplayCameras;
|
||||
public final int layoutPaddingCutoutPx;
|
||||
public final int layoutPaddingLeftPx;
|
||||
@@ -452,6 +462,10 @@ public final class SbtSettings {
|
||||
boolean clockShowDatePrefix,
|
||||
boolean clockCustomFormatEnabled,
|
||||
String clockCustomFormat,
|
||||
boolean drawerClockCustomFormatEnabled,
|
||||
String drawerClockCustomFormat,
|
||||
boolean drawerDateCustomFormatEnabled,
|
||||
String drawerDateCustomFormat,
|
||||
boolean clockIgnoreUnderDisplayCameras,
|
||||
int layoutPaddingCutoutPx,
|
||||
int layoutPaddingLeftPx,
|
||||
@@ -622,6 +636,14 @@ public final class SbtSettings {
|
||||
this.clockCustomFormat = clockCustomFormat != null
|
||||
? clockCustomFormat
|
||||
: SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT;
|
||||
this.drawerClockCustomFormatEnabled = drawerClockCustomFormatEnabled;
|
||||
this.drawerClockCustomFormat = drawerClockCustomFormat != null
|
||||
? drawerClockCustomFormat
|
||||
: SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT;
|
||||
this.drawerDateCustomFormatEnabled = drawerDateCustomFormatEnabled;
|
||||
this.drawerDateCustomFormat = drawerDateCustomFormat != null
|
||||
? drawerDateCustomFormat
|
||||
: SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT;
|
||||
this.clockIgnoreUnderDisplayCameras = clockIgnoreUnderDisplayCameras;
|
||||
this.layoutPaddingCutoutPx = layoutPaddingCutoutPx;
|
||||
this.layoutPaddingLeftPx = layoutPaddingLeftPx;
|
||||
@@ -871,6 +893,10 @@ public final class SbtSettings {
|
||||
SbtDefaults.CLOCK_SHOW_DATE_PREFIX_DEFAULT,
|
||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT,
|
||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT,
|
||||
SbtDefaults.CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT,
|
||||
SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT,
|
||||
SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT,
|
||||
@@ -1158,6 +1184,14 @@ public final class SbtSettings {
|
||||
prefs.getBoolean(KEY_CLOCK_SHOW_DATE_PREFIX, SbtDefaults.CLOCK_SHOW_DATE_PREFIX_DEFAULT),
|
||||
prefs.getBoolean(KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
prefs.getString(KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT),
|
||||
prefs.getBoolean(KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
prefs.getString(KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT),
|
||||
prefs.getBoolean(KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
prefs.getString(KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT),
|
||||
prefs.getBoolean(KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS, SbtDefaults.CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT),
|
||||
prefs.getInt(KEY_LAYOUT_PADDING_CUTOUT_PX, SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT),
|
||||
prefs.getInt(KEY_LAYOUT_PADDING_LEFT_PX, SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT),
|
||||
@@ -1464,6 +1498,14 @@ public final class SbtSettings {
|
||||
bundle.getBoolean(KEY_CLOCK_SHOW_DATE_PREFIX, SbtDefaults.CLOCK_SHOW_DATE_PREFIX_DEFAULT),
|
||||
bundle.getBoolean(KEY_CLOCK_CUSTOM_FORMAT_ENABLED, SbtDefaults.CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
bundle.getString(KEY_CLOCK_CUSTOM_FORMAT, SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT),
|
||||
bundle.getBoolean(KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
bundle.getString(KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT),
|
||||
bundle.getBoolean(KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT),
|
||||
bundle.getString(KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT),
|
||||
bundle.getBoolean(KEY_CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS, SbtDefaults.CLOCK_IGNORE_UNDER_DISPLAY_CAMERAS_DEFAULT),
|
||||
bundle.getInt(KEY_LAYOUT_PADDING_CUTOUT_PX, SbtDefaults.LAYOUT_PADDING_CUTOUT_PX_DEFAULT),
|
||||
bundle.getInt(KEY_LAYOUT_PADDING_LEFT_PX, SbtDefaults.LAYOUT_PADDING_LEFT_PX_DEFAULT),
|
||||
|
||||
@@ -146,6 +146,22 @@ public class SbtSettingsProvider extends ContentProvider {
|
||||
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_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
prefs.getBoolean(
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT));
|
||||
out.putBoolean(SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
prefs.getBoolean(
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT));
|
||||
out.putString(SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
prefs.getString(
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_DATE_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));
|
||||
|
||||
@@ -55,6 +55,18 @@ public final class ClockFragment extends Fragment {
|
||||
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);
|
||||
CheckBox drawerClockCustomFormatEnabled =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_enabled);
|
||||
View drawerClockCustomFormatContainer =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_container);
|
||||
EditText drawerClockCustomFormatInput =
|
||||
root.findViewById(R.id.drawer_clock_custom_format_input);
|
||||
CheckBox drawerDateCustomFormatEnabled =
|
||||
root.findViewById(R.id.drawer_date_custom_format_enabled);
|
||||
View drawerDateCustomFormatContainer =
|
||||
root.findViewById(R.id.drawer_date_custom_format_container);
|
||||
EditText drawerDateCustomFormatInput =
|
||||
root.findViewById(R.id.drawer_date_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);
|
||||
@@ -69,10 +81,40 @@ public final class ClockFragment extends Fragment {
|
||||
String customFormatValue = prefs.getString(
|
||||
SbtSettings.KEY_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.CLOCK_CUSTOM_FORMAT_DEFAULT);
|
||||
boolean drawerClockCustomFormatEnabledValue = prefs.getBoolean(
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED_DEFAULT);
|
||||
String drawerClockCustomFormatValue = prefs.getString(
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_CLOCK_CUSTOM_FORMAT_DEFAULT);
|
||||
boolean drawerDateCustomFormatEnabledValue = prefs.getBoolean(
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_ENABLED_DEFAULT);
|
||||
String drawerDateCustomFormatValue = prefs.getString(
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
SbtDefaults.DRAWER_DATE_CUSTOM_FORMAT_DEFAULT);
|
||||
|
||||
customFormatEnabled.setChecked(customFormatEnabledValue);
|
||||
customFormatInput.setText(customFormatValue);
|
||||
updateCustomFormatVisibility(customFormatContainer, customFormatEnabledValue);
|
||||
bindCustomFormat(
|
||||
prefs,
|
||||
drawerClockCustomFormatEnabled,
|
||||
drawerClockCustomFormatContainer,
|
||||
drawerClockCustomFormatInput,
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT_ENABLED,
|
||||
SbtSettings.KEY_DRAWER_CLOCK_CUSTOM_FORMAT,
|
||||
drawerClockCustomFormatEnabledValue,
|
||||
drawerClockCustomFormatValue);
|
||||
bindCustomFormat(
|
||||
prefs,
|
||||
drawerDateCustomFormatEnabled,
|
||||
drawerDateCustomFormatContainer,
|
||||
drawerDateCustomFormatInput,
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT_ENABLED,
|
||||
SbtSettings.KEY_DRAWER_DATE_CUSTOM_FORMAT,
|
||||
drawerDateCustomFormatEnabledValue,
|
||||
drawerDateCustomFormatValue);
|
||||
updateCustomFormatHelpVisibility(customFormatHelpContainer, customFormatHelpToggle, false);
|
||||
applyBulletText(customFormatHelpCommon);
|
||||
applyBulletText(customFormatHelpStyling);
|
||||
@@ -126,6 +168,44 @@ public final class ClockFragment extends Fragment {
|
||||
customFormatContainer.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
private void bindCustomFormat(SharedPreferences prefs,
|
||||
CheckBox enabledView,
|
||||
View container,
|
||||
EditText input,
|
||||
String enabledKey,
|
||||
String formatKey,
|
||||
boolean enabled,
|
||||
String format) {
|
||||
if (enabledView == null || input == null) {
|
||||
return;
|
||||
}
|
||||
enabledView.setChecked(enabled);
|
||||
input.setText(format != null ? format : "");
|
||||
updateCustomFormatVisibility(container, enabled);
|
||||
enabledView.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
updateCustomFormatVisibility(container, isChecked);
|
||||
prefs.edit().putBoolean(enabledKey, isChecked).apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
});
|
||||
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) {
|
||||
prefs.edit()
|
||||
.putString(formatKey, s != null ? s.toString() : "")
|
||||
.apply();
|
||||
SbtSettings.ensureReadable(requireContext());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCustomFormatHelpVisibility(View helpContainer,
|
||||
TextView helpToggle,
|
||||
boolean visible) {
|
||||
|
||||
@@ -74,12 +74,71 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/clock_font_picker_button" />
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/drawer_clock_custom_format_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/drawer_clock_custom_format_enabled" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/drawer_clock_custom_format_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_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" />
|
||||
</LinearLayout>
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/drawer_date_custom_format_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@string/drawer_date_custom_format_enabled" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/drawer_date_custom_format_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/drawer_date_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" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/clock_custom_format_help_toggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/clock_custom_format_help_show"
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
|
||||
@@ -125,7 +184,6 @@
|
||||
android:textAppearance="?attr/textAppearanceCaption" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
@@ -237,6 +237,8 @@
|
||||
<string name="clock_middle_side_right">Right side</string>
|
||||
<string name="clock_custom_format_title">Custom format</string>
|
||||
<string name="clock_custom_format_enabled">Use custom date/time pattern</string>
|
||||
<string name="drawer_clock_custom_format_enabled">Use custom drawer clock pattern</string>
|
||||
<string name="drawer_date_custom_format_enabled">Use custom drawer date pattern</string>
|
||||
<string name="clock_custom_format_hint">Examples: HH:mm, EEE d MMM HH:mm:ss, {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big @sans-serif-condensed}HH:mm</string>
|
||||
<string name="clock_custom_format_help_common">Date/time patterns:\n\u2022 Uses Java SimpleDateFormat-style pattern letters\n\u2022 Common symbols: yyyy yy MMMM MMM MM EEEE EEE dd d HH hh mm ss a\n\nTags:\n\u2022 Supported tags: <i>, <u>, <small>, <big>, <font color=\'...\'>, <font face=\'...\'>\n\u2022 <b> is not supported\n\u2022 Custom font tags: <sans>, <serif>, <mono>, <condensed>\n\u2022 {/font} closes any open <font ...> tag\n\u2022 Opening a new colour or font face automatically closes the previous one</string>
|
||||
<string name="clock_custom_format_help_styling">Shorthand blocks:\n\u2022 {#FA0 big @sans-serif-condensed} is shorthand for opening multiple tags in order\n\u2022 {#FA0} is shorthand for {color(#FA0)}\n\u2022 In shorthand: / closes a tag, color(...) sets font colour, @ sets font face\n\u2022 Each whitespace-separated token inside {} becomes its own tag, except color(...) which may contain spaces\n\nLine breaks:\n\u2022 Use \\n, {\\n} or {\\n15}; the number is the line-height delta in px\n\u2022 Omit the number to use the default row spacing\n\u2022 { /\\n } style line breaks are written without spaces: {/\\n} or {/\\n15}\n\u2022 Normal line breaks propagate open tags onto the next row\n\u2022 A / line break ends all currently open tags before the next row starts\n\nPipes and grouping:\n\u2022 Maximum two pipes per row; more than two is a syntax error for that row only\n\u2022 Pipes inside single quotes or {} do not split the row\n\u2022 One pipe splits into left and right segments\n\u2022 Two pipes remove the middle section, then split using the outer pipes\n\u2022 Tags are logically continued across pipe splits, even if the middle section is ignored\n\u2022 Rows without pipes are independent; rows with pipes share one common pipe group\n\u2022 A syntax-error row becomes the text Syntax error, with no whole-clock fallback\n\nColours:\n\u2022 Supported operands: #RGB, #ARGB, #RRGGBB, #AARRGGBB, #batterybar, [R,G,B], [A,R,G,B], and numbers\n\u2022 Colour expressions use reverse Polish notation: operands first, operator last\n\u2022 Use ; to separate bright/dark variants; the dark side starts with the raw result from the bright side on the stack\n\u2022 Operators: + - * / ^ inv min max sqrt clip dup\n\u2022 Operators can be channel-filtered, for example +A, *RGB, invRGB, maxRGB, clipA\n\u2022 Final values are clipped to 0..255; a final number becomes opaque grayscale</string>
|
||||
|
||||
Reference in New Issue
Block a user