From 544a2cc56a3d14fef53bd6da4684bb2d500d6333 Mon Sep 17 00:00:00 2001 From: ajp_anton Date: Sun, 7 Jun 2026 12:33:15 +0000 Subject: [PATCH] Customize drawer clock text --- .../runtime/features/clock/ClockFeature.java | 5 + .../clock/DrawerClockTextController.java | 451 ++++++++++++++++++ .../shell/settings/SbtDefaults.java | 4 + .../shell/settings/SbtSettings.java | 42 ++ .../shell/settings/SbtSettingsProvider.java | 16 + .../shell/ui/ClockFragment.java | 80 ++++ app/src/main/res/layout/fragment_clock.xml | 138 ++++-- app/src/main/res/values/strings.xml | 2 + 8 files changed, 698 insertions(+), 40 deletions(-) create mode 100644 app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockFeature.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockFeature.java index 8177dff..d08fcb7 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockFeature.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/ClockFeature.java @@ -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); } } diff --git a/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java new file mode 100644 index 0000000..d9c2e1c --- /dev/null +++ b/app/src/main/java/se/ajpanton/statusbartweak/runtime/features/clock/DrawerClockTextController.java @@ -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 trackedRoles = new WeakHashMap<>(); + private final Map stockSnapshots = new WeakHashMap<>(); + private final Map tickers = new WeakHashMap<>(); + private final Set 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 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 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 textViewsIn(View root) { + ArrayList textViews = new ArrayList<>(); + if (!(root instanceof ViewGroup rootGroup)) { + if (root instanceof TextView textView) { + textViews.add(textView); + } + return textViews; + } + ArrayDeque 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 : ""; + } + } +} diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java index 2941d34..5cf513b 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtDefaults.java @@ -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; diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java index 8e5357d..2ade2e3 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettings.java @@ -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), diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java index 2ac6d57..930d171 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/settings/SbtSettingsProvider.java @@ -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)); diff --git a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java index 07c2d9d..d7532e5 100644 --- a/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java +++ b/app/src/main/java/se/ajpanton/statusbartweak/shell/ui/ClockFragment.java @@ -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) { diff --git a/app/src/main/res/layout/fragment_clock.xml b/app/src/main/res/layout/fragment_clock.xml index 586d8ab..c0e40f9 100644 --- a/app/src/main/res/layout/fragment_clock.xml +++ b/app/src/main/res/layout/fragment_clock.xml @@ -74,56 +74,114 @@ android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/clock_font_picker_button" /> + + + + + + + + + + + + + + + + + + + + + - + android:layout_marginTop="4dp" + android:text="@string/clock_custom_format_help_examples" + android:textAppearance="?attr/textAppearanceCaption" /> - - - - - - - - + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cbb2ea9..9e594b2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -237,6 +237,8 @@ Right side Custom format Use custom date/time pattern + Use custom drawer clock pattern + Use custom drawer date pattern Examples: HH:mm, EEE d MMM HH:mm:ss, {color(#batterybar 1.2 * ; invRGB 0.5 *RGB) big @sans-serif-condensed}HH:mm 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 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