diff --git a/core/java/android/content/res/TypedArray.java b/core/java/android/content/res/TypedArray.java index e1b07858d2d84..4b3f1a633de3f 100644 --- a/core/java/android/content/res/TypedArray.java +++ b/core/java/android/content/res/TypedArray.java @@ -1388,7 +1388,12 @@ private boolean getValueAt(int index, TypedValue outValue) { outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava( data[index + STYLE_CHANGING_CONFIGURATIONS]); outValue.density = data[index + STYLE_DENSITY]; - outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null; + try { + outValue.string = (type == TypedValue.TYPE_STRING) ? loadStringValueAt(index) : null; + } catch (IndexOutOfBoundsException e) { + android.util.Log.e("TypedArray", "Caught IndexOutOfBoundsException loading string value (likely due to rapid theme/overlay changes). Returning null.", e); + outValue.string = null; + } outValue.sourceResourceId = data[index + STYLE_SOURCE_RESOURCE_ID]; return true; } diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 10a2941abd7ea..8f276d00b97b6 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -597,9 +597,15 @@ public static String userActivityEventToString(@UserActivityEvent int userActivi public static final int GO_TO_SLEEP_REASON_UNKNOWN = 14; /** + * Go to sleep reason code: Going to sleep due to a touch tap or double tap. * @hide */ - public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_UNKNOWN; + public static final int GO_TO_SLEEP_REASON_TOUCH = 15; + + /** + * @hide + */ + public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_TOUCH; /** * @hide @@ -620,6 +626,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { case GO_TO_SLEEP_REASON_QUIESCENT: return "quiescent"; case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button"; case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout"; + case GO_TO_SLEEP_REASON_TOUCH: return "touch"; case GO_TO_SLEEP_REASON_UNKNOWN: return "unknown"; default: return Integer.toString(sleepReason); } @@ -733,6 +740,7 @@ public static String sleepReasonToString(@GoToSleepReason int sleepReason) { GO_TO_SLEEP_REASON_QUIESCENT, GO_TO_SLEEP_REASON_SLEEP_BUTTON, GO_TO_SLEEP_REASON_TIMEOUT, + GO_TO_SLEEP_REASON_TOUCH, GO_TO_SLEEP_REASON_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fe142d13cc1cb..313fadb72bf40 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -15569,6 +15569,32 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String LOCKSCREEN_SMARTSPACE_ENABLED = "lockscreen_smartspace_enabled"; + /** + * Selects the lockscreen smartspace data source. + * + * + * + * @hide + */ + public static final String LOCKSCREEN_SMARTSPACE_SOURCE = "lockscreen_smartspace_source"; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_AXION = 0; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_GOOGLE = 1; + + /** @hide */ + public static final int LOCKSCREEN_SMARTSPACE_SOURCE_NONE = 2; + /** * Whether the feature that the device will fire a haptic when users scroll and hit * the edge of the screen is enabled. diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index eb1c6bdbdfe94..81f4d02c1e079 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -873,6 +873,17 @@ public long getExpectedPresentationTimeMillis() { * @hide */ public long getLatestExpectedPresentTimeNanos() { + // mLastVsyncEventData is refreshed at the start of every doFrame + // (see copyFrom() under mLock at the end of the synchronized block in doFrame). + // Once at least one frame has run, the cached value is identical to what the + // IPC would return for the same vsync, so we can skip the binder round-trip on + // the UI thread. Same staleness contract as getFrameDeadline() / getVsyncId(), + // which already trust this field unconditionally. mLastFrameTimeNanos is + // initialized to Long.MIN_VALUE in the constructor and only becomes positive + // once doFrame has populated mLastVsyncEventData via copyFrom(). + if (mLastFrameTimeNanos > 0L) { + return mLastVsyncEventData.preferredFrameTimeline().expectedPresentationTime; + } if (mDisplayEventReceiver == null) { return System.nanoTime(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 9486887f3e01e..9247f5af2b376 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -708,8 +708,7 @@ default void onConfigurationChanged(@NonNull Configuration overrideConfig, // This is used to reduce the race between window focus changes being dispatched from // the window manager and input events coming through the input system. - @GuardedBy("this") - boolean mWindowFocusChanged; + volatile boolean mWindowFocusChanged; @GuardedBy("this") boolean mUpcomingWindowFocus; @GuardedBy("this") @@ -4915,6 +4914,9 @@ private void maybeHandleWindowMove(Rect frame) { } private void handleWindowFocusChanged() { + if (!mWindowFocusChanged) { + return; + } final boolean hasWindowFocus; synchronized (this) { if (!mWindowFocusChanged) { diff --git a/core/res/res/values-bg-rBG/alpha_strings.xml b/core/res/res/values-bg-rBG/alpha_strings.xml index ed7bbcb56202b..409a1b6781521 100644 --- a/core/res/res/values-bg-rBG/alpha_strings.xml +++ b/core/res/res/values-bg-rBG/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Не сега - Рестартиране на launcher-a за прилагане на промените... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-cs-rCZ/alpha_strings.xml b/core/res/res/values-cs-rCZ/alpha_strings.xml index 2e454b207ff5a..09ea4c9b0801d 100644 --- a/core/res/res/values-cs-rCZ/alpha_strings.xml +++ b/core/res/res/values-cs-rCZ/alpha_strings.xml @@ -17,9 +17,6 @@ Je vyžadován restart SystemUI For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Teď ne - Restartuje se SystemUI, aby se projevily změny… Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-de-rDE/alpha_strings.xml b/core/res/res/values-de-rDE/alpha_strings.xml index 0e5c81101faaa..f69841e0ff784 100644 --- a/core/res/res/values-de-rDE/alpha_strings.xml +++ b/core/res/res/values-de-rDE/alpha_strings.xml @@ -17,9 +17,6 @@ System-UI-Neustart erforderlich For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Später - Neustart der Systemoberfläche, um Änderungen anzuwenden... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-el-rGR/alpha_strings.xml b/core/res/res/values-el-rGR/alpha_strings.xml index ffe157a000e17..409a1b6781521 100644 --- a/core/res/res/values-el-rGR/alpha_strings.xml +++ b/core/res/res/values-el-rGR/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Όχι τώρα - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-es-rES/alpha_strings.xml b/core/res/res/values-es-rES/alpha_strings.xml index a1f71638efce8..f1b80fbf8315c 100644 --- a/core/res/res/values-es-rES/alpha_strings.xml +++ b/core/res/res/values-es-rES/alpha_strings.xml @@ -17,9 +17,6 @@ Se requiere el reinicio de la interfaz de usuario del sistema Para que todos los cambios surtan efecto, es necesario reiniciar SystemUI. ¿Reiniciar SystemUI ahora? - Si - Ahora no - Reiniciando interfaz de usuario para aplicar los cambios... Es necesario reiniciar el lanzador Para que todos los cambios surtan efecto, es necesario reiniciar el lanzador. ¿Reiniciar el lanzador ahora? diff --git a/core/res/res/values-fr-rFR/alpha_strings.xml b/core/res/res/values-fr-rFR/alpha_strings.xml index 83bbbe8f388ae..409a1b6781521 100644 --- a/core/res/res/values-fr-rFR/alpha_strings.xml +++ b/core/res/res/values-fr-rFR/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Oui - Plus tard - Redémarrer l\'interface système pour appliquer les changements... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-hi-rIN/alpha_strings.xml b/core/res/res/values-hi-rIN/alpha_strings.xml index 30092a5f8f98a..409a1b6781521 100644 --- a/core/res/res/values-hi-rIN/alpha_strings.xml +++ b/core/res/res/values-hi-rIN/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Not now - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-hu-rHU/alpha_strings.xml b/core/res/res/values-hu-rHU/alpha_strings.xml index be0ffaf958013..0f96f28189cf3 100644 --- a/core/res/res/values-hu-rHU/alpha_strings.xml +++ b/core/res/res/values-hu-rHU/alpha_strings.xml @@ -17,9 +17,6 @@ Rendszerfelület-újraindítás szükséges For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Igen - Most nem - Rendszerfelület-újraindítás a változások életbe léptetéséhez... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-in-rID/alpha_strings.xml b/core/res/res/values-in-rID/alpha_strings.xml index 4a4c00a68dfac..1b3bfcfcdf2f5 100644 --- a/core/res/res/values-in-rID/alpha_strings.xml +++ b/core/res/res/values-in-rID/alpha_strings.xml @@ -17,9 +17,6 @@ Diperlukan mengulang SystemUI Agar semua perubahan bisa terlihat, SystemUI butuh dijalankan ulang. Jalankan ulang SystemUI sekarang? - Ya - Tidak sekarang - Mulai ulang SystemUI untuk menerapkan perubahan... Peluncur butuh dijalankan ulang Agar semua perubahan bisa terlihat, Peluncur butuh dijalankan ulang. Jalankan ulang Peluncur sekarang? diff --git a/core/res/res/values-it-rIT/alpha_strings.xml b/core/res/res/values-it-rIT/alpha_strings.xml index fc11da48d79fe..75f6b0d9371e2 100644 --- a/core/res/res/values-it-rIT/alpha_strings.xml +++ b/core/res/res/values-it-rIT/alpha_strings.xml @@ -17,9 +17,6 @@ Riavvio SystemUI richiesto For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Non ora - Riavvio SystemUI per applicare i cambiamenti... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-pl-rPL/alpha_strings.xml b/core/res/res/values-pl-rPL/alpha_strings.xml index 64c1e731be3fd..89c46c856315b 100644 --- a/core/res/res/values-pl-rPL/alpha_strings.xml +++ b/core/res/res/values-pl-rPL/alpha_strings.xml @@ -17,9 +17,6 @@ Wymagany restart interfejsu systemowego For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Nie teraz - Ponowne uruchamianie interfejsu systemowego, aby zastosować zmiany... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-pt-rBR/alpha_strings.xml b/core/res/res/values-pt-rBR/alpha_strings.xml index 6c9f8ee125fa5..a027042a7b64c 100644 --- a/core/res/res/values-pt-rBR/alpha_strings.xml +++ b/core/res/res/values-pt-rBR/alpha_strings.xml @@ -17,9 +17,6 @@ É necessário reiniciar a Interface do Sistema Para que todas as alterações entrem em vigor, é necessário reiniciar o SystemUI. Reiniciar o SystemUI agora? - Sim - Agora não - Reiniciando a Interface do Sistema para aplicar as alterações... É necessário reiniciar o Launcher Para que todas as alterações entrem em vigor, é necessário reiniciar o Launcher. Reiniciar o Launcher agora? diff --git a/core/res/res/values-pt-rPT/alpha_strings.xml b/core/res/res/values-pt-rPT/alpha_strings.xml index 9ed423dc6711f..9b3a4929912d4 100644 --- a/core/res/res/values-pt-rPT/alpha_strings.xml +++ b/core/res/res/values-pt-rPT/alpha_strings.xml @@ -17,9 +17,6 @@ É necessário o reinício da UI do sistema É necessário reiniciar a interface do sistema para que todas as modificações tenham efeito. Reiniciar agora? - Sim - Agora não - A reiniciar a interface do sistema para aplicar as alterações... Reinício do launcher necessário É necessário reiniciar o launcher para que todas as modificações tenham efeito. Reiniciar agora? diff --git a/core/res/res/values-ro-rRO/alpha_strings.xml b/core/res/res/values-ro-rRO/alpha_strings.xml index 8d6b1e62efeb4..de419dfe94ed6 100644 --- a/core/res/res/values-ro-rRO/alpha_strings.xml +++ b/core/res/res/values-ro-rRO/alpha_strings.xml @@ -17,9 +17,6 @@ Este necesară repornirea interfeței sistemului For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Nu acum - Se repornește interfața sistem pentru a aplica modificările... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-sq-rAL/alpha_strings.xml b/core/res/res/values-sq-rAL/alpha_strings.xml index 30092a5f8f98a..409a1b6781521 100644 --- a/core/res/res/values-sq-rAL/alpha_strings.xml +++ b/core/res/res/values-sq-rAL/alpha_strings.xml @@ -17,9 +17,6 @@ SystemUI restart required For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - Not now - Restarting SystemUI to apply changes... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values-tr-rTR/alpha_strings.xml b/core/res/res/values-tr-rTR/alpha_strings.xml index 9acdc782c71ee..0df3601103093 100644 --- a/core/res/res/values-tr-rTR/alpha_strings.xml +++ b/core/res/res/values-tr-rTR/alpha_strings.xml @@ -17,9 +17,6 @@ Sistem arayüzünü yeniden başlatmak gerekli Tüm değişikliklerin etkili olabilmesi için, sistem arayüzü yeniden başlatılmalı. Yeniden başlatmak ister misiniz? - Evet - Şimdi değil - Değişiklikleri uygulamak için sistem arayüzü yeniden başlatılıyor... Başlatıcının yeniden başlatılması gerekiyor Tüm değişikliklerin etkili olması için başlatıcının yeniden başlatılması gerekiyor. Başlatıcıyı şimdi yeniden başlatmak ister misiniz? diff --git a/core/res/res/values-vi-rVN/alpha_strings.xml b/core/res/res/values-vi-rVN/alpha_strings.xml index 3ef578d28a55a..99f660d1b09de 100644 --- a/core/res/res/values-vi-rVN/alpha_strings.xml +++ b/core/res/res/values-vi-rVN/alpha_strings.xml @@ -7,20 +7,17 @@ An error occured while uploading the log - Digital Wellbeing + Sức khoẻ kỹ thuật số Settings restart required For all changes to take effect, a Settings app restart is required. Restart Settings app now? - System restart required - For all the changes to take effect, a system restart is required. Perform system restart now? + Yêu cầu khởi động lại hệ thống + Để áp dụng tất cả thay đổi, cần khởi động lại hệ thống. Thực hiện khởi động lại ngay? Yêu cầu khởi động lại giao diện hệ thống For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - - Không phải bây giờ - Đang mở lại giao diện hệ thống để áp dụng các thay đổi... - Launcher restart required - For all changes to take effect, a Launcher restart is required. Restart Launcher now? + Yêu cầu khởi động lại Trình khởi chạy + Để tất cả các thay đổi có hiệu lực, cần phải khởi động lại Trình khởi chạy. Khởi động lại Trình khởi chạy ngay bây giờ? diff --git a/core/res/res/values-zh-rCN/alpha_strings.xml b/core/res/res/values-zh-rCN/alpha_strings.xml index 0c40991660c9e..c40eae1cbb83e 100644 --- a/core/res/res/values-zh-rCN/alpha_strings.xml +++ b/core/res/res/values-zh-rCN/alpha_strings.xml @@ -17,9 +17,6 @@ 需要重启系统界面 要使所有更改生效,需要重新启动系统界面。现在重新启动系统界面吗? - - 暂不 - 正在重启系统界面以应用更改…… 需要重启系统桌面 要使所有更改生效,需要重新启动桌面。现在重新启动桌面吗? diff --git a/core/res/res/values-zh-rTW/alpha_strings.xml b/core/res/res/values-zh-rTW/alpha_strings.xml index ee1484547d328..7d28f2a1f3c07 100644 --- a/core/res/res/values-zh-rTW/alpha_strings.xml +++ b/core/res/res/values-zh-rTW/alpha_strings.xml @@ -17,9 +17,6 @@ 需要重新啟動系統介面 For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? - Yes - 暫時不要 - 正在重新啟動系統介面以套用變更... Launcher restart required For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index a4b5fb010acb6..9b88eb96dc801 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -146,17 +146,17 @@ false - 200 + 150 - 400 + 300 - 500 + 350 - 150 - 220 + 120 + 170 116 diff --git a/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml index fa42452485f45..42f9c0dc773cc 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values-hi-rIN/alpha_strings.xml @@ -4,6 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - Installed: %1$s - Updated: %1$s + स्थापित: %1$s + अपडेट किया गया: %1$s diff --git a/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml b/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml index fa42452485f45..5204c1ba9a97e 100644 --- a/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml +++ b/packages/SettingsLib/SpaPrivileged/res/values-sq-rAL/alpha_strings.xml @@ -4,6 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - Installed: %1$s - Updated: %1$s + Instaluar: %1$s + Përditësuar: %1$s diff --git a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java index c2d45e2119d67..74e3e3501f44f 100644 --- a/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java +++ b/packages/SettingsLib/UsageProgressBarPreference/src/com/android/settingslib/widget/UsageProgressBarPreference.java @@ -25,6 +25,7 @@ import android.text.style.TextAppearanceSpan; import android.util.AttributeSet; import android.view.View; +import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ProgressBar; @@ -55,6 +56,8 @@ public class UsageProgressBarPreference extends Preference implements GroupSecti private CharSequence mBottomSummaryContentDescription; private ImageView mCustomImageView; private int mPercent = -1; + private int mLastAnimatedPercent = -1; + private ValueAnimator mProgressAnimator; /** * Perform inflation from XML and apply a class-specific base style. @@ -209,7 +212,7 @@ public void onBindViewHolder(PreferenceViewHolder holder) { progressBar.setIndeterminate(true); } else { progressBar.setIndeterminate(false); - animateBatteryLevel(progressBar, 0, mPercent); + animateBatteryLevel(progressBar, mPercent); } } @@ -244,16 +247,26 @@ private CharSequence enlargeFontOfNumber(CharSequence summary) { return summary; } - private void animateBatteryLevel(final ProgressBar progressbar, final int startValue, final int endValue) { - final ValueAnimator animator = ValueAnimator.ofInt(startValue, endValue); - animator.setDuration(1000); - animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { - @Override - public void onAnimationUpdate(ValueAnimator animation) { - int animatedValue = (int) animation.getAnimatedValue(); - progressbar.setProgress(animatedValue); - } - }); - animator.start(); + private void animateBatteryLevel(final ProgressBar progressbar, final int endValue) { + // Reuse a single animator across rebinds. Stacking ValueAnimator instances on the same + // ProgressBar produces visible jitter because each animator drives setProgress() on its + // own frame cadence and the underlying LinearProgressIndicator runs its own smooth + // animator on top, so the two clocks fight each other. + final int startValue = mLastAnimatedPercent < 0 ? 0 : mLastAnimatedPercent; + if (mProgressAnimator != null) { + mProgressAnimator.cancel(); + } + if (startValue == endValue) { + progressbar.setProgress(endValue, false); + mLastAnimatedPercent = endValue; + return; + } + mProgressAnimator = ValueAnimator.ofInt(startValue, endValue); + mProgressAnimator.setDuration(700L); + mProgressAnimator.setInterpolator(new PathInterpolator(0.4f, 0f, 0.2f, 1f)); + mProgressAnimator.addUpdateListener(animation -> + progressbar.setProgress((int) animation.getAnimatedValue(), false)); + mProgressAnimator.start(); + mLastAnimatedPercent = endValue; } } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 07ce4fe84ecd3..42634ced71278 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -281,6 +281,7 @@ public class SecureSettings { Settings.Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, Settings.Secure.HINGE_ANGLE_LIDEVENT_ENABLED, Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED, + Settings.Secure.LOCKSCREEN_SMARTSPACE_SOURCE, Settings.Secure.HEARING_AID_RINGTONE_ROUTING, Settings.Secure.HEARING_AID_CALL_ROUTING, Settings.Secure.HEARING_AID_MEDIA_ROUTING, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 678d3b94fde88..4bb0c96e4f69c 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -448,6 +448,10 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_APP, ANY_STRING_VALIDATOR); VALIDATORS.put(Secure.CUSTOM_BUGREPORT_HANDLER_USER, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.LOCKSCREEN_SMARTSPACE_ENABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(Secure.LOCKSCREEN_SMARTSPACE_SOURCE, + new InclusiveIntegerRangeValidator( + Secure.LOCKSCREEN_SMARTSPACE_SOURCE_AXION, + Secure.LOCKSCREEN_SMARTSPACE_SOURCE_NONE)); VALIDATORS.put(Secure.CONTEXTUAL_SCREEN_TIMEOUT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HINGE_ANGLE_LIDEVENT_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HEARING_AID_RINGTONE_ROUTING, TRI_STATE_VALIDATOR); diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index ed774d95d2d5c..66faf49f5290b 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -518,6 +518,7 @@ android_library { "//frameworks/libs/systemui:compilelib", "com.android.systemui.pods-api-aosp-handheld", "ax_platform_hooks", + "axquicklook-client", "SystemUI-res", "WifiTrackerLib", "WindowManager-Shell", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 21947c58c595d..cfe7025474062 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -438,6 +438,11 @@ + + + + + + + 104dp 0dp 74dp - \ No newline at end of file + diff --git a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt index 8309689808799..d5696aabd6088 100644 --- a/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt +++ b/packages/SystemUI/customization/clocks/common/src/com/android/systemui/customization/clocks/DefaultClockFaceLayout.kt @@ -83,7 +83,7 @@ open class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { override fun LockscreenScope.LockscreenElement() { clockView( view, - Modifier.wrapContentSize() + Modifier.wrapContentSize(unbounded = true) .then(contentScope.largeClockModifier()) .then(context.burnInModifier), ) @@ -204,17 +204,13 @@ open class DefaultClockFaceLayout(val view: View) : ClockFaceLayout { } constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) - constrainHeight( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - res.getDimensionPixelSize(clocksR.dimen.small_clock_height), - ) + constrainHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, START, PARENT_ID, START, - res.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - clockPreviewConfig.statusViewMarginHorizontal, + 0, ) val smallClockTopMargin = clockPreviewConfig.getSmallClockTopPadding() diff --git a/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml b/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml new file mode 100644 index 0000000000000..6a6706a73f642 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/audio_bars_playing.xml @@ -0,0 +1,457 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/graphic_tick.xml b/packages/SystemUI/customization/res/drawable/graphic_tick.xml new file mode 100644 index 0000000000000..5ffcd3e681b09 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/graphic_tick.xml @@ -0,0 +1,11 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml b/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml new file mode 100644 index 0000000000000..d594ad8a059cf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/graphic_tick_light.xml @@ -0,0 +1,13 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ic_music_note.xml b/packages/SystemUI/customization/res/drawable/ic_music_note.xml new file mode 100644 index 0000000000000..30959a870a02b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ic_music_note.xml @@ -0,0 +1,24 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_0.xml b/packages/SystemUI/customization/res/drawable/intervar_0.xml new file mode 100644 index 0000000000000..d0aa59874c0a3 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_0.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_0_light.xml b/packages/SystemUI/customization/res/drawable/intervar_0_light.xml new file mode 100644 index 0000000000000..b7176401530c1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_0_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_1.xml b/packages/SystemUI/customization/res/drawable/intervar_1.xml new file mode 100644 index 0000000000000..6d13a64057a51 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_1.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_1_light.xml b/packages/SystemUI/customization/res/drawable/intervar_1_light.xml new file mode 100644 index 0000000000000..3b0c563a600e1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_1_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_2.xml b/packages/SystemUI/customization/res/drawable/intervar_2.xml new file mode 100644 index 0000000000000..84fc90b4cd6c0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_2.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_2_light.xml b/packages/SystemUI/customization/res/drawable/intervar_2_light.xml new file mode 100644 index 0000000000000..1481fde632200 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_2_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_3.xml b/packages/SystemUI/customization/res/drawable/intervar_3.xml new file mode 100644 index 0000000000000..d4fe235dc32d9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_3.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_3_light.xml b/packages/SystemUI/customization/res/drawable/intervar_3_light.xml new file mode 100644 index 0000000000000..1045820ef287e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_3_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_4.xml b/packages/SystemUI/customization/res/drawable/intervar_4.xml new file mode 100644 index 0000000000000..d51ff6d2ca7fe --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_4.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_4_light.xml b/packages/SystemUI/customization/res/drawable/intervar_4_light.xml new file mode 100644 index 0000000000000..e556c47f6774e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_4_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_5.xml b/packages/SystemUI/customization/res/drawable/intervar_5.xml new file mode 100644 index 0000000000000..7d6ff10723e63 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_5.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_5_light.xml b/packages/SystemUI/customization/res/drawable/intervar_5_light.xml new file mode 100644 index 0000000000000..d12a4fcb9cee0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_5_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_6.xml b/packages/SystemUI/customization/res/drawable/intervar_6.xml new file mode 100644 index 0000000000000..cbd55a8980b5d --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_6.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_6_light.xml b/packages/SystemUI/customization/res/drawable/intervar_6_light.xml new file mode 100644 index 0000000000000..f5af044a0633e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_6_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_7.xml b/packages/SystemUI/customization/res/drawable/intervar_7.xml new file mode 100644 index 0000000000000..03b4316b4d8b5 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_7.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_7_light.xml b/packages/SystemUI/customization/res/drawable/intervar_7_light.xml new file mode 100644 index 0000000000000..d2c96bb7451d7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_7_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_8.xml b/packages/SystemUI/customization/res/drawable/intervar_8.xml new file mode 100644 index 0000000000000..5ee72cb869430 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_8.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_8_light.xml b/packages/SystemUI/customization/res/drawable/intervar_8_light.xml new file mode 100644 index 0000000000000..052c2828a1779 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_8_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_9.xml b/packages/SystemUI/customization/res/drawable/intervar_9.xml new file mode 100644 index 0000000000000..5a78fcf88a0e8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_9.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/intervar_9_light.xml b/packages/SystemUI/customization/res/drawable/intervar_9_light.xml new file mode 100644 index 0000000000000..711acdb30f314 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/intervar_9_light.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_0.xml b/packages/SystemUI/customization/res/drawable/london_ug_0.xml new file mode 100644 index 0000000000000..6facb6e2c2680 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_0.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml new file mode 100644 index 0000000000000..032ba2ae03775 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_0_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_1.xml b/packages/SystemUI/customization/res/drawable/london_ug_1.xml new file mode 100644 index 0000000000000..6d649bf5e89aa --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_1.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml new file mode 100644 index 0000000000000..82c9081fc260c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_1_16b.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_2.xml b/packages/SystemUI/customization/res/drawable/london_ug_2.xml new file mode 100644 index 0000000000000..edf3fe92e95d8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_2.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml new file mode 100644 index 0000000000000..908761bdc2437 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_2_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_3.xml b/packages/SystemUI/customization/res/drawable/london_ug_3.xml new file mode 100644 index 0000000000000..6af349f1cdaed --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_3.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml new file mode 100644 index 0000000000000..515d82aaeb30e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_3_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_4.xml b/packages/SystemUI/customization/res/drawable/london_ug_4.xml new file mode 100644 index 0000000000000..bc0b998828cd5 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_4.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml new file mode 100644 index 0000000000000..4e6f4281aa815 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_4_16b.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_5.xml b/packages/SystemUI/customization/res/drawable/london_ug_5.xml new file mode 100644 index 0000000000000..aa2351927d9a3 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_5.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml new file mode 100644 index 0000000000000..81f1b4d8bd105 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_5_16b.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_6.xml b/packages/SystemUI/customization/res/drawable/london_ug_6.xml new file mode 100644 index 0000000000000..8281c49c7188f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_6.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml new file mode 100644 index 0000000000000..71bb38a80cdbd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_6_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_7.xml b/packages/SystemUI/customization/res/drawable/london_ug_7.xml new file mode 100644 index 0000000000000..ee3717159c762 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_7.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml new file mode 100644 index 0000000000000..17622a99e0a43 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_7_16b.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_8.xml b/packages/SystemUI/customization/res/drawable/london_ug_8.xml new file mode 100644 index 0000000000000..6e4ed6db92622 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_8.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml new file mode 100644 index 0000000000000..b8f9602e91566 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_8_16b.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_9.xml b/packages/SystemUI/customization/res/drawable/london_ug_9.xml new file mode 100644 index 0000000000000..07423814301a1 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_9.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml b/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml new file mode 100644 index 0000000000000..9e83076f5ce49 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/london_ug_9_16b.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0.xml b/packages/SystemUI/customization/res/drawable/ndot_0.xml new file mode 100644 index 0000000000000..72d0a564caf24 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml new file mode 100644 index 0000000000000..865920ccba04b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_light.xml b/packages/SystemUI/customization/res/drawable/ndot_0_light.xml new file mode 100644 index 0000000000000..d4e0bdead70a0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml new file mode 100644 index 0000000000000..622a7ce03ebe7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_0_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1.xml b/packages/SystemUI/customization/res/drawable/ndot_1.xml new file mode 100644 index 0000000000000..764082fb9f338 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml new file mode 100644 index 0000000000000..2a1e9429ac27f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_light.xml b/packages/SystemUI/customization/res/drawable/ndot_1_light.xml new file mode 100644 index 0000000000000..d9fa490a77e50 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml new file mode 100644 index 0000000000000..70363d723ec6c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_1_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2.xml b/packages/SystemUI/customization/res/drawable/ndot_2.xml new file mode 100644 index 0000000000000..5cdb0b2eb06e4 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml new file mode 100644 index 0000000000000..83949670df7c2 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_light.xml b/packages/SystemUI/customization/res/drawable/ndot_2_light.xml new file mode 100644 index 0000000000000..151475be13679 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml new file mode 100644 index 0000000000000..7e8d48601ccbd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_2_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3.xml b/packages/SystemUI/customization/res/drawable/ndot_3.xml new file mode 100644 index 0000000000000..732b45960cabf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml new file mode 100644 index 0000000000000..be236f2ab9b02 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_light.xml b/packages/SystemUI/customization/res/drawable/ndot_3_light.xml new file mode 100644 index 0000000000000..8575d64843ce9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml new file mode 100644 index 0000000000000..2723f89076129 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_3_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4.xml b/packages/SystemUI/customization/res/drawable/ndot_4.xml new file mode 100644 index 0000000000000..aa2e4be002241 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml new file mode 100644 index 0000000000000..513a5539a5b49 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_light.xml b/packages/SystemUI/customization/res/drawable/ndot_4_light.xml new file mode 100644 index 0000000000000..253e7bf8761ee --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml new file mode 100644 index 0000000000000..095a74f8d3d5a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_4_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5.xml b/packages/SystemUI/customization/res/drawable/ndot_5.xml new file mode 100644 index 0000000000000..7598807bd06b9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml new file mode 100644 index 0000000000000..cdd6cd86536c7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_light.xml b/packages/SystemUI/customization/res/drawable/ndot_5_light.xml new file mode 100644 index 0000000000000..35dd1f5ba08fd --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml new file mode 100644 index 0000000000000..7ec960a66b3bf --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_5_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6.xml b/packages/SystemUI/customization/res/drawable/ndot_6.xml new file mode 100644 index 0000000000000..582aea74f5557 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml new file mode 100644 index 0000000000000..80fa4f0c3cdca --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_light.xml b/packages/SystemUI/customization/res/drawable/ndot_6_light.xml new file mode 100644 index 0000000000000..b55968bbd6341 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml new file mode 100644 index 0000000000000..1f2c5b54c10ea --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_6_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7.xml b/packages/SystemUI/customization/res/drawable/ndot_7.xml new file mode 100644 index 0000000000000..768636a456b6e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml new file mode 100644 index 0000000000000..45b05f4c819a4 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_light.xml b/packages/SystemUI/customization/res/drawable/ndot_7_light.xml new file mode 100644 index 0000000000000..2a390a8c80666 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml new file mode 100644 index 0000000000000..04c2dcf651f2f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_7_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8.xml b/packages/SystemUI/customization/res/drawable/ndot_8.xml new file mode 100644 index 0000000000000..212de49990a60 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml new file mode 100644 index 0000000000000..1b4f6fe023e00 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_light.xml b/packages/SystemUI/customization/res/drawable/ndot_8_light.xml new file mode 100644 index 0000000000000..c50de834cbe9a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml new file mode 100644 index 0000000000000..1480f7f7da972 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_8_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9.xml b/packages/SystemUI/customization/res/drawable/ndot_9.xml new file mode 100644 index 0000000000000..81a088ded2f6f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml new file mode 100644 index 0000000000000..dfc2b8cd21572 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_16b.xml @@ -0,0 +1,10 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_light.xml b/packages/SystemUI/customization/res/drawable/ndot_9_light.xml new file mode 100644 index 0000000000000..b782fbc52baba --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_light.xml @@ -0,0 +1,16 @@ + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml b/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml new file mode 100644 index 0000000000000..f91d7a5ae9789 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ndot_9_light_16b.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_0.xml b/packages/SystemUI/customization/res/drawable/ntype_0.xml new file mode 100644 index 0000000000000..88eb6d4730433 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_0.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml new file mode 100644 index 0000000000000..a6f2f8068b29e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_0_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_1.xml b/packages/SystemUI/customization/res/drawable/ntype_1.xml new file mode 100644 index 0000000000000..96ad18e8771ec --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_1.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml new file mode 100644 index 0000000000000..fb308f76e8b56 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_1_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_2.xml b/packages/SystemUI/customization/res/drawable/ntype_2.xml new file mode 100644 index 0000000000000..d3677b3e826f9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_2.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml new file mode 100644 index 0000000000000..063f76bd8e50f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_2_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_3.xml b/packages/SystemUI/customization/res/drawable/ntype_3.xml new file mode 100644 index 0000000000000..3d7e9d2cea685 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_3.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml new file mode 100644 index 0000000000000..28cefa14dd4c0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_3_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_4.xml b/packages/SystemUI/customization/res/drawable/ntype_4.xml new file mode 100644 index 0000000000000..d8b43a487fe27 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_4.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml new file mode 100644 index 0000000000000..edadcd2dfb6a9 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_4_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_5.xml b/packages/SystemUI/customization/res/drawable/ntype_5.xml new file mode 100644 index 0000000000000..d6cc5d50fb59b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_5.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml new file mode 100644 index 0000000000000..641c4ba8f212b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_5_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_6.xml b/packages/SystemUI/customization/res/drawable/ntype_6.xml new file mode 100644 index 0000000000000..facdf86bbed21 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_6.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml new file mode 100644 index 0000000000000..12c290db1cdeb --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_6_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_7.xml b/packages/SystemUI/customization/res/drawable/ntype_7.xml new file mode 100644 index 0000000000000..532d782b97908 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_7.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml new file mode 100644 index 0000000000000..601aff8a43502 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_7_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_8.xml b/packages/SystemUI/customization/res/drawable/ntype_8.xml new file mode 100644 index 0000000000000..1bbfb0cdf01eb --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_8.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml new file mode 100644 index 0000000000000..36f2f192b61bc --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_8_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_9.xml b/packages/SystemUI/customization/res/drawable/ntype_9.xml new file mode 100644 index 0000000000000..44d543e71b358 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_9.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml b/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml new file mode 100644 index 0000000000000..acba31f5ebb78 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/ntype_9_16b.xml @@ -0,0 +1,12 @@ + + + + diff --git a/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml b/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml new file mode 100644 index 0000000000000..99052f9e96913 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/old_quick_look_alarm_icon.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/drawable/polyline_0.xml b/packages/SystemUI/customization/res/drawable/polyline_0.xml new file mode 100644 index 0000000000000..201e69cdfe929 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_0.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_1.xml b/packages/SystemUI/customization/res/drawable/polyline_1.xml new file mode 100644 index 0000000000000..b36776a04e2df --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_1.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_2.xml b/packages/SystemUI/customization/res/drawable/polyline_2.xml new file mode 100644 index 0000000000000..61a5a819f8f4f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_2.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_3.xml b/packages/SystemUI/customization/res/drawable/polyline_3.xml new file mode 100644 index 0000000000000..01c23ced13315 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_3.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_4.xml b/packages/SystemUI/customization/res/drawable/polyline_4.xml new file mode 100644 index 0000000000000..aa1ff3885279f --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_4.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_5.xml b/packages/SystemUI/customization/res/drawable/polyline_5.xml new file mode 100644 index 0000000000000..3db0eb6beacd0 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_5.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_6.xml b/packages/SystemUI/customization/res/drawable/polyline_6.xml new file mode 100644 index 0000000000000..f3d9a4ecfee7e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_6.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_7.xml b/packages/SystemUI/customization/res/drawable/polyline_7.xml new file mode 100644 index 0000000000000..566d26f4a9b9b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_7.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_8.xml b/packages/SystemUI/customization/res/drawable/polyline_8.xml new file mode 100644 index 0000000000000..0b7e1d51c9c7a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_8.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/polyline_9.xml b/packages/SystemUI/customization/res/drawable/polyline_9.xml new file mode 100644 index 0000000000000..5c61d0e21fd00 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/polyline_9.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_0.xml b/packages/SystemUI/customization/res/drawable/space_age_0.xml new file mode 100644 index 0000000000000..69fce63020d48 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_0.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_1.xml b/packages/SystemUI/customization/res/drawable/space_age_1.xml new file mode 100644 index 0000000000000..eb29b842e9d44 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_1.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_2.xml b/packages/SystemUI/customization/res/drawable/space_age_2.xml new file mode 100644 index 0000000000000..09da8433ad6a7 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_2.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_3.xml b/packages/SystemUI/customization/res/drawable/space_age_3.xml new file mode 100644 index 0000000000000..cac8e6666620e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_3.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_4.xml b/packages/SystemUI/customization/res/drawable/space_age_4.xml new file mode 100644 index 0000000000000..4f411c36289c8 --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_4.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_5.xml b/packages/SystemUI/customization/res/drawable/space_age_5.xml new file mode 100644 index 0000000000000..fbb17db752e7a --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_5.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_6.xml b/packages/SystemUI/customization/res/drawable/space_age_6.xml new file mode 100644 index 0000000000000..0e22fcb7d624b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_6.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_7.xml b/packages/SystemUI/customization/res/drawable/space_age_7.xml new file mode 100644 index 0000000000000..0a9d4ac93c42b --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_7.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_8.xml b/packages/SystemUI/customization/res/drawable/space_age_8.xml new file mode 100644 index 0000000000000..766c67473e87e --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_8.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/drawable/space_age_9.xml b/packages/SystemUI/customization/res/drawable/space_age_9.xml new file mode 100644 index 0000000000000..fe7c90c03160c --- /dev/null +++ b/packages/SystemUI/customization/res/drawable/space_age_9.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/customization/res/layout/clock_axion_age.xml b/packages/SystemUI/customization/res/layout/clock_axion_age.xml new file mode 100644 index 0000000000000..6273236ebe3b9 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_axion_age.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml b/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml new file mode 100644 index 0000000000000..fbaffe4577cae --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_axion_age_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml b/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml new file mode 100644 index 0000000000000..5efc483c0bd95 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_bitmap_compose.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml new file mode 100644 index 0000000000000..46e64aa5b8fc6 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_bitmap_compose_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml b/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml new file mode 100644 index 0000000000000..4c13aa50fd926 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_cyberpunk.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml b/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml new file mode 100644 index 0000000000000..130e2ca9e57eb --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_cyberpunk_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_general.xml b/packages/SystemUI/customization/res/layout/clock_general.xml new file mode 100644 index 0000000000000..e439231450a29 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_general.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_general_large.xml b/packages/SystemUI/customization/res/layout/clock_general_large.xml new file mode 100644 index 0000000000000..96f5fe2f2859f --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_general_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml b/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml new file mode 100644 index 0000000000000..e689238f5eebd --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_old_quick_look.xml @@ -0,0 +1,4 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml new file mode 100644 index 0000000000000..5f6c37e934e11 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_old_quick_look_large.xml @@ -0,0 +1,5 @@ + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish2.xml b/packages/SystemUI/customization/res/layout/clock_stylish2.xml new file mode 100644 index 0000000000000..018bb9f50daf9 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish2.xml @@ -0,0 +1,8 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml b/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml new file mode 100644 index 0000000000000..ad80e55d70fb7 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish2_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish7.xml b/packages/SystemUI/customization/res/layout/clock_stylish7.xml new file mode 100644 index 0000000000000..cf315deb65cb5 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish7.xml @@ -0,0 +1,8 @@ + + + diff --git a/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml b/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml new file mode 100644 index 0000000000000..83cf0a6019f81 --- /dev/null +++ b/packages/SystemUI/customization/res/layout/clock_stylish7_large.xml @@ -0,0 +1,9 @@ + + + diff --git a/packages/SystemUI/customization/res/values/axion_dimens.xml b/packages/SystemUI/customization/res/values/axion_dimens.xml new file mode 100644 index 0000000000000..7539ad1485d45 --- /dev/null +++ b/packages/SystemUI/customization/res/values/axion_dimens.xml @@ -0,0 +1,78 @@ + + + + + 28dp + 6dp + 16dp + 7dp + 8dp + 28dp + 18sp + 24dp + 8dp + 20dp + 4dp + 4dp + 4dp + 166dp + 44dp + 28dp + 14dp + 8dp + 12dp + 30dp + 24dp + 16dp + 134dp + 20dp + 28dp + 18dp + 2dp + 3dp + 6dp + 32dp + 10dp + 8dp + 6dp + 4dp + 10dp + 24dp + 28dp + 4dp + 80dp + 28dp + 22dp + 16dp + 8dp + 8dp + 16dp + 26dp + 4dp + 2dp + 0.3334dp + 5dp + 16dp + 6dp + 8dp + 36dp + + + 4dp + 16dp + diff --git a/packages/SystemUI/customization/res/values/colors.xml b/packages/SystemUI/customization/res/values/colors.xml new file mode 100644 index 0000000000000..fe85c2605e46d --- /dev/null +++ b/packages/SystemUI/customization/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + #d71921 + #ffc700 + diff --git a/packages/SystemUI/customization/res/values/ids.xml b/packages/SystemUI/customization/res/values/ids.xml new file mode 100644 index 0000000000000..c40956ef42f10 --- /dev/null +++ b/packages/SystemUI/customization/res/values/ids.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/customization/res/values/strings.xml b/packages/SystemUI/customization/res/values/strings.xml index 897c842b6d4b0..4d271c5be7ca7 100644 --- a/packages/SystemUI/customization/res/values/strings.xml +++ b/packages/SystemUI/customization/res/values/strings.xml @@ -22,4 +22,24 @@ Digital default - \ No newline at end of file + + GENERAL + GRAPHIC + LONDON_UG + NDOT + DEFAULT + OLD_QUICKLOOK + POLYLINE + SPACE_AGE + CYBERPUNK + AXION_AGE + SEGMENTS + STYLISH_2 + STYLISH_7 + ? + __° + : ) + Calendar events and reminders + Schedule + EEEMMMd + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt new file mode 100644 index 0000000000000..f93511bb0b86d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockAnimations.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView +import com.android.systemui.shared.clocks.view.animateAppear +import com.android.systemui.shared.clocks.view.animateCharge + +class AxClockAnimations( + private val clockView: AxClockView, + dozeFraction: Float, + foldFraction: Float +) : ClockAnimations { + + internal val dozeState = ClockAnimationState(dozeFraction) + private val foldState = ClockAnimationState(foldFraction) + + override fun enter() { + if (!dozeState.isActive) { + clockView.animateAppear() + } + } + + override fun doze(fraction: Float) { + dozeState.update(fraction) + } + + override fun fold(fraction: Float) { + foldState.update(fraction) + } + + override fun charge() { + clockView.animateCharge() + } + + override fun onPickerCarouselSwiping(swipingFraction: Float) { + clockView.translationY = 0.5f * clockView.bottom * (1 - swipingFraction) + } + + override fun onFidgetTap(x: Float, y: Float) {} + + override fun onPositionAnimated(args: ClockPositionAnimationArgs) {} + + override fun onFontAxesChanged(style: ClockAxisStyle) {} + + class ClockAnimationState(var fraction: Float) { + var isActive: Boolean = fraction > 0.5f + + fun update(newFraction: Float): Pair { + if (newFraction == fraction) { + return Pair(isActive, false) + } + val wasActive = isActive + val hasJumped = + (fraction == 0f && newFraction == 1f) || (fraction == 1f && newFraction == 0f) + isActive = newFraction > fraction + fraction = newFraction + return Pair(wasActive != isActive, hasJumped) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt new file mode 100644 index 0000000000000..d9a70a6f59bf6 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockController.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.ViewGroup +import android.util.Log +import android.widget.FrameLayout +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.BitmapDigitComposeClockView +import com.android.systemui.shared.clocks.view.AxClockView +import java.io.PrintWriter + +class AxClockController @JvmOverloads constructor( + context: Context, + clockType: AxClockType, + layoutInflater: LayoutInflater, + clockMessageBuffers: ClockMessageBuffers? = null +) : ClockController { + private val TAG = "AxClockController" + + override val smallClock: AxClockFaceController + override val largeClock: AxClockFaceController + override val eventListeners = ClockEventListeners() + override val events: ClockEvents + override val config: ClockConfig + + init { + val container = FrameLayout(context) + + val tickRate = if (clockType.bitmapFaceStyle?.needsPerSecondTick == true) { + ClockTickRate.PER_SECOND + } else { + ClockTickRate.PER_MINUTE + } + + val smallClockView = layoutInflater.inflate(clockType.viewId, container, false) as AxClockView + clockType.bitmapFaceStyle?.let { style -> + (smallClockView as? BitmapDigitComposeClockView)?.faceStyle = style + } + + smallClock = AxClockFaceController( + context, + smallClockView, + "lockscreen_clock_view", + tickRate, + clockMessageBuffers?.smallClockMessageBuffer, + isLargeClock = false + ) + + val largeClockView = layoutInflater.inflate(clockType.largeViewId, container, false) as AxClockView + largeClockView.isLargeClock = true + clockType.bitmapFaceStyle?.let { style -> + (largeClockView as? BitmapDigitComposeClockView)?.faceStyle = style + } + + largeClock = AxClockFaceController( + context, + largeClockView, + "lockscreen_clock_view_large", + tickRate, + clockMessageBuffers?.largeClockMessageBuffer, + isLargeClock = true + ) + + events = AxClockEvents(smallClockView, largeClockView) + + val clockName = context.getString(clockType.clockId) + + config = ClockConfig(clockName, "", "", false, false) + + Log.d(TAG, "init") + } + + override fun dump(pw: PrintWriter) {} + + override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) { + smallClock.animations = AxClockAnimations(smallClock.view, dozeFraction, foldFraction) + largeClock.animations = AxClockAnimations(largeClock.view, dozeFraction, foldFraction) + + events.onUiModeChanged(isDarkTheme) + + smallClock.events.onTimeTick() + largeClock.events.onTimeTick() + + Log.d(TAG, "initialize") + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt new file mode 100644 index 0000000000000..77c594aa99e7c --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockEvents.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.res.Resources +import android.icu.util.TimeZone +import android.text.format.DateFormat +import android.view.View +import com.android.systemui.plugins.keyguard.data.model.* +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView +import java.util.* + +class AxClockEvents( + private val smallClockView: AxClockView, + private val largeClockView: AxClockView? = null +) : ClockEvents { + + private val views: List + get() = listOfNotNull(smallClockView, largeClockView) + + override var isReactiveTouchInteractionEnabled: Boolean = false + + override fun onAlarmDataChanged(data: AlarmData) { + views.forEach { it.onAlarmDataChanged(data) } + } + + override fun onWeatherDataChanged(data: WeatherData) {} + + override fun onZenDataChanged(data: ZenData) { + requireNotNull(data) { "ZenData cannot be null" } + } + + override fun onLocaleChanged(locale: Locale) { + requireNotNull(locale) { "Locale cannot be null" } + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour, locale) } + } + + override fun onTimeFormatChanged(formatKind: TimeFormatKind) { + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour) } + } + + override fun onTimeZoneChanged(timeZone: TimeZone) { + requireNotNull(timeZone) { "TimeZone cannot be null" } + views.forEach { it.onTimeZoneChanged(timeZone) } + + val is24Hour = DateFormat.is24HourFormat(smallClockView.context) + views.forEach { it.refreshFormat(is24Hour) } + } + + override fun onUiModeChanged(isDarkTheme: Boolean) { + views.forEach { it.onThemeChanged(isDarkTheme) } + } + + override fun onDateChanged() { + views.forEach { it.onDateChanged() } + } + + override fun onClockDataChanged(data: ClockData) { + views.forEach { it.onClockDataChanged(data) } + } + + override fun onMetadataChanged(track: String, artist: String, packageName: String) { + views.forEach { it.onMetadataChanged(track, artist, packageName) } + } + + override fun onPlaybackStateChanged(playing: Boolean) { + views.forEach { it.onPlaybackStateChanged(playing) } + } + + override fun onNowPlayingUpdate(nowPlayingText: String) { + views.forEach { it.onNowPlayingUpdate(nowPlayingText) } + } + override fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) { + views.forEach { it.onClockLayoutChanged(isCentered, isLargeClockVisible) } + } + override fun onDepthEffectVisibilityChanged(visible: Boolean) { + views.forEach { it.onDepthEffectVisibilityChanged(visible) } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt new file mode 100644 index 0000000000000..4c803caf8ab42 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockFaceController.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.graphics.Point +import android.graphics.Rect +import android.view.View +import com.android.systemui.customization.clocks.DefaultClockFaceLayout +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.view.AxClockView + +class AxClockFaceController( + context: Context, + override val view: AxClockView, + clockViewId: String, + clockTickRate: ClockTickRate = ClockTickRate.PER_MINUTE, + messageBuffer: MessageBuffer? = null, + isLargeClock: Boolean = false +) : ClockFaceController { + + override var animations: ClockAnimations = AxClockAnimations(view, 0.0f, 0.0f) + internal set + + // hasCustomWeatherDataDisplay = true for both small + large variants: every Axion face + // renders its own date/weather row via EnhancedDateArea (or an equivalent inline path), + // so the framework should hide BC date_smartspace_view / weather_smartspace_view whenever + // an Axion face is active, regardless of clock size. See §12 of axion-integration-plan.md. + override val config: ClockFaceConfig = + ClockFaceConfig(clockTickRate, true, false, false) + + override var theme = ThemeConfig(true, null) + + override val events = + object : ClockFaceEvents { + override fun onTimeTick() = view.refreshTime() + + override fun onThemeChanged(theme: ThemeConfig) { + } + + override fun onTargetRegionChanged(targetRegion: Rect?) { + } + + override fun onFontSettingChanged(fontSizePx: Float) { + view.onFontSettingChanged() + } + + override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {} + + override fun onDozeChanged(dozing: Boolean) { + view.onDozeChanged(dozing) + } + + override fun onDozeAmountChanged(linear: Float, eased: Float) { + view.onDozeAmountChanged(linear, eased) + } + + override fun onPulsingChanged(pulsing: Boolean) { + view.onPulsingChanged(pulsing) + } + + override fun onScreenOff(screenOff: Boolean) { + view.onScreenOff(screenOff) + } + + override fun onRegionDarknessChanged(isRegionDark: Boolean) { + view.onRegionDarknessChanged(isRegionDark) + } + + override fun onStartedWakingUp() { + view.onStartedWakingUp() + } + + override fun onStartedGoingToSleep(isKeyguardVisible: Boolean) { + view.onStartedGoingToSleep(isKeyguardVisible) + } + + override fun onWakefulnessStateChanged( + isWakingUp: Boolean, + tapPosition: Point? + ) { + view.onWakefulnessStateChanged(isWakingUp, tapPosition) + } + } + + override val layout = + DefaultClockFaceLayout(view).apply { + views[0].id = if (isLargeClock) ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE else ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt new file mode 100644 index 0000000000000..7099e6f60d4e8 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockProvider.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.content.res.Resources +import android.util.Log +import android.view.LayoutInflater +import com.android.systemui.customization.R +import com.android.systemui.plugins.keyguard.ui.clocks.* + +class AxClockProvider( + private val layoutInflater: LayoutInflater, + private val resources: Resources, +) : ClockProvider { + + private val tag = "AxClockProvider" + private var messageBuffers: ClockMessageBuffers? = null + + init { + Log.d(tag, "Initialized AxClockProvider") + } + + override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig { + return ClockPickerConfig( + settings.clockId ?: "NTYPE", + resources.getString(R.string.clock_id_general), + resources.getString(R.string.clock_id_general), + resources.getDrawable(R.drawable.clock_default_thumbnail, null), + isReactiveToTone = true, + axes = emptyList(), + presetConfig = null, + ) + } + + override fun createClock(ctx: Context, settings: ClockSettings): AxClockController { + val clockId = settings.clockId + val resolvedType = AxClockType.values().firstOrNull { + clockId == resources.getString(it.clockId) + } ?: AxClockType.NTYPE + + return AxClockController(ctx, resolvedType, layoutInflater, messageBuffers) + } + + override fun getClocks(): List { + val availableTypes = buildList { + add(AxClockType.NTYPE) + add(AxClockType.NDOT) + add(AxClockType.GRAPHIC) + add(AxClockType.GENERAL) + add(AxClockType.LONDON_UG) + add(AxClockType.OLD_QUICKLOOK) + add(AxClockType.SPACE_AGE) + add(AxClockType.POLYLINE) + add(AxClockType.CYBERPUNK) + add(AxClockType.AXION_AGE) + add(AxClockType.SEGMENTS) + add(AxClockType.STYLISH_2) + add(AxClockType.STYLISH_7) + } + + return availableTypes.map { type -> + val id = resources.getString(type.clockId) + ClockMetadata(id) + } + } + + override fun initialize(buffers: ClockMessageBuffers?) { + messageBuffers = buffers + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt new file mode 100644 index 0000000000000..87522fbe4ced0 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/AxClockType.kt @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.view.ClockFaceStyle + +enum class AxClockType( + val clockId: Int, + val viewId: Int, + val largeViewId: Int = viewId, + val bitmapFaceStyle: ClockFaceStyle? = null +) { + GENERAL( + clockId = R.string.clock_id_general, + viewId = R.layout.clock_general, + largeViewId = R.layout.clock_general_large + ), + GRAPHIC( + clockId = R.string.clock_id_graphic, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.GRAPHIC + ), + LONDON_UG( + clockId = R.string.clock_id_london_ug, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.LONDON_UG + ), + NDOT( + clockId = R.string.clock_id_ndot, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.NDOT + ), + NTYPE( + clockId = R.string.clock_id_ntype, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.NTYPE + ), + OLD_QUICKLOOK( + clockId = R.string.clock_id_old_quick_look, + viewId = R.layout.clock_old_quick_look, + largeViewId = R.layout.clock_old_quick_look_large + ), + SPACE_AGE( + clockId = R.string.clock_id_space_age, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.SPACE_AGE + ), + POLYLINE( + clockId = R.string.clock_id_polyline, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.POLYLINE + ), + CYBERPUNK( + clockId = R.string.clock_id_cyberpunk, + viewId = R.layout.clock_cyberpunk, + largeViewId = R.layout.clock_cyberpunk_large + ), + AXION_AGE( + clockId = R.string.clock_id_axion_age, + viewId = R.layout.clock_axion_age, + largeViewId = R.layout.clock_axion_age_large + ), + SEGMENTS( + clockId = R.string.clock_id_segments, + viewId = R.layout.clock_bitmap_compose, + largeViewId = R.layout.clock_bitmap_compose_large, + bitmapFaceStyle = ClockFaceStyle.SEGMENTS + ), + STYLISH_2( + clockId = R.string.clock_id_stylish2, + viewId = R.layout.clock_stylish2, + largeViewId = R.layout.clock_stylish2_large + ), + STYLISH_7( + clockId = R.string.clock_id_stylish7, + viewId = R.layout.clock_stylish7, + largeViewId = R.layout.clock_stylish7_large + ); + + companion object { + val entries: List = values().toList() + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt new file mode 100644 index 0000000000000..a83783f2a2372 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockConfigs.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import com.android.systemui.customization.R + +object ClockConfigs { + + const val ALIGNMENT_LEFT = "left" + const val ALIGNMENT_CENTER = "center" + const val ALIGNMENT_RIGHT = "right" + + data class ClockStyleConfig( + val position: Position, + val align: Align, + val visible: Boolean = true, + val customHeightRes: Int? = null, + val customDateMarginTop: Int? = null, + val placeholderTextRes: Int? = null, + ) + + enum class Position { ABOVE, BELOW } + enum class Align { LEFT, CENTER, RIGHT } + + fun resolveConfig(context: Context, className: String, isLarge: Boolean = false): ClockStyleConfig? = + resolveConfig(className, isLarge, ClockSettingsRepository.alignment.value) + + fun resolveConfig(className: String, isLarge: Boolean, alignValue: String): ClockStyleConfig? { + val key = if (isLarge) "${className}_large" else className + val base = clockConfigMap[key] ?: clockConfigMap[className] ?: return null + if (base.position == Position.BELOW) return base + + val resolvedAlign = when (alignValue) { + ALIGNMENT_LEFT -> Align.LEFT + ALIGNMENT_CENTER -> Align.CENTER + ALIGNMENT_RIGHT -> Align.RIGHT + else -> base.align + } + + if (resolvedAlign == base.align) return base + return base.copy(align = resolvedAlign) + } + + data class DateParts(val main: String, val secondary: String) + + fun parseDateDisplay(date: String): DateParts { + val parts = date.split(" ") + return DateParts( + parts.getOrNull(0)?.trim().orEmpty(), + parts.getOrNull(1)?.trim().orEmpty(), + ) + } + + val clockConfigMap = mapOf( + "GeneralClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + placeholderTextRes = R.string.clock_text_placeholder, + ), + "OldQuickLookClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + placeholderTextRes = R.string.clock_old_quick_look_image_placeholder, + ), + "OldQuickLookClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "GeneralClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "BitmapDigitComposeClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "BitmapDigitComposeClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "CyberpunkClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "CyberpunkClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "AxionAgeClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "AxionAgeClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "Stylish2ClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "Stylish2ClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + "Stylish7ClockView" to ClockStyleConfig( + Position.ABOVE, + Align.CENTER, + visible = false, + ), + "Stylish7ClockView_large" to ClockStyleConfig( + Position.BELOW, + Align.CENTER, + visible = false, + ), + ) +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt new file mode 100644 index 0000000000000..60a2d8cd62126 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockSettingsRepository.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.json.JSONObject + +object ClockSettingsRepository { + + const val SETTING_CLOCK_FACE = "lock_screen_custom_clock_face" + const val SETTING_ALIGNMENT = "ax_clock_alignment" + const val SETTING_SIZE = "ax_clock_size" + const val SETTING_DATE_POSITION = "ax_clock_compose_date_position" + const val SETTING_CLOCK_COLOR = "ax_clock_color" + + const val COLOR_AUTO = "auto" + + const val ALIGNMENT_LEFT = "left" + const val ALIGNMENT_CENTER = "center" + const val ALIGNMENT_RIGHT = "right" + const val SIZE_DEFAULT = "default" + const val SIZE_LARGE = "large" + const val DATE_POSITION_ABOVE = "above" + const val DATE_POSITION_BELOW = "below" + + private const val LARGE_SIZE_SCALE = 1.4f + + @JvmField val clockFaceUri: Uri = Settings.Secure.getUriFor(SETTING_CLOCK_FACE) + @JvmField val alignmentUri: Uri = Settings.Secure.getUriFor(SETTING_ALIGNMENT) + @JvmField val sizeUri: Uri = Settings.Secure.getUriFor(SETTING_SIZE) + @JvmField val datePositionUri: Uri = Settings.Secure.getUriFor(SETTING_DATE_POSITION) + @JvmField val clockColorUri: Uri = Settings.Secure.getUriFor(SETTING_CLOCK_COLOR) + + private val _clockId = MutableStateFlow("DEFAULT") + val clockId: StateFlow = _clockId.asStateFlow() + + private val _alignment = MutableStateFlow(ALIGNMENT_CENTER) + val alignment: StateFlow = _alignment.asStateFlow() + + private val _sizeScale = MutableStateFlow(1f) + val sizeScale: StateFlow = _sizeScale.asStateFlow() + + private val _isDateBelow = MutableStateFlow(false) + val isDateBelow: StateFlow = _isDateBelow.asStateFlow() + + private val _clockColorOverride = MutableStateFlow(null) + val clockColorOverride: StateFlow = _clockColorOverride.asStateFlow() + + private val _shouldCenterIcons = MutableStateFlow(true) + val shouldCenterIcons: StateFlow = _shouldCenterIcons.asStateFlow() + + private var contentResolver: ContentResolver? = null + private var registered = false + private val handler = Handler(Looper.getMainLooper()) + + private val observer = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + val cr = contentResolver ?: return + when (uri) { + clockFaceUri -> { + _clockId.value = readClockId(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + alignmentUri -> { + _alignment.value = readAlignment(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + sizeUri -> { + _sizeScale.value = readSizeScale(cr) + } + datePositionUri -> { + _isDateBelow.value = readDateBelow(cr) + } + clockColorUri -> { + _clockColorOverride.value = readClockColor(cr) + } + } + } + } + + @JvmStatic + fun init(context: Context) { + contentResolver = context.contentResolver + val cr = context.contentResolver + + if (registered) { + _clockColorOverride.value = readClockColor(cr) + return + } + registered = true + + cr.registerContentObserver(clockFaceUri, false, observer) + cr.registerContentObserver(alignmentUri, false, observer) + cr.registerContentObserver(sizeUri, false, observer) + cr.registerContentObserver(datePositionUri, false, observer) + cr.registerContentObserver(clockColorUri, false, observer) + + _clockId.value = readClockId(cr) + _alignment.value = readAlignment(cr) + _sizeScale.value = readSizeScale(cr) + _isDateBelow.value = readDateBelow(cr) + _clockColorOverride.value = readClockColor(cr) + _shouldCenterIcons.value = computeShouldCenter(cr) + } + + @JvmStatic + fun shouldCenterIcons(context: Context): Boolean { + init(context) + return _shouldCenterIcons.value + } + + private fun readClockId(cr: ContentResolver): String { + return try { + val json = Settings.Secure.getString(cr, SETTING_CLOCK_FACE) + if (!json.isNullOrEmpty()) JSONObject(json).optString("clockId", "DEFAULT") + else "DEFAULT" + } catch (_: Exception) { + "DEFAULT" + } + } + + private fun readAlignment(cr: ContentResolver): String { + return Settings.Secure.getString(cr, SETTING_ALIGNMENT) ?: ALIGNMENT_CENTER + } + + private fun readSizeScale(cr: ContentResolver): Float { + return if (Settings.Secure.getString(cr, SETTING_SIZE) == SIZE_LARGE) LARGE_SIZE_SCALE + else 1f + } + + private fun readDateBelow(cr: ContentResolver): Boolean { + return Settings.Secure.getString(cr, SETTING_DATE_POSITION) == DATE_POSITION_BELOW + } + + private fun readClockColor(cr: ContentResolver): Int? { + val raw = Settings.Secure.getString(cr, SETTING_CLOCK_COLOR) + if (raw.isNullOrEmpty() || raw == COLOR_AUTO) return null + return try { + android.graphics.Color.parseColor(raw) + } catch (_: Exception) { + null + } + } + + private fun computeShouldCenter(cr: ContentResolver): Boolean { + val align = readAlignment(cr) + if (align == ALIGNMENT_LEFT || align == ALIGNMENT_RIGHT) return false + if (align == ALIGNMENT_CENTER) return true + val id = readClockId(cr) + return !isDefaultLeftAlignedClock(id) + } + + @JvmStatic + fun isDefaultLeftAlignedClock(clockId: String): Boolean { + return clockId.equals("GENERAL", ignoreCase = true) || + clockId.equals("OLD_QUICKLOOK", ignoreCase = true) || + clockId.equals("AXION_AGE", ignoreCase = true) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt new file mode 100644 index 0000000000000..cdd6d34b8f32d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockViewExtensions.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks.extensions + +import android.content.Context +import android.graphics.Bitmap +import android.util.SparseArray +import android.view.View +import androidx.annotation.IdRes +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R + +private val viewCacheKey = Any() + +private const val MAX_SCALE_RATIO = 1.0f + +val Context.scaleRatio: Float + get() { + val displayMetrics = resources.displayMetrics + val sw = minOf(displayMetrics.widthPixels, displayMetrics.heightPixels) / displayMetrics.density + val ratio = sw / 420f + return minOf(ratio, MAX_SCALE_RATIO) + } + +fun Context.scaledDimen(resId: Int): Float { + return resources.getDimension(resId) * scaleRatio +} + +fun Context.scaledDimenInt(resId: Int): Int { + return scaledDimen(resId).toInt() +} + +fun createBitmaps(context: Context, resIds: IntArray): List { + return resIds.map { resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap() + } +} + +inline fun View.bindView(@IdRes id: Int): T { + val cache = getTag(R.id.view_cache_key) as? SparseArray + ?: SparseArray().also { setTag(R.id.view_cache_key, it) } + + val view = cache.get(id) ?: findViewById(id)!!.also { cache.put(id, it) } + return view as T +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt new file mode 100644 index 0000000000000..0ddd6da3830f6 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DepthWallpaperProvider.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.app.WallpaperManager +import android.content.ContentResolver +import android.content.Context +import android.database.ContentObserver +import android.graphics.Path +import android.os.Handler +import android.os.Looper +import android.provider.Settings +import android.util.Base64 +import android.util.Log +import java.nio.ByteBuffer +import java.nio.ByteOrder + +object DepthWallpaperProvider { + + private const val TAG = "DepthWallpaperProvider" + private const val SETTING_DEPTH_MASK = "ax_depth_subject_mask" + private const val SETTING_DEPTH_ENABLED = "ax_depth_clock_enabled" + private const val PATH_VERSION = 0x01 + + @Volatile + var subjectPath: Path? = null + private set + + @Volatile + var pathAspect: Float = 1f + private set + + @Volatile + var isEnabled: Boolean = false + private set + + private val listeners = mutableSetOf() + private val handler = Handler(Looper.getMainLooper()) + private var registered = false + private var contentResolver: ContentResolver? = null + private var wallpaperManager: WallpaperManager? = null + + interface DepthMaskListener { + fun onDepthDataChanged(path: Path?, pathAspect: Float) + } + + private val observer = object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + refreshAsync() + } + } + + fun init(context: Context) { + if (registered) return + registered = true + contentResolver = context.contentResolver + wallpaperManager = WallpaperManager.getInstance(context) + + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_DEPTH_MASK), + false, observer + ) + context.contentResolver.registerContentObserver( + Settings.Secure.getUriFor(SETTING_DEPTH_ENABLED), + false, observer + ) + + refreshAsync() + } + + fun addListener(listener: DepthMaskListener) { + listeners.add(listener) + listener.onDepthDataChanged( + if (isEnabled) subjectPath else null, + pathAspect + ) + if (subjectPath == null) { + refreshAsync() + } + } + + fun removeListener(listener: DepthMaskListener) { + listeners.remove(listener) + } + + private fun refreshAsync() { + val cr = contentResolver ?: return + Thread { + try { + val isLiveWallpaper = wallpaperManager?.wallpaperInfo != null + val enabled = !isLiveWallpaper && + Settings.Secure.getInt(cr, SETTING_DEPTH_ENABLED, 0) == 1 + val maskStr = if (enabled) { + Settings.Secure.getString(cr, SETTING_DEPTH_MASK) + } else null + + val pathResult = maskStr?.let { decodePath(it) } + + isEnabled = enabled + subjectPath = pathResult?.first + pathAspect = pathResult?.second ?: 1f + + handler.post { + notifyListeners(if (enabled) pathResult?.first else null) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to refresh depth data", e) + handler.post { notifyListeners(null) } + } + }.start() + } + + private fun decodePath(base64Str: String): Pair? { + return try { + val bytes = Base64.decode(base64Str, Base64.NO_WRAP) + if (bytes.size < 7) return null + + if (bytes[0].toInt() and 0xFF != PATH_VERSION) { + Log.w(TAG, "Unknown path format version: ${bytes[0]}") + return null + } + + val buf = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN) + buf.get() + + val extractW = buf.short.toInt() and 0xFFFF + val extractH = buf.short.toInt() and 0xFFFF + val numContours = buf.short.toInt() and 0xFFFF + + if (extractW <= 0 || extractH <= 0 || numContours <= 0) return null + + val path = Path() + path.fillType = Path.FillType.WINDING + + for (c in 0 until numContours) { + if (buf.remaining() < 2) break + val numPoints = buf.short.toInt() and 0xFFFF + if (numPoints < 3 || buf.remaining() < numPoints * 4) continue + + for (p in 0 until numPoints) { + val x = (buf.short.toInt() and 0xFFFF).toFloat() + val y = (buf.short.toInt() and 0xFFFF).toFloat() + if (p == 0) path.moveTo(x, y) else path.lineTo(x, y) + } + path.close() + } + + Pair(path, extractW.toFloat() / extractH) + } catch (e: Exception) { + Log.e(TAG, "Failed to decode depth path", e) + null + } + } + + private fun notifyListeners(path: Path?) { + val aspect = pathAspect + for (listener in listeners) { + listener.onDepthDataChanged(path, aspect) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt new file mode 100644 index 0000000000000..5419376cfcd04 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/WeatherUtils.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import com.android.internal.util.alpha.OmniJawsClient +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData + +object WeatherUtils { + + fun getWeatherIcon(context: Context, conditionCode: Int): Drawable? { + return OmniJawsClient.get().getWeatherConditionImage(context, conditionCode) + } + + fun resolveWeatherBitmap( + context: Context, + weather: ClockWeatherData?, + iconSize: Int + ): Bitmap? { + if (weather == null || weather == ClockWeatherData.EMPTY) return null + + val iconBytes = weather.iconBytes + if (iconBytes != null && iconBytes.isNotEmpty()) { + return try { + val raw = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) + ?: return null + Bitmap.createScaledBitmap(raw, iconSize, iconSize, true) + } catch (_: Exception) { null } + } + + if (weather.conditionCode == 0) return null + return try { + val drawable = getWeatherIcon(context, weather.conditionCode) ?: return null + val bmp = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bmp) + drawable.setBounds(0, 0, iconSize, iconSize) + drawable.draw(canvas) + bmp + } catch (_: Exception) { null } + } +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt new file mode 100644 index 0000000000000..1c8a48986de97 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockAnimator.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.animation.ValueAnimator +import android.util.Log +import androidx.compose.ui.geometry.Offset +import com.android.app.animation.Interpolators + +fun AxClockView.animateAppear() { + Log.d(tag, "animateAppear") + animAlpha = 0f + ValueAnimator.ofFloat(0f, 1f).apply { + duration = APPEAR_DURATION + interpolator = Interpolators.EMPHASIZED_DECELERATE + addUpdateListener { animAlpha = it.animatedValue as Float } + start() + } +} + +fun AxClockView.animateCharge() { + Log.d(tag, "animateCharge") + if (isPreviewMode) return + val cx = width / 2f + val cy = height / 2f + state.fidgetPosition.value = Offset(cx, cy) + state.fidgetTrigger.value = System.currentTimeMillis() + onChargeAnimation() +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt new file mode 100644 index 0000000000000..f88a41a36fd34 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockConstants.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import androidx.compose.animation.core.CubicBezierEasing + +internal const val FONT_FAMILY_BODY = "variable-title-small" +internal const val FONT_FAMILY_DATE = "variable-title-medium-emphasized" + +internal const val SMALL_CLOCK_BOTTOM_PAD_DP = 12f + +internal const val ALARM_VISIBILITY_HOURS = 12L + +internal const val CLOCK_PATTERN_12 = "hhmm" +internal const val CLOCK_PATTERN_12_STANDARD = "hh:mm" +internal const val CLOCK_PATTERN_24 = "HHmm" +internal const val CLOCK_PATTERN_24_STANDARD = "HH:mm" +internal const val CLOCK_PATTERN_ALL = "hhmmss" + +internal const val APPEAR_DURATION = 400L + +const val PREVIEW_TIME_12 = "1008" +const val PREVIEW_TIME_12_STANDARD = "10:08" +const val PREVIEW_TIME_24 = "1008" +const val PREVIEW_TIME_24_STANDARD = "10:08" +const val PREVIEW_TIME_ALL = "100830" +const val PREVIEW_DATE = "Wed, 12 Mar" + +internal const val COMPOSE_FIDGET_SQUEEZE = 0.95f +internal const val COMPOSE_FIDGET_EXPAND = 1.03f +internal const val COMPOSE_FIDGET_PHASE_MS = 120 +internal const val COMPOSE_FIDGET_SETTLE_MS = 150 +internal val COMPOSE_FIDGET_EASING = CubicBezierEasing(0.26873f, 0f, 0.45042f, 1f) + +internal const val DOZE_WAKE_START = 0.96f +internal const val DOZE_WAKE_MS = 600 +internal val DOZE_EASING = CubicBezierEasing(0.2f, 0f, 0f, 1f) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt new file mode 100644 index 0000000000000..29b86130f5b9b --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockDisplayResolver.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.app.PendingIntent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.axion.quicklook.client.R as QuickLookR +import com.android.systemui.customization.R +import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData +import com.android.systemui.shared.clocks.WeatherUtils + +internal fun AxClockView.resolveDisplay( + clockData: ClockData, + media: ClockMediaState, + nowPlaying: String, + nowPlayingTapAction: PendingIntent?, + alarm: String, + date: String, +): DateDisplay { + if (media.isPlaying && media.trackTitle.isNotEmpty()) { + val fullText = if (media.artistName.isNotEmpty()) { + "${media.trackTitle} - ${media.artistName}" + } else { + "Now playing ${media.trackTitle}" + } + return DateDisplay.IconText(fullText, loadNowPlayingIcon(media.packageName), media.packageName.isEmpty()) + } + + if (nowPlaying.isNotBlank()) { + return DateDisplay.IconText(nowPlaying, loadNowPlayingIcon(media.packageName), false, nowPlayingTapAction) + } + + val activeSmartspace = clockData.smartspace.firstOrNull { + it.title.isNotEmpty() && !it.isSensitive + } + if (activeSmartspace != null) { + val text = if (activeSmartspace.subtitle.isNotEmpty()) { + "${activeSmartspace.title} · ${activeSmartspace.subtitle}" + } else { + activeSmartspace.title + } + return DateDisplay.IconText( + text, + decodeSmartspaceIcon(activeSmartspace.iconBytes), + true, + activeSmartspace.tapAction, + ) + } + + if (alarm.isNotBlank()) { + return DateDisplay.IconText(alarm, loadDrawableIcon(QuickLookR.drawable.ic_alarm), true) + } + + val cal = clockData.calendar.takeIf { it != CalendarSimpleData.EMPTY } + if (cal != null && cal.isEventVisible() && !cal.title.isNullOrEmpty()) { + return DateDisplay.IconText( + cal.description, + loadDrawableIcon(QuickLookR.drawable.ic_calendar), + true, + cal.tapAction, + ) + } + + val weather = clockData.weather.takeIf { it != ClockWeatherData.EMPTY } + if (weather != null && weather.temp.isNotEmpty()) { + return DateDisplay.Weather( + date, + weather.formattedTemp, + WeatherUtils.resolveWeatherBitmap(context, weather, iconSize), + weather.tintIcon, + weather.tapAction, + ) + } + + return DateDisplay.DateOnly(date) +} + +internal fun AxClockView.loadDrawableIcon(resId: Int): Bitmap? { + return try { + ContextCompat.getDrawable(context, resId) + ?.toBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + } catch (e: Exception) { + Log.e(tag, "Error loading icon", e) + null + } +} + +internal fun AxClockView.loadNowPlayingIcon(packageName: String): Bitmap? { + if (packageName.isNotEmpty()) { + try { + val appIcon = context.packageManager.getApplicationIcon(packageName) + return appIcon.toBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888) + } catch (_: Exception) {} + } + return loadDrawableIcon(QuickLookR.drawable.ic_now_playing_music_note) +} + +internal fun AxClockView.decodeSmartspaceIcon(iconBytes: ByteArray?): Bitmap? { + if (iconBytes == null || iconBytes.isEmpty()) return null + return try { + val raw = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size) ?: return null + Bitmap.createScaledBitmap(raw, iconSize, iconSize, true) + } catch (e: Exception) { + Log.e(tag, "Error decoding smartspace icon", e) + null + } +} + diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt new file mode 100644 index 0000000000000..d323fb69bce76 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockHost.kt @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.util.Log +import android.view.View +import android.view.ViewGroup +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.unit.LayoutDirection +import com.android.axion.compose.host.AxComposeView +import com.android.systemui.shared.clocks.ClockSettingsRepository + +class AxClockHost(private val clock: AxClockView) { + + private lateinit var composeView: AxComposeView + + fun attach(content: @Composable () -> Unit) { + clock.setWillNotDraw(false) + clock.clipChildren = false + clock.clipToPadding = false + clock.layoutDirection = View.LAYOUT_DIRECTION_LTR + ClockSettingsRepository.init(clock.context) + + composeView = AxComposeView(clock.context).apply { + setContent { Host { content() } } + } + + try { + clock.addView(composeView, ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + )) + } catch (e: Exception) { + Log.d("AxClockHost", "AxClockHost init failed error: $e") + } + } + + val view: AxComposeView get() = composeView + + @Composable + private fun Host(content: @Composable () -> Unit) { + val state = clock.state + + LaunchedEffect(Unit) { + ClockSettingsRepository.isDateBelow.collect { state.dateBelowState.value = it } + } + LaunchedEffect(Unit) { + ClockSettingsRepository.alignment.collect { state.alignmentState.value = it } + } + LaunchedEffect(Unit) { + ClockSettingsRepository.clockColorOverride.collect { state.clockColorOverrideState.value = it } + } + + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + val align = state.alignmentState.value + val trigger by state.fidgetTrigger + val isDoze by state.dozeFlow.collectAsState() + val animScale = remember { Animatable(1f) } + var initialDoze by remember { mutableStateOf(true) } + + LaunchedEffect(trigger) { + if (trigger == 0L || clock.useGlitchInteraction) return@LaunchedEffect + animScale.snapTo(1f) + animScale.animateTo(COMPOSE_FIDGET_SQUEEZE, tween(COMPOSE_FIDGET_PHASE_MS, easing = COMPOSE_FIDGET_EASING)) + animScale.animateTo(COMPOSE_FIDGET_EXPAND, tween(COMPOSE_FIDGET_PHASE_MS, easing = COMPOSE_FIDGET_EASING)) + animScale.animateTo(1f, tween(COMPOSE_FIDGET_SETTLE_MS, easing = COMPOSE_FIDGET_EASING)) + } + + LaunchedEffect(isDoze) { + if (initialDoze) { initialDoze = false; return@LaunchedEffect } + if (!isDoze) { + animScale.snapTo(DOZE_WAKE_START) + animScale.animateTo(1f, tween(DOZE_WAKE_MS, easing = DOZE_EASING)) + } + } + + val sizeModifier = if (clock.isLargeClock) { + Modifier.fillMaxWidth().wrapContentHeight() + } else { + Modifier.fillMaxSize() + } + Box( + modifier = sizeModifier + .graphicsLayer { + val a = animScale.value + scaleX = a + scaleY = a + when (align) { + ClockSettingsRepository.ALIGNMENT_LEFT -> + transformOrigin = TransformOrigin(0f, 0.5f) + ClockSettingsRepository.ALIGNMENT_RIGHT -> + transformOrigin = TransformOrigin(1f, 0.5f) + } + } + ) { + content() + } + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt new file mode 100644 index 0000000000000..04055fc654c46 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockInteractor.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.icu.util.TimeZone +import android.text.format.DateFormat +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import java.text.SimpleDateFormat +import java.util.* +import java.util.TimeZone as JavaTimeZone + +class AxClockInteractor( + private val context: Context, + val state: AxClockState, + val quickLook: QuickLookController, +) { + + val calendar: Calendar = Calendar.getInstance() + var format: String? = null + var timeStr: String = "" + internal set + var locale: Locale = Locale.getDefault() + + private var cachedSdf: SimpleDateFormat? = null + private var cachedDateFormat: SimpleDateFormat? = null + private var cachedDateLocale: Locale = locale + + var needsSeconds: Boolean = false + var useStandardFormat: Boolean = false + + fun onAlarmDataChanged(data: AlarmData) { quickLook.onAlarmDataChanged(data) } + fun onClockDataChanged(data: ClockData) { quickLook.onClockDataChanged(data) } + fun onPlaybackStateChanged(playing: Boolean) { quickLook.onPlaybackStateChanged(playing) } + fun onMetadataChanged(track: String, artist: String, packageName: String) { quickLook.onMetadataChanged(track, artist, packageName) } + fun onNowPlayingUpdate(text: String) { quickLook.onNowPlayingUpdate(text) } + + fun onDozeChanged(doze: Boolean) { state.isDoze = doze } + fun onScreenOff(screenOff: Boolean) { state.isScreenOff = screenOff } + fun onRegionDarknessChanged(regionDark: Boolean) { state.isRegionDark = regionDark } + fun onFontSettingChanged() { state.fontVersion.intValue++ } + + fun onStartedWakingUp() { + state.isDoze = false + state.isScreenOff = false + } + + fun onTimeZoneChanged(timeZone: TimeZone) { + calendar.timeZone = JavaTimeZone.getTimeZone(timeZone.id) + } + + fun refreshFormat(use24: Boolean, newLocale: Locale = locale) { + val newFormat = when { + useStandardFormat -> if (use24) CLOCK_PATTERN_24_STANDARD else CLOCK_PATTERN_12_STANDARD + needsSeconds -> CLOCK_PATTERN_ALL + else -> if (use24) CLOCK_PATTERN_24 else CLOCK_PATTERN_12 + } + + if (format == newFormat && locale == newLocale) return + format = newFormat + locale = newLocale + cachedSdf = null + cachedDateFormat = null + refreshTime() + } + + fun refreshTime(): Boolean { + format ?: return false + calendar.timeInMillis = System.currentTimeMillis() + if (cachedSdf == null) { + cachedSdf = SimpleDateFormat(format, Locale.ENGLISH) + } + val newTime = cachedSdf!!.format(calendar.time) + refreshDate() + val changed = timeStr != newTime + if (changed) { + timeStr = newTime + } + state.timeState.value = timeStr + return changed + } + + fun refreshDate() { + if (cachedDateFormat == null || cachedDateLocale != locale) { + // Respect the locale's preferred day/month order (en_US: "Thu, May 14", + // en_GB/pt_PT: "Thu, 14 May") instead of hardcoding day-first. Matches the + // skeleton used by BC IcuDateTextView (cr_strings smartspace_icu_date_pattern). + val pattern = DateFormat.getBestDateTimePattern(locale, "EEEMMMd") + cachedDateFormat = SimpleDateFormat(pattern, locale) + cachedDateLocale = locale + } + state.dateStrFlow.value = cachedDateFormat!!.format(calendar.time) + } + + fun setupPreview(setPreviewMode: () -> Unit) { + setPreviewMode() + + val use24 = DateFormat.is24HourFormat(context) + refreshFormat(use24) + + val previewFormat = format ?: if (use24) CLOCK_PATTERN_24 else CLOCK_PATTERN_12 + timeStr = when (previewFormat) { + CLOCK_PATTERN_12_STANDARD, CLOCK_PATTERN_24_STANDARD -> PREVIEW_TIME_12_STANDARD + CLOCK_PATTERN_ALL -> PREVIEW_TIME_ALL + else -> PREVIEW_TIME_12 + } + state.dateStrFlow.value = PREVIEW_DATE + state.timeState.value = timeStr + state.dozeAmountFlow.value = 0f + } + + val talkBackContent: String + get() { + val pattern = if (DateFormat.is24HourFormat(context)) CLOCK_PATTERN_24_STANDARD else "hh:mm" + return SimpleDateFormat(pattern, Locale.ENGLISH).format(calendar.time) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt new file mode 100644 index 0000000000000..539834bfc4647 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockModels.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.graphics.Bitmap + +data class ClockMediaState( + val isPlaying: Boolean = false, + val trackTitle: String = "", + val artistName: String = "", + val packageName: String = "", +) + +data class ClockUiState( + val time: String = "", + val date: String = "", + val isDoze: Boolean = false, + val screenOff: Boolean = false, + val regionDark: Boolean = false, + val icon: Bitmap? = null, + val tintIcon: Boolean = true, + val display: DateDisplay = DateDisplay.DateOnly(""), + val colorOverride: Int? = null, +) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt new file mode 100644 index 0000000000000..d653b2a010d1e --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockState.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.geometry.Offset +import com.android.systemui.shared.clocks.ClockSettingsRepository +import kotlinx.coroutines.flow.MutableStateFlow + +class AxClockState { + + val dozeFlow = MutableStateFlow(false) + val screenOffFlow = MutableStateFlow(false) + val regionDarkFlow = MutableStateFlow(false) + val dozeAmountFlow = MutableStateFlow(0f) + val dateStrFlow = MutableStateFlow("") + + val timeState = mutableStateOf("") + val fidgetTrigger = mutableStateOf(0L) + val fidgetPosition = mutableStateOf(Offset.Zero) + val dateBelowState = mutableStateOf(false) + val alignmentState = mutableStateOf(ClockSettingsRepository.alignment.value) + val clockColorOverrideState = mutableStateOf(ClockSettingsRepository.clockColorOverride.value) + val fontVersion = mutableIntStateOf(0) + + var isDoze: Boolean + get() = dozeFlow.value + set(value) { dozeFlow.value = value } + var isScreenOff: Boolean + get() = screenOffFlow.value + set(value) { screenOffFlow.value = value } + var isRegionDark: Boolean + get() = regionDarkFlow.value + set(value) { regionDarkFlow.value = value } + + val dateStr: String get() = dateStrFlow.value +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt new file mode 100644 index 0000000000000..8a61175a94b64 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockView.kt @@ -0,0 +1,372 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.content.res.Configuration +import android.graphics.* +import android.icu.util.TimeZone +import android.text.format.DateFormat +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.customization.R +import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.* +import com.android.systemui.shared.clocks.ClockConfigs +import com.android.systemui.shared.clocks.ClockSettingsRepository +import com.android.systemui.shared.clocks.extensions.* +import java.util.Locale +import kotlinx.coroutines.* + +abstract class AxClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : ViewGroup(context, attrs, defStyleAttr, defStyleRes) { + + val state = AxClockState() + val quickLook = QuickLookController(this) + val interactor = AxClockInteractor(context, state, quickLook) + val viewModel = AxClockViewModel(state, quickLook) + internal val host = AxClockHost(this) + + private var uiScope: CoroutineScope? = null + + var isLargeClock = false + + var isPreviewMode = false + set(value) { + field = value + if (value) { + touchEnabled = false + depthEffectEnabled = false + animAlpha = 1f + } + } + + var animAlpha: Float = 1f + set(value) { + if (isPreviewMode && value != 1f) return + field = value + alpha = value + } + + var touchEnabled: Boolean = true + + private val depthController = ClockDepthController(this) + var depthEffectEnabled: Boolean + get() = depthController.enabled + set(value) { depthController.enabled = value } + + protected open val clockHeightBase: Int get() = context.scaledDimenInt(R.dimen.clock_height) + val clockPaddingTop get() = context.scaledDimen(R.dimen.clock_padding_top) + val clockPaddingStart get() = context.scaledDimen(R.dimen.clock_padding_start) + val clockDateTextSize get() = context.scaledDimen(R.dimen.clock_date_text_size) + val clockDateMarginTop get() = context.scaledDimen(R.dimen.clock_date_margin_top) + val scaleRatio get() = context.scaleRatio + val sizeScale get() = when { + isPreviewMode -> 1f + isLargeClock -> 1f + else -> ClockSettingsRepository.sizeScale.value + } + val iconSize get() = context.scaledDimenInt(R.dimen.clock_icon_secondary_size) + + protected val config: ClockConfigs.ClockStyleConfig? + get() { + val className = this::class.simpleName ?: return null + return ClockConfigs.resolveConfig(className, isLargeClock, state.alignmentState.value) + } + + val isLeftAligned: Boolean get() = config?.align == ClockConfigs.Align.LEFT + val isRightAligned: Boolean get() = config?.align == ClockConfigs.Align.RIGHT + val isSideAligned: Boolean get() = isLeftAligned || isRightAligned + + val clockHeight: Int + get() { + val resHeight = config?.customHeightRes?.let { context.scaledDimenInt(it) } ?: clockHeightBase + val bottomPad = if (!isLargeClock) { + (SMALL_CLOCK_BOTTOM_PAD_DP * context.resources.displayMetrics.density).toInt() + } else 0 + return ((resHeight + dateHeight) * sizeScale).toInt() + bottomPad + } + + val dateMarginTop: Int + get() { + val cfg = config ?: return 0 + if (!cfg.visible) return 0 + return (cfg.customDateMarginTop?.let { context.scaledDimen(it) } ?: clockDateMarginTop).toInt() + } + + val dateHeight: Int + get() { + val cfg = config ?: return 0 + if (!cfg.visible) return 0 + return when (cfg.position) { + ClockConfigs.Position.ABOVE -> (clockDateTextSize + dateMarginTop + clockPaddingTop).toInt() + ClockConfigs.Position.BELOW -> clockDateTextSize.toInt() + else -> 0 + } + } + + var isDoze: Boolean + get() = state.isDoze + set(value) { state.isDoze = value } + var isScreenOff: Boolean + get() = state.isScreenOff + set(value) { state.isScreenOff = value } + var isRegionDark: Boolean + get() = state.isRegionDark + set(value) { state.isRegionDark = value } + val dateStr: String get() = state.dateStr + + init { + host.attach { Content() } + } + + abstract override fun getTag(): String + + @Composable + protected abstract fun Content() + + internal open val useGlitchInteraction: Boolean = false + + open fun onAlarmDataChanged(data: AlarmData) { interactor.onAlarmDataChanged(data) } + open fun onClockDataChanged(data: ClockData) { interactor.onClockDataChanged(data) } + open fun onDateChanged() {} + open fun onThemeChanged(isDarkTheme: Boolean) {} + open fun onPlaybackStateChanged(playing: Boolean) { interactor.onPlaybackStateChanged(playing) } + open fun onMetadataChanged(track: String, artist: String, packageName: String) { interactor.onMetadataChanged(track, artist, packageName) } + open fun onNowPlayingUpdate(npText: String) { interactor.onNowPlayingUpdate(npText) } + open fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) {} + fun onDepthEffectVisibilityChanged(visible: Boolean) { depthController.setDepthVisible(visible) } + fun setMessageBuffer(buffer: MessageBuffer) {} + open fun onDozeChanged(doze: Boolean) { interactor.onDozeChanged(doze) } + open fun onChargeAnimation() {} + open fun onPulsingChanged(doze: Boolean) {} + open fun onScreenOff(screenOff: Boolean) { interactor.onScreenOff(screenOff) } + open fun onRegionDarknessChanged(regionDark: Boolean) { interactor.onRegionDarknessChanged(regionDark) } + open fun onFontSettingChanged() { interactor.onFontSettingChanged() } + open fun onTimeZoneChanged(timeZone: TimeZone) { interactor.onTimeZoneChanged(timeZone) } + + open fun onDozeAmountChanged(linear: Float, eased: Float) { + if (isPreviewMode) return + state.dozeAmountFlow.value = eased + } + + open fun onStartedWakingUp() { + interactor.onStartedWakingUp() + uiScope?.launch { + delay(1250) + interactor.refreshTime() + } + } + + open fun onStartedGoingToSleep(isKeyguardVisible: Boolean) {} + open fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) {} + + open fun refreshFormat(use24: Boolean, newLocale: Locale = interactor.locale) { + interactor.needsSeconds = (this as? BitmapDigitComposeClockView)?.faceStyle?.needsPerSecondTick == true + interactor.useStandardFormat = this is OldQuickLookClockView + interactor.refreshFormat(use24, newLocale) + } + + open fun refreshTime() { + if (interactor.refreshTime()) { + contentDescription = interactor.talkBackContent + } + } + + fun refreshDate() { interactor.refreshDate() } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + if (!touchEnabled) return false + return super.dispatchTouchEvent(ev) + } + + override fun draw(canvas: Canvas) { + if (!depthController.shouldApplyDepth()) { + super.draw(canvas) + return + } + depthController.drawWithDepth(canvas) { super.draw(it) } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + Log.d(tag, "onAttachedToWindow") + ClockSettingsRepository.init(context) + uiScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + (parent as? ViewGroup)?.let { + it.clipChildren = false + it.clipToPadding = false + } + depthController.onAttached() + uiScope?.launch { + ClockSettingsRepository.sizeScale.collect { requestLayout() } + } + refreshTime() + state.timeState.value = interactor.timeStr + state.dateBelowState.value = ClockSettingsRepository.isDateBelow.value + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + Log.d(tag, "onDetachedFromWindow") + uiScope?.cancel() + depthController.onDetached() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val newLocale = newConfig.locale + if (newLocale != interactor.locale) { + uiScope?.launch { refreshFormat(DateFormat.is24HourFormat(context), newLocale) } + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val w = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec) + val cv = host.view + val mode = MeasureSpec.getMode(heightMeasureSpec) + val maxH = MeasureSpec.getSize(heightMeasureSpec) + + cv.measure( + MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), + ) + val naturalH = cv.measuredHeight + val floor = if (isLargeClock) 0 else clockHeight + val finalH = when (mode) { + MeasureSpec.EXACTLY -> maxOf(naturalH, maxH, floor) + else -> maxOf(naturalH, floor) + } + setMeasuredDimension(w, finalH) + if (w > 0 && finalH > 0 && finalH != naturalH) { + cv.measure( + MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(finalH, MeasureSpec.EXACTLY), + ) + } + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val cv = host.view + if (!cv.isAttachedToWindow) return + if (!isLargeClock && (isPreviewMode)) { + cv.measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY), + ) + } + cv.layout(0, 0, width, height) + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + pivotX = w / 2f + pivotY = h / 2f + } + + protected open fun getContentBounds(): RectF? = null + + open fun setupPreview() { + interactor.setupPreview { + isPreviewMode = true + isDoze = false + isScreenOff = false + isRegionDark = false + } + } + + @Composable + protected fun rememberClockState(): ClockUiState = viewModel.rememberClockState() + + @Composable + protected fun tintColor(isDoze: Boolean, screenOff: Boolean, regionDark: Boolean): Color = + viewModel.tintColor(isDoze, screenOff, regionDark) + + @Composable + protected fun inverseSizeScaleModifier(): Modifier = Modifier + + @Composable + protected fun digitScaleModifier(): Modifier { + if (isLargeClock || isPreviewMode) return Modifier + val scaleValue by ClockSettingsRepository.sizeScale.collectAsState() + if (scaleValue == 1f) return Modifier + return Modifier.graphicsLayer { + scaleX = scaleValue + scaleY = scaleValue + } + } + + @Composable + protected fun EnhancedDateArea( + modifier: Modifier = Modifier, + textColor: Color = tintColor(state.dozeFlow.value, state.screenOffFlow.value, state.regionDarkFlow.value) + .copy(alpha = if (state.dozeFlow.value) 0.6f else 0.8f), + textSize: TextUnit = 18.sp, + fontFamily: FontFamily = remember(state.fontVersion.intValue) { resolveDateFontFamily() }, + fontWeight: FontWeight = FontWeight.Medium, + letterSpacing: TextUnit = 0.sp, + iconSize: Dp = 16.dp, + uppercase: Boolean = false, + rowArrangement: Arrangement.Horizontal = when { + isLeftAligned -> Arrangement.Start + isRightAligned -> Arrangement.End + else -> Arrangement.Center + }, + ) { + val display = viewModel.rememberResolvedDisplay() + val inverseModifier = inverseSizeScaleModifier() + QuickLookDateArea( + modifier = modifier.then(inverseModifier), + display = display, + dateStr = state.dateStr, + sizeScale = 1f, + textColor = textColor, + textSize = textSize, + fontFamily = fontFamily, + fontWeight = fontWeight, + letterSpacing = letterSpacing, + iconSize = iconSize, + uppercase = uppercase, + rowArrangement = rowArrangement, + ) + } + + companion object { + const val DEBUG = false + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt new file mode 100644 index 0000000000000..16ebc7415fbf4 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxClockViewModel.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color + +class AxClockViewModel( + private val state: AxClockState, + private val quickLook: QuickLookController, +) { + + @Composable + fun rememberClockState(): ClockUiState { + val time by state.timeState + val isDoze by state.dozeFlow.collectAsState() + val screenOff by state.screenOffFlow.collectAsState() + val regionDark by state.regionDarkFlow.collectAsState() + val colorOverride by state.clockColorOverrideState + val date by state.dateStrFlow.collectAsState() + val display = quickLook.rememberResolvedDisplay(date) + val displayDate = when (display) { + is DateDisplay.Weather -> { + if (display.temp.isNotEmpty()) "${display.date} ${display.temp}" else display.date + } + is DateDisplay.IconText -> display.text + is DateDisplay.DateOnly -> display.text + } + return ClockUiState( + time, displayDate, isDoze, screenOff, regionDark, + display.icon, display.tintIcon, display, colorOverride, + ) + } + + @Composable + fun tintColor(isDoze: Boolean, screenOff: Boolean, regionDark: Boolean): Color { + val override by state.clockColorOverrideState + if (isDoze || screenOff) return Color.White + override?.let { return Color(it) } + return if (regionDark) Color.White else Color.Black + } + + @Composable + fun rememberResolvedDisplay(): DateDisplay { + val date by state.dateStrFlow.collectAsState() + return quickLook.rememberResolvedDisplay(date) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt new file mode 100644 index 0000000000000..edacd475cd4cd --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/AxionAgeClockView.kt @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository +import kotlinx.coroutines.launch + +class AxionAgeClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override val useGlitchInteraction: Boolean = true + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val density = context.resources.displayMetrics.density + return ((SMALL_DIGIT_DP + SMALL_INFO_DP) * density * scaleRatio).toInt() + } + + override fun getTag(): String = + if (isLargeClock) "AxionAgeLargeClockView" else "AxionAgeClockView" + + @Composable + override fun Content() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val fidgetByTrigger by state.fidgetTrigger + + val weightFidget = remember { Animatable(0f) } + + LaunchedEffect(fidgetByTrigger) { + if (fidgetByTrigger > 0) { + launch { + weightFidget.animateTo( + targetValue = 1f, + animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) + ) + weightFidget.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } + } + } + + val large = isLargeClock + val fidgetValue = weightFidget.value + val textColor = tintColor(isDoze, screenOff, regionDark) + val isDark = isDoze || regionDark + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val sz = if (large) 1f else dynSizeScale + val digitW = (if (large) 90.dp else 48.dp) * sz + val digitH = (if (large) 150.dp else 108.dp) * sz + val digitSpacing = (if (large) 8.dp else 2.dp) * sz + val digitStroke = if (large) 1.5.dp else 1.dp + val glowExtra = if (large) 4f else 3f + val infoTextSize = if (large) 18.sp else 14.sp + val infoIconSize = if (large) 24.dp else 16.dp + val infoSpacing = if (large) 6.dp else 4.dp + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = when { + large -> Alignment.Center + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + }, + ) { + if (large) { + LargeLayout(time, date, isDark, fidgetValue, digitW, digitH, digitSpacing, digitStroke, glowExtra, infoTextSize, infoIconSize, infoSpacing, textColor) + } else { + SmallLayout(time, date, isDark, fidgetValue, digitW, digitH, digitSpacing, digitStroke, glowExtra, infoTextSize, infoIconSize, infoSpacing, textColor) + } + } + } + + @Composable + private fun LargeLayout( + time: String, date: String, isDark: Boolean, fidgetValue: Float, + digitW: Dp, digitH: Dp, digitSpacing: Dp, digitStroke: Dp, glowExtra: Float, + infoTextSize: androidx.compose.ui.unit.TextUnit, infoIconSize: Dp, infoSpacing: Dp, + textColor: Color, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (time.length >= 4) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(digitSpacing)) { + AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + AxDigit(time[1], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + Row(horizontalArrangement = Arrangement.spacedBy(digitSpacing)) { + AxDigit(time[2], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + AxDigit(time[3], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + } + } + EnhancedDateArea( + textColor = textColor, + textSize = infoTextSize, + iconSize = infoIconSize, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun SmallLayout( + time: String, date: String, isDark: Boolean, fidgetValue: Float, + digitW: Dp, digitH: Dp, digitSpacing: Dp, digitStroke: Dp, glowExtra: Float, + infoTextSize: androidx.compose.ui.unit.TextUnit, infoIconSize: Dp, infoSpacing: Dp, + textColor: Color, + ) { + val rowArrangement = when { + isLeftAligned -> Arrangement.Start + isRightAligned -> Arrangement.End + else -> Arrangement.Center + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = rowArrangement, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 28.dp), + ) { + if (time.length >= 4) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AxDigit(time[0], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[1], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[2], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + Spacer(modifier = Modifier.width(digitSpacing)) + AxDigit(time[3], isDark, fidgetValue, digitW, digitH, digitStroke, glowExtra, textColor) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + + EnhancedDateArea( + textColor = textColor, + textSize = infoTextSize, + iconSize = infoIconSize, + rowArrangement = Arrangement.Start, + ) + } + } + + @Composable + private fun AxDigit( + char: Char, isDark: Boolean, fidgetValue: Float, + w: Dp, h: Dp, strokeW: Dp, glowExtra: Float, + baseColor: Color, + ) { + val isDoze by state.dozeFlow.collectAsState() + val fillTop = when { + isDoze -> 0.6f + isDark -> 0.45f + fidgetValue * 0.2f + else -> 0.7f + fidgetValue * 0.15f + } + val fillBottom = when { + isDoze -> 0.3f + isDark -> 0.15f + fidgetValue * 0.1f + else -> 0.35f + fidgetValue * 0.1f + } + val brush = Brush.verticalGradient( + colors = listOf( + baseColor.copy(alpha = fillTop), + baseColor.copy(alpha = fillBottom), + ), + ) + val borderAlpha = when { + isDoze -> 0.5f + isDark -> 0.25f + fidgetValue * 0.15f + else -> 0.5f + fidgetValue * 0.15f + } + Canvas(modifier = Modifier.size(width = w, height = h)) { + drawDigit(char, brush, baseColor.copy(alpha = borderAlpha), strokeW.toPx(), baseColor, isDark, fidgetValue, glowExtra) + } + } + + private fun DrawScope.drawDigit( + char: Char, + brush: Brush, + borderColor: Color, + strokeWidth: Float, + accentColor: Color, + isDark: Boolean, + fidgetValue: Float, + glowExtra: Float, + ) { + val w = size.width + val h = size.height + val padding = w * 0.15f + val dw = w - padding * 2 + val dh = h - padding * 2 + val centerX = w / 2 + val centerY = h / 2 + + val fidgetWall = fidgetValue * (dw * -0.12f) + val wall = dw * 0.35f + fidgetWall + val path = Path() + + when (char) { + '0' -> path.addRoundRect(RoundRect(padding, padding, w - padding, h - padding, CornerRadius(dw / 2))) + '1' -> { path.moveTo(centerX, padding); path.lineTo(centerX, h - padding) } + '2' -> { + path.arcTo(Rect(padding, padding, w - padding, padding + dw), 180f, 180f, true) + path.lineTo(padding, h - padding) + path.lineTo(w - padding, h - padding) + } + '3' -> { + path.arcTo(Rect(padding, padding, w - padding, padding + dh / 2), 180f, 270f, false) + path.arcTo(Rect(padding, centerY, w - padding, h - padding), 270f, 270f, false) + } + '4' -> { + path.moveTo(w - padding, h - padding); path.lineTo(w - padding, padding) + path.moveTo(w - padding, centerY); path.lineTo(padding, centerY); path.lineTo(padding, padding) + } + '5' -> { + path.moveTo(w - padding, padding); path.lineTo(padding, padding); path.lineTo(padding, centerY) + path.arcTo(Rect(padding, centerY, w - padding, h - padding), -90f, 270f, false) + } + '6' -> { + path.moveTo(w - padding, padding); path.lineTo(padding, h - padding - dw / 2) + path.addOval(Rect(padding, h - padding - dw, w - padding, h - padding)) + } + '7' -> { + path.moveTo(padding, padding); path.lineTo(w - padding, padding); path.lineTo(centerX, h - padding) + } + '8' -> { + path.addOval(Rect(padding, padding, w - padding, centerY)) + path.addOval(Rect(padding, centerY, w - padding, h - padding)) + } + '9' -> { + path.moveTo(padding, h - padding); path.lineTo(w - padding, padding + dw / 2) + path.addOval(Rect(padding, padding, w - padding, padding + dw)) + } + } + + val strokeStyle = Stroke(width = wall, cap = StrokeCap.Round, join = StrokeJoin.Round) + drawPath(path = path, brush = brush, style = strokeStyle) + drawPath(path = path, color = borderColor, style = Stroke(width = wall + strokeWidth, cap = StrokeCap.Round, join = StrokeJoin.Round)) + + val glowAlpha = if (isDark) 0.2f else 0.12f + drawPath( + path = path, + color = accentColor.copy(alpha = glowAlpha), + style = Stroke(width = wall + glowExtra, cap = StrokeCap.Round, join = StrokeJoin.Round), + ) + } + + companion object { + private const val SMALL_DIGIT_DP = 108f + private const val SMALL_INFO_DP = 24f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt new file mode 100644 index 0000000000000..6a9a0fd65ed25 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/BitmapDigitComposeClockView.kt @@ -0,0 +1,933 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.Point +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.PorterDuffXfermode +import android.graphics.Rect +import android.graphics.RectF +import android.graphics.Typeface +import android.text.TextUtils +import android.text.format.DateFormat +import android.util.AttributeSet +import android.util.LruCache +import android.view.animation.PathInterpolator +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.foundation.basicMarquee +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository +import com.android.systemui.shared.clocks.extensions.createBitmaps +import com.android.systemui.shared.clocks.extensions.scaledDimen +import com.android.systemui.shared.clocks.extensions.scaleRatio +import kotlin.math.min + +private data class LargeFontLayout( + val visualTopOffset: Float, + val visualHeight: Float, + val cellWidth: Float, + val canvasHeightDp: Float, +) + +class BitmapDigitComposeClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + var faceStyle: ClockFaceStyle = ClockFaceStyle.DEFAULT + set(value) { + field = value + cachedConfig = BitmapFaceConfigs.getConfig(value) + (cachedConfig?.renderMode as? RenderMode.FontDigit)?.let { initFontMode(it) } + } + + private var cachedConfig: BitmapFaceConfig? = BitmapFaceConfigs.getConfig(faceStyle) + + private val typefaceCache = LruCache(16) + private val fontPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val fontWeights = IntArray(DIGIT_COUNT) { 0 } + private val digitAnimators = arrayOfNulls(DIGIT_COUNT) + private val digitAnimStarted = BooleanArray(DIGIT_COUNT) + private val digitRects = arrayOfNulls(DIGIT_COUNT) + private var isWakingUp = true + private var isKeyguardVisibleState = true + private var maxRadius = -1 + private var tapPos = Point(0, 0) + private var dozingAmountChanging = false + private var fidgetWeightAnimator: AnimatorSet? = null + private val weightVersion = mutableIntStateOf(0) + private val screenWidth = context.resources.displayMetrics.widthPixels + private val screenHeight = context.resources.displayMetrics.heightPixels + + private val forceResetRunnable = Runnable { + val mode = fontDigitMode ?: return@Runnable + if (!isWakingUp) return@Runnable + for (i in 0 until DIGIT_COUNT) { + animateDigitTo(i, mode.lsFontWeight, FORCE_ANIM_MS) + } + dozingAmountChanging = false + } + + private val fontDigitMode: RenderMode.FontDigit? + get() = (cachedConfig?.renderMode as? RenderMode.FontDigit) + + private val dotPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + private val handPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + strokeCap = Paint.Cap.ROUND + } + private val tickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL } + + private fun initFontMode(mode: RenderMode.FontDigit) { + fontPaint.textSize = mode.fontSize * context.scaleRatio + for (i in 0 until DIGIT_COUNT) { + fontWeights[i] = mode.lsFontWeight + } + } + + private fun getCachedTypeface(weight: Int, fontPath: String): Typeface { + val snapped = (weight / WEIGHT_SNAP) * WEIGHT_SNAP + return typefaceCache.get(snapped) ?: Typeface.Builder(context.assets, fontPath) + .setFontVariationSettings("'wght' $snapped") + .build() + .also { typefaceCache.put(snapped, it) } + } + + private fun dateTextColor( + config: BitmapFaceConfig, + isDoze: Boolean, + screenOff: Boolean, + regionDark: Boolean, + ): Color { + val base = androidColorToComposeColor(config.clockColor(isDoze, screenOff, regionDark)) + return base.copy(alpha = if (isDoze) 0.6f else 0.8f) + } + + private fun animateDigitTo( + index: Int, + target: Int, + duration: Long = RIPPLE_ANIM_MS, + ) { + val current = fontWeights[index] + if (current == target) return + digitAnimators[index]?.cancel() + digitAnimators[index] = ValueAnimator.ofInt(current, target).apply { + this.duration = duration + interpolator = RIPPLE_INTERPOLATOR + addUpdateListener { + fontWeights[index] = it.animatedValue as Int + weightVersion.intValue++ + } + start() + } + } + + private fun cancelDigitAnimators() { + for (i in 0 until DIGIT_COUNT) { + digitAnimators[i]?.cancel() + digitAnimators[i] = null + } + } + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val config = cachedConfig ?: return super.clockHeightBase + val density = context.resources.displayMetrics.density + val contentHeight = when (val mode = config.renderMode) { + is RenderMode.BitmapDigit -> { + if (config.digitResIds.isEmpty()) return super.clockHeightBase + val d = ContextCompat.getDrawable(context, config.digitResIds[0]) + ?: return super.clockHeightBase + d.intrinsicHeight * scaleRatio * config.smallScaleMultiplier + } + is RenderMode.FontDigit -> { + fontPaint.textSize = mode.fontSize * scaleRatio + val metrics = fontPaint.fontMetrics + metrics.descent - metrics.ascent + } + is RenderMode.AnalogClock -> return super.clockHeightBase + } + return (contentHeight + (DATE_ROW_DP + config.dateSpacingDp + config.topPaddingDp + config.bottomPaddingDp) * density).toInt() + } + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val config = cachedConfig ?: return + when (config.renderMode) { + is RenderMode.BitmapDigit -> SmallBitmapContent(config) + is RenderMode.FontDigit -> SmallFontContent(config, config.renderMode) + is RenderMode.AnalogClock -> SmallAnalogContent(config) + } + } + + @Composable + private fun SmallBitmapContent(config: BitmapFaceConfig) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val bitmaps = remember(config) { loadDigitBitmaps(config.digitResIds) } + val lightBitmaps = remember(config) { + config.digitLightResIds?.let { loadDigitBitmaps(it) } + } + + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * config.smallScaleMultiplier * dynSizeScale + val spacing = if (config.negativeSpacing) { + -context.scaledDimen(config.digitSpacingRes) * dynSizeScale + } else { + context.scaledDimen(config.digitSpacingRes) * dynSizeScale + } + val overlapPadding = if (config.overlapSpacingRes != 0) context.scaledDimen(config.overlapSpacingRes) * dynSizeScale else 0f + + val dotSize = context.scaledDimen(config.dotSizeRes) * dynSizeScale + val dotMargin = context.scaledDimen(config.dotMarginRes) * dynSizeScale + val dotCenterMargin = context.scaledDimen(config.dotCenterMarginRes) * dynSizeScale + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val sampleBitmap = remember(config) { + ContextCompat.getDrawable(context, config.digitResIds[0]) + } + val canvasHeight = remember(sampleBitmap, scale) { + ((sampleBitmap?.intrinsicHeight ?: 0) * scale).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp), + ) { + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val naturalWidth = computeTotalWidth( + time, bitmaps, scale, spacing, overlapPadding, + config, dotCenterMargin + ) + if (naturalWidth <= 0f) return@Canvas + + val fitScale = fitToWidth(naturalWidth) + val drawScale = scale * fitScale + val drawSpacing = spacing * fitScale + val drawOverlap = overlapPadding * fitScale + val drawDotSize = dotSize * fitScale + val drawDotMargin = dotMargin * fitScale + val drawDotCenterMargin = dotCenterMargin * fitScale + val finalWidth = naturalWidth * fitScale + val startX = when { + isLeftAligned -> clockPaddingStart + isRightAligned -> size.width - clockPaddingStart - finalWidth + else -> (size.width - finalWidth) / 2f + } + val minuteStartIndex = if (time.length == 4) 2 else 1 + val minuteTint = if (config.minuteAlpha < 1f && !isDoze && !screenOff) { + tintColor.copy(alpha = config.minuteAlpha) + } else { + tintColor + } + + var x = startX + time.forEachIndexed { index, char -> + val useLightVariant = shouldUseLightVariant(config, time, index, isDoze, screenOff) + val bitmapMap = if (useLightVariant && lightBitmaps != null) lightBitmaps else bitmaps + val bitmap = bitmapMap[char] ?: return@forEachIndexed + + val yOffset = (size.height - bitmap.height * drawScale) / 2f + + val digitTint = if (index >= minuteStartIndex) minuteTint else tintColor + drawScaledBitmap(bitmap, x, yOffset, drawScale, digitTint) + + x += bitmap.width * drawScale + x += getCustomSpacing(config, time, index, drawSpacing, drawOverlap) + + if (shouldDrawSeparator(config, time, index)) { + x += drawDotSeparator( + x, yOffset, bitmap.height * drawScale, + drawDotSize, drawDotMargin, drawDotCenterMargin, tintColor + ) + } else if (index < time.lastIndex) { + x += drawSpacing + } + } + } + } + } + + @Composable + private fun SmallFontContent(config: BitmapFaceConfig, mode: RenderMode.FontDigit) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * dynSizeScale + val canvasHeight = remember(scale) { + fontPaint.textSize = mode.fontSize * scale + val metrics = fontPaint.fontMetrics + (metrics.descent - metrics.ascent).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp), + ) { + weightVersion.intValue + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val nativeCanvas = drawContext.canvas.nativeCanvas + val displayTime = formatDisplayTime(time) + if (displayTime.isEmpty()) return@Canvas + + fontPaint.textSize = mode.fontSize * scale + fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) + + val naturalWidths = FloatArray(displayTime.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(displayTime[i].toString()) + } + val naturalWidth = naturalWidths.sum() + val fitScale = fitToWidth(naturalWidth) + if (fitScale < 1f) fontPaint.textSize = mode.fontSize * scale * fitScale + val digitWidths = if (fitScale < 1f) { + FloatArray(displayTime.length) { i -> + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.measureText(displayTime[i].toString()) + } + } else naturalWidths + val totalWidth = digitWidths.sum() + + val metrics = fontPaint.fontMetrics + val baselineY = (size.height / 2f) - ((metrics.top + metrics.bottom) / 2f) + + val indices = displayTime.indices.let { range -> + if (isWakingUp && !isSideAligned) range.reversed() else range.toList() + } + + val firstBounds = Rect() + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(0) { mode.lsFontWeight }, mode.fontPath + ) + fontPaint.getTextBounds(displayTime[0].toString(), 0, 1, firstBounds) + val leftBearing = firstBounds.left.toFloat() + + val startX = when { + isLeftAligned -> clockPaddingStart - leftBearing + isRightAligned -> size.width - clockPaddingStart - totalWidth + isWakingUp -> (size.width / 2f) + (totalWidth / 2f) + else -> (size.width - totalWidth) / 2f + } + var x = startX + + for (i in indices) { + if (isWakingUp && !isSideAligned) { + x -= digitWidths[i] + } + + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(i) { mode.lsFontWeight }, mode.fontPath + ) + nativeCanvas.drawText(displayTime[i].toString(), x, baselineY, fontPaint) + + val charRect = Rect() + fontPaint.getTextBounds(displayTime[i].toString(), 0, 1, charRect) + charRect.offset(x.toInt(), baselineY.toInt()) + val loc = IntArray(2) + getLocationOnScreen(loc) + charRect.offset(loc[0], loc[1]) + if (i < DIGIT_COUNT) digitRects[i] = charRect + + if (!isWakingUp || isSideAligned) { + x += digitWidths[i] + } + } + } + } + } + + @Composable + private fun SmallAnalogContent(config: BitmapFaceConfig) { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + val dateColor = dateTextColor(config, isDoze, screenOff, regionDark) + + SmallShell(config, dateColor) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .analogDrawModifier(config, time, isDoze, screenOff, regionDark), + ) + Spacer(modifier = Modifier.height(ANALOG_DATE_GAP_DP.dp)) + } + } + + @Composable + private fun SmallShell( + config: BitmapFaceConfig, + textColor: Color, + content: @Composable ColumnScope.() -> Unit, + ) { + val dateBelow by state.dateBelowState + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val topPadding = config.topPaddingDp.dp + val bottomPadding = config.bottomPaddingDp.dp + val dateSpacing = config.dateSpacingDp.dp + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + val datePaddingModifier = if (isSideAligned) Modifier.padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ) else Modifier + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + if (!dateBelow) { + if (topPadding > 0.dp) Spacer(modifier = Modifier.height(topPadding)) + EnhancedDateArea(modifier = datePaddingModifier, textColor = textColor) + Spacer(modifier = Modifier.height(dateSpacing)) + } + + content() + + if (dateBelow) { + Spacer(modifier = Modifier.height(dateSpacing)) + EnhancedDateArea(modifier = datePaddingModifier, textColor = textColor) + if (topPadding > 0.dp) Spacer(modifier = Modifier.height(topPadding)) + } + + if (!dateBelow && bottomPadding > 0.dp) Spacer(modifier = Modifier.height(bottomPadding)) + } + } + + @Composable + private fun LargeContent() { + val config = cachedConfig ?: return + when (config.renderMode) { + is RenderMode.BitmapDigit -> LargeBitmapContent(config) + is RenderMode.FontDigit -> LargeFontContent(config, config.renderMode) + is RenderMode.AnalogClock -> LargeAnalogContent(config) + } + } + + @Composable + private fun LargeBitmapContent(config: BitmapFaceConfig) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val bitmaps = remember(config) { + loadDigitBitmaps(config.digitLargeResIds ?: config.digitResIds) + } + + val baseScale = minOf(context.scaleRatio, MAX_TABLET_SCALE) + val scale = baseScale * config.largeScaleMultiplier + val digitSpacing = context.scaledDimen(config.largeDigitSpacingRes) + val lineSpacing = context.scaledDimen(config.largeLineSpacingRes) + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val finalSpacing = if (config.negativeSpacing) { + -context.scaledDimen(config.largeDigitSpacingRes) + } else { + digitSpacing + } + + val sampleBmp = remember(config) { bitmaps['0'] } + val digitHeightPx = (sampleBmp?.height ?: 0) * scale + val canvasHeightDp = with(LocalDensity.current) { + (digitHeightPx * 2 + lineSpacing).toDp() + } + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp), + ) { + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val (hours, minutes) = splitTimeLines(time) + if (hours.isEmpty()) return@Canvas + + val hoursWidth = computeLineWidth(hours, bitmaps, scale, finalSpacing) + val minutesWidth = computeLineWidth(minutes, bitmaps, scale, finalSpacing) + + val sampleBitmap = bitmaps['0'] ?: return@Canvas + val digitHeight = sampleBitmap.height * scale + + val hoursX = (size.width - hoursWidth) / 2f + val minutesX = (size.width - minutesWidth) / 2f + + drawDigitLine(hours, bitmaps, scale, hoursX, 0f, finalSpacing, tintColor) + drawDigitLine(minutes, bitmaps, scale, minutesX, digitHeight + lineSpacing, finalSpacing, tintColor) + } + + Spacer(modifier = Modifier.height(16.dp)) + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun LargeFontContent(config: BitmapFaceConfig, mode: RenderMode.FontDigit) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + + val scale = context.scaleRatio + val largeFontSize = mode.fontSize * scale * mode.largeScale + val fontLayout = remember(largeFontSize, mode.lineSpacing, scale, mode.fontPath, mode.lsFontWeight) { + val tempPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = largeFontSize + typeface = getCachedTypeface(mode.lsFontWeight, mode.fontPath) + } + val bounds = Rect() + var topMin = Int.MAX_VALUE + var bottomMax = Int.MIN_VALUE + var advanceMax = 0f + for (d in '0'..'9') { + tempPaint.getTextBounds(d.toString(), 0, 1, bounds) + if (bounds.top < topMin) topMin = bounds.top + if (bounds.bottom > bottomMax) bottomMax = bounds.bottom + val adv = tempPaint.measureText(d.toString()) + if (adv > advanceMax) advanceMax = adv + } + val visualHeight = (bottomMax - topMin).toFloat() + val canvasPx = visualHeight * 2f + mode.lineSpacing * scale + LargeFontLayout( + visualTopOffset = topMin.toFloat(), + visualHeight = visualHeight, + cellWidth = advanceMax, + canvasHeightDp = canvasPx / context.resources.displayMetrics.density, + ) + } + val canvasHeightDp = fontLayout.canvasHeightDp + + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp.dp), + ) { + weightVersion.intValue + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val nativeCanvas = drawContext.canvas.nativeCanvas + fontPaint.textSize = largeFontSize + fontPaint.typeface = getCachedTypeface(mode.lsFontWeight, mode.fontPath) + fontPaint.color = config.clockColor(isDoze, screenOff, regionDark) + + val displayTime = formatDisplayTime(time) + if (displayTime.isEmpty()) return@Canvas + + val (hours, minutes) = splitTimeLines(displayTime) + if (hours.isEmpty()) return@Canvas + + val cellWidth = fontLayout.cellWidth + val hoursBaselineY = -fontLayout.visualTopOffset + val minutesBaselineY = hoursBaselineY + fontLayout.visualHeight + mode.lineSpacing * scale + + fun drawCenteredLine(digits: String, baselineY: Float, weightStart: Int) { + if (digits.isEmpty()) return + val lineWidth = cellWidth * digits.length + var cellX = (size.width - lineWidth) / 2f + for (i in digits.indices) { + fontPaint.typeface = getCachedTypeface( + fontWeights.getOrElse(weightStart + i) { mode.lsFontWeight }, + mode.fontPath, + ) + val glyphW = fontPaint.measureText(digits[i].toString()) + val glyphX = cellX + (cellWidth - glyphW) / 2f + nativeCanvas.drawText(digits[i].toString(), glyphX, baselineY, fontPaint) + cellX += cellWidth + } + } + + drawCenteredLine(hours, hoursBaselineY, 0) + drawCenteredLine(minutes, minutesBaselineY, hours.length) + } + + Spacer(modifier = Modifier.height(16.dp)) + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun LargeAnalogContent(config: BitmapFaceConfig) { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + Box( + modifier = Modifier.fillMaxWidth().aspectRatio(1f), + contentAlignment = Alignment.Center, + ) { + Box(modifier = Modifier.fillMaxSize().analogDrawModifier(config, time, isDoze, screenOff, regionDark)) + + val tintColor = androidColorToComposeColor( + config.clockColor(isDoze, screenOff, regionDark) + ) + Box( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp), + contentAlignment = Alignment.Center, + ) { + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + } + + private fun loadDigitBitmaps(resIds: IntArray): Map { + val result = mutableMapOf() + resIds.forEachIndexed { index, resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap()?.let { bmp -> + result['0' + index] = bmp + } + } + return result + } + + private fun Modifier.analogDrawModifier( + config: BitmapFaceConfig, + time: String, + isDoze: Boolean, + screenOff: Boolean, + regionDark: Boolean, + ): Modifier = drawWithContent { + drawContent() + state.clockColorOverrideState.value + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@drawWithContent + + val canvas = drawContext.canvas.nativeCanvas + val clockSz = min(size.width, size.height) + val cx = size.width / 2f + val cy = size.height / 2f + val color = config.clockColor(isDoze, screenOff, regionDark) + val highlightColor = ContextCompat.getColor(context, R.color.clock_dot_color) + val bitmaps = createBitmaps(context, config.tickResIds) + val tick = bitmaps.getOrNull(0) + val tickLight = bitmaps.getOrNull(1) + + val baseScale = context.scaleRatio * sizeScale + val tickNaturalW = (tick?.width ?: 0) * baseScale + val fitScale = fitToWidth(tickNaturalW) + val scale = baseScale * fitScale + val dotSize = context.scaledDimen(R.dimen.dot_size) * sizeScale * fitScale + val handSize = context.scaledDimen(R.dimen.clock_hand_size) * sizeScale * fitScale + + if (tick != null && tickLight != null) { + tickPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) + val tw = tick.width * scale + val left = cx - tw / 2f + val top = cy - tick.height * scale / 2f + val matrix = Matrix().apply { + postScale(scale, scale) + postTranslate(left, top) + } + if (!isDoze && !screenOff) { + canvas.drawBitmap(tickLight, matrix, tickPaint) + } + canvas.drawBitmap(tick, matrix, tickPaint) + } + + dotPaint.color = color + dotPaint.xfermode = null + val outerR = dotSize * 1.5f / 2f + canvas.drawOval( + RectF(cx - outerR, cy - outerR, cx + outerR, cy + outerR), + dotPaint, + ) + + try { + val seconds = time.takeLast(2).toInt() + val minutes = time.dropLast(2).takeLast(2).toInt() + val hours = time.dropLast(4).toInt() + val hourAngle = (hours + minutes / 60f) * 5 + val minuteAngle = minutes + seconds / 60f + + drawClockHand(canvas, handPaint, hourAngle, handSize * 2, 0.22f, 0.38f, color, cx, cy, clockSz) + drawClockHand(canvas, handPaint, minuteAngle, handSize, 0.22f, 0.42f, color, cx, cy, clockSz) + drawClockHand(canvas, handPaint, seconds.toFloat(), handSize / 2, 0.19f, 0.42f, highlightColor, cx, cy, clockSz) + } catch (_: NumberFormatException) {} + + if (!isDoze && !screenOff) { + dotPaint.color = highlightColor + dotPaint.xfermode = null + } else { + dotPaint.color = android.graphics.Color.TRANSPARENT + dotPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } + val innerR = dotSize / 2f + canvas.drawOval( + RectF(cx - innerR, cy - innerR, cx + innerR, cy + innerR), + dotPaint, + ) + } + + private fun formatDisplayTime(time: String): String { + if (time.length < 3) return time + if (!DateFormat.is24HourFormat(context)) { + val hour = time.substring(0, 2).toIntOrNull() ?: return time + if (hour < 10) return time.drop(1) + } + return time + } + + + + override fun onDozeAmountChanged(linear: Float, eased: Float) { + super.onDozeAmountChanged(linear, eased) + val mode = fontDigitMode ?: return + dozingAmountChanging = true + if (!isWakingUp) handler?.removeCallbacks(forceResetRunnable) + + val target = if (linear == 0f) mode.lsFontWeight + else if (linear == 1f) mode.aodFontWeight + else null + + if (target != null) { + for (i in 0 until DIGIT_COUNT) { + val anim = digitAnimators[i] + if (anim != null && anim.isRunning) continue + if (fontWeights[i] != target) { + animateDigitTo(i, target, SETTLE_ANIM_MS) + } + } + dozingAmountChanging = false + return + } + + if (!isWakingUp && !isKeyguardVisibleState) return + + val hasDigitRects = digitRects.any { it != null } + if (!hasDigitRects) { + val weight = if (isWakingUp) { + (mode.aodFontWeight + (mode.lsFontWeight - mode.aodFontWeight) * (1f - linear)).toInt() + } else { + (mode.lsFontWeight - (mode.lsFontWeight - mode.aodFontWeight) * linear).toInt() + } + for (i in 0 until DIGIT_COUNT) { + fontWeights[i] = weight + } + weightVersion.intValue++ + return + } + + val targetWeight = if (isWakingUp) mode.lsFontWeight else mode.aodFontWeight + val radius = maxRadius * (1f - linear) + for (i in 0 until DIGIT_COUNT) { + val rect = digitRects[i] ?: continue + val reached = if (isWakingUp) { + circleIntersectsRect(tapPos.x.toFloat(), tapPos.y.toFloat(), radius, rect) + } else { + circleNotFullyContainsRect(tapPos.x.toFloat(), tapPos.y.toFloat(), radius, rect) + } + if (reached && !digitAnimStarted[i]) { + animateDigitTo(i, targetWeight, RIPPLE_ANIM_MS) + digitAnimStarted[i] = true + } + } + } + + override fun onStartedWakingUp() { + super.onStartedWakingUp() + if (fontDigitMode == null) return + isWakingUp = true + dozingAmountChanging = false + handler?.postDelayed(forceResetRunnable, FORCE_RESET_DELAY_MS) + } + + override fun onStartedGoingToSleep(isKeyguardVisible: Boolean) { + val mode = fontDigitMode ?: return + isWakingUp = false + dozingAmountChanging = false + isKeyguardVisibleState = isKeyguardVisible + if (!isKeyguardVisible) { + for (i in 0 until DIGIT_COUNT) { + animateDigitTo(i, mode.aodFontWeight, SETTLE_ANIM_MS) + } + } + } + + override fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) { + if (fontDigitMode == null) return + this.isWakingUp = isWakingUp + val pos = tapPosition ?: Point(screenWidth / 2, screenHeight / 2) + digitAnimStarted.fill(false) + cancelDigitAnimators() + maxRadius = maxOf( + pos.x, screenWidth - pos.x, + pos.y, screenHeight - pos.y + ) + tapPos = pos + } + + override fun onChargeAnimation() { + animateWeightPulse() + } + + private fun animateWeightPulse() { + val mode = fontDigitMode ?: return + fidgetWeightAnimator?.cancel() + + val squeeze = ValueAnimator.ofInt(mode.lsFontWeight, FIDGET_THIN_WEIGHT).apply { + duration = FIDGET_WEIGHT_DURATION + interpolator = FIDGET_INTERPOLATOR + addUpdateListener { anim -> + val w = anim.animatedValue as Int + for (i in 0 until DIGIT_COUNT) fontWeights[i] = w + weightVersion.intValue++ + } + } + val restore = ValueAnimator.ofInt(FIDGET_THIN_WEIGHT, mode.lsFontWeight).apply { + duration = FIDGET_WEIGHT_DURATION + interpolator = FIDGET_INTERPOLATOR + addUpdateListener { anim -> + val w = anim.animatedValue as Int + for (i in 0 until DIGIT_COUNT) fontWeights[i] = w + weightVersion.intValue++ + } + } + + fidgetWeightAnimator = AnimatorSet().apply { + playSequentially(squeeze, restore) + start() + } + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + fidgetWeightAnimator?.cancel() + handler?.removeCallbacks(forceResetRunnable) + cancelDigitAnimators() + } + + override fun getTag(): String = + if (isLargeClock) "BitmapDigitComposeLargeClockView" else "BitmapDigitComposeClockView" + + companion object { + private const val MAX_TABLET_SCALE = 1.2f + private const val DATE_ROW_DP = 24f + private const val ANALOG_DATE_GAP_DP = 16f + private const val DIGIT_COUNT = 4 + private const val WEIGHT_SNAP = 10 + private const val RIPPLE_ANIM_MS = 500L + private const val SETTLE_ANIM_MS = 300L + private const val FORCE_ANIM_MS = 350L + private const val FORCE_RESET_DELAY_MS = 800L + private const val FIDGET_THIN_WEIGHT = 300 + private const val FIDGET_WEIGHT_DURATION = 250L + private val RIPPLE_INTERPOLATOR = PathInterpolator(0.6f, 0f, 0.2f, 1f) + private val FIDGET_INTERPOLATOR = PathInterpolator(0.26873f, 0f, 0.45042f, 1f) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt new file mode 100644 index 0000000000000..9635440ad9a94 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockCanvasHelpers.kt @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.Rect +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import kotlin.math.cos +import kotlin.math.sin + +internal fun DrawScope.fitToWidth(naturalWidth: Float, marginDp: Float = 16f): Float { + val marginPx = marginDp * density + val available = (size.width - 2f * marginPx).coerceAtLeast(1f) + return if (naturalWidth > available) available / naturalWidth else 1f +} + +internal fun DrawScope.drawScaledBitmap( + bitmap: Bitmap, + x: Float, + y: Float, + scale: Float, + tint: Color, +) { + val imageBitmap = bitmap.asImageBitmap() + val dstWidth = (bitmap.width * scale).toInt() + val dstHeight = (bitmap.height * scale).toInt() + drawImage( + image = imageBitmap, + srcOffset = IntOffset.Zero, + srcSize = IntSize(bitmap.width, bitmap.height), + dstOffset = IntOffset(x.toInt(), y.toInt()), + dstSize = IntSize(dstWidth, dstHeight), + colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn), + ) +} + +internal fun DrawScope.drawDotSeparator( + x: Float, + baseY: Float, + digitHeight: Float, + dotSize: Float, + dotMargin: Float, + dotCenterMargin: Float, + tint: Color, +): Float { + val centerX = x + dotCenterMargin + val radius = dotSize / 2 + val dotY = baseY + digitHeight / 2 - dotMargin / 2 + + drawOval( + color = tint, + topLeft = Offset(centerX - radius, dotY - radius), + size = Size(dotSize, dotSize), + ) + drawOval( + color = tint, + topLeft = Offset(centerX - radius, dotY + dotMargin - radius), + size = Size(dotSize, dotSize), + ) + + return dotCenterMargin * 2 +} + +internal fun DrawScope.drawDigitLine( + digits: String, + bitmaps: Map, + scale: Float, + startX: Float, + y: Float, + spacing: Float, + tint: Color, +) { + var x = startX + digits.forEach { char -> + val bitmap = bitmaps[char] ?: return@forEach + drawScaledBitmap(bitmap, x, y, scale, tint) + x += bitmap.width * scale + spacing + } +} + +internal fun drawClockHand( + canvas: android.graphics.Canvas, + paint: Paint, + position: Float, + thickness: Float, + startMul: Float, + endMul: Float, + color: Int, + cx: Float, + cy: Float, + clockSize: Float, +) { + val angleRad = Math.toRadians((position * 6 - 90).toDouble()) + val startLen = startMul * clockSize + val endLen = endMul * clockSize + val sx = cx - cos(angleRad).toFloat() * startLen + val sy = cy - sin(angleRad).toFloat() * startLen + val ex = cx + cos(angleRad).toFloat() * endLen + val ey = cy + sin(angleRad).toFloat() * endLen + paint.color = color + paint.strokeWidth = thickness + canvas.drawLine(sx, sy, ex, ey, paint) +} + +internal fun shouldDrawSeparator(config: BitmapFaceConfig, time: String, index: Int): Boolean { + if (!config.hasSeparator) return false + return when (time.length) { + 4 -> index == 1 + 3 -> index == 0 + else -> false + } +} + +internal fun shouldUseLightVariant( + config: BitmapFaceConfig, + time: String, + index: Int, + isDoze: Boolean, + screenOff: Boolean, +): Boolean { + if (!config.hasLightVariants) return false + if (isDoze || screenOff) return false + return when (config.lightVariantRule) { + BitmapFaceConfig.LightVariantRule.NONE -> false + BitmapFaceConfig.LightVariantRule.SECOND_HALF -> when (time.length) { + 3 -> index >= 1 + 4 -> index >= 2 + else -> false + } + } +} + +internal fun getCustomSpacing( + config: BitmapFaceConfig, + time: String, + index: Int, + baseSpacing: Float, + overlapPadding: Float, +): Float { + if (config.overlapPairs.isNotEmpty()) { + return getNTypeCustomSpacing(config, time, index, overlapPadding) + } + if (config.lightVariantRule == BitmapFaceConfig.LightVariantRule.SECOND_HALF) { + return getNDotCustomSpacing(time, index, baseSpacing, overlapPadding) + } + return 0f +} + +private fun getNTypeCustomSpacing( + config: BitmapFaceConfig, + time: String, + index: Int, + overlapPadding: Float, +): Float { + val str = when { + time.length == 4 && (index == 0 || index == 2) -> time.substring(index, index + 2) + time.length == 3 && index == 1 -> time.substring(index) + else -> "" + } + val multiplier = config.overlapPairs[str] ?: return 0f + return -overlapPadding * multiplier +} + +private fun getNDotCustomSpacing( + time: String, + index: Int, + clockPadding: Float, + overlapPadding: Float, +): Float { + return when (time.length) { + 3 -> when (index) { + 0 -> clockPadding + 1 -> { + val secondHalf = time.substring(1) + if ('1' !in secondHalf) overlapPadding else clockPadding + } + else -> 0f + } + 4 -> when (index) { + 0 -> { + val firstHalf = time.substring(0, 2) + if ('1' in firstHalf) clockPadding else overlapPadding + } + 1 -> clockPadding + 2 -> { + val secondHalf = time.substring(2) + if ('1' !in secondHalf) overlapPadding else clockPadding + } + else -> 0f + } + else -> 0f + } +} + +internal fun computeTotalWidth( + time: String, + bitmaps: Map, + scale: Float, + spacing: Float, + overlapPadding: Float, + config: BitmapFaceConfig, + dotCenterMargin: Float, +): Float { + var total = 0f + time.forEachIndexed { index, char -> + val bmp = bitmaps[char] ?: return@forEachIndexed + total += bmp.width * scale + total += getCustomSpacing(config, time, index, spacing, overlapPadding) + if (shouldDrawSeparator(config, time, index)) { + total += dotCenterMargin * 2 + } else if (index < time.lastIndex) { + total += spacing + } + } + return total +} + +internal fun computeLineWidth( + digits: String, + bitmaps: Map, + scale: Float, + spacing: Float, +): Float { + var total = 0f + digits.forEachIndexed { index, char -> + val bmp = bitmaps[char] ?: return@forEachIndexed + total += bmp.width * scale + if (index < digits.lastIndex) { + total += spacing + } + } + return total +} + +internal fun circleIntersectsRect(cx: Float, cy: Float, radius: Float, rect: Rect): Boolean { + val dx = cx - cx.coerceIn(rect.left.toFloat(), rect.right.toFloat()) + val dy = cy - cy.coerceIn(rect.top.toFloat(), rect.bottom.toFloat()) + return (dx * dx + dy * dy) <= radius * radius +} + +internal fun circleNotFullyContainsRect(cx: Float, cy: Float, radius: Float, rect: Rect): Boolean { + val r2 = radius * radius + return arrayOf( + rect.left.toFloat() to rect.top.toFloat(), + rect.right.toFloat() to rect.top.toFloat(), + rect.left.toFloat() to rect.bottom.toFloat(), + rect.right.toFloat() to rect.bottom.toFloat(), + ).any { (px, py) -> + val dx = px - cx + val dy = py - cy + (dx * dx + dy * dy) > r2 + } +} + +internal fun androidColorToComposeColor(color: Int): Color = Color(color) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt new file mode 100644 index 0000000000000..83549f61fc4b1 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockDepthController.kt @@ -0,0 +1,281 @@ +package com.android.systemui.shared.clocks.view + +import android.animation.ValueAnimator +import android.graphics.* +import android.view.View +import com.android.app.animation.Interpolators +import com.android.systemui.shared.clocks.DepthWallpaperProvider +import android.content.res.Resources +import android.provider.Settings +import android.util.DisplayMetrics +import kotlin.math.ceil +import kotlin.math.floor + +class ClockDepthController(private val view: View) { + + var enabled = true + + private var subjectPath: Path? = null + private var pathAspect: Float = 1f + private var depthActive = false + private var depthVisible = true + private var depthSuppressed = false + private var maskAlpha = 0f + private var revealProgress = 0f + private var maskAnimator: ValueAnimator? = null + + private val transformedPath = Path() + private val pathMatrix = Matrix() + private val coverageRegion = Region() + private val coverageClip = Region() + private val coverageDiff = Region() + private val location = IntArray(2) + private val maskPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + style = Paint.Style.FILL + } + + private val currentGlobalMatrix = Matrix() + private val lastGlobalMatrix = Matrix() + + private val preDrawListener = android.view.ViewTreeObserver.OnPreDrawListener { + if (shouldApplyDepth()) { + currentGlobalMatrix.reset() + view.transformMatrixToGlobal(currentGlobalMatrix) + if (currentGlobalMatrix != lastGlobalMatrix) { + lastGlobalMatrix.set(currentGlobalMatrix) + view.invalidate() + } + } + true + } + + private val wallpaperMaxScale: Float = try { + val id = Resources.getSystem().getIdentifier( + "config_wallpaperMaxScale", "dimen", "android" + ) + if (id != 0) Resources.getSystem().getFloat(id) else 1.1f + } catch (_: Exception) { 1.1f } + + private fun getZoom(): Float { + return try { + val str = Settings.Secure.getString( + view.context.contentResolver, "ax_depth_zoom" + ) + str?.toFloatOrNull() ?: wallpaperMaxScale + } catch (_: Exception) { wallpaperMaxScale } + } + + private val listener = object : DepthWallpaperProvider.DepthMaskListener { + override fun onDepthDataChanged(path: Path?, pathAspect: Float) { + val wasActive = depthActive + subjectPath = path + this@ClockDepthController.pathAspect = pathAspect + depthActive = path != null && !path.isEmpty + + if (!depthVisible) { + view.postInvalidateOnAnimation() + return + } + + maskAnimator?.cancel() + if (depthActive && !wasActive) { + animateReveal() + } else if (!depthActive && wasActive) { + animateHide() + } else { + view.postInvalidateOnAnimation() + } + } + } + + fun onAttached() { + if (!enabled) return + DepthWallpaperProvider.init(view.context) + DepthWallpaperProvider.addListener(listener) + view.viewTreeObserver.addOnPreDrawListener(preDrawListener) + } + + fun onDetached() { + maskAnimator?.cancel() + maskAnimator = null + maskAlpha = 0f + revealProgress = 0f + view.viewTreeObserver.removeOnPreDrawListener(preDrawListener) + DepthWallpaperProvider.removeListener(listener) + subjectPath = null + depthActive = false + depthSuppressed = false + } + + fun setDepthVisible(visible: Boolean) { + if (depthVisible == visible) return + depthVisible = visible + + if (!depthActive) return + + maskAnimator?.cancel() + if (visible) { + animateReveal() + } else { + animateHide() + } + } + + fun shouldApplyDepth(): Boolean { + val path = subjectPath + return depthActive && depthVisible && path != null && !path.isEmpty && maskAlpha > 0f + } + + fun drawWithDepth(canvas: Canvas, drawSuper: (Canvas) -> Unit) { + val path = subjectPath ?: run { drawSuper(canvas); return } + + val realMetrics = DisplayMetrics() + view.context.display?.getRealMetrics(realMetrics) + val screenW = realMetrics.widthPixels.toFloat() + val screenH = realMetrics.heightPixels.toFloat() + + val zoom = getZoom() + + val wallAspect = pathAspect + val screenAspect = screenW / screenH + val cropLeft: Float + val cropTop: Float + val visibleW: Float + val visibleH: Float + if (wallAspect > screenAspect) { + // Wallpaper wider than screen — left-aligned on lockscreen (offset 0) + visibleW = (screenAspect / wallAspect) * 10000f + visibleH = 10000f + cropLeft = 0f + cropTop = 0f + } else { + visibleW = 10000f + visibleH = (wallAspect / screenAspect) * 10000f + cropLeft = 0f + cropTop = (10000f - visibleH) / 2f + } + + pathMatrix.reset() + pathMatrix.setTranslate(-cropLeft, -cropTop) + pathMatrix.postScale(screenW / visibleW, screenH / visibleH) + if (zoom != 1f) { + pathMatrix.postScale(zoom, zoom, screenW / 2f, screenH / 2f) + } + + // Map from screen coordinates to view-local canvas coordinates + val globalToLocal = Matrix() + view.transformMatrixToGlobal(globalToLocal) + globalToLocal.invert(globalToLocal) + pathMatrix.postConcat(globalToLocal) + + transformedPath.reset() + path.transform(pathMatrix, transformedPath) + + val pathBounds = RectF() + transformedPath.computeBounds(pathBounds, true) + + val layerRect = RectF(0f, 0f, screenW, screenH) + globalToLocal.mapRect(layerRect) + + if (!RectF.intersects(pathBounds, layerRect)) { + drawSuper(canvas) + return + } + + val suppressed = pathBounds.contains(layerRect) && pathFullyCovers(layerRect) + if (suppressed) { + if (maskAnimator?.isRunning == true) { + maskAnimator?.cancel() + } + maskAlpha = 0f + revealProgress = 0f + depthSuppressed = true + drawSuper(canvas) + return + } + if (depthSuppressed) { + depthSuppressed = false + if (depthActive && depthVisible) { + animateReveal() + } + } + + if (revealProgress >= 1f && maskAlpha >= 1f) { + canvas.save() + canvas.clipOutPath(transformedPath) + drawSuper(canvas) + canvas.restore() + } else { + if (revealProgress < 1f) { + val bounds = RectF() + transformedPath.computeBounds(bounds, false) + val revealMatrix = Matrix() + val s = REVEAL_MIN_SCALE + (1f - REVEAL_MIN_SCALE) * revealProgress + revealMatrix.setScale(s, s, bounds.centerX(), bounds.centerY()) + revealMatrix.postTranslate(0f, PARALLAX_PX * (1f - revealProgress)) + transformedPath.transform(revealMatrix) + } + + val layerCount = canvas.saveLayer(layerRect.left, layerRect.top, layerRect.right, layerRect.bottom, null) + drawSuper(canvas) + + maskPaint.alpha = (maskAlpha * 255f).toInt().coerceIn(0, 255) + maskPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + canvas.drawPath(transformedPath, maskPaint) + maskPaint.xfermode = null + maskPaint.alpha = 255 + + canvas.restoreToCount(layerCount) + } + } + + private fun pathFullyCovers(layerRect: RectF): Boolean { + coverageClip.set( + floor(layerRect.left).toInt(), + floor(layerRect.top).toInt(), + ceil(layerRect.right).toInt(), + ceil(layerRect.bottom).toInt() + ) + coverageRegion.setPath(transformedPath, coverageClip) + coverageDiff.set(coverageClip) + coverageDiff.op(coverageRegion, Region.Op.DIFFERENCE) + return coverageDiff.isEmpty + } + + private fun animateReveal() { + maskAnimator?.cancel() + maskAnimator = ValueAnimator.ofFloat(maskAlpha, 1f).apply { + duration = REVEAL_DURATION + interpolator = Interpolators.EMPHASIZED_DECELERATE + addUpdateListener { + val v = it.animatedValue as Float + maskAlpha = v + revealProgress = v + view.postInvalidateOnAnimation() + } + start() + } + } + + private fun animateHide() { + maskAnimator?.cancel() + maskAnimator = ValueAnimator.ofFloat(maskAlpha, 0f).apply { + duration = REVEAL_DURATION / 2 + interpolator = Interpolators.EMPHASIZED_ACCELERATE + addUpdateListener { + val v = it.animatedValue as Float + maskAlpha = v + revealProgress = v + view.postInvalidateOnAnimation() + } + start() + } + } + + private companion object { + const val REVEAL_DURATION = 600L + const val REVEAL_MIN_SCALE = 0.97f + const val PARALLAX_PX = 24f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt new file mode 100644 index 0000000000000..2387db353a7ce --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/ClockFaceStyle.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.graphics.Color +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository +import kotlin.math.roundToInt + +enum class ClockFaceStyle(val key: String) { + NTYPE("ntype"), + NDOT("ndot"), + LONDON_UG("london_ug"), + SPACE_AGE("space_age"), + POLYLINE("polyline"), + CYBERPUNK("cyberpunk"), + AXION_AGE("axion_age"), + SEGMENTS("segments"), + GRAPHIC("graphic"); + + val isComposeClock: Boolean + get() = this != CYBERPUNK && this != AXION_AGE + + val needsPerSecondTick: Boolean + get() = this == GRAPHIC + + companion object { + const val SETTINGS_KEY = "ax_clock_compose_face" + val DEFAULT = NTYPE + + fun fromKey(key: String?): ClockFaceStyle = + entries.firstOrNull { it.key == key } ?: DEFAULT + } +} + +sealed interface RenderMode { + data object BitmapDigit : RenderMode + data class FontDigit( + val fontPath: String, + val fontSize: Float, + val lsFontWeight: Int, + val aodFontWeight: Int, + val largeScale: Float = 1.85f, + val lineSpacing: Float = 16f, + ) : RenderMode + data object AnalogClock : RenderMode +} + +data class BitmapFaceConfig( + val renderMode: RenderMode = RenderMode.BitmapDigit, + val digitResIds: IntArray = intArrayOf(), + val digitLightResIds: IntArray? = null, + val digitLargeResIds: IntArray? = null, + val digitLightLargeResIds: IntArray? = null, + val digitSpacingRes: Int = R.dimen.clock_padding, + val topMarginRes: Int = R.dimen.clock_center_date_margin_top, + val clockOffsetRes: Int = 0, + val smallScaleMultiplier: Float = 1f, + val largeScaleMultiplier: Float = 1.8f, + val largeDigitSpacingRes: Int = R.dimen.large_clock_digit_spacing, + val largeLineSpacingRes: Int = R.dimen.large_clock_line_spacing, + val hasSeparator: Boolean = false, + val negativeSpacing: Boolean = false, + val hasLightVariants: Boolean = false, + val colorMode: ColorMode = ColorMode.STANDARD, + val overlapSpacingRes: Int = 0, + val overlapPairs: Map = emptyMap(), + val lightVariantRule: LightVariantRule = LightVariantRule.NONE, + val dotSizeRes: Int = R.dimen.dot_size, + val dotMarginRes: Int = R.dimen.dot_margin, + val dotCenterMarginRes: Int = R.dimen.dot_margin_center, + val minuteAlpha: Float = 1f, + val dateSpacingDp: Float = DATE_SPACING_DEFAULT, + val topPaddingDp: Float = 0f, + val bottomPaddingDp: Float = 0f, + val tickResIds: IntArray = intArrayOf(), +) { + enum class ColorMode { STANDARD, LONDON_UG } + enum class LightVariantRule { NONE, SECOND_HALF } + + companion object { + const val DATE_SPACING_DEFAULT = 24f + } + + fun clockColor(isDoze: Boolean, isScreenOff: Boolean, isRegionDark: Boolean): Int { + if (isDoze || isScreenOff) return Color.WHITE + ClockSettingsRepository.clockColorOverride.value?.let { return it } + return when (colorMode) { + ColorMode.STANDARD -> { + if (isRegionDark) Color.WHITE else Color.BLACK + } + ColorMode.LONDON_UG -> { + if (isRegionDark) Color.WHITE + else Color.argb((0.6f * 255).roundToInt(), 0, 0, 0) + } + } + } +} + +object BitmapFaceConfigs { + val configs: Map = mapOf( + ClockFaceStyle.NTYPE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.ntype_0, R.drawable.ntype_1, R.drawable.ntype_2, + R.drawable.ntype_3, R.drawable.ntype_4, R.drawable.ntype_5, + R.drawable.ntype_6, R.drawable.ntype_7, R.drawable.ntype_8, + R.drawable.ntype_9 + ), + digitLargeResIds = intArrayOf( + R.drawable.ntype_0_16b, R.drawable.ntype_1_16b, R.drawable.ntype_2_16b, + R.drawable.ntype_3_16b, R.drawable.ntype_4_16b, R.drawable.ntype_5_16b, + R.drawable.ntype_6_16b, R.drawable.ntype_7_16b, R.drawable.ntype_8_16b, + R.drawable.ntype_9_16b + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + clockOffsetRes = R.dimen.clock_offset, + hasSeparator = true, + largeScaleMultiplier = 1.1f, + overlapSpacingRes = R.dimen.overlap_small_padding, + overlapPairs = mapOf("14" to 6f, "17" to 1f, "19" to 2f), + dateSpacingDp = 0f, + ), + ClockFaceStyle.NDOT to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.ndot_0, R.drawable.ndot_1, R.drawable.ndot_2, + R.drawable.ndot_3, R.drawable.ndot_4, R.drawable.ndot_5, + R.drawable.ndot_6, R.drawable.ndot_7, R.drawable.ndot_8, + R.drawable.ndot_9 + ), + digitLightResIds = intArrayOf( + R.drawable.ndot_0_light, R.drawable.ndot_1_light, R.drawable.ndot_2_light, + R.drawable.ndot_3_light, R.drawable.ndot_4_light, R.drawable.ndot_5_light, + R.drawable.ndot_6_light, R.drawable.ndot_7_light, R.drawable.ndot_8_light, + R.drawable.ndot_9_light + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + clockOffsetRes = R.dimen.ndot_clock_offset, + hasLightVariants = true, + largeScaleMultiplier = 1.5f, + overlapSpacingRes = R.dimen.overlap_padding, + lightVariantRule = BitmapFaceConfig.LightVariantRule.SECOND_HALF, + minuteAlpha = 0.6f, + dateSpacingDp = 0f, + ), + ClockFaceStyle.LONDON_UG to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.london_ug_0, R.drawable.london_ug_1, R.drawable.london_ug_2, + R.drawable.london_ug_3, R.drawable.london_ug_4, R.drawable.london_ug_5, + R.drawable.london_ug_6, R.drawable.london_ug_7, R.drawable.london_ug_8, + R.drawable.london_ug_9 + ), + topMarginRes = R.dimen.bitmap_digit_clocks_margin_top_v2, + smallScaleMultiplier = 56f / 32f, + largeScaleMultiplier = 2.2f, + colorMode = BitmapFaceConfig.ColorMode.LONDON_UG, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.SPACE_AGE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.space_age_0, R.drawable.space_age_1, R.drawable.space_age_2, + R.drawable.space_age_3, R.drawable.space_age_4, R.drawable.space_age_5, + R.drawable.space_age_6, R.drawable.space_age_7, R.drawable.space_age_8, + R.drawable.space_age_9 + ), + digitSpacingRes = R.dimen.space_age_clock_sticky_offset, + negativeSpacing = true, + smallScaleMultiplier = 0.85f, + largeScaleMultiplier = 1.5f, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.POLYLINE to BitmapFaceConfig( + digitResIds = intArrayOf( + R.drawable.polyline_0, R.drawable.polyline_1, R.drawable.polyline_2, + R.drawable.polyline_3, R.drawable.polyline_4, R.drawable.polyline_5, + R.drawable.polyline_6, R.drawable.polyline_7, R.drawable.polyline_8, + R.drawable.polyline_9 + ), + digitSpacingRes = R.dimen.polyline_clock_padding, + largeScaleMultiplier = 1.5f, + dateSpacingDp = 20f, + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ClockFaceStyle.SEGMENTS to BitmapFaceConfig( + renderMode = RenderMode.FontDigit( + fontPath = "fonts/nsegments_vf.otf", + fontSize = 280f, + lsFontWeight = 700, + aodFontWeight = 100, + largeScale = 1.85f, + lineSpacing = 16f, + ), + dateSpacingDp = 0f, + topPaddingDp = 16f, + ), + ClockFaceStyle.GRAPHIC to BitmapFaceConfig( + renderMode = RenderMode.AnalogClock, + tickResIds = intArrayOf(R.drawable.graphic_tick, R.drawable.graphic_tick_light), + topPaddingDp = 16f, + bottomPaddingDp = 24f, + ), + ) + + fun getConfig(style: ClockFaceStyle): BitmapFaceConfig? = configs[style] +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt new file mode 100644 index 0000000000000..8aa944dbfe5f5 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/CyberpunkClockView.kt @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CutCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.android.systemui.shared.clocks.ClockSettingsRepository +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +class CyberpunkClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override val useGlitchInteraction: Boolean = true + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze) = rememberClockState() + val fidget by state.fidgetTrigger + + val cpYellow = Color(0xFFFCEE0A) + val cpCyan = Color(0xFF00F0FF) + val cpRed = Color(0xFFFF5E5E) + + val primaryTimeColor = if (isDoze) Color.White else cpYellow + val frameColor = if (isDoze) Color.White else cpYellow + val textColor = if (isDoze) Color.White else cpCyan + + val statColor1 = if (isDoze) Color.White else cpRed + val statColor2 = if (isDoze) Color.White else cpCyan + val statColor3 = if (isDoze) Color.White else cpYellow + + val glitchProgress = remember { Animatable(0f) } + + LaunchedEffect(fidget) { + if (fidget > 0) { + glitchProgress.snapTo(1f) + glitchProgress.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400, easing = FastOutSlowInEasing) + ) + } + } + + val progress = glitchProgress.value + val glitchSeed = remember(fidget) { kotlin.random.Random.nextFloat() } + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier.fillMaxSize().padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign + ) { + Box( + modifier = Modifier + .graphicsLayer { + if (progress > 0f) { + translationX = (if (glitchSeed > 0.5f) 8f else -8f) * progress + alpha = if (glitchSeed > 0.7f) 0.4f else 1f + } + } + ) { + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .offset(x = (-6).dp * progress * 1.5f, y = 2.dp * progress) + .graphicsLayer { + alpha = 0.6f * progress + scaleX = 1.02f + } + ) { + ClockBadge( + time, date, + cpCyan, + frameColor.copy(alpha = 0.3f), + statColor1.copy(alpha = 0.5f), statColor2.copy(alpha = 0.5f), statColor3.copy(alpha = 0.5f), + cpCyan, + progress, isDoze + ) + } + } + + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .offset(x = 6.dp * progress * 1.5f, y = (-2).dp * progress) + .graphicsLayer { + alpha = 0.6f * progress + scaleX = 1.02f + } + ) { + ClockBadge( + time, date, + cpRed, + frameColor.copy(alpha = 0.3f), + statColor1.copy(alpha = 0.5f), statColor2.copy(alpha = 0.5f), statColor3.copy(alpha = 0.5f), + cpRed, + progress, isDoze + ) + } + } + + ClockBadge(time, date, primaryTimeColor, frameColor, statColor1, statColor2, statColor3, textColor, progress, isDoze) + } + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze) = rememberClockState() + val fidget by state.fidgetTrigger + + val cpYellow = Color(0xFFFCEE0A) + val cpCyan = Color(0xFF00F0FF) + val primaryTimeColor = if (isDoze) Color.White else cpYellow + val accentColor = if (isDoze) Color.White else cpCyan + + val glitchProgress = remember { Animatable(0f) } + + LaunchedEffect(fidget) { + if (fidget > 0) { + glitchProgress.snapTo(1f) + glitchProgress.animateTo( + targetValue = 0f, + animationSpec = tween(durationMillis = 400) + ) + } + } + + val progress = glitchProgress.value + val glitchSeed = remember(fidget) { kotlin.random.Random.nextFloat() } + + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier + .wrapContentSize() + .padding(horizontal = 16.dp) + .graphicsLayer { + if (progress > 0f) { + translationX = (if (glitchSeed > 0.5f) 15f else -15f) * progress + alpha = if (glitchSeed > 0.8f) 0.3f else 1f + } + }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val (hours, minutes) = splitTimeLines(time) + + Text( + text = hours, + style = TextStyle( + fontSize = 160.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryTimeColor, + letterSpacing = (-8).sp, + lineHeight = 160.sp, + drawStyle = if (isDoze) Stroke(width = 8f) else Fill + ) + ) + + val barColor = if (isDoze) Color.White else accentColor + + Row( + modifier = Modifier + .fillMaxWidth(0.9f) + .padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .height(2.dp) + .weight(1f) + .background(barColor.copy(alpha = 0.6f)) + ) + + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "//", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor.copy(alpha = 0.8f), + letterSpacing = (-1).sp + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + + // Decorative label only; the real date is rendered once by + // EnhancedDateArea below to avoid duplicating it with BC smartspace. + Text( + text = "2077", + modifier = Modifier.padding(horizontal = 4.dp), + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor, + letterSpacing = 2.sp + ) + ) + + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "//", + style = TextStyle( + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = barColor.copy(alpha = 0.8f), + letterSpacing = (-1).sp + ) + ) + Spacer(modifier = Modifier.width(8.dp)) + + Box( + modifier = Modifier + .height(2.dp) + .weight(1f) + .background(barColor.copy(alpha = 0.6f)) + ) + } + + Text( + text = minutes, + style = TextStyle( + fontSize = 160.sp, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryTimeColor, + letterSpacing = (-8).sp, + lineHeight = 160.sp, + drawStyle = if (isDoze) Stroke(width = 8f) else Fill + ) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + EnhancedDateArea( + textColor = barColor, + textSize = 14.sp, + fontFamily = FontFamily.Monospace, + fontWeight = FontWeight.Bold, + letterSpacing = 1.sp, + iconSize = 16.dp, + uppercase = true, + rowArrangement = Arrangement.Center, + ) + } + + if (progress > 0.05f && !isDoze) { + Box( + modifier = Modifier + .fillMaxSize() + .offset(x = (if (glitchSeed > 0.5f) 12.dp else -12.dp) * progress) + .graphicsLayer { alpha = progress * 0.4f } + ) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + val (gh, gm) = splitTimeLines(time) + Text(gh, style = TextStyle(fontSize = 160.sp, fontWeight = FontWeight.Black, fontFamily = FontFamily.Monospace, color = cpCyan, letterSpacing = (-8).sp, lineHeight = 160.sp)) + Spacer(modifier = Modifier.height(30.dp)) + Text(gm, style = TextStyle(fontSize = 160.sp, fontWeight = FontWeight.Black, fontFamily = FontFamily.Monospace, color = cpCyan, letterSpacing = (-8).sp, lineHeight = 160.sp)) + } + } + } + } + } + + @Composable + private fun ClockBadge( + time: String, + date: String, + primaryColor: Color, + frameColor: Color, + c1: Color, c2: Color, c3: Color, + textColor: Color, + progress: Float, + isDoze: Boolean + ) { + val colAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val badgeSeed = remember(progress > 0f) { kotlin.random.Random.nextFloat() } + + Column( + horizontalAlignment = colAlign, + verticalArrangement = Arrangement.Center, + modifier = Modifier.graphicsLayer { + if (progress > 0f) { + rotationZ = (badgeSeed - 0.5f) * 3f * progress + } + } + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + val blockColor = if (isDoze) Color.White else primaryColor + + Box(modifier = Modifier.width(4.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(10.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(20.dp).height(6.dp).background(blockColor)) + Box(modifier = Modifier.width(12.dp).height(6.dp).background(blockColor)) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Box( + modifier = Modifier + .border( + width = 1.5.dp, + color = frameColor.copy(alpha = 0.8f), + shape = CutCornerShape(topStart = 16.dp, bottomEnd = 16.dp) + ) + .padding(horizontal = 24.dp, vertical = 6.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + Text( + text = time, + style = TextStyle( + fontSize = 80.sp * dynSizeScale, + fontWeight = FontWeight.Black, + fontFamily = FontFamily.Monospace, + color = primaryColor, + letterSpacing = (-4).sp, + drawStyle = if (isDoze) Stroke(width = 5f) else Fill + ) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.Start + ) { + Column( + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + StatBar(c1, 1.0f) + StatBar(c2, 0.75f) + StatBar(c3, 0.5f) + } + + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "V.2077", + style = TextStyle( + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = if (isDoze) Color.White else primaryColor, + letterSpacing = 0.sp + ) + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = date.uppercase().ifEmpty { "2077" }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = if (isDoze) Color.White else primaryColor, + letterSpacing = 1.sp, + ), + ) + } + } + + @Composable + private fun StatBar(color: Color, widthScale: Float) { + Row(verticalAlignment = Alignment.CenterVertically) { + Box( + modifier = Modifier + .size(4.dp) + .background(color) + ) + Spacer(modifier = Modifier.width(3.dp)) + + Box( + modifier = Modifier + .height(4.dp) + .width(32.dp * widthScale) + .background(color.copy(alpha = 0.8f)) + ) + } + } + + override fun getTag(): String = if (isLargeClock) "CyberpunkLargeClockView" else "CyberpunkClockView" +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt new file mode 100644 index 0000000000000..7490aa6fc43e1 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DateDisplay.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.app.PendingIntent +import android.graphics.Bitmap + +sealed interface DateDisplay { + val text: String + val icon: Bitmap? + val tintIcon: Boolean + val tapAction: PendingIntent? + + data class DateOnly(override val text: String) : DateDisplay { + override val icon: Bitmap? get() = null + override val tintIcon: Boolean get() = true + override val tapAction: PendingIntent? get() = null + } + + data class IconText( + override val text: String, + override val icon: Bitmap?, + override val tintIcon: Boolean, + override val tapAction: PendingIntent? = null, + ) : DateDisplay + + data class Weather( + val date: String, + val temp: String, + override val icon: Bitmap?, + override val tintIcon: Boolean, + override val tapAction: PendingIntent? = null, + ) : DateDisplay { + override val text: String get() = date + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt new file mode 100644 index 0000000000000..4c2c3ff34feb0 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/GeneralClockView.kt @@ -0,0 +1,417 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Typeface +import android.text.TextUtils +import android.util.AttributeSet +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.customization.R +import com.android.systemui.shared.clocks.ClockSettingsRepository +import com.android.systemui.shared.clocks.extensions.scaledDimen +import com.android.systemui.shared.clocks.extensions.scaleRatio +import java.util.Locale +import kotlin.math.min + +class GeneralClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "GeneralLargeClockView" else "GeneralClockView" + + private val digitResIds = intArrayOf( + R.drawable.intervar_0, R.drawable.intervar_1, R.drawable.intervar_2, + R.drawable.intervar_3, R.drawable.intervar_4, R.drawable.intervar_5, + R.drawable.intervar_6, R.drawable.intervar_7, R.drawable.intervar_8, + R.drawable.intervar_9 + ) + + private val digitLightResIds = intArrayOf( + R.drawable.intervar_0_light, R.drawable.intervar_1_light, R.drawable.intervar_2_light, + R.drawable.intervar_3_light, R.drawable.intervar_4_light, R.drawable.intervar_5_light, + R.drawable.intervar_6_light, R.drawable.intervar_7_light, R.drawable.intervar_8_light, + R.drawable.intervar_9_light + ) + + private var bitmaps: Map = loadDigitBitmaps(digitResIds) + private var lightBitmaps: Map = loadDigitBitmaps(digitLightResIds) + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val d = ContextCompat.getDrawable(context, digitResIds[0]) + ?: return super.clockHeightBase + val density = context.resources.displayMetrics.density + return (d.intrinsicHeight * scaleRatio + TEXT_AREA_DP * density * scaleRatio).toInt() + } + + private val textPrimarySize = 28.sp + private val textMajorSize = 20.sp + + override fun onFontSettingChanged() { + super.onFontSettingChanged() + bitmaps = loadDigitBitmaps(digitResIds) + lightBitmaps = loadDigitBitmaps(digitLightResIds) + } + + private fun loadDigitBitmaps(resIds: IntArray): Map { + val result = mutableMapOf() + resIds.forEachIndexed { index, resId -> + ContextCompat.getDrawable(context, resId)?.toBitmap()?.let { bmp -> + result['0' + index] = bmp + } + } + return result + } + + @Composable + override fun Content() { + if (isLargeClock) { + LargeContent() + } else { + SmallContent() + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + + val largeScale = min(context.scaleRatio, MAX_TABLET_SCALE) * LARGE_SCALE_MULTIPLIER + val digitSpacing = context.scaledDimen(R.dimen.large_clock_digit_spacing) + val lineSpacing = context.scaledDimen(R.dimen.large_clock_line_spacing) + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val sampleBmp = remember { bitmaps['0'] } + val digitH = (sampleBmp?.height ?: 0) * largeScale + val canvasHeightDp = with(LocalDensity.current) { + (digitH * 2 + lineSpacing).toDp() + } + Column( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp), + ) { + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val (hours, minutes) = splitTimeLines(time) + if (hours.isEmpty()) return@Canvas + + val sampleDigit = bitmaps['0'] ?: return@Canvas + val dh = sampleDigit.height * largeScale + + fun lineWidth(digits: String): Float { + var w = 0f + digits.forEachIndexed { i, c -> + val bmp = bitmaps[c] ?: return@forEachIndexed + w += bmp.width * largeScale + if (i < digits.lastIndex) w += digitSpacing + } + return w + } + + fun drawLine(digits: String, lineX: Float, lineY: Float) { + var x = lineX + digits.forEach { c -> + val bmp = bitmaps[c] ?: return@forEach + drawImage( + image = bmp.asImageBitmap(), + srcOffset = IntOffset.Zero, + srcSize = IntSize(bmp.width, bmp.height), + dstOffset = IntOffset(x.toInt(), lineY.toInt()), + dstSize = IntSize((bmp.width * largeScale).toInt(), (bmp.height * largeScale).toInt()), + colorFilter = ColorFilter.tint(tintColor, BlendMode.SrcIn), + ) + x += bmp.width * largeScale + digitSpacing + } + } + + val hoursW = lineWidth(hours) + val minutesW = lineWidth(minutes) + + drawLine(hours, (size.width - hoursW) / 2f, 0f) + drawLine(minutes, (size.width - minutesW) / 2f, dh + lineSpacing) + } + + Spacer(modifier = Modifier.height(12.dp)) + + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() + + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val scale = context.scaleRatio * dynSizeScale + val paddingV = context.scaledDimen(R.dimen.clock_padding) * dynSizeScale + val dotSz = context.scaledDimen(R.dimen.dot_small_size) + val dotMgn = context.scaledDimen(R.dimen.dot_margin) + + val tintColor = tintColor(isDoze, screenOff, regionDark) + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + val placeholderText = config?.placeholderTextRes?.let { context.getString(it) } + val hasSpecialContent = display !is DateDisplay.DateOnly + val bottomText = when (display) { + is DateDisplay.Weather -> (display as DateDisplay.Weather).temp + else -> date + } + val useLight = hasSpecialContent + + val sampleBmp = remember { bitmaps['0'] } + val canvasHeight = remember(sampleBmp, scale) { + ((sampleBmp?.height ?: 0) * scale).coerceAtLeast(1f) + } + val canvasHeightDp = with(LocalDensity.current) { canvasHeight.toDp() } + + val inverseModifier = inverseSizeScaleModifier() + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + Text( + text = dateStr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = tintColor.copy(alpha = if (isDoze) 0.6f else 0.8f), + letterSpacing = 0.5.sp, + ), + modifier = Modifier + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + top = 4.dp, + bottom = 4.dp, + ) + .then(inverseModifier), + ) + + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(canvasHeightDp), + ) { + if (time.isEmpty() || !TextUtils.isDigitsOnly(time)) return@Canvas + + val bitmapMap = if (useLight) lightBitmaps else bitmaps + + var naturalWidth = 0f + for ((i, char) in time.withIndex()) { + val bmp = bitmapMap[char] ?: continue + naturalWidth += bmp.width * scale + if (time.length - i == 3) naturalWidth += 2 * paddingV + } + if (naturalWidth <= 0f) return@Canvas + + val fitScale = fitToWidth(naturalWidth) + val drawScale = scale * fitScale + val drawPaddingV = paddingV * fitScale + val totalWidth = naturalWidth * fitScale + + val startX = when { + isLeftAligned -> clockPaddingStart + isRightAligned -> size.width - clockPaddingStart - totalWidth + else -> (size.width - totalWidth) / 2f + } + var x = startX + + for ((i, char) in time.withIndex()) { + val bmp = bitmapMap[char] ?: continue + val yOffset = (size.height - bmp.height * drawScale) / 2f + + drawImage( + image = bmp.asImageBitmap(), + srcOffset = IntOffset.Zero, + srcSize = IntSize(bmp.width, bmp.height), + dstOffset = IntOffset(x.toInt(), yOffset.toInt()), + dstSize = IntSize((bmp.width * drawScale).toInt(), (bmp.height * drawScale).toInt()), + colorFilter = ColorFilter.tint(tintColor, BlendMode.SrcIn), + ) + x += bmp.width * drawScale + + if (time.length - i == 3) { + val centerX = x + drawPaddingV + val centerY = size.height / 2f + val dotRadius = dotSz * fitScale / 2 + val topDotY = centerY - (dotMgn * fitScale / 2 + dotRadius) + val bottomDotY = centerY + (dotMgn * fitScale / 2 + dotRadius) + val adjDotSz = dotSz * fitScale + drawOval( + color = tintColor, + topLeft = Offset(centerX - dotRadius, topDotY - dotRadius), + size = Size(adjDotSz, adjDotSz), + ) + drawOval( + color = tintColor, + topLeft = Offset(centerX - dotRadius, bottomDotY - dotRadius), + size = Size(adjDotSz, adjDotSz), + ) + x += 2 * drawPaddingV + } + } + } + + BottomArea( + placeholderText = placeholderText, + hasSpecialContent = hasSpecialContent, + text = bottomText, + icon = icon, + tintIcon = tintIcon, + textColor = tintColor, + startPadding = sidePadding, + horizontalAlign = horizontalAlign, + modifier = inverseModifier, + ) + } + } + + @Composable + private fun BottomArea( + placeholderText: String?, + hasSpecialContent: Boolean, + text: String, + icon: Bitmap?, + tintIcon: Boolean, + textColor: Color, + startPadding: androidx.compose.ui.unit.Dp, + horizontalAlign: Alignment.Horizontal, + modifier: Modifier = Modifier, + ) { + Column( + horizontalAlignment = horizontalAlign, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding( + start = if (isRightAligned) 0.dp else startPadding, + end = if (isRightAligned) startPadding else 0.dp, + bottom = 4.dp, + ) + .then(modifier), + ) { + if (hasSpecialContent) { + Row(verticalAlignment = Alignment.CenterVertically) { + icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(7.dp)) + } + Text( + text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = textMajorSize, + fontWeight = FontWeight.Medium, + color = textColor, + ), + ) + } + } else if (placeholderText != null) { + val isJpLang = Locale.getDefault().language == "ja" + val fontName = if (isJpLang) "NDot77JPExtended" else "nothingdot" + val fontFamily = FontFamily(Typeface.create(fontName, Typeface.NORMAL)) + Text( + text = placeholderText, + maxLines = 1, + style = TextStyle( + fontSize = textPrimarySize, + fontWeight = FontWeight.Normal, + fontFamily = fontFamily, + color = textColor, + ), + ) + } + } + } + + companion object { + private const val MAX_TABLET_SCALE = 1.2f + private const val LARGE_SCALE_MULTIPLIER = 1.8f + private const val TEXT_AREA_DP = 56f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt new file mode 100644 index 0000000000000..992aec7301518 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/OldQuickLookClockView.kt @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Typeface +import android.util.AttributeSet +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.customization.clocks.R as clocksR +import com.android.systemui.shared.clocks.ClockSettingsRepository +import java.util.Locale + +class OldQuickLookClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = "OLDQuickLookClockView" + + private val primaryTextSize = 80.sp + private val secondaryTextSize = 18.sp + private val dateTextSize = 14.sp + + override val clockHeightBase: Int + get() { + if (isLargeClock) return super.clockHeightBase + val density = context.resources.displayMetrics.density + val scaledDensity = context.resources.displayMetrics.scaledDensity + val timePx = PRIMARY_TEXT_SP * scaledDensity * scaleRatio + val infoPx = INFO_AREA_DP * density * scaleRatio + return (timePx + infoPx).toInt() + } + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark, icon, tintIcon, display) = rememberClockState() + + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val textColor = tintColor(isDoze, screenOff, regionDark) + val horizontalAlign = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + val isJpLang = Locale.getDefault().language == "ja" + val infoFontName = if (isJpLang) "NDot77JPExtended" else "nothingdot" + val infoFontFamily = FontFamily(Typeface.create(infoFontName, Typeface.NORMAL)) + val clockFontFamily = FontFamily(Typeface.create("nothingdot57", Typeface.NORMAL)) + + val placeholderText = config?.placeholderTextRes?.let { context.getString(it) } + val hasSpecialContent = display !is DateDisplay.DateOnly + val bottomText = when (display) { + is DateDisplay.Weather -> (display as DateDisplay.Weather).temp + else -> date + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + verticalArrangement = Arrangement.Center, + horizontalAlignment = horizontalAlign, + ) { + Text( + text = time, + maxLines = 1, + style = TextStyle( + fontSize = primaryTextSize * dynSizeScale, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + ), + ) + + Text( + text = dateStr, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = dateTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + + Column( + horizontalAlignment = horizontalAlign, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(top = 6.dp), + ) { + if (hasSpecialContent) { + Row(verticalAlignment = Alignment.CenterVertically) { + icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(16.dp), + ) + Spacer(modifier = Modifier.width(7.dp)) + } + Text( + text = bottomText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TextStyle( + fontSize = secondaryTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + } + } else if (placeholderText != null) { + Text( + text = placeholderText, + maxLines = 1, + style = TextStyle( + fontSize = secondaryTextSize, + fontWeight = FontWeight.Normal, + fontFamily = infoFontFamily, + color = textColor, + ), + ) + } + } + } + } + + @Composable + private fun LargeContent() { + val (time, _, isDoze, screenOff, regionDark) = rememberClockState() + + val textColor = tintColor(isDoze, screenOff, regionDark) + val clockFontFamily = FontFamily(Typeface.create("nothingdot57", Typeface.NORMAL)) + + val (hours, minutes) = splitTimeLines(time) + + val largeFontSize = with(LocalDensity.current) { + context.resources.getDimensionPixelSize( + clocksR.dimen.large_clock_text_size + ).toSp() + } + + Box( + modifier = Modifier.fillMaxWidth().wrapContentHeight(), + contentAlignment = Alignment.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = hours, + maxLines = 1, + style = TextStyle( + fontSize = largeFontSize, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + lineHeight = largeFontSize, + ), + ) + Text( + text = minutes, + maxLines = 1, + style = TextStyle( + fontSize = largeFontSize, + fontWeight = FontWeight.Bold, + fontFamily = clockFontFamily, + color = textColor, + lineHeight = largeFontSize, + ), + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + EnhancedDateArea( + textColor = textColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + } + + companion object { + private const val PRIMARY_TEXT_SP = 80f + private const val INFO_AREA_DP = 60f + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt new file mode 100644 index 0000000000000..bf50b66378e19 --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookController.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.app.PendingIntent +import android.text.format.DateFormat +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import com.android.systemui.plugins.keyguard.data.model.AlarmData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import kotlinx.coroutines.flow.MutableStateFlow +import java.util.concurrent.TimeUnit + +class QuickLookController(private val view: AxClockView) { + + val clockDataFlow = MutableStateFlow(ClockData.EMPTY) + val mediaStateFlow = MutableStateFlow(ClockMediaState()) + val nowPlayingFlow = MutableStateFlow("") + val nowPlayingTapActionFlow = MutableStateFlow(null) + val nextAlarmFlow = MutableStateFlow("") + + fun onClockDataChanged(data: ClockData) { + clockDataFlow.value = data + } + + fun onPlaybackStateChanged(playing: Boolean) { + mediaStateFlow.value = mediaStateFlow.value.copy(isPlaying = playing) + } + + fun onMetadataChanged(track: String, artist: String, packageName: String) { + mediaStateFlow.value = mediaStateFlow.value.copy( + trackTitle = track, + artistName = artist, + packageName = packageName, + ) + } + + fun onNowPlayingUpdate(text: String) { + nowPlayingFlow.value = text + } + + fun onAlarmDataChanged(data: AlarmData) { + val nextAlarmMillis = data.nextAlarmMillis ?: 0L + val now = System.currentTimeMillis() + val futureLimit = now + TimeUnit.HOURS.toMillis(ALARM_VISIBILITY_HOURS) + val withinHours = nextAlarmMillis in now..futureLimit + nextAlarmFlow.value = if (withinHours) { + val fmt = if (DateFormat.is24HourFormat(view.context)) "HH:mm" else "h:mm" + DateFormat.format(fmt, nextAlarmMillis).toString() + } else "" + } + + @Composable + fun rememberResolvedDisplay(dateStr: String): DateDisplay { + val clockData by clockDataFlow.collectAsState() + val media by mediaStateFlow.collectAsState() + val nowPlaying by nowPlayingFlow.collectAsState() + val nowPlayingTapAction by nowPlayingTapActionFlow.collectAsState() + val alarm by nextAlarmFlow.collectAsState() + + return remember(clockData, media, nowPlaying, nowPlayingTapAction, alarm, dateStr) { + view.resolveDisplay(clockData, media, nowPlaying, nowPlayingTapAction, alarm, dateStr) + } + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt new file mode 100644 index 0000000000000..161f51f737b4d --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookDateArea.kt @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.app.PendingIntent +import android.util.Log +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +private const val TAG = "QuickLookDateArea" + +@Composable +fun QuickLookDateArea( + modifier: Modifier = Modifier, + display: DateDisplay, + dateStr: String, + sizeScale: Float, + textColor: Color, + textSize: TextUnit = 18.sp, + fontFamily: FontFamily, + fontWeight: FontWeight = FontWeight.Medium, + letterSpacing: TextUnit = 0.sp, + iconSize: Dp = 16.dp, + uppercase: Boolean = false, + rowArrangement: Arrangement.Horizontal = Arrangement.Center, +) { + val style = TextStyle( + fontSize = textSize, + fontWeight = fontWeight, + fontFamily = fontFamily, + color = textColor, + letterSpacing = letterSpacing, + lineHeight = 24.sp, + ) + + val inverseScale = if (sizeScale > 0f) 1f / sizeScale else 1f + val tapModifier = display.tapAction?.let { action -> + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + ) { fireTapAction(action) } + } ?: Modifier + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = rowArrangement, + modifier = modifier + .widthIn(max = 280.dp) + .graphicsLayer { + scaleX = inverseScale + scaleY = inverseScale + } + .then(tapModifier), + ) { + if (display is DateDisplay.Weather) { + val dateText = if (uppercase) dateStr.uppercase() else dateStr + Text(text = dateText, maxLines = 1, style = style, modifier = Modifier.basicMarquee()) + display.icon?.let { + Spacer(modifier = Modifier.width(6.dp)) + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (display.tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(iconSize), + ) + } + if (display.temp.isNotEmpty()) { + Spacer(modifier = Modifier.width(6.dp)) + val tempText = if (uppercase) display.temp.uppercase() else display.temp + Text(text = tempText, maxLines = 1, style = style) + } + } else { + display.icon?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = null, + colorFilter = if (display.tintIcon) ColorFilter.tint(textColor) else null, + modifier = Modifier.size(iconSize), + ) + Spacer(modifier = Modifier.width(6.dp)) + } + val text = when (display) { + is DateDisplay.IconText -> display.text + is DateDisplay.DateOnly -> dateStr + else -> dateStr + } + val displayText = if (uppercase) text.uppercase() else text + Text( + text = displayText, + maxLines = 1, + style = style, + modifier = Modifier.basicMarquee(), + ) + } + } +} + +internal fun fireTapAction(action: PendingIntent) { + try { + action.send() + } catch (e: PendingIntent.CanceledException) { + Log.w(TAG, "Tap action cancelled", e) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt new file mode 100644 index 0000000000000..0a1544cf4198f --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/QuickLookUtils.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.clocks.view + +import android.graphics.Typeface +import androidx.compose.ui.text.font.FontFamily + +fun splitTimeLines(time: String): Pair { + if (time.contains(":")) { + val parts = time.split(":") + if (parts.size == 2) { + return parts[0] to parts[1] + } + } + return when (time.length) { + 4 -> time.substring(0, 2) to time.substring(2, 4) + 3 -> time.substring(0, 1) to time.substring(1, 3) + else -> "" to "" + } +} + +fun resolveBodyFontFamily(): FontFamily { + val typeface = Typeface.create(FONT_FAMILY_BODY, Typeface.NORMAL) + return FontFamily(typeface) +} + +fun resolveDateFontFamily(): FontFamily { + val typeface = Typeface.create(FONT_FAMILY_DATE, Typeface.NORMAL) + return FontFamily(typeface) +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt new file mode 100644 index 0000000000000..e92dc90a0485b --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish2ClockView.kt @@ -0,0 +1,291 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.util.AttributeSet +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class Stylish2ClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "Stylish2LargeClockView" else "Stylish2ClockView" + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val bgColor = if (isDoze) Color.Transparent else tintColor.copy(alpha = 0.12f) + val textOnBg = tintColor + val greetingAlpha = if (isDoze) 0.6f else 0.85f + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + + val (hours, minutes) = splitTimeLines(time) + + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign, + ) { + Column( + horizontalAlignment = when { + isLeftAligned -> Alignment.Start + isRightAligned -> Alignment.End + else -> Alignment.CenterHorizontally + }, + ) { + Text( + text = "Have a great", + style = TextStyle( + fontSize = 28.sp * dynSizeScale, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + Text( + text = "$dayOfWeek !", + style = TextStyle( + fontSize = 28.sp * dynSizeScale, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TimeBox(hours, textOnBg, bgColor, dynSizeScale, isDoze) + + Spacer(modifier = Modifier.width(8.dp)) + ColonDots(tintColor, dynSizeScale) + Spacer(modifier = Modifier.width(8.dp)) + + TimeBox(minutes, textOnBg, bgColor, dynSizeScale, isDoze) + + Spacer(modifier = Modifier.width(8.dp)) + + DateBox(monthDay, textOnBg, bgColor, dynSizeScale, isDoze) + } + } + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val tintColor = tintColor(isDoze, screenOff, regionDark) + + val bgColor = if (isDoze) Color.Transparent else tintColor.copy(alpha = 0.12f) + val textOnBg = tintColor + val greetingAlpha = if (isDoze) 0.6f else 0.85f + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + + val (hours, minutes) = splitTimeLines(time) + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Have a great", + style = TextStyle( + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + Text( + text = "$dayOfWeek !", + style = TextStyle( + fontSize = 36.sp, + fontWeight = FontWeight.Bold, + color = tintColor.copy(alpha = greetingAlpha), + ), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + TimeBox(hours, textOnBg, bgColor, 1.3f, isDoze) + + Spacer(modifier = Modifier.width(10.dp)) + ColonDots(tintColor, 1.3f) + Spacer(modifier = Modifier.width(10.dp)) + + TimeBox(minutes, textOnBg, bgColor, 1.3f, isDoze) + + Spacer(modifier = Modifier.width(10.dp)) + + DateBox(monthDay, textOnBg, bgColor, 1.3f, isDoze) + } + + Spacer(modifier = Modifier.height(12.dp)) + + EnhancedDateArea( + textColor = tintColor, + textSize = 16.sp, + iconSize = 18.dp, + rowArrangement = Arrangement.Center, + ) + } + } + + @Composable + private fun TimeBox( + text: String, + textColor: Color, + bgColor: Color, + scale: Float, + isDoze: Boolean, + ) { + val shape = RoundedCornerShape(16.dp) + val bg = if (isDoze) Color.White.copy(alpha = 0.08f) else bgColor + Box( + modifier = Modifier + .clip(shape) + .background(bg) + .padding(horizontal = (16 * scale).dp, vertical = (10 * scale).dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + style = TextStyle( + fontSize = 48.sp * scale, + fontWeight = FontWeight.Black, + color = textColor, + letterSpacing = (-1).sp, + ), + ) + } + } + + @Composable + private fun ColonDots(color: Color, scale: Float) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier + .size((6 * scale).dp) + .background(color, CircleShape), + ) + Box( + modifier = Modifier + .size((6 * scale).dp) + .background(color, CircleShape), + ) + } + } + + @Composable + private fun DateBox( + text: String, + textColor: Color, + bgColor: Color, + scale: Float, + isDoze: Boolean, + ) { + val shape = RoundedCornerShape(16.dp) + val bg = if (isDoze) Color.White.copy(alpha = 0.08f) else bgColor + Box( + modifier = Modifier + .clip(shape) + .background(bg) + .padding(horizontal = (12 * scale).dp, vertical = (10 * scale).dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = text, + style = TextStyle( + fontSize = 16.sp * scale, + fontWeight = FontWeight.Medium, + color = textColor.copy(alpha = 0.8f), + ), + ) + } + } + + private fun formatDayOfWeek(): String { + val sdf = SimpleDateFormat("EEEE", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } + + private fun formatMonthDay(): String { + val sdf = SimpleDateFormat("MMM dd", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } +} diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt new file mode 100644 index 0000000000000..963670adfd2ce --- /dev/null +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/Stylish7ClockView.kt @@ -0,0 +1,394 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.shared.clocks.view + +import android.content.Context +import android.graphics.Bitmap +import android.os.UserManager +import android.util.AttributeSet +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.systemui.shared.clocks.ClockSettingsRepository +import java.text.SimpleDateFormat +import java.util.Locale + +class Stylish7ClockView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : AxClockView(context, attrs, defStyleAttr, defStyleRes) { + + override fun getTag(): String = + if (isLargeClock) "Stylish7LargeClockView" else "Stylish7ClockView" + + private val userManager: UserManager? = + context.getSystemService(Context.USER_SERVICE) as? UserManager + + private fun loadUserAvatar(): Bitmap? { + return try { + val userId = android.app.ActivityManager.getCurrentUser() + var icon = userManager?.getUserIcon(userId) + if (icon == null) { + val drawable = com.android.internal.util.UserIcons.getDefaultUserIcon( + context.resources, userId, false + ) + icon = com.android.internal.util.UserIcons.convertToBitmap(drawable) + } + icon + } catch (_: Exception) { null } + } + + private fun loadUserName(): String { + return try { + val userId = android.app.ActivityManager.getCurrentUser() + val userInfo = userManager?.getUserInfo(userId) + userInfo?.name?.takeIf { it.isNotBlank() } ?: "\u0CA0\u1D25\u0CA0" + } catch (_: Exception) { "\u0CA0\u1D25\u0CA0" } + } + + @Composable + override fun Content() { + if (isLargeClock) LargeContent() else SmallContent() + } + + @Composable + private fun SmallContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + + val accent1 = Color(context.getColor(android.R.color.system_accent1_600)) + val accent3 = Color(context.getColor(android.R.color.system_accent3_600)) + + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + val (hours, minutes) = splitTimeLines(time) + val timeDisplay = "$hours \u2022 $minutes" + val avatar = remember { loadUserAvatar() } + val userName = remember { loadUserName() } + + val contentAlign = when { + isLeftAligned -> Alignment.CenterStart + isRightAligned -> Alignment.CenterEnd + else -> Alignment.Center + } + val sidePadding = if (isSideAligned) { + (clockPaddingStart / context.resources.displayMetrics.density).dp + } else { + 0.dp + } + + Box( + modifier = Modifier + .fillMaxSize() + .padding( + start = if (isRightAligned) 0.dp else sidePadding, + end = if (isRightAligned) sidePadding else 0.dp, + ), + contentAlignment = contentAlign, + ) { + ClockBody( + timeDisplay = timeDisplay, + monthDay = monthDay, + dayOfWeek = dayOfWeek, + accent1 = accent1, + accent3 = accent3, + isDoze = isDoze, + scale = dynSizeScale, + avatar = avatar, + userName = userName, + profileFrameSize = 75.dp, + profileImageSize = 64.dp, + timeFontSize = 34f, + dateFontSize = 16f, + dayFontSize = 20f, + userNameSize = 14f, + dividerHeight = 32.dp, + ) + } + } + + @Composable + private fun LargeContent() { + val (time, date, isDoze, screenOff, regionDark) = rememberClockState() + val dynSizeScale by ClockSettingsRepository.sizeScale.collectAsState() + + val accent1 = Color(context.getColor(android.R.color.system_accent1_600)) + val accent3 = Color(context.getColor(android.R.color.system_accent3_600)) + + val dayOfWeek = remember(date) { formatDayOfWeek() } + val monthDay = remember(date) { formatMonthDay() } + val (hours, minutes) = splitTimeLines(time) + val timeDisplay = "$hours \u2022 $minutes" + val avatar = remember { loadUserAvatar() } + val userName = remember { loadUserName() } + + Column( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ClockBody( + timeDisplay = timeDisplay, + monthDay = monthDay, + dayOfWeek = dayOfWeek, + accent1 = accent1, + accent3 = accent3, + isDoze = isDoze, + scale = dynSizeScale, + avatar = avatar, + userName = userName, + profileFrameSize = 96.dp, + profileImageSize = 82.dp, + timeFontSize = 38f, + dateFontSize = 20f, + dayFontSize = 24f, + userNameSize = 18f, + dividerHeight = 40.dp, + ) + } + } + + @Composable + private fun ClockBody( + timeDisplay: String, + monthDay: String, + dayOfWeek: String, + accent1: Color, + accent3: Color, + isDoze: Boolean, + scale: Float, + avatar: Bitmap?, + userName: String, + profileFrameSize: Dp, + profileImageSize: Dp, + timeFontSize: Float, + dateFontSize: Float, + dayFontSize: Float, + userNameSize: Float, + dividerHeight: Dp, + ) { + val bgBrush = if (isDoze) { + Brush.horizontalGradient(listOf(Color.White.copy(alpha = 0.08f), Color.White.copy(alpha = 0.08f))) + } else { + Brush.horizontalGradient(listOf(accent1, accent3)) + } + val dayPillBg = if (isDoze) Color.White.copy(alpha = 0.08f) else Color.White + val dayTextColor = if (isDoze) Color.White else accent1 + + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box { + // Background Card + Box( + modifier = Modifier + .padding(top = (profileFrameSize * scale) / 2) + .clip(RoundedCornerShape(20.dp * scale)) + .background(bgBrush) + ) { + // Foreground Content + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding( + start = 20.dp * scale, + end = 20.dp * scale, + top = 0.dp, + bottom = 24.dp * scale + ) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.width(profileFrameSize * scale) + ) { + Spacer(modifier = Modifier.height((profileFrameSize * scale) / 2)) + Spacer(modifier = Modifier.height(8.dp * scale)) + Text( + text = userName, + maxLines = 1, + style = TextStyle( + fontSize = (userNameSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ) + ) + } + + Spacer(modifier = Modifier.width(12.dp * scale)) + + Box( + modifier = Modifier + .width(2.dp * scale) + .height(dividerHeight * scale) + .background(Color.White), + ) + + Spacer(modifier = Modifier.width(12.dp * scale)) + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = timeDisplay, + modifier = Modifier.padding(top = 6.dp * scale), + style = TextStyle( + fontSize = (timeFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ), + ) + Text( + text = monthDay, + style = TextStyle( + fontSize = (dateFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ), + ) + } + } + } + + // Avatar Box + Box( + modifier = Modifier + .padding(start = 20.dp * scale) + .size(profileFrameSize * scale) + .background(Color.White, CircleShape), + contentAlignment = Alignment.Center, + ) { + if (avatar != null) { + Image( + bitmap = avatar.asImageBitmap(), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(profileImageSize * scale) + .clip(CircleShape), + ) + } else { + Text( + text = "\u0CA0\u1D25\u0CA0", + style = TextStyle( + fontSize = (profileFrameSize.value * 0.3f * scale).sp, + color = accent1, + ), + ) + } + } + } + + // Day Pill (overlapping the main card by offset) + Box( + modifier = Modifier + .offset(y = (-16.dp * scale)) + .clip(RoundedCornerShape(12.dp * scale)) + .background(dayPillBg) + .padding(horizontal = 16.dp * scale, vertical = 8.dp * scale), + contentAlignment = Alignment.Center, + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = dayOfWeek.uppercase(), + style = TextStyle( + fontSize = (dayFontSize * scale).sp, + fontWeight = FontWeight.Bold, + color = dayTextColor, + letterSpacing = 2.sp, + textAlign = TextAlign.Center, + ), + ) + WeatherRow( + textColor = dayTextColor, + scale = scale, + ) + } + } + } + } + + @Composable + private fun WeatherRow( + textColor: Color, + scale: Float, + ) { + val display = viewModel.rememberResolvedDisplay() + if (display is DateDisplay.Weather && display.temp.isNotEmpty()) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Spacer(modifier = Modifier.width(4.dp * scale)) + display.icon?.let { icon -> + Image( + bitmap = icon.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size((18 * scale).dp), + ) + Spacer(modifier = Modifier.width(4.dp * scale)) + } + Text( + text = display.temp, + style = TextStyle( + fontSize = (18f * scale).sp, + fontWeight = FontWeight.Bold, + color = textColor, + letterSpacing = 2.sp, + ), + ) + } + } + } + + private fun formatDayOfWeek(): String { + val sdf = SimpleDateFormat("EEEE", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } + + private fun formatMonthDay(): String { + val sdf = SimpleDateFormat("MMMM dd", interactor.locale) + sdf.timeZone = interactor.calendar.timeZone + return sdf.format(interactor.calendar.time) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java deleted file mode 100644 index e6c27d4290ee5..0000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.keyguard; - -import static org.junit.Assume.assumeFalse; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.pm.PackageManager; -import android.os.Handler; -import android.testing.TestableLooper; -import android.testing.TestableLooper.RunWithLooper; -import android.view.View; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.settings.FakeDisplayTracker; -import com.android.systemui.statusbar.policy.ConfigurationController; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@RunWithLooper(setAsMainLooper = true) -public class KeyguardSliceViewControllerTest extends SysuiTestCase { - @Mock - private KeyguardSliceView mView; - @Mock - private ConfigurationController mConfigurationController; - @Mock - private ActivityStarter mActivityStarter; - private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); - private DumpManager mDumpManager = new DumpManager(); - private Handler mHandler; - private Handler mBgHandler; - private KeyguardSliceViewController mController; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - TestableLooper testableLooper = TestableLooper.get(this); - assert testableLooper != null; - mHandler = new Handler(testableLooper.getLooper()); - mBgHandler = new Handler(testableLooper.getLooper()); - when(mView.isAttachedToWindow()).thenReturn(true); - when(mView.getContext()).thenReturn(mContext); - mController = new KeyguardSliceViewController(mHandler, mBgHandler, mView, - mActivityStarter, mConfigurationController, mDumpManager, - mDisplayTracker, null, null); - mController.setupUri(KeyguardSliceProvider.KEYGUARD_SLICE_URI); - } - - @After - public void tearDown() { - mController.onViewDetached(); - } - - @Test - public void refresh_replacesSliceContentAndNotifiesListener() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - mController.refresh(); - verify(mView).hideSlice(); - } - - @Test - public void onAttachedToWindow_registersListeners() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - mController.init(); - verify(mConfigurationController).addCallback( - any(ConfigurationController.ConfigurationListener.class)); - } - - @Test - public void onDetachedFromWindow_unregistersListeners() { - // Skips the test if running on a watch because watches don't have a SliceManager system - // service. - assumeFalse(isWatch()); - - ArgumentCaptor attachListenerArgumentCaptor = - ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class); - - mController.init(); - verify(mView).addOnAttachStateChangeListener(attachListenerArgumentCaptor.capture()); - - attachListenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView); - - verify(mConfigurationController).removeCallback( - any(ConfigurationController.ConfigurationListener.class)); - } - - private boolean isWatch() { - final PackageManager pm = mContext.getPackageManager(); - return pm.hasSystemFeature(PackageManager.FEATURE_WATCH); - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java deleted file mode 100644 index d96518abc0070..0000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewTest.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright (C) 2018 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ -package com.android.keyguard; - -import android.graphics.Color; -import android.net.Uri; -import android.testing.TestableLooper.RunWithLooper; -import android.view.LayoutInflater; - -import androidx.slice.Slice; -import androidx.slice.SliceProvider; -import androidx.slice.SliceSpecs; -import androidx.slice.builders.ListBuilder; -import androidx.slice.widget.RowContent; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.res.R; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.MockitoAnnotations; - -import java.util.Collections; -import java.util.HashSet; -import java.util.concurrent.atomic.AtomicBoolean; - -@SmallTest -@RunWithLooper(setAsMainLooper = true) -@RunWith(AndroidJUnit4.class) -public class KeyguardSliceViewTest extends SysuiTestCase { - private KeyguardSliceView mKeyguardSliceView; - private Uri mSliceUri; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - LayoutInflater layoutInflater = LayoutInflater.from(getContext()); - mKeyguardSliceView = (KeyguardSliceView) layoutInflater - .inflate(R.layout.keyguard_slice_view, null); - mSliceUri = Uri.parse(KeyguardSliceProvider.KEYGUARD_SLICE_URI); - SliceProvider.setSpecs(new HashSet<>(Collections.singletonList(SliceSpecs.LIST))); - } - - @Test - public void showSlice_notifiesListener() { - ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY); - builder.setHeader(new ListBuilder.HeaderBuilder().setTitle("header title!")); - Slice slice = builder.build(); - RowContent rowContent = new RowContent(slice.getItemArray()[0], 0); - - AtomicBoolean notified = new AtomicBoolean(); - mKeyguardSliceView.setContentChangeListener(()-> notified.set(true)); - mKeyguardSliceView.showSlice(rowContent, Collections.EMPTY_LIST); - Assert.assertTrue("Listener should be notified about slice changes.", - notified.get()); - } - - @Test - public void showSlice_emptySliceNotifiesListener() { - AtomicBoolean notified = new AtomicBoolean(); - mKeyguardSliceView.setContentChangeListener(()-> notified.set(true)); - mKeyguardSliceView.showSlice(null, Collections.EMPTY_LIST); - Assert.assertTrue("Listener should be notified about slice changes.", - notified.get()); - } - - @Test - public void hasHeader_readsSliceData() { - ListBuilder builder = new ListBuilder(getContext(), mSliceUri, ListBuilder.INFINITY); - mKeyguardSliceView.showSlice(null, Collections.EMPTY_LIST); - Assert.assertFalse("View should not have a header", mKeyguardSliceView.hasHeader()); - - builder.setHeader(new ListBuilder.HeaderBuilder().setTitle("header title!")); - Slice slice = builder.build(); - RowContent rowContent = new RowContent(slice.getItemArray()[0], 0); - mKeyguardSliceView.showSlice(rowContent, Collections.EMPTY_LIST); - Assert.assertTrue("View should have a header", mKeyguardSliceView.hasHeader()); - } - - @Test - public void getTextColor_whiteTextWhenAOD() { - // Set text color to red since the default is white and test would always pass - mKeyguardSliceView.setTextColor(Color.RED); - mKeyguardSliceView.setDarkAmount(0); - Assert.assertEquals("Should be using regular text color", Color.RED, - mKeyguardSliceView.getTextColor()); - mKeyguardSliceView.setDarkAmount(1); - Assert.assertEquals("Should be using AOD text color", Color.WHITE, - mKeyguardSliceView.getTextColor()); - } -} diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt new file mode 100644 index 0000000000000..caff6a70d6887 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/CalendarSimpleData.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.keyguard.ui.clocks + +import android.app.PendingIntent +import android.text.TextUtils +import java.util.Objects + +class CalendarSimpleData( + val id: Long = 0L, + val title: String? = null, + val startTime: Long = 0L, + val endTime: Long = 0L, + val location: String? = null, + val description: String = "", + val formattedTime: String = "", + val eventStatus: Int = EVENT_STATUS_TO_SCHEDULE, + val tapAction: PendingIntent? = null, +) { + + fun isEventVisible(): Boolean = + eventStatus == EVENT_STATUS_TO_BEGIN || eventStatus == EVENT_STATUS_NOW + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is CalendarSimpleData) return false + + return id == other.id && + startTime == other.startTime && + endTime == other.endTime && + eventStatus == other.eventStatus && + TextUtils.equals(title, other.title) && + TextUtils.equals(location, other.location) + } + + override fun hashCode(): Int { + return Objects.hash(id, title, startTime, endTime, location, eventStatus) + } + + companion object { + const val EVENT_STATUS_TO_SCHEDULE = 0 + const val EVENT_STATUS_TO_BEGIN = 1 + const val EVENT_STATUS_NOW = 2 + const val EVENT_STATUS_END = 3 + + val EMPTY = CalendarSimpleData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt index 21a0990f999b8..e95720355f504 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockAnimations.kt @@ -19,31 +19,31 @@ import com.android.systemui.plugins.annotations.ProtectedInterface @ProtectedInterface interface ClockAnimations { /** Runs an enter animation (if any) */ - fun enter() + fun enter() {} /** Sets how far into AOD the device currently is. */ - fun doze(fraction: Float) + fun doze(fraction: Float) {} /** Sets how far into the folding animation the device is. */ - fun fold(fraction: Float) + fun fold(fraction: Float) {} /** Runs the battery animation (if any). */ - fun charge() + fun charge() {} /** Runs when the clock's position changed during the move animation. */ - fun onPositionAnimated(anim: ClockPositionAnimationArgs) + fun onPositionAnimated(anim: ClockPositionAnimationArgs) {} /** * Runs when swiping clock picker, swipingFraction: 1.0 -> clock is scaled up in the preview, * 0.0 -> clock is scaled down in the shade; previewRatio is previewSize / screenSize */ - fun onPickerCarouselSwiping(swipingFraction: Float) + fun onPickerCarouselSwiping(swipingFraction: Float) {} /** Runs when an animation when the view is tapped on the lockscreen */ - fun onFidgetTap(x: Float, y: Float) + fun onFidgetTap(x: Float, y: Float) {} /** Update reactive axes for this clock */ - fun onFontAxesChanged(style: ClockAxisStyle) + fun onFontAxesChanged(style: ClockAxisStyle) {} } data class ClockPositionAnimationArgs(val fromLeft: Int, val direction: Int, val fraction: Float) diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt new file mode 100644 index 0000000000000..eb76a846bc5e5 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockData.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.keyguard.ui.clocks + +data class ClockData( + val weather: ClockWeatherData = ClockWeatherData.EMPTY, + val calendar: CalendarSimpleData = CalendarSimpleData.EMPTY, + val smartspace: List = emptyList() +) { + companion object { + val EMPTY = ClockData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt index 3bd443c5b8ae5..77ef47ceb66cd 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockEvents.kt @@ -69,6 +69,14 @@ interface ClockEvents { /** Call with zen/dnd information */ fun onZenDataChanged(data: ZenData) + fun onUiModeChanged(isDarkTheme: Boolean) {} + fun onDateChanged() {} + fun onClockDataChanged(data: ClockData) {} + fun onMetadataChanged(track: String, artist: String, packageName: String) {} + fun onPlaybackStateChanged(playing: Boolean) {} + fun onNowPlayingUpdate(nowPlayingText: String) {} + fun onClockLayoutChanged(isCentered: Boolean, isLargeClockVisible: Boolean) {} + fun onDepthEffectVisibilityChanged(visible: Boolean) {} } class ClockEventListeners { diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt index f0699f4298c59..3e8aff35d2ee5 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockFaceEvents.kt @@ -15,6 +15,7 @@ package com.android.systemui.plugins.keyguard.ui.clocks import android.content.Context import android.graphics.Color +import android.graphics.Point import android.graphics.Rect import com.android.systemui.monet.ColorScheme import com.android.systemui.plugins.annotations.ProtectedInterface @@ -53,6 +54,14 @@ interface ClockFaceEvents { /** Called to notify the clock about its display. */ fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) + fun onStartedWakingUp() {} + fun onStartedGoingToSleep(isKeyguardVisible: Boolean) {} + fun onWakefulnessStateChanged(isWakingUp: Boolean, tapPosition: Point?) {} + fun onRegionDarknessChanged(isRegionDark: Boolean) {} + fun onScreenOff(screenOff: Boolean) {} + fun onDozeChanged(dozing: Boolean) {} + fun onDozeAmountChanged(linear: Float, eased: Float) {} + fun onPulsingChanged(pulsing: Boolean) {} } /** Contains Theming information for the clock face */ diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt new file mode 100644 index 0000000000000..4c6f9ab816fa7 --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/ClockWeatherData.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.keyguard.ui.clocks + +import android.app.PendingIntent + +data class ClockWeatherData( + val temp: String = "", + val condition: String = "", + val conditionCode: Int = 0, + val city: String? = null, + val humidity: String? = null, + val wind: String? = null, + val windDirection: String? = null, + val tempUnit: String? = null, + val windUnit: String? = null, + val pinWheel: String? = null, + val timestamp: Long = 0L, + val iconBytes: ByteArray? = null, + val tintIcon: Boolean = true, + val tapAction: PendingIntent? = null, +) { + val formattedTemp: String + get() = if (temp.isNotEmpty()) "$temp${tempUnit ?: "°"}" else "" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ClockWeatherData) return false + return temp == other.temp && + condition == other.condition && + conditionCode == other.conditionCode && + city == other.city && + timestamp == other.timestamp && + tintIcon == other.tintIcon && + iconBytes.contentEquals(other.iconBytes) + } + + override fun hashCode(): Int { + var result = temp.hashCode() + result = 31 * result + condition.hashCode() + result = 31 * result + conditionCode + result = 31 * result + (city?.hashCode() ?: 0) + result = 31 * result + timestamp.hashCode() + result = 31 * result + tintIcon.hashCode() + result = 31 * result + (iconBytes?.contentHashCode() ?: 0) + return result + } + + companion object { + val EMPTY = ClockWeatherData() + } +} + diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt new file mode 100644 index 0000000000000..d3d2d35bd02bb --- /dev/null +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/keyguard/ui/clocks/SmartspaceData.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package com.android.systemui.plugins.keyguard.ui.clocks + +import android.app.PendingIntent + +data class SmartspaceData( + val id: String, + val title: String, + val subtitle: String, + val featureType: Int, + val iconBytes: ByteArray?, + val componentName: String?, + val isSensitive: Boolean, + val sourceType: Int, + val creationTime: Long, + val score: Float, + val tapAction: PendingIntent? = null, +) { + val isFromSmartspacer: Boolean + get() = sourceType == SOURCE_SMARTSPACER + + val isFromGoogleSmartspace: Boolean + get() = sourceType == SOURCE_GOOGLE_SMARTSPACE + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is SmartspaceData) return false + return id == other.id && + title == other.title && + subtitle == other.subtitle && + featureType == other.featureType && + sourceType == other.sourceType + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + title.hashCode() + result = 31 * result + subtitle.hashCode() + result = 31 * result + featureType + result = 31 * result + sourceType + return result + } + + companion object { + const val SOURCE_SMARTSPACER = 100 + const val SOURCE_GOOGLE_SMARTSPACE = 200 + + val EMPTY = SmartspaceData( + id = "", + title = "", + subtitle = "", + featureType = 0, + iconBytes = null, + componentName = null, + isSensitive = false, + sourceType = 0, + creationTime = 0L, + score = 0f + ) + } +} + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml b/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml deleted file mode 100644 index e6cba028abd3e..0000000000000 --- a/packages/SystemUI/res-keyguard/layout/keyguard_slice_view.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml deleted file mode 100644 index 8fa1eaafcf83a..0000000000000 --- a/packages/SystemUI/res-keyguard/layout/keyguard_weather_area.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_widgets_area.xml b/packages/SystemUI/res-keyguard/layout/keyguard_widgets_area.xml new file mode 100644 index 0000000000000..e220b76949d5f --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_widgets_area.xml @@ -0,0 +1,23 @@ + + diff --git a/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml b/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml new file mode 100644 index 0000000000000..7551c2c56c7bb --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_humidity_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml b/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml new file mode 100644 index 0000000000000..807485f162d28 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wind_direction_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml b/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml new file mode 100644 index 0000000000000..120ee713250b2 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wind_symbol_small.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/layout/weather_forecast_day_item.xml b/packages/SystemUI/res/layout/weather_forecast_day_item.xml new file mode 100644 index 0000000000000..62d0bdfa6644d --- /dev/null +++ b/packages/SystemUI/res/layout/weather_forecast_day_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/weather_tile_dialog.xml b/packages/SystemUI/res/layout/weather_tile_dialog.xml new file mode 100644 index 0000000000000..7945e7bd9dbed --- /dev/null +++ b/packages/SystemUI/res/layout/weather_tile_dialog.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/axion_dimens.xml b/packages/SystemUI/res/values/axion_dimens.xml new file mode 100644 index 0000000000000..15ff6e99d6af0 --- /dev/null +++ b/packages/SystemUI/res/values/axion_dimens.xml @@ -0,0 +1,29 @@ + + + + + 9.2989dp + 9.2989dp + 37.1956dp + + + 4dp + 14dp + 16dp + 16dp + diff --git a/packages/SystemUI/res/values/cr_strings.xml b/packages/SystemUI/res/values/cr_strings.xml index 8c663785d0b4f..6f2d930e270d3 100644 --- a/packages/SystemUI/res/values/cr_strings.xml +++ b/packages/SystemUI/res/values/cr_strings.xml @@ -135,6 +135,10 @@ No weather data Weather + + Current weather condition + Forecast weather condition + Refresh rate Auto @@ -280,6 +284,7 @@ Final Halftime Upcoming + Live event Screenshot diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 37f09c1b79527..e443d6b0587f8 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -315,4 +315,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 1ff18673b1805..a5f093f54161f 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -16,6 +16,7 @@ package com.android.keyguard import android.R +import android.app.PendingIntent import android.app.NotificationManager.zenModeFromInterruptionFilter import android.content.BroadcastReceiver import android.content.Context @@ -62,9 +63,12 @@ import com.android.systemui.plugins.keyguard.ui.clocks.ClockController import com.android.systemui.plugins.keyguard.ui.clocks.ClockEventListener import com.android.systemui.plugins.keyguard.ui.clocks.ClockFaceController import com.android.systemui.plugins.keyguard.ui.clocks.ClockFaceController.Companion.updateTheme +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.shared.clocks.view.AxClockView import com.android.systemui.plugins.keyguard.ui.clocks.ClockMessageBuffers import com.android.systemui.plugins.keyguard.ui.clocks.ClockTickRate import com.android.systemui.plugins.keyguard.ui.clocks.TimeFormatKind +import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R as SysuiR import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.settings.UserTracker @@ -74,6 +78,7 @@ import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChang import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.ZenModeController import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor +import com.android.systemui.util.ScrimUtils import com.android.systemui.util.annotations.DeprecatedSysuiVisibleForTesting import com.android.systemui.util.concurrency.DelayableExecutor import dagger.Lazy @@ -102,6 +107,7 @@ constructor( private val keyguardUpdateMonitor: KeyguardUpdateMonitor, // TODO b/362719719 - We should use the configuration controller associated with the display. private val configurationController: ConfigurationController, + private val statusBarStateController: StatusBarStateController, @DisplaySpecific private val resources: Resources, @DisplaySpecific val context: Context, @Main private val mainExecutor: DelayableExecutor, @@ -187,6 +193,16 @@ constructor( } zenData?.let { clock.events.onZenDataChanged(it) } alarmData?.let { clock.events.onAlarmDataChanged(it) } + quickLookClockData?.let { clock.events.onClockDataChanged(it) } + if (qlMediaPlaying || qlTrack.isNotEmpty()) { + clock.events.onPlaybackStateChanged(qlMediaPlaying) + clock.events.onMetadataChanged(qlTrack, qlArtist, qlMediaPackage) + } + if (qlNowPlaying.isNotEmpty()) { + clock.events.onNowPlayingUpdate(qlNowPlaying) + (clock.smallClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = qlNowPlayingAction + (clock.largeClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = qlNowPlayingAction + } smallClockOnAttachStateChangeListener = object : OnAttachStateChangeListener { @@ -257,6 +273,9 @@ constructor( context.theme.resolveAttribute(R.attr.isLightTheme, isLightTheme, true) return isLightTheme.data == 0 } + private fun onClockUiModeChanged() { + clock?.run { events.onUiModeChanged(isDarkTheme()) } + } private fun updateColors() { val isDarkTheme = isDarkTheme() @@ -277,6 +296,10 @@ constructor( smallClock.updateTheme { it.copy(isDarkTheme = isDarkTheme) } largeClock.updateTheme { it.copy(isDarkTheme = isDarkTheme) } } + clock?.run { + smallClock.events.onRegionDarknessChanged(isDarkTheme) + largeClock.events.onRegionDarknessChanged(isDarkTheme) + } } protected open fun createRegionSampler( @@ -312,6 +335,13 @@ constructor( private var weatherData: WeatherData? = null private var zenData: ZenData? = null private var alarmData: AlarmData? = null + private var quickLookClockData: ClockData? = null + private var qlMediaPlaying: Boolean = false + private var qlTrack: String = "" + private var qlArtist: String = "" + private var qlMediaPackage: String = "" + private var qlNowPlaying: String = "" + private var qlNowPlayingAction: PendingIntent? = null private val clockListener = object : ClockEventListener { @@ -337,6 +367,9 @@ constructor( logger.i("onDensityOrFontScaleChanged") updateFontSizes() } + override fun onUiModeChanged() { + onClockUiModeChanged() + } } private val batteryCallback = @@ -355,9 +388,16 @@ constructor( private val localeBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - clock?.run { - events.onLocaleChanged(Locale.getDefault()) - events.onTimeFormatChanged(getTimeFormatKind()) + when (intent.action) { + Intent.ACTION_LOCALE_CHANGED -> { + clock?.run { + events.onLocaleChanged(Locale.getDefault()) + events.onTimeFormatChanged(getTimeFormatKind()) + } + } + Intent.ACTION_DATE_CHANGED -> { + clock?.run { events.onDateChanged() } + } } } } @@ -392,9 +432,47 @@ constructor( weatherData = data clock?.run { events.onWeatherDataChanged(data) } } + override fun onClockDataChanged(data: ClockData) { + quickLookClockData = data + clock?.run { events.onClockDataChanged(data) } + } + override fun onQLPlaybackStateChanged(play: Boolean) { + qlMediaPlaying = play + clock?.run { events.onPlaybackStateChanged(play) } + } + override fun onQLMetadataChanged(track: String, artist: String, packageName: String) { + qlTrack = track + qlArtist = artist + qlMediaPackage = packageName + clock?.run { + events.onMetadataChanged(track, artist, packageName) + } + } + override fun onNowPlayingUpdate(nowPlayingText: String, tapAction: PendingIntent?) { + qlNowPlaying = nowPlayingText + qlNowPlayingAction = tapAction + clock?.run { + events.onNowPlayingUpdate(nowPlayingText) + (smallClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = tapAction + (largeClock.view as? AxClockView)?.quickLook?.nowPlayingTapActionFlow?.value = tapAction + } + } + override fun onStartedWakingUp() { + clock?.smallClock?.events?.onStartedWakingUp() + clock?.largeClock?.events?.onStartedWakingUp() + } + override fun onStartedGoingToSleep(why: Int) { + clock?.smallClock?.events?.onScreenOff(true) + clock?.largeClock?.events?.onScreenOff(true) + val keyguardVisible = ScrimUtils.get().isKeyguardShowing() + clock?.smallClock?.events?.onStartedGoingToSleep(keyguardVisible) + clock?.largeClock?.events?.onStartedGoingToSleep(keyguardVisible) + } override fun onTimeChanged() { - refreshTime() + if (ScrimUtils.get().isKeyguardShowing()) { + refreshTime() + } } private fun refreshTime() { @@ -436,6 +514,49 @@ constructor( } } + private val dozeCallback = + object : StatusBarStateController.StateListener { + override fun onDozingChanged(isDozing: Boolean) { + clock?.smallClock?.events?.onDozeChanged(isDozing) + clock?.largeClock?.events?.onDozeChanged(isDozing) + } + override fun onDozeAmountChanged(linear: Float, eased: Float) { + clock?.smallClock?.events?.onDozeAmountChanged(linear, eased) + clock?.largeClock?.events?.onDozeAmountChanged(linear, eased) + } + override fun onPulsingChanged(pulsing: Boolean) { + clock?.smallClock?.events?.onPulsingChanged(pulsing) + clock?.largeClock?.events?.onPulsingChanged(pulsing) + } + } + private var depthBlockedByDozeAmount = false + private val depthScrimListener = object : ScrimUtils.ScrimEventListener { + override fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { + updateDepthVisibility() + } + override fun onKeyguardGoingAwayChanged(goingAway: Boolean) { + updateDepthVisibility() + } + override fun onPrimaryBouncerShowingChanged(showing: Boolean) { + updateDepthVisibility() + } + override fun onDozingChanged(dozing: Boolean) { + updateDepthVisibility() + } + override fun onKeyguardShowingChanged(showing: Boolean) { + updateDepthVisibility() + } + override fun onKeyguardAlphaChanged(alpha: Float) { + updateDepthVisibility() + } + } + private fun updateDepthVisibility() { + val scrim = ScrimUtils.get() + val visible = scrim.isKeyguardShowing() + && !scrim.isDozing() + && !depthBlockedByDozeAmount + clock?.events?.onDepthEffectVisibilityChanged(visible) + } private fun handleZenMode(zen: Int) { val mode = ZenMode.fromInt(zen) if (mode == null) { @@ -478,18 +599,24 @@ constructor( isRegistered = true logger.i("registerListeners(isPreview = $isPreview)") + val filter = IntentFilter().apply { + addAction(Intent.ACTION_LOCALE_CHANGED) + addAction(Intent.ACTION_DATE_CHANGED) + } broadcastDispatcher.registerReceiver( localeBroadcastReceiver, - IntentFilter(Intent.ACTION_LOCALE_CHANGED), + filter, ) // Proactively update timezone on listener registration to avoid race conditions on startup. clock?.events?.onTimeZoneChanged(IcuTimeZone.getDefault()) + statusBarStateController.addCallback(dozeCallback) configurationController.addCallback(configListener) batteryController.addCallback(batteryCallback) keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) zenModeController.addCallback(zenModeCallback) + ScrimUtils.get().addListener(depthScrimListener) if (SceneContainerFlag.isEnabled) { handleDoze( when (AOD) { @@ -514,10 +641,12 @@ constructor( logger.i("unregisterListeners(isPreview = $isPreview)") broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver) + statusBarStateController.removeCallback(dozeCallback) configurationController.removeCallback(configListener) batteryController.removeCallback(batteryCallback) keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback) zenModeController.removeCallback(zenModeCallback) + ScrimUtils.get().removeListener(depthScrimListener) smallRegionSampler?.stopRegionSampler() largeRegionSampler?.stopRegionSampler() smallTimeListener?.stop() @@ -582,14 +711,6 @@ constructor( } } - fun handleFidgetTap(x: Float, y: Float) { - if (isPreview) return - clock?.run { - smallClock.animations.onFidgetTap(x, y) - largeClock.animations.onFidgetTap(x, y) - } - } - private fun handleDoze(doze: Float) { if (isPreview) { dozeAmount.value = doze @@ -607,6 +728,12 @@ constructor( smallTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) largeTimeListener?.update(doze < DOZE_TICKRATE_THRESHOLD) dozeAmount.value = doze + + val blocked = doze > 0f && doze < 1f + if (depthBlockedByDozeAmount != blocked) { + depthBlockedByDozeAmount = blocked + updateDepthVisibility() + } } @DeprecatedSysuiVisibleForTesting diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java deleted file mode 100644 index a1ab0f6248d82..0000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceView.java +++ /dev/null @@ -1,500 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -package com.android.keyguard; - -import android.animation.LayoutTransition; -import android.animation.ObjectAnimator; -import android.animation.PropertyValuesHolder; -import android.annotation.ColorInt; -import android.annotation.StyleRes; -import android.app.PendingIntent; -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.InsetDrawable; -import android.graphics.text.LineBreaker; -import android.net.Uri; -import android.os.Trace; -import android.text.TextUtils; -import android.text.TextUtils.TruncateAt; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.view.animation.Animation; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.slice.SliceItem; -import androidx.slice.core.SliceQuery; -import androidx.slice.widget.RowContent; -import androidx.slice.widget.SliceContent; - -import com.android.app.animation.Interpolators; -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.graphics.ColorUtils; -import com.android.settingslib.Utils; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.res.R; -import com.android.systemui.util.wakelock.KeepAwakeAnimationListener; - -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; - -/** - * View visible under the clock on the lock screen and AoD. - */ -public class KeyguardSliceView extends LinearLayout { - - private static final String TAG = "KeyguardSliceView"; - public static final int DEFAULT_ANIM_DURATION = 550; - - private final LayoutTransition mLayoutTransition; - @VisibleForTesting - TextView mTitle; - private Row mRow; - private int mTextColor; - private float mDarkAmount = 0; - - private int mIconSize; - private int mIconSizeWithHeader; - /** - * Runnable called whenever the view contents change. - */ - private Runnable mContentChangeListener; - private boolean mHasHeader; - private View.OnClickListener mOnClickListener; - - public KeyguardSliceView(Context context, AttributeSet attrs) { - super(context, attrs); - - Resources resources = context.getResources(); - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.setStagger(LayoutTransition.CHANGE_APPEARING, DEFAULT_ANIM_DURATION / 2); - mLayoutTransition.setDuration(LayoutTransition.APPEARING, DEFAULT_ANIM_DURATION); - mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 2); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING); - mLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); - mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, - Interpolators.FAST_OUT_SLOW_IN); - mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, Interpolators.ALPHA_OUT); - mLayoutTransition.setAnimateParentHierarchy(false); - } - - @Override - protected void onFinishInflate() { - super.onFinishInflate(); - mTitle = findViewById(R.id.title); - mRow = findViewById(R.id.row); - mTextColor = Utils.getColorAttrDefaultColor(mContext, R.attr.wallpaperTextColor); - mIconSize = (int) mContext.getResources().getDimension(R.dimen.widget_icon_size); - mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); - mTitle.setBreakStrategy(LineBreaker.BREAK_STRATEGY_BALANCED); - } - - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - setLayoutTransition(isVisible ? mLayoutTransition : null); - } - - /** - * Returns whether the current visible slice has a title/header. - */ - public boolean hasHeader() { - return mHasHeader; - } - - void hideSlice() { - mTitle.setVisibility(GONE); - mRow.setVisibility(GONE); - mHasHeader = false; - if (mContentChangeListener != null) { - mContentChangeListener.run(); - } - } - - private LinearLayout createSubRow() { - LinearLayout layout = new LinearLayout(mContext); - LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( - (LinearLayout.LayoutParams) mRow.getLayoutParams()); - lp.setMarginEnd(mContext.getResources().getDimensionPixelSize( - R.dimen.widget_horizontal_padding)); - layout.setLayoutParams(lp); - layout.setOrientation(LinearLayout.HORIZONTAL); - layout.setVisibility(View.VISIBLE); - return layout; - } - - Map showSlice(RowContent header, List subItems) { - Trace.beginSection("KeyguardSliceView#showSlice"); - mHasHeader = header != null; - Map clickActions = new HashMap<>(); - - if (!mHasHeader) { - mTitle.setVisibility(GONE); - } else { - mTitle.setVisibility(VISIBLE); - - SliceItem mainTitle = header.getTitleItem(); - CharSequence title = mainTitle != null ? mainTitle.getText() : null; - mTitle.setText(title); - if (header.getPrimaryAction() != null - && header.getPrimaryAction().getAction() != null) { - clickActions.put(mTitle, header.getPrimaryAction().getAction()); - } - } - - final int subItemsCount = subItems.size(); - final int blendedColor = getTextColor(); - final int startIndex = mHasHeader ? 1 : 0; // First item is header; skip it - mRow.setVisibility(subItemsCount > 0 ? VISIBLE : GONE); - LinearLayout.LayoutParams layoutParams = (LayoutParams) mRow.getLayoutParams(); - layoutParams.gravity = Gravity.START; - mRow.setLayoutParams(layoutParams); - - int rowIndex = 0, colIndex = 0; - LinearLayout subRow = null; - for (SliceContent sc : subItems.subList(startIndex, subItems.size())) { - subRow = (LinearLayout) mRow.getChildAt(rowIndex); - if (subRow == null) { - subRow = createSubRow(); - mRow.addView(subRow, rowIndex); - } - RowContent rc = (RowContent) sc; - SliceItem item = rc.getSliceItem(); - final Uri itemTag = item.getSlice().getUri(); - // Try to reuse the view if already exists in the layout - KeyguardSliceTextView button = mRow.findViewWithTag(itemTag); - if (button == null) { - button = new KeyguardSliceTextView(mContext); - button.setTextColor(blendedColor); - button.setTag(itemTag); - subRow.addView(button, colIndex); - } else { - LinearLayout buttonParent = (LinearLayout) button.getParent(); - if (mRow.indexOfChild(buttonParent) != rowIndex - || buttonParent.indexOfChild(button) != colIndex) { - buttonParent.removeView(button); - subRow.addView(button, colIndex); - } - } - colIndex++; - - PendingIntent pendingIntent = null; - if (rc.getPrimaryAction() != null) { - pendingIntent = rc.getPrimaryAction().getAction(); - } - clickActions.put(button, pendingIntent); - - final SliceItem titleItem = rc.getTitleItem(); - button.setText(titleItem == null ? null : titleItem.getText()); - button.setContentDescription(rc.getContentDescription()); - - Drawable iconDrawable = null; - SliceItem icon = SliceQuery.find(item.getSlice(), - android.app.slice.SliceItem.FORMAT_IMAGE); - if (icon != null) { - final int iconSize = mHasHeader ? mIconSizeWithHeader : mIconSize; - iconDrawable = icon.getIcon().loadDrawable(mContext); - if (iconDrawable != null) { - if (iconDrawable instanceof InsetDrawable) { - // System icons (DnD) use insets which are fine for centered slice content - // but will cause a slight indent for left/right-aligned slice views - iconDrawable = ((InsetDrawable) iconDrawable).getDrawable(); - } - final int width = (int) (iconDrawable.getIntrinsicWidth() - / (float) iconDrawable.getIntrinsicHeight() * iconSize); - iconDrawable.setBounds(0, 0, Math.max(width, 1), iconSize); - } - } - button.setCompoundDrawablesRelative(iconDrawable, null, null, null); - button.setOnClickListener(mOnClickListener); - button.setClickable(pendingIntent != null); - - if (rc.hasBottomDivider()) { - rowIndex++; - colIndex = 0; - } else { - LinearLayout.LayoutParams lp = - (LinearLayout.LayoutParams) button.getLayoutParams(); - lp.setMarginEnd(mContext.getResources().getDimensionPixelSize( - R.dimen.widget_horizontal_padding)); - button.setLayoutParams(lp); - } - } - - // Removing old views - for (int i = 0; i < mRow.getChildCount(); i++) { - subRow = (LinearLayout) mRow.getChildAt(i); - for (int j = 0; j < subRow.getChildCount(); j++) { - View child = subRow.getChildAt(j); - if (!clickActions.containsKey(child)) { - subRow.removeView(child); - j--; - } - } - if (subRow.getChildCount() == 0) { - mRow.removeView(subRow); - i--; - } - } - - if (mContentChangeListener != null) { - mContentChangeListener.run(); - } - Trace.endSection(); - - return clickActions; - } - - public void setDarkAmount(float darkAmount) { - mDarkAmount = darkAmount; - mRow.setDarkAmount(darkAmount); - updateTextColors(); - } - - private void updateTextColors() { - final int blendedColor = getTextColor(); - mTitle.setTextColor(blendedColor); - mRow.performOnChildren(tv -> tv.setTextColor(blendedColor)); - } - - /** - * Runnable that gets invoked every time the title or the row visibility changes. - * @param contentChangeListener The listener. - */ - public void setContentChangeListener(Runnable contentChangeListener) { - mContentChangeListener = contentChangeListener; - } - - @VisibleForTesting - int getTextColor() { - return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount); - } - - @VisibleForTesting - void setTextColor(@ColorInt int textColor) { - mTextColor = textColor; - updateTextColors(); - } - - void onDensityOrFontScaleChanged() { - mIconSize = mContext.getResources().getDimensionPixelSize(R.dimen.widget_icon_size); - mIconSizeWithHeader = (int) mContext.getResources().getDimension(R.dimen.header_icon_size); - - mRow.performOnChildren(tv -> tv.onDensityOrFontScaleChanged()); - } - - void onOverlayChanged() { - mRow.performOnChildren(tv -> tv.onOverlayChanged()); - } - - public void dump(PrintWriter pw, String[] args) { - pw.println("KeyguardSliceView:"); - pw.println(" mTitle: " + (mTitle == null ? "null" : mTitle.getVisibility() == VISIBLE)); - pw.println(" mRow: " + (mRow == null ? "null" : mRow.getVisibility() == VISIBLE)); - pw.println(" mTextColor: " + Integer.toHexString(mTextColor)); - pw.println(" mDarkAmount: " + mDarkAmount); - pw.println(" mHasHeader: " + mHasHeader); - } - - @Override - public void setOnClickListener(View.OnClickListener onClickListener) { - mOnClickListener = onClickListener; - mTitle.setOnClickListener(onClickListener); - } - - public static class Row extends LinearLayout { - /** - * This view is visible in AOD, which means that the device will sleep if we - * don't hold a wake lock. We want to enter doze only after all views have reached - * their desired positions. - */ - private final Animation.AnimationListener mKeepAwakeListener; - private LayoutTransition mLayoutTransition; - private float mDarkAmount; - - public Row(Context context) { - this(context, null); - } - - public Row(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public Row(Context context, AttributeSet attrs, int defStyleAttr) { - this(context, attrs, defStyleAttr, 0); - } - - public Row(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - mKeepAwakeListener = new KeepAwakeAnimationListener(mContext); - } - - @Override - protected void onFinishInflate() { - mLayoutTransition = new LayoutTransition(); - mLayoutTransition.setDuration(DEFAULT_ANIM_DURATION); - - PropertyValuesHolder left = PropertyValuesHolder.ofInt("left", 0, 1); - PropertyValuesHolder right = PropertyValuesHolder.ofInt("right", 0, 1); - ObjectAnimator changeAnimator = ObjectAnimator.ofPropertyValuesHolder((Object) null, - left, right); - mLayoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, changeAnimator); - mLayoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeAnimator); - mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_APPEARING, - Interpolators.ACCELERATE_DECELERATE); - mLayoutTransition.setInterpolator(LayoutTransition.CHANGE_DISAPPEARING, - Interpolators.ACCELERATE_DECELERATE); - mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_APPEARING, - DEFAULT_ANIM_DURATION); - mLayoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, - DEFAULT_ANIM_DURATION); - - ObjectAnimator appearAnimator = ObjectAnimator.ofFloat(null, "alpha", 0f, 1f); - mLayoutTransition.setAnimator(LayoutTransition.APPEARING, appearAnimator); - mLayoutTransition.setInterpolator(LayoutTransition.APPEARING, Interpolators.ALPHA_IN); - - ObjectAnimator disappearAnimator = ObjectAnimator.ofFloat(null, "alpha", 1f, 0f); - mLayoutTransition.setInterpolator(LayoutTransition.DISAPPEARING, - Interpolators.ALPHA_OUT); - mLayoutTransition.setDuration(LayoutTransition.DISAPPEARING, DEFAULT_ANIM_DURATION / 4); - mLayoutTransition.setAnimator(LayoutTransition.DISAPPEARING, disappearAnimator); - - mLayoutTransition.setAnimateParentHierarchy(false); - } - - @Override - public void onVisibilityAggregated(boolean isVisible) { - super.onVisibilityAggregated(isVisible); - setLayoutTransition(isVisible ? mLayoutTransition : null); - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int width = MeasureSpec.getSize(widthMeasureSpec); - - performOnChildren(tv -> tv.setMaxWidth(Integer.MAX_VALUE)); - - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - } - - /** - * Set the amount (ratio) that the device has transitioned to doze. - * - * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake. - */ - public void setDarkAmount(float darkAmount) { - boolean isDozing = darkAmount != 0; - boolean wasDozing = mDarkAmount != 0; - if (isDozing == wasDozing) { - return; - } - mDarkAmount = darkAmount; - setLayoutAnimationListener(isDozing ? null : mKeepAwakeListener); - } - - @Override - public boolean hasOverlappingRendering() { - return false; - } - - public void performOnChildren(Consumer action) { - for (int i = 0; i < getChildCount(); i++) { - LinearLayout subRow = (LinearLayout) getChildAt(i); - for (int j = 0; j < subRow.getChildCount(); j++) { - View child = subRow.getChildAt(j); - if (child instanceof KeyguardSliceTextView) { - action.accept((KeyguardSliceTextView) child); - } - } - } - } - } - - /** - * Representation of an item that appears under the clock on main keyguard message. - */ - @VisibleForTesting - static class KeyguardSliceTextView extends TextView { - - @StyleRes - private static int sStyleId = R.style.TextAppearance_Keyguard_Secondary; - - KeyguardSliceTextView(Context context) { - super(context, null /* attrs */, 0 /* styleAttr */, sStyleId); - onDensityOrFontScaleChanged(); - setEllipsize(TruncateAt.END); - } - - public void onDensityOrFontScaleChanged() { - updatePadding(); - } - - public void onOverlayChanged() { - setTextAppearance(sStyleId); - } - - @Override - public void setText(CharSequence text, BufferType type) { - super.setText(text, type); - updatePadding(); - } - - private void updatePadding() { - boolean hasText = !TextUtils.isEmpty(getText()); - boolean isDate = Uri.parse(KeyguardSliceProvider.KEYGUARD_DATE_URI).equals(getTag()); - int padding = (int) getContext().getResources() - .getDimension(R.dimen.widget_horizontal_padding) / 2; - int iconPadding = (int) mContext.getResources() - .getDimension(R.dimen.widget_icon_padding); - // orientation is vertical, so add padding to top & bottom - setPadding(0, padding, 0, hasText ? padding : 0); - - setCompoundDrawablePadding(iconPadding); - } - - @Override - public void setTextColor(int color) { - super.setTextColor(color); - updateDrawableColors(); - } - - @Override - public void setCompoundDrawablesRelative(Drawable start, Drawable top, Drawable end, - Drawable bottom) { - super.setCompoundDrawablesRelative(start, top, end, bottom); - updateDrawableColors(); - updatePadding(); - } - - private void updateDrawableColors() { - final int color = getCurrentTextColor(); - for (Drawable drawable : getCompoundDrawables()) { - if (drawable != null) { - drawable.setTint(color); - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java deleted file mode 100644 index f8bde98014abe..0000000000000 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSliceViewController.java +++ /dev/null @@ -1,283 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.keyguard; - -import static android.app.slice.Slice.HINT_LIST_ITEM; - -import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; - -import android.app.PendingIntent; -import android.net.Uri; -import android.os.Handler; -import android.os.Trace; -import android.util.Log; -import android.view.Display; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.Observer; -import androidx.slice.Slice; -import androidx.slice.SliceViewManager; -import androidx.slice.widget.ListContent; -import androidx.slice.widget.RowContent; -import androidx.slice.widget.SliceContent; -import androidx.slice.widget.SliceLiveData; - -import com.android.systemui.Dumpable; -import com.android.systemui.Flags; -import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import com.android.systemui.dump.DumpManager; -import com.android.systemui.keyguard.KeyguardSliceProvider; -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.power.domain.interactor.PowerInteractor; -import com.android.systemui.power.shared.model.ScreenPowerState; -import com.android.systemui.settings.DisplayTracker; -import com.android.systemui.statusbar.policy.ConfigurationController; -import com.android.systemui.util.ViewController; - -import kotlin.coroutines.CoroutineContext; -import kotlin.coroutines.EmptyCoroutineContext; - -import java.io.PrintWriter; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -/** Controller for a {@link KeyguardSliceView}. */ -public class KeyguardSliceViewController extends ViewController implements - Dumpable { - private static final String TAG = "KeyguardSliceViewCtrl"; - - private final Handler mHandler; - private final Handler mBgHandler; - private final ActivityStarter mActivityStarter; - private final ConfigurationController mConfigurationController; - private final DumpManager mDumpManager; - private final DisplayTracker mDisplayTracker; - private int mDisplayId; - private LiveData mLiveData; - private Uri mKeyguardSliceUri; - private Slice mSlice; - private Map mClickActions; - private KeyguardInteractor mKeyguardInteractor; - private PowerInteractor mPowerInteractor; - - ConfigurationController.ConfigurationListener mConfigurationListener = - new ConfigurationController.ConfigurationListener() { - @Override - public void onDensityOrFontScaleChanged() { - mView.onDensityOrFontScaleChanged(); - } - @Override - public void onThemeChanged() { - mView.onOverlayChanged(); - } - }; - - Observer mObserver = new Observer() { - @Override - public void onChanged(Slice slice) { - mSlice = slice; - showSlice(slice); - } - }; - - private View.OnClickListener mOnClickListener = new View.OnClickListener() { - @Override - public void onClick(View v) { - final PendingIntent action = mClickActions.get(v); - if (action != null && mActivityStarter != null) { - mActivityStarter.startPendingIntentDismissingKeyguard(action); - } - } - }; - - @Inject - public KeyguardSliceViewController( - @Main Handler handler, - @Background Handler bgHandler, - KeyguardSliceView keyguardSliceView, - ActivityStarter activityStarter, - ConfigurationController configurationController, - DumpManager dumpManager, - DisplayTracker displayTracker, - KeyguardInteractor keyguardInteractor, - PowerInteractor powerInteractor) { - super(keyguardSliceView); - mHandler = handler; - mBgHandler = bgHandler; - mActivityStarter = activityStarter; - mConfigurationController = configurationController; - mDumpManager = dumpManager; - mDisplayTracker = displayTracker; - mKeyguardInteractor = keyguardInteractor; - mPowerInteractor = powerInteractor; - } - - @Override - public void onInit() { - setupUri(null); - startCoroutines(EmptyCoroutineContext.INSTANCE); - } - - void startCoroutines(CoroutineContext context) { - collectFlow(mView, mKeyguardInteractor.getDozeTimeTick(), - (Long millis) -> { - refresh(); - }, context); - - collectFlow(mView, mPowerInteractor.getScreenPowerState(), - (ScreenPowerState powerState) -> { - if (powerState == ScreenPowerState.SCREEN_TURNING_ON) { - refresh(); - } - }, context); - } - - @Override - protected void onViewAttached() { - Display display = mView.getDisplay(); - if (display != null) { - mDisplayId = display.getDisplayId(); - } - // Make sure we always have the most current slice - if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) { - mLiveData.observeForever(mObserver); - } - mConfigurationController.addCallback(mConfigurationListener); - mDumpManager.registerNormalDumpable( - TAG + "@" + Integer.toHexString( - KeyguardSliceViewController.this.hashCode()), - KeyguardSliceViewController.this); - } - - @Override - protected void onViewDetached() { - // TODO(b/117344873) Remove below work around after this issue be fixed. - if (mDisplayId == mDisplayTracker.getDefaultDisplayId() && mLiveData != null) { - mLiveData.removeObserver(mObserver); - } - mConfigurationController.removeCallback(mConfigurationListener); - mDumpManager.unregisterDumpable( - TAG + "@" + Integer.toHexString( - KeyguardSliceViewController.this.hashCode())); - } - - void updateTopMargin(float clockTopTextPadding) { - ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mView.getLayoutParams(); - lp.topMargin = (int) clockTopTextPadding; - mView.setLayoutParams(lp); - } - - /** - * Sets the slice provider Uri. - */ - public void setupUri(String uriString) { - if (uriString == null) { - uriString = KeyguardSliceProvider.KEYGUARD_SLICE_URI; - } - - boolean wasObserving = false; - if (mLiveData != null && mLiveData.hasActiveObservers()) { - wasObserving = true; - mLiveData.removeObserver(mObserver); - } - - mKeyguardSliceUri = Uri.parse(uriString); - mLiveData = SliceLiveData.fromUri(mView.getContext(), mKeyguardSliceUri); - - if (wasObserving) { - mLiveData.observeForever(mObserver); - } - } - - /** - * Update contents of the view. - */ - public void refresh() { - - Trace.beginSection("KeyguardSliceViewController#refresh"); - try { - Slice slice; - if (KeyguardSliceProvider.KEYGUARD_SLICE_URI.equals(mKeyguardSliceUri.toString())) { - KeyguardSliceProvider instance = KeyguardSliceProvider.getAttachedInstance(); - if (instance != null) { - if (Flags.sliceManagerBinderCallBackground()) { - mBgHandler.post(() -> { - Slice _slice = instance.onBindSlice(mKeyguardSliceUri); - mHandler.post(() -> mObserver.onChanged(_slice)); - }); - return; - } - slice = instance.onBindSlice(mKeyguardSliceUri); - } else { - Log.w(TAG, "Keyguard slice not bound yet?"); - slice = null; - } - } else { - // TODO: Make SliceViewManager injectable - slice = SliceViewManager.getInstance(mView.getContext()).bindSlice( - mKeyguardSliceUri); - } - mObserver.onChanged(slice); - } finally { - Trace.endSection(); - } - } - - public void translate(int x, int y) { - mView.setTranslationX(x); - mView.setTranslationY(y); - } - - void showSlice(Slice slice) { - Trace.beginSection("KeyguardSliceViewController#showSlice"); - if (slice == null) { - mView.hideSlice(); - Trace.endSection(); - return; - } - - ListContent lc = new ListContent(slice); - RowContent headerContent = lc.getHeader(); - boolean hasHeader = - headerContent != null && !headerContent.getSliceItem().hasHint(HINT_LIST_ITEM); - - List subItems = lc.getRowItems().stream().filter(sliceContent -> { - String itemUri = sliceContent.getSliceItem().getSlice().getUri().toString(); - // Filter out the action row - return !KeyguardSliceProvider.KEYGUARD_ACTION_URI.equals(itemUri); - }).collect(Collectors.toList()); - - - mClickActions = mView.showSlice(hasHeader ? headerContent : null, subItems); - - Trace.endSection(); - } - - @Override - public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { - pw.println(" mSlice: " + mSlice); - pw.println(" mClickActions: " + mClickActions); - } -} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 0fa8a359561d8..d5cf8304c185e 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -132,6 +132,7 @@ import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.broadcast.BroadcastDispatcher; +import com.android.systemui.quicklook.QuickLookClient; import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -155,6 +156,7 @@ import com.android.systemui.keyguard.shared.constants.TrustAgentUiEvent; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.keyguard.data.model.WeatherData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.SceneInteractor; @@ -330,6 +332,7 @@ public void onExpandedChanged(boolean isExpanded) { }; private final FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig; private final Provider mDozeParameters; + private final QuickLookClient mQuickLookClient; private final Object mSimDataLockObject = new Object(); HashMap mSimDatasBySlotId = new HashMap<>(); @@ -476,6 +479,44 @@ public boolean isPocketLockVisible(){ return mPocketManager.isPocketLockVisible(); } + private final QuickLookClient.Callback mQuickLookCallback = new QuickLookClient.Callback() { + @Override + public void onClockDataChanged(ClockData data) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onClockDataChanged(data); + } + } + } + @Override + public void onQLPlaybackStateChanged(boolean play) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onQLPlaybackStateChanged(play); + } + } + } + @Override + public void onQLMetadataChanged(String track, String artist, String packageName) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onQLMetadataChanged(track, artist, packageName); + } + } + } + @Override + public void onNowPlayingUpdate(String nowPlayingText, android.app.PendingIntent tapAction) { + for (int i = 0; i < mCallbacks.size(); i++) { + KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get(); + if (cb != null) { + cb.onNowPlayingUpdate(nowPlayingText, tapAction); + } + } + } + }; private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback = new IBiometricEnabledOnKeyguardCallback.Stub() { @Override @@ -861,6 +902,12 @@ public void setKeyguardShowing(boolean showing, boolean occluded) { if (occlusionChanged || showingChanged) { updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } + if (!showingChanged) return; + if (mKeyguardShowing) { + mQuickLookClient.addCallback(mQuickLookCallback); + } else { + mQuickLookClient.removeCallback(mQuickLookCallback); + } } /** @@ -945,7 +992,6 @@ private boolean isSecureLockDeviceStrongBiometricAuthFlagSet(int userId) { STRONG_BIOMETRIC_AUTH_REQUIRED_FOR_SECURE_LOCK_DEVICE); } - @VisibleForTesting public void onFingerprintAuthenticated(int userId, boolean isStrongBiometric) { try { @@ -1523,7 +1569,6 @@ public boolean getUserUnlockedWithBiometric(int userId) { return fingerprintAllowed || unlockedByFace; } - /** * Returns whether the user is unlocked with face. * @deprecated Use {@link DeviceEntryFaceAuthInteractor#isAuthenticated()} instead @@ -2304,7 +2349,8 @@ protected KeyguardUpdateMonitor( Provider communalSceneInteractor, Provider keyguardServiceShowLockscreenInteractor, - Provider dozeParameters) { + Provider dozeParameters, + QuickLookClient quickLookClient) { mContext = context; mSubscriptionManager = subscriptionManager; mUserTracker = userTracker; @@ -2358,6 +2404,7 @@ protected KeyguardUpdateMonitor( mSceneInteractor = sceneInteractor; mCommunalSceneInteractor = communalSceneInteractor; mKeyguardServiceShowLockscreenInteractor = keyguardServiceShowLockscreenInteractor; + mQuickLookClient = quickLookClient; mPocketManager = (PocketManager) context.getSystemService(Context.POCKET_SERVICE); if (mPocketManager != null) { @@ -2706,7 +2753,6 @@ void onAlternateBouncerVisibilityChange(boolean isAlternateBouncerVisible) { setAlternateBouncerShowing(isAlternateBouncerVisible); } - @VisibleForTesting void onTransitionStateChanged(ObservableTransitionState transitionState) { int primaryBouncerFullyShown = isPrimaryBouncerFullyShown(transitionState) ? 1 : 0; @@ -2965,7 +3011,6 @@ private void requestActiveUnlock( } } - /** * Attempts to trigger active unlock from trust agent. * Only dismisses the keyguard under certain conditions. @@ -3249,7 +3294,6 @@ public boolean shouldListenForFace() { return getFaceAuthInteractor() != null && getFaceAuthInteractor().canFaceAuthRun(); } - private void logListenerModelData(@NonNull KeyguardListenModel model) { mLogger.logKeyguardListenerModel(model); if (model instanceof KeyguardFingerprintListenModel) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java index a0a7f153ec613..e337235de8a66 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java @@ -25,6 +25,7 @@ import com.android.internal.annotations.WeaklyReferencedCallback; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.plugins.keyguard.data.model.WeatherData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; import com.android.systemui.statusbar.KeyguardIndicationController; import java.util.TimeZone; @@ -342,4 +343,11 @@ public void onBiometricEnrollmentStateChanged(BiometricSourceType biometricSourc * On force is dismissible state changed. */ public void onForceIsDismissibleChanged(boolean forceIsDismissible) { } + public void onClockDataChanged(ClockData data) { } + public void onQLPlaybackStateChanged(boolean play) { } + public void onQLMetadataChanged(String track, String artist, String packageName) { } + public void onNowPlayingUpdate(String nowPlayingText) { } + public void onNowPlayingUpdate(String nowPlayingText, android.app.PendingIntent tapAction) { + onNowPlayingUpdate(nowPlayingText); + } } diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java index b9e0aa98e8b60..64c7084505f00 100644 --- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java +++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java @@ -18,11 +18,8 @@ import android.content.Context; import android.content.res.Resources; -import android.os.Vibrator; import android.view.LayoutInflater; -import androidx.annotation.Nullable; - import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.dagger.qualifiers.Background; @@ -34,7 +31,7 @@ import com.android.systemui.res.R; import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.shared.clocks.ClockRegistry; -import com.android.systemui.shared.clocks.DefaultClockProvider; +import com.android.systemui.shared.clocks.AxClockProvider; import com.android.systemui.util.ThreadAssert; import dagger.Module; @@ -58,8 +55,7 @@ public static ClockRegistry getClockRegistry( FeatureFlags featureFlags, @Main Resources resources, LayoutInflater layoutInflater, - ClockMessageBuffers clockBuffers, - @Nullable Vibrator vibrator) { + ClockMessageBuffers clockBuffers) { ClockRegistry registry = new ClockRegistry( context, pluginManager, @@ -69,12 +65,7 @@ public static ClockRegistry getClockRegistry( com.android.systemui.shared.Flags.lockscreenCustomClocks() || featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS), /* handleAllUsers= */ true, - new DefaultClockProvider( - layoutInflater, - resources, - com.android.systemui.shared.Flags.clockReactiveVariants(), - vibrator - ), + new AxClockProvider(layoutInflater, resources), context.getString(R.string.lockscreen_clock_id_fallback), clockBuffers, /* keepAllLoaded = */ false, diff --git a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java index 5bf2f4b7e689c..c35e8cc8ea57b 100644 --- a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java @@ -101,6 +101,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; +import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; import com.android.systemui.model.SysUiState.SysUiStateCallback; @@ -190,6 +191,7 @@ public class LauncherProxyService implements CallbackController mUnfoldTransitionProgressForwarder; private final UiEventLogger mUiEventLogger; @@ -529,8 +531,11 @@ public void toggleQuickSettingsPanel() { public void onSleepEvent(MotionEvent event) { verifyCallerAndClearCallingIdentity("onSleepEvent", () -> { mHandler.post(() -> { + mPowerInteractor.setLastTouchToSleepPosition( + event.getX(), event.getY()); mContext.getSystemService(PowerManager.class) - .goToSleep(event.getEventTime()); + .goToSleep(event.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); event.recycle(); }); }); @@ -804,6 +809,7 @@ public LauncherProxyService(Context context, UserTracker userTracker, UserManager userManager, WakefulnessLifecycle wakefulnessLifecycle, + PowerInteractor powerInteractor, UiEventLogger uiEventLogger, DisplayTracker displayTracker, KeyguardUnlockAnimationController sysuiUnlockAnimationController, @@ -846,6 +852,7 @@ public LauncherProxyService(Context context, mShadeModeInteractor = shadeModeInteractor; mShadeDisplayPolicy = shadeDisplayPolicy; mUserTracker = userTracker; + mPowerInteractor = powerInteractor; mConnectionBackoffAttempts = 0; int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt new file mode 100644 index 0000000000000..8659ca10531ee --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherContentManager.kt @@ -0,0 +1,180 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.content.Context +import android.graphics.drawable.Drawable +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import com.android.internal.util.alpha.OmniJawsClient +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.res.R +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "WeatherContentManager" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +@SysUISingleton +class WeatherContentManager +@Inject +constructor( + @Background private val bgLooper: Looper, +) { + private lateinit var contentView: View + private lateinit var context: Context + private lateinit var coroutineScope: CoroutineScope + + private lateinit var currentIcon: ImageView + private lateinit var currentTemp: TextView + private lateinit var cityName: TextView + private lateinit var windInfo: TextView + private lateinit var windDirection: TextView + private lateinit var humidityInfo: TextView + private lateinit var forecastContainer: LinearLayout + private lateinit var noDataView: TextView + + private val mainHandler = Handler(Looper.getMainLooper()) + private val bgHandler = Handler(bgLooper) + + private val omniJawsObserver = object : OmniJawsClient.OmniJawsObserver { + override fun weatherUpdated() { + refreshWeatherAsync() + } + + override fun weatherError(errorReason: Int) { + if (DEBUG) Log.d(TAG, "weatherError: $errorReason") + mainHandler.post { showNoData() } + } + } + + fun bind(view: View, scope: CoroutineScope) { + if (DEBUG) Log.d(TAG, "bind") + contentView = view + context = view.context + coroutineScope = scope + + currentIcon = view.findViewById(R.id.weather_current_icon) + currentTemp = view.findViewById(R.id.weather_current_temp) + cityName = view.findViewById(R.id.weather_city_name) + windInfo = view.findViewById(R.id.weather_wind_info) + windDirection = view.findViewById(R.id.weather_wind_direction) + humidityInfo = view.findViewById(R.id.weather_humidity_info) + forecastContainer = view.findViewById(R.id.weather_forecast_container) + noDataView = view.findViewById(R.id.weather_no_data) + } + + fun start() { + if (DEBUG) Log.d(TAG, "start") + OmniJawsClient.get().addObserver(context, omniJawsObserver) + refreshWeatherAsync() + } + + fun stop() { + if (DEBUG) Log.d(TAG, "stop") + OmniJawsClient.get().removeObserver(context, omniJawsObserver) + } + + private fun refreshWeatherAsync() { + bgHandler.post { + OmniJawsClient.get().queryWeather(context) + val info = OmniJawsClient.get().weatherInfo + mainHandler.post { + if (info != null) { + populateWeather(info) + } else { + showNoData() + } + } + } + } + + private fun populateWeather(info: OmniJawsClient.WeatherInfo) { + noDataView.visibility = View.GONE + currentIcon.visibility = View.VISIBLE + currentTemp.visibility = View.VISIBLE + cityName.visibility = View.VISIBLE + windInfo.visibility = View.VISIBLE + windDirection.visibility = View.VISIBLE + humidityInfo.visibility = View.VISIBLE + forecastContainer.visibility = View.VISIBLE + + val condIcon: Drawable? = OmniJawsClient.get().getWeatherConditionImage(context, info.conditionCode) + if (condIcon != null) { + currentIcon.setImageDrawable(condIcon) + } + + currentTemp.text = "${info.temp}${info.tempUnits}" + cityName.text = info.city ?: "" + + windInfo.text = "${info.windSpeed} ${info.windUnits}" + windDirection.text = info.windDirection ?: "" + + humidityInfo.text = "${info.humidity}%" + + populateForecasts(info.forecasts ?: emptyList()) + } + + private fun populateForecasts(forecasts: List) { + forecastContainer.removeAllViews() + + val inflater = android.view.LayoutInflater.from(context) + val displayForecasts = forecasts.take(5) + + for (day in displayForecasts) { + val dayView = inflater.inflate(R.layout.weather_forecast_day_item, forecastContainer, false) + + val dayLabel = dayView.findViewById(R.id.forecast_day_label) + val dayIcon = dayView.findViewById(R.id.forecast_day_icon) + val dayHigh = dayView.findViewById(R.id.forecast_day_high) + val dayLow = dayView.findViewById(R.id.forecast_day_low) + + dayLabel.text = formatForecastDate(day.date) + + val icon: Drawable? = OmniJawsClient.get().getWeatherConditionImage(context, day.conditionCode) + if (icon != null) { + dayIcon.setImageDrawable(icon) + } + + dayHigh.text = day.high + dayLow.text = day.low + + forecastContainer.addView(dayView) + } + } + + private fun formatForecastDate(dateStr: String?): String { + if (dateStr.isNullOrEmpty()) return "" + return try { + val inputFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US) + val date: Date = inputFormat.parse(dateStr) ?: return dateStr + SimpleDateFormat("EEE", Locale.getDefault()).format(date) + } catch (e: Exception) { + dateStr + } + } + + private fun showNoData() { + noDataView.visibility = View.VISIBLE + currentIcon.visibility = View.GONE + currentTemp.visibility = View.GONE + cityName.visibility = View.GONE + windInfo.visibility = View.GONE + windDirection.visibility = View.GONE + humidityInfo.visibility = View.GONE + forecastContainer.visibility = View.GONE + } +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt new file mode 100644 index 0000000000000..9433e54dd14df --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogController.kt @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import com.android.systemui.animation.DialogTransitionAnimator + +/** Wrapper controller that skips exit animation checks for the weather dialog. */ +class WeatherDialogController( + private val delegate: DialogTransitionAnimator.Controller +) : DialogTransitionAnimator.Controller by delegate { + + override val skipExitAnimationChecks: Boolean = true +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt new file mode 100644 index 0000000000000..9b51404203d48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogDelegate.kt @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.Window +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeDialogContextInteractor +import com.android.systemui.statusbar.phone.SystemUIDialog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope + +private const val TAG = "WeatherDialogDelegate" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +/** Delegate for the weather forecast QS tile dialog. */ +class WeatherDialogDelegate +@AssistedInject +constructor( + private val weatherContentManager: WeatherContentManager, + private val weatherDialogManager: WeatherDialogManager, + private val systemUIDialogFactory: SystemUIDialog.Factory, + private val shadeDialogContextInteractor: ShadeDialogContextInteractor, + @Assisted private val coroutineScope: CoroutineScope, +) : SystemUIDialog.Delegate { + + private lateinit var dialog: SystemUIDialog + private lateinit var lifecycleRegistry: LifecycleRegistry + private lateinit var lifecycleOwner: LifecycleOwner + + @AssistedFactory + interface Factory { + fun create(coroutineScope: CoroutineScope): WeatherDialogDelegate + } + + override fun createDialog(): SystemUIDialog { + if (DEBUG) Log.d(TAG, "createDialog") + + dialog = systemUIDialogFactory.create(this, shadeDialogContextInteractor.context) + + lifecycleOwner = object : LifecycleOwner { + override val lifecycle: Lifecycle + get() = lifecycleRegistry + } + lifecycleRegistry = LifecycleRegistry(lifecycleOwner) + + return dialog + } + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + if (DEBUG) Log.d(TAG, "onCreate") + + val context = dialog.context + val dialogView = LayoutInflater.from(context).inflate( + R.layout.weather_tile_dialog, null + ) + dialogView.accessibilityPaneTitle = context.getText(R.string.omnijaws_label_default) + + val window: Window = dialog.window!! + window.setContentView(dialogView) + window.setWindowAnimations(R.style.Animation_InternetDialog) + + val params = window.attributes + // ~65% of screen width — compact forecast card + params.width = (context.resources.displayMetrics.widthPixels * 0.65).toInt() + window.attributes = params + + lifecycleRegistry.currentState = Lifecycle.State.CREATED + weatherContentManager.bind(dialogView, coroutineScope) + } + + override fun onStart(dialog: SystemUIDialog) { + if (DEBUG) Log.d(TAG, "onStart") + lifecycleRegistry.currentState = Lifecycle.State.RESUMED + weatherContentManager.start() + } + + override fun onStop(dialog: SystemUIDialog) { + if (DEBUG) Log.d(TAG, "onStop") + lifecycleRegistry.currentState = Lifecycle.State.DESTROYED + weatherContentManager.stop() + weatherDialogManager.destroyDialog() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt new file mode 100644 index 0000000000000..b799ff71c2735 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/alpha/tiles/dialog/WeatherDialogManager.kt @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 AlphaDroid + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.alpha.tiles.dialog + +import android.util.Log +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.animation.Expandable +import com.android.systemui.coroutines.newTracingContext +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.phone.SystemUIDialog +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel + +private const val TAG = "WeatherDialogManager" +private val DEBUG = Log.isLoggable(TAG, Log.DEBUG) + +@SysUISingleton +class WeatherDialogManager +@Inject +constructor( + private val dialogTransitionAnimator: DialogTransitionAnimator, + private val dialogFactory: WeatherDialogDelegate.Factory, + @Main private val mainDispatcher: CoroutineDispatcher, +) { + private lateinit var coroutineScope: CoroutineScope + + companion object { + private const val INTERACTION_JANK_TAG = "weather" + var dialog: SystemUIDialog? = null + } + + fun create(expandable: Expandable?) { + if (dialog != null) { + if (DEBUG) Log.d(TAG, "WeatherDialog is already showing.") + return + } + + if (DEBUG) Log.d(TAG, "Creating weather dialog") + + coroutineScope = CoroutineScope(mainDispatcher + newTracingContext("WeatherDialogScope")) + dialog = dialogFactory.create(coroutineScope).createDialog() + + val standardController = expandable?.dialogTransitionController( + DialogCuj(InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, INTERACTION_JANK_TAG) + ) + + val controller = if (standardController != null) { + WeatherDialogController(standardController) + } else { + null + } + + if (controller != null) { + dialogTransitionAnimator.show( + dialog!!, + controller, + animateBackgroundBoundsChange = true, + ) + } else { + dialog?.show() + } + } + + fun destroyDialog() { + if (DEBUG) Log.d(TAG, "destroyDialog") + if (::coroutineScope.isInitialized) { + coroutineScope.cancel() + } + dialog = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt index 5d9bc6db0bdb5..64c55c805c222 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/AxDynamicBarKeyguardChip.kt @@ -800,7 +800,12 @@ private fun KeyguardPrimaryText(event: IslandEvent, color: Color, modifier: Modi event.shortText.ifEmpty { event.title.ifEmpty { event.appName } }, color, modifier, ) is IslandEvent.Sports -> MarqueeText( - "${event.score1}-${event.score2}", color, modifier, + when { + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + event.team1Name.isNotEmpty() -> event.team1Name + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) + }, + color, modifier, ) is IslandEvent.NowPlaying -> MarqueeText( "${event.songTitle} · ${event.artist}".trimEnd(' ', '·', ' '), color, modifier, @@ -834,7 +839,12 @@ private fun secondaryTextFor(event: IslandEvent): String? = when (event) { is IslandEvent.AppSwitch -> null is IslandEvent.Notification -> event.text?.take(30) is IslandEvent.PromotedOngoing -> event.text.takeIf { it.isNotBlank() }?.take(20) - is IslandEvent.Sports -> "${event.team1Name} ${stringResource(R.string.ax_dynamic_bar_sports_vs)} ${event.team2Name}" + is IslandEvent.Sports -> when { + event.team1Name.isNotEmpty() || event.team2Name.isNotEmpty() -> + "${event.team1Name} ${stringResource(R.string.ax_dynamic_bar_sports_vs)} ${event.team2Name}" + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) + } is IslandEvent.KeyguardIndication -> when (event.indicationType) { IslandEvent.KeyguardIndication.IndicationType.BIOMETRIC -> stringResource(R.string.ax_dynamic_bar_biometric) IslandEvent.KeyguardIndication.IndicationType.TRUST -> stringResource(R.string.ax_dynamic_bar_trust) diff --git a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt index f460a77f4fddc..68b9c82b44847 100644 --- a/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt +++ b/packages/SystemUI/src/com/android/systemui/axdynamicbar/ui/compose/PillIslandContent.kt @@ -25,9 +25,11 @@ import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape @@ -972,26 +974,50 @@ private fun SportsPillIcon(event: IslandEvent.Sports) { @Composable private fun SportsText(event: IslandEvent.Sports, modifier: Modifier, overrideColor: Color? = null) { val color = overrideColor ?: accentColorFor(event) - val scoreText = when { - event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" - event.team2Name.isEmpty() -> event.team1Name - else -> "vs" - } Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Text(scoreText, color = color, style = PillAccent, maxLines = 1) - event.team2Icon?.let { icon -> - Box(modifier = Modifier.padding(start = 4.dp)) { - Image( - bitmap = icon.toScaledBitmap(14.dp), - contentDescription = null, - modifier = Modifier.size(14.dp).clip(CircleShape), - contentScale = ContentScale.Crop, - ) + if (event.team2Name.isNotEmpty()) { + SportsTeamLabel(event.team1Name, event.team1Icon, color) + Spacer(Modifier.width(SpaceXs)) + Text( + if (event.score1.isNotEmpty()) "${event.score1} - ${event.score2}" + else stringResource(R.string.ax_dynamic_bar_sports_vs), + color = color, + style = PillAccent, + maxLines = 1, + ) + Spacer(Modifier.width(SpaceXs)) + SportsTeamLabel(event.team2Name, event.team2Icon, color) + } else { + val fallback = when { + event.team1Name.isNotEmpty() -> event.team1Name + event.score1.isNotEmpty() -> "${event.score1}-${event.score2}" + else -> stringResource(R.string.ax_dynamic_bar_sports_live_event) } + Text(fallback, color = color, style = PillAccent, maxLines = 1) } } } +@Composable +private fun SportsTeamLabel(name: String, icon: Drawable?, color: Color) { + val badgeSize = 14.dp + if (icon != null) { + Image( + bitmap = icon.toScaledBitmap(badgeSize), + contentDescription = name, + modifier = Modifier.size(badgeSize).clip(CircleShape), + contentScale = ContentScale.Crop, + ) + } else { + Text( + name.take(3).uppercase(), + color = color, + style = TsBadge, + maxLines = 1, + ) + } +} + @Composable private fun AppSwitchPillIcon(event: IslandEvent.AppSwitch) { val app = event.previousApp ?: event.recentApps.firstOrNull() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index c6ca2d0a6d952..07db3b0ee3596 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -53,6 +53,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM +import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.KeyguardStateController import dagger.Lazy import javax.inject.Inject @@ -125,6 +126,10 @@ const val LAUNCHER_ICONS_ANIMATION_DURATION_MS = 633L */ const val CANNED_UNLOCK_START_DELAY_MS = 25L +const val LOCKSCREEN_WALLPAPER_ZOOM = 1f +const val SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS = 550L +const val SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS = 0L + /** * Duration for the alpha animation on the surface behind. This plays to fade in the surface during * a swipe to unlock (and to fade it back out if the swipe is cancelled). @@ -164,7 +169,11 @@ constructor( private val notificationShadeWindowController: NotificationShadeWindowController, private val powerManager: PowerManager, private val wallpaperManager: WallpaperManager, -) : KeyguardStateController.Callback, ISysuiUnlockAnimationController.Stub() { + private val wakefulnessLifecycle: WakefulnessLifecycle, +) : + KeyguardStateController.Callback, + WakefulnessLifecycle.Observer, + ISysuiUnlockAnimationController.Stub() { interface KeyguardUnlockAnimationListener { /** @@ -304,6 +313,12 @@ constructor( */ @VisibleForTesting val surfaceBehindEntryAnimator = ValueAnimator.ofFloat(0f, 1f) + private var wallpaperZoomAnimator: ValueAnimator? = null + private var hasPendingScreenOnWallpaperZoom = false + private val setScreenOnWallpaperZoomRunnable = Runnable { + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } + /** Rounded corner radius to apply to the surface behind the keyguard. */ private var roundedCornerRadius = 0f @@ -437,6 +452,7 @@ constructor( // Listen for changes in the dismiss amount. keyguardStateController.addCallback(this) + wakefulnessLifecycle.addObserver(this) roundedCornerRadius = resources.getDimensionPixelSize(R.dimen.rounded_corner_radius).toFloat() @@ -451,6 +467,56 @@ constructor( listeners.remove(listener) } + private fun isDozeWakeUp(): Boolean = + statusBarStateController.isDozing || statusBarStateController.dozeAmount > 0f + + private fun getScreenOnWallpaperZoomStartDelay(): Long = + if (isDozeWakeUp()) { + CentralSurfaces.FADE_KEYGUARD_DURATION_PULSING.toLong() + } else { + SCREEN_ON_WALLPAPER_ZOOM_START_DELAY_MS + } + + private fun getLauncherUnlockRevealStartDelay(): Long { + val biometricUnlockController = biometricUnlockControllerLazy.get() + return if (biometricUnlockController.isWakeAndUnlock || isDozeWakeUp()) { + CentralSurfaces.FADE_KEYGUARD_DURATION_PULSING.toLong() + } else if (keyguardStateController.isKeyguardFadingAway) { + keyguardStateController.keyguardFadingAwayDelay + + keyguardStateController.keyguardFadingAwayDuration + } else { + (CentralSurfaces.FADE_KEYGUARD_START_DELAY + + CentralSurfaces.FADE_KEYGUARD_DURATION).toLong() + } + } + + override fun onStartedWakingUp() { + cancelWallpaperZoomAnimator() + hasPendingScreenOnWallpaperZoom = + keyguardStateController.isShowing || statusBarStateController.isDozing + if (hasPendingScreenOnWallpaperZoom) { + keyguardViewController.viewRootImpl.view?.postOnAnimation( + setScreenOnWallpaperZoomRunnable + ) ?: setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } else { + setKeyguardWallpaperZoom(0f) + } + } + + override fun onFinishedWakingUp() { + if (!hasPendingScreenOnWallpaperZoom) { + return + } + hasPendingScreenOnWallpaperZoom = false + animateKeyguardWallpaperZoom() + } + + override fun onStartedGoingToSleep() { + hasPendingScreenOnWallpaperZoom = false + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) + } + /** * Whether we should be able to do the in-window launcher animations given the current state of * the device. @@ -670,12 +736,18 @@ constructor( val isWakeAndUnlockNotFromDream = biometricUnlockControllerLazy.get().isWakeAndUnlock && biometricUnlockControllerLazy.get().mode != MODE_WAKE_AND_UNLOCK_FROM_DREAM + val unlockAnimationStartDelay = + if (willUnlockWithInWindowLauncherAnimations) { + getLauncherUnlockRevealStartDelay() + } else { + CANNED_UNLOCK_START_DELAY_MS + } listeners.forEach { it.onUnlockAnimationStarted( playingCannedUnlockAnimation /* playingCannedAnimation */, isWakeAndUnlockNotFromDream /* isWakeAndUnlockNotFromDream */, - CANNED_UNLOCK_START_DELAY_MS /* unlockStartDelay */, + unlockAnimationStartDelay /* unlockStartDelay */, LAUNCHER_ICONS_ANIMATION_DURATION_MS, /* unlockAnimationDuration */ ) } @@ -696,6 +768,9 @@ constructor( private fun playCannedUnlockAnimation() { Log.d(TAG, "playCannedUnlockAnimation") playingCannedUnlockAnimation = true + hasPendingScreenOnWallpaperZoom = false + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) when { // If we're set up for in-window launcher animations, ask Launcher to play its in-window @@ -749,7 +824,7 @@ constructor( launcherUnlockController?.playUnlockAnimation( true /* unlocked */, LAUNCHER_ICONS_ANIMATION_DURATION_MS /* duration */, - CANNED_UNLOCK_START_DELAY_MS, /* startDelay */ + getLauncherUnlockRevealStartDelay(), /* startDelay */ ) } catch (e: DeadObjectException) { // Hello! If you are here investigating a bug where Launcher is blank (no icons) @@ -1103,6 +1178,17 @@ constructor( wallpaperCannedUnlockAnimator.cancel() wallpaperFadeOutUnlockAnimator.cancel() + if (!showKeyguard) { + cancelWallpaperZoomAnimator() + setKeyguardWallpaperZoom(0f) + } else if (wallpaperZoomAnimator == null && hasPendingScreenOnWallpaperZoom) { + hasPendingScreenOnWallpaperZoom = false + animateKeyguardWallpaperZoom() + } else if (wallpaperZoomAnimator == null) { + setKeyguardWallpaperZoom(0f) + } + hasPendingScreenOnWallpaperZoom = false + // That target is no longer valid since the animation finished, null it out. surfaceBehindRemoteAnimationTargets = null openingWallpaperTargets = null @@ -1114,6 +1200,51 @@ constructor( willUnlockWithSmartspaceTransition = false } + private fun animateKeyguardWallpaperZoom() { + cancelWallpaperZoomAnimator() + wallpaperZoomAnimator = + ValueAnimator.ofFloat(LOCKSCREEN_WALLPAPER_ZOOM, 0f).apply { + duration = SCREEN_ON_WALLPAPER_ZOOM_DURATION_MS + startDelay = getScreenOnWallpaperZoomStartDelay() + interpolator = Interpolators.FAST_OUT_SLOW_IN + addUpdateListener { valueAnimator -> + setKeyguardWallpaperZoom(valueAnimator.animatedValue as Float) + } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator) { + setKeyguardWallpaperZoom(LOCKSCREEN_WALLPAPER_ZOOM) + } + + override fun onAnimationEnd(animation: Animator) { + setKeyguardWallpaperZoom(0f) + if (wallpaperZoomAnimator === animation) { + wallpaperZoomAnimator = null + } + } + } + ) + start() + } + } + + private fun cancelWallpaperZoomAnimator() { + wallpaperZoomAnimator?.cancel() + wallpaperZoomAnimator = null + keyguardViewController.viewRootImpl.view?.removeCallbacks(setScreenOnWallpaperZoomRunnable) + } + + private fun setKeyguardWallpaperZoom(zoom: Float) { + val boundedZoom = zoom.coerceIn(0f, 1f) + val rootView = keyguardViewController.viewRootImpl.view ?: return + val windowToken = rootView.windowToken ?: return + try { + wallpaperManager.setWallpaperZoomOut(windowToken, boundedZoom) + } catch (e: IllegalArgumentException) { + Log.w(TAG, "Unable to set keyguard wallpaper zoom", e) + } + } + /** * Asks the keyguard view to hide, using the start time from the beginning of the remote * animation. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt index d1d7da50a69df..a03b705d9d6e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt @@ -76,6 +76,7 @@ interface KeyguardClockRepository { val forcedClockSize: Flow + val areLockscreenWidgetsEnabled: Boolean fun setClockSize(size: ClockSize) } @@ -162,6 +163,21 @@ constructor( initialValue = null, ) + override val areLockscreenWidgetsEnabled: Boolean + get() { + val isEnabled = Settings.System.getIntForUser( + context.contentResolver, + "lockscreen_widgets_enabled", + 0, + UserHandle.USER_CURRENT + ) == 1 + val widgetConfig = Settings.System.getStringForUser( + context.contentResolver, + "lockscreen_widgets_config", + UserHandle.USER_CURRENT + ) ?: "" + return isEnabled && widgetConfig.isNotEmpty() + } private fun getClockSize(): ClockSizeSetting { val isDoubleLineClock = secureSettings.getIntForUser( Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 4f45d44e9c0c3..c96084d5a05e4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -189,6 +189,10 @@ interface KeyguardRepository { val lastDozeTapToWakePosition: StateFlow + val lastTouchToSleepPosition: StateFlow + + fun setLastTouchToSleepPosition(position: Point) + /** Last point that [KeyguardRootView] was tapped */ val lastRootViewTapPosition: MutableStateFlow @@ -284,6 +288,8 @@ interface KeyguardRepository { fun setLastDozeTapToWakePosition(position: Point) + fun clearLastTouchToSleepPosition() + fun setIsDozing(isDozing: Boolean) fun dozeTimeTick() @@ -437,6 +443,17 @@ constructor( _lastDozeTapToWakePosition.value = position } + private val _lastTouchToSleepPosition = MutableStateFlow(null) + override val lastTouchToSleepPosition = _lastTouchToSleepPosition.asStateFlow() + + override fun setLastTouchToSleepPosition(position: Point) { + _lastTouchToSleepPosition.value = position + } + + override fun clearLastTouchToSleepPosition() { + _lastTouchToSleepPosition.value = null + } + override val lastRootViewTapPosition: MutableStateFlow = MutableStateFlow(null) override val ambientIndicationVisible: MutableStateFlow = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt index e193fd13c58d6..aaea20019fe73 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardSmartspaceRepository.kt @@ -16,23 +16,16 @@ package com.android.systemui.keyguard.data.repository -import android.os.UserHandle -import android.provider.Settings import android.view.View import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.settings.UserTracker import com.android.systemui.util.settings.SecureSettings -import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn interface KeyguardSmartspaceRepository { val bcSmartspaceVisibility: StateFlow @@ -52,28 +45,16 @@ constructor( private val _bcSmartspaceVisibility: MutableStateFlow = MutableStateFlow(View.GONE) override val bcSmartspaceVisibility: StateFlow = _bcSmartspaceVisibility.asStateFlow() override val isWeatherEnabled: StateFlow = - secureSettings - .observerFlow( - names = arrayOf(Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED), - userId = UserHandle.USER_ALL, - ) - .onStart { emit(Unit) } - .map { getLockscreenWeatherEnabled() } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = getLockscreenWeatherEnabled() - ) + MutableStateFlow(true) override fun setBcSmartspaceVisibility(visibility: Int) { _bcSmartspaceVisibility.value = visibility } - private fun getLockscreenWeatherEnabled(): Boolean { - return secureSettings.getIntForUser( - Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED, - 1, - userTracker.userId - ) == 1 - } + /** + * Weather data is always considered enabled because AxQuickLook handles weather + * rendering inside the active Axion clock face. The BC smartspace pipeline is + * permanently disabled — see LockscreenSmartspaceController.isEnabled. + */ + private fun getLockscreenWeatherEnabled(): Boolean = true } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt index ad7c25e67b459..bd930f90eba31 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/LightRevealScrimRepository.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.power.data.repository.PowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakeSleepReason.TAP +import com.android.systemui.power.shared.model.WakeSleepReason.TOUCH_TO_SLEEP import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.CircleReveal @@ -46,7 +47,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -134,17 +137,34 @@ constructor( /** The reveal effect we'll use for the next non-biometric unlock (tap, power button, etc). */ private val nonBiometricRevealEffect: Flow = - powerRepository.wakefulness.flatMapLatest { wakefulnessModel -> + combine( + powerRepository.wakefulness, + keyguardRepository.lastTouchToSleepPosition, + ) { wakefulnessModel, touchSleepPos -> + Pair(wakefulnessModel, touchSleepPos) + }.flatMapLatest { (wakefulnessModel, touchSleepPos) -> when { wakefulnessModel.isAwakeOrAsleepFrom(WakeSleepReason.POWER_BUTTON) -> powerButtonRevealEffect wakefulnessModel.isAwakeFrom(TAP) -> tapRevealEffect + wakefulnessModel.isAwakeOrAsleepFrom(TOUCH_TO_SLEEP) && touchSleepPos != null -> + flowOf(constructCircleRevealFromPoint(touchSleepPos)) else -> flowOf(LiftReveal) } } private val revealAmountAnimator = ValueAnimator.ofFloat(0f, 1f) + init { + backgroundScope.launch { + powerRepository.wakefulness.collect { model -> + if (model.isAwake()) { + keyguardRepository.clearLastTouchToSleepPosition() + } + } + } + } + override val wallpaperSupportsAmbientMode: MutableStateFlow = MutableStateFlow(false) override val useDarkWallpaperScrim: StateFlow = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index babf7de0f6583..c90d0ba744ab7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -92,6 +92,8 @@ constructor( var clock: ClockController? by keyguardClockRepository.clockEventController::clock + val areLockscreenWidgetsEnabled: Boolean + get() = keyguardClockRepository.areLockscreenWidgetsEnabled val isAodPromotedNotificationPresent: Flow = if (PromotedNotificationUi.isEnabled) { aodPromotedNotificationInteractor.isPresent @@ -135,7 +137,9 @@ constructor( val clockSize: StateFlow = selectedClockSize .flatMapLatestConflated { selectedSize -> - if (selectedSize == ClockSizeSetting.SMALL) { + if (selectedSize == ClockSizeSetting.SMALL || + keyguardClockRepository.areLockscreenWidgetsEnabled + ) { flowOf(ClockSize.SMALL) } else { dynamicClockSize @@ -220,8 +224,6 @@ constructor( if (wallpaperFocalAreaInteractor.hasFocalArea.value) { wallpaperFocalAreaInteractor.sendTapPosition(x, y) - } else { - clockEventController.handleFidgetTap(x, y) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 7f11f5332d99e..a9a7b5a458eec 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -111,15 +111,20 @@ object KeyguardClockViewBinder { } launch { - viewModel.clockShouldBeCentered.collect { - viewModel.currentClock.value?.let { - if (it.largeClock.config.hasCustomPositionUpdatedAnimation) { - blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) - } else { - blueprintInteractor.refreshBlueprint(Type.DefaultTransition) + combine( + viewModel.clockShouldBeCentered, + viewModel.isLargeClockVisible + ) { centered, large -> centered to large } + .collect { (isCentered, isLargeVisible) -> + viewModel.currentClock.value?.let { clock -> + clock.events.onClockLayoutChanged(isCentered, isLargeVisible) + if (clock.largeClock.config.hasCustomPositionUpdatedAnimation) { + blueprintInteractor.refreshBlueprint(Type.DefaultClockStepping) + } else { + blueprintInteractor.refreshBlueprint(Type.DefaultTransition) + } } } - } } launch { @@ -275,7 +280,7 @@ object KeyguardClockViewBinder { clockSection.applyConstraints(constraintSet) if (animated) { set?.let { TransitionManager.beginDelayedTransition(rootView, it) } - ?: run { TransitionManager.beginDelayedTransition(rootView, defaultTransition) } + ?: run { TransitionManager.beginDelayedTransition(rootView) } } constraintSet.applyTo(rootView) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index 50c4ec3b8a49d..c9bbad173f580 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -209,8 +209,7 @@ object KeyguardRootViewBinder { viewModel.alpha(viewState).collect { alpha -> view.alpha = alpha childViews[burnInLayerId]?.alpha = alpha - childViews[sliceViewId]?.alpha = alpha - childViews[weatherAreaId]?.alpha = alpha + childViews[widgetArea]?.alpha = alpha } } @@ -239,8 +238,7 @@ object KeyguardRootViewBinder { // need to add translation to it here same as translationX viewModel.translationY.collect { y -> childViews[burnInLayerId]?.translationY = y - childViews[sliceViewId]?.translationY = y - childViews[weatherAreaId]?.translationY = y + childViews[widgetArea]?.translationY = y childViews[largeClockId]?.translationY = y if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { childViews[largeClockDateId]?.translationY = y @@ -257,8 +255,7 @@ object KeyguardRootViewBinder { state.isToOrFrom(KeyguardState.AOD) -> { // Large Clock is not translated in the x direction childViews[burnInLayerId]?.translationX = px - childViews[sliceViewId]?.translationX = px - childViews[weatherAreaId]?.translationX = px + childViews[widgetArea]?.translationX = px childViews[aodPromotedNotificationId]?.translationX = px childViews[aodNotificationIconContainerId]?.translationX = px } @@ -307,8 +304,7 @@ object KeyguardRootViewBinder { launch { viewModel.burnInLayerVisibility.collect { visibility -> childViews[burnInLayerId]?.visibility = visibility - childViews[sliceViewId]?.visibility = visibility - childViews[weatherAreaId]?.visibility = visibility + childViews[widgetArea]?.visibility = visibility } } @@ -600,8 +596,6 @@ object KeyguardRootViewBinder { } private val burnInLayerId = R.id.burn_in_layer - private val sliceViewId = R.id.keyguard_slice_view - private val weatherAreaId = R.id.keyguard_weather_area private val aodPromotedNotificationId = AodPromotedNotificationSection.viewId private val aodNotificationIconContainerId = R.id.aod_notification_icon_container private val largeClockId = ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE @@ -614,6 +608,7 @@ object KeyguardRootViewBinder { private val endButton = R.id.end_button private val deviceEntryIcon = R.id.device_entry_icon_view private val nsslPlaceholderId = R.id.nssl_placeholder + private val widgetArea = R.id.keyguard_widgets_area private val authInteractionProperties = AuthInteractionProperties() private val authUiIds = setOf(deviceEntryIcon, indicationArea) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt deleted file mode 100644 index b420373099acf..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSliceViewBinder.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.keyguard.ui.binder - -import android.annotation.SuppressLint -import android.view.ViewGroup -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.keyguard.KeyguardSliceViewController -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.DisposableHandle - -/** Binding for KeyguardSliceView */ -object KeyguardSliceViewBinder { - @SuppressLint("ClickableViewAccessibility") - @JvmStatic - fun bind( - view: ViewGroup, - keyguardInteractor: KeyguardInteractor, - keyguardSliceViewController: KeyguardSliceViewController, - aodBurnInViewModel: AodBurnInViewModel, - ): DisposableHandle { - return view.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - keyguardInteractor.dozeTimeTick.collect { - keyguardSliceViewController.refresh() - } - } - - launch { - aodBurnInViewModel.movement.collect { burnInModel -> - keyguardSliceViewController.translate( - burnInModel.translationX, - burnInModel.translationY, - ) - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt index 4940123ac0908..a0209fa3202c8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/DefaultKeyguardBlueprint.kt @@ -35,8 +35,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule.Companion.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import java.util.Optional import javax.inject.Inject @@ -68,8 +67,7 @@ constructor( aodBurnInSection: AodBurnInSection, clockSection: ClockSection, smartspaceSection: SmartspaceSection, - keyguardWeatherViewSection: KeyguardWeatherViewSection, - keyguardSliceViewSection: KeyguardSliceViewSection, + keyguardWidgetViewSection: KeyguardWidgetViewSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection, ) : KeyguardBlueprint { @@ -91,8 +89,7 @@ constructor( smartspaceSection, aodBurnInSection, clockSection, - keyguardWeatherViewSection, - keyguardSliceViewSection, + keyguardWidgetViewSection, defaultDeviceEntrySection, udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt index 99a8e8585566c..392c534184e3a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/SplitShadeKeyguardBlueprint.kt @@ -33,8 +33,7 @@ import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSec import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection import com.android.systemui.keyguard.ui.view.layout.sections.AxDynamicBarKeyguardChipSection import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection -import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWeatherViewSection +import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardWidgetViewSection import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeGuidelines import com.android.systemui.keyguard.ui.view.layout.sections.SplitShadeMediaSection @@ -69,10 +68,9 @@ constructor( aodNotificationIconsSection: AodNotificationIconsSection, aodBurnInSection: AodBurnInSection, clockSection: ClockSection, - smartspaceSection: SmartspaceSection, + keyguardWidgetViewSection: KeyguardWidgetViewSection, mediaSection: SplitShadeMediaSection, - keyguardWeatherViewSection: KeyguardWeatherViewSection, - keyguardSliceViewSection: KeyguardSliceViewSection, + smartspaceSection: SmartspaceSection, axDynamicBarKeyguardChipSection: AxDynamicBarKeyguardChipSection, ) : KeyguardBlueprint { override val id: String = ID @@ -92,11 +90,10 @@ constructor( splitShadeGuidelines, aodPromotedNotificationSection, aodNotificationIconsSection, - smartspaceSection, aodBurnInSection, clockSection, - keyguardWeatherViewSection, - keyguardSliceViewSection, + smartspaceSection, + keyguardWidgetViewSection, mediaSection, defaultDeviceEntrySection, // Add LAST: Intentionally has z-order above other views. ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index 234dee7871e0f..71e81cb0706d2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -18,6 +18,10 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.database.ContentObserver +import android.net.Uri +import android.os.Handler +import android.os.Looper import android.view.View import android.view.View.GONE import android.view.View.VISIBLE @@ -45,6 +49,7 @@ import com.android.systemui.statusbar.ui.SystemBarUtilsState import com.android.systemui.util.ui.value import javax.inject.Inject import kotlinx.coroutines.DisposableHandle +import com.android.systemui.shared.clocks.ClockSettingsRepository class AodNotificationIconsSection @Inject @@ -62,13 +67,36 @@ constructor( private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer + private var shouldCenterNic = true + private val clockSettingsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + updateShouldCenterNic() + } + } + private fun updateShouldCenterNic() { + val newShouldCenter = ClockSettingsRepository.shouldCenterIcons(context) + if (shouldCenterNic != newShouldCenter) { + shouldCenterNic = newShouldCenter + if (::nic.isInitialized) { + nic.setPaddingRelative( + if (shouldCenterNic) 0 + else context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), + 0, + 0, + 0, + ) + } + } + } override fun addViews(constraintLayout: ConstraintLayout) { + updateShouldCenterNic() nic = NotificationIconContainer(context, null).apply { id = nicId setPaddingRelative( - resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), + if (shouldCenterNic) 0 + else resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons), 0, resources.getDimensionPixelOffset(R.dimen.shelf_icon_container_padding), 0, @@ -77,6 +105,17 @@ constructor( } constraintLayout.addView(nic) + ClockSettingsRepository.init(context) + context.contentResolver.registerContentObserver( + ClockSettingsRepository.clockFaceUri, + false, + clockSettingsObserver + ) + context.contentResolver.registerContentObserver( + ClockSettingsRepository.alignmentUri, + false, + clockSettingsObserver + ) } override fun bindData(constraintLayout: ConstraintLayout) { @@ -111,12 +150,14 @@ constructor( setGoneMargin(nicId, BOTTOM, bottomMargin) setVisibility(nicId, if (isVisible.value) VISIBLE else GONE) + clear(nicId, START) + clear(nicId, END) if (PromotedNotificationUi.isEnabled && !isFullWidthShade) { // Don't create a start constraint, so the icons can hopefully right-align. } else { - connect(nicId, START, PARENT_ID, START, horizontalMargin) + connect(nicId, START, PARENT_ID, START) } - connect(nicId, END, PARENT_ID, END, horizontalMargin) + connect(nicId, END, PARENT_ID, END) constrainHeight(nicId, height) } @@ -125,5 +166,6 @@ constructor( override fun removeViews(constraintLayout: ConstraintLayout) { constraintLayout.removeView(nicId) nicBindingDisposable?.dispose() + context.contentResolver.unregisterContentObserver(clockSettingsObserver) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt index 8a6bd9e064f84..f7a92a1d2c30d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AxDynamicBarKeyguardChipSection.kt @@ -37,9 +37,7 @@ private val HIDDEN_VIEW_IDS = listOf( R.id.bc_smartspace_view, R.id.smartspace_card_pager, R.id.smartspace_page_indicator, - R.id.keyguard_slice_view, - R.id.keyguard_weather_area, - R.id.clock_ls, + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, ) private fun Float.dpToPx(context: Context): Int = diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index 6b278212c50a3..a30607715fc1f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View -import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -29,8 +28,9 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE +import androidx.constraintlayout.widget.ConstraintSet.MATCH_CONSTRAINT import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT -import com.android.systemui.customization.clocks.R as clocksR +import com.android.systemui.customization.R as custR import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor @@ -109,45 +109,34 @@ constructor( clock: ClockController, constraintSet: ConstraintSet, ): ConstraintSet { + val targetFace = getTargetClockFace(clock) + val nonTargetFace = getNonTargetClockFace(clock) + val targetViews = targetFace.views + val nonTargetViews = nonTargetFace.views // Add constraint between rootView and clockContainer applyDefaultConstraints(constraintSet) - getNonTargetClockFace(clock).applyConstraints(constraintSet) - getTargetClockFace(clock).applyConstraints(constraintSet) + nonTargetFace.applyConstraints(constraintSet) + targetFace.applyConstraints(constraintSet) // Add constraint between elements in clock and clock container return constraintSet.apply { - setVisibility(getTargetClockFace(clock).views, VISIBLE) - setVisibility(getNonTargetClockFace(clock).views, GONE) - setAlpha(getTargetClockFace(clock).views, 1F) - setAlpha(getNonTargetClockFace(clock).views, 0F) + setVisibility(targetViews, VISIBLE) + setVisibility(nonTargetViews, GONE) + setAlpha(targetViews, 1F) + setAlpha(nonTargetViews, 0F) - if (!keyguardClockViewModel.isLargeClockVisible.value) { - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { - connect( - sharedR.id.bc_smartspace_view, - TOP, - sharedR.id.date_smartspace_view, - BOTTOM, - ) - } else { - connect( - sharedR.id.bc_smartspace_view, - TOP, - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - BOTTOM, - ) - } - } else { - if (aodBurnInViewModel.movement.value.scaleClockOnly) { - setScaleX( - getTargetClockFace(clock).views, - aodBurnInViewModel.movement.value.scale, - ) - setScaleY( - getTargetClockFace(clock).views, - aodBurnInViewModel.movement.value.scale, - ) - } + if ( + keyguardClockViewModel.isLargeClockVisible.value && + aodBurnInViewModel.movement.value.scaleClockOnly + ) { + setScaleX( + getTargetClockFace(clock).views, + aodBurnInViewModel.movement.value.scale, + ) + setScaleY( + getTargetClockFace(clock).views, + aodBurnInViewModel.movement.value.scale, + ) } } } @@ -160,38 +149,6 @@ constructor( if (keyguardClockViewModel.isLargeClockVisible.value) clock.smallClock.layout else clock.largeClock.layout - private fun constrainWeatherClockDateIconsBarrier(constraints: ConstraintSet) { - constraints.apply { - createBarrier( - R.id.weather_clock_bc_smartspace_bottom, - Barrier.BOTTOM, - context.resources.getDimensionPixelSize(clocksR.dimen.enhanced_smartspace_height), - (ClockViewIds.WEATHER_CLOCK_TIME), - ) - if ( - rootViewModel.isNotifIconContainerVisible.value.value && - keyguardClockViewModel.hasAodIcons.value - ) { - createBarrier( - ClockViewIds.WEATHER_CLOCK_DATE_BARRIER_BOTTOM, - Barrier.BOTTOM, - 0, - *intArrayOf( - R.id.aod_notification_icon_container, - R.id.weather_clock_bc_smartspace_bottom, - ), - ) - } else { - createBarrier( - ClockViewIds.WEATHER_CLOCK_DATE_BARRIER_BOTTOM, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.weather_clock_bc_smartspace_bottom), - ) - } - } - } - fun applyDefaultConstraints(constraints: ConstraintSet) { val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID @@ -199,69 +156,43 @@ constructor( constraints.apply { connect(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, START, PARENT_ID, START) connect(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, END, guideline, END) - if ( - com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout() && - !com.android.systemui.shared.Flags.clockReactiveVariants() - ) { - connect( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, - BOTTOM, - R.id.device_entry_icon_view, - TOP, - context.resources.getDimensionPixelSize( - clocksR.dimen.date_weather_view_height - ) * 2, - ) - } else { - connect( - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, - BOTTOM, - R.id.device_entry_icon_view, - TOP, - ) - } - val largeClockTopMargin = - if (com.android.systemui.shared.Flags.clockReactiveSmartspaceLayout()) { - keyguardClockViewModel.getLargeClockTopMargin() + - context.resources.getDimensionPixelSize( - clocksR.dimen.enhanced_smartspace_height - ) - } else { - keyguardClockViewModel.getLargeClockTopMargin() + - context.resources.getDimensionPixelSize( - clocksR.dimen.date_weather_view_height - ) + - context.resources.getDimensionPixelSize( - clocksR.dimen.enhanced_smartspace_height - ) - } connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, + BOTTOM, + R.id.device_entry_icon_view, TOP, - PARENT_ID, + ) + val largeClockTopMargin = keyguardClockViewModel.getLargeClockTopMargin() + connect( + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, TOP, + R.id.smart_space_barrier_bottom, + BOTTOM, largeClockTopMargin, ) - constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, WRAP_CONTENT) + constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, MATCH_CONSTRAINT) // The following two lines make LOCKSCREEN_CLOCK_VIEW_LARGE is constrained to available // height when it goes beyond constraints; otherwise, it use WRAP_CONTENT constrainHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, WRAP_CONTENT) constrainMaxHeight(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, 0) - constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, WRAP_CONTENT) + constrainWidth(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, MATCH_CONSTRAINT) constrainHeight( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - context.resources.getDimensionPixelSize(clocksR.dimen.small_clock_height), + context.resources.getDimensionPixelSize(custR.dimen.clock_height), ) connect( ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, START, PARENT_ID, START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize( - clocksR.dimen.status_view_margin_horizontal - ), + 0 + ) + connect( + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, + END, + guideline, + END, ) val smallClockTopMargin = keyguardClockViewModel.getSmallClockTopMargin() + context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) @@ -277,10 +208,6 @@ constructor( // Explicitly clear pivot to force recalculate pivot instead of using legacy value setTransformPivot(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, Float.NaN, Float.NaN) - val smallClockBottom = - keyguardClockViewModel.getSmallClockTopMargin() + - context.resources.getDimensionPixelSize(clocksR.dimen.small_clock_height) - - context.resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) val marginBetweenSmartspaceAndNotification = context.resources.getDimensionPixelSize( R.dimen.keyguard_status_view_bottom_margin @@ -291,25 +218,11 @@ constructor( 0 } - if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { - val dateWeatherSmartspaceHeight = - context.resources - .getDimensionPixelSize(clocksR.dimen.date_weather_view_height) - .toFloat() - clockInteractor.setNotificationStackDefaultTop( - smallClockBottom + - dateWeatherSmartspaceHeight + - marginBetweenSmartspaceAndNotification - ) - } else { - clockInteractor.setNotificationStackDefaultTop( - (keyguardClockViewModel.getSmallClockTopMargin() + - marginBetweenSmartspaceAndNotification) - .toFloat() - ) - } + clockInteractor.setNotificationStackDefaultTop( + (keyguardClockViewModel.getSmallClockTopMargin() + + marginBetweenSmartspaceAndNotification) + .toFloat() + ) } - - constrainWeatherClockDateIconsBarrier(constraints) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt deleted file mode 100644 index 4362032e13536..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardSliceViewSection.kt +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import android.os.Handler -import android.view.LayoutInflater -import androidx.constraintlayout.widget.Barrier -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import com.android.keyguard.KeyguardSliceView -import com.android.keyguard.KeyguardSliceViewController -import com.android.systemui.customization.clocks.R as clocksR -import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.dump.DumpManager -import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor -import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.keyguard.ui.binder.KeyguardSliceViewBinder -import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel -import com.android.systemui.plugins.ActivityStarter -import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds -import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.res.R -import com.android.systemui.settings.DisplayTracker -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import com.android.systemui.statusbar.policy.ConfigurationController -import javax.inject.Inject -import kotlinx.coroutines.DisposableHandle - -class KeyguardSliceViewSection -@Inject -constructor( - private val context: Context, - val smartspaceController: LockscreenSmartspaceController, - val layoutInflater: LayoutInflater, - @Main val handler: Handler, - @Background val bgHandler: Handler, - val activityStarter: ActivityStarter, - val configurationController: ConfigurationController, - val dumpManager: DumpManager, - val displayTracker: DisplayTracker, - val keyguardInteractor: KeyguardInteractor, - val powerInteractor: PowerInteractor, - val aodBurnInViewModel: AodBurnInViewModel, -) : KeyguardSection() { - private lateinit var sliceView: KeyguardSliceView - private var disposableHandle: DisposableHandle? = null - - override fun addViews(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - sliceView = - layoutInflater.inflate(R.layout.keyguard_slice_view, null, false) as KeyguardSliceView - constraintLayout.addView(sliceView) - } - - override fun bindData(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - val controller = - KeyguardSliceViewController( - handler, - bgHandler, - sliceView, - activityStarter, - configurationController, - dumpManager, - displayTracker, - keyguardInteractor, - powerInteractor, - ) - controller.setupUri(null) - controller.init() - - disposableHandle?.dispose() - disposableHandle = - KeyguardSliceViewBinder.bind( - sliceView, - keyguardInteractor, - controller, - aodBurnInViewModel, - ) - } - - override fun applyConstraints(constraintSet: ConstraintSet) { - if (smartspaceController.isEnabled) return - constraintSet.apply { - connect( - R.id.keyguard_slice_view, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize( - clocksR.dimen.status_view_margin_horizontal - ), - ) - connect( - R.id.keyguard_slice_view, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END, - ) - constrainHeight(R.id.keyguard_slice_view, ConstraintSet.WRAP_CONTENT) - - connect( - R.id.keyguard_slice_view, - ConstraintSet.TOP, - ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, - ConstraintSet.BOTTOM, - ) - - if (!smartspaceController.isOmniWeatherEnabled) { - createBarrier( - R.id.smart_space_barrier_bottom, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.keyguard_slice_view), - ) - } else { - createBarrier( - R.id.keyguard_weather_area, - Barrier.BOTTOM, - 0, - *intArrayOf(R.id.keyguard_slice_view), - ) - } - } - } - - override fun removeViews(constraintLayout: ConstraintLayout) { - if (smartspaceController.isEnabled) return - - constraintLayout.removeView(R.id.keyguard_slice_view) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt deleted file mode 100644 index 453fcf3aeca75..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWeatherViewSection.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024-2026 crDroid Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package com.android.systemui.keyguard.ui.view.layout.sections - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.constraintlayout.widget.Barrier -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.clocks.R as clocksR -import com.android.systemui.keyguard.shared.model.KeyguardSection -import com.android.systemui.res.R -import com.android.systemui.shared.R as sharedR -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController -import javax.inject.Inject - -import com.android.systemui.weather.WeatherInfoView - -class KeyguardWeatherViewSection -@Inject -constructor( - private val context: Context, - val layoutInflater: LayoutInflater, - val smartspaceController: LockscreenSmartspaceController, -) : KeyguardSection() { - private lateinit var weatherView: WeatherInfoView - - override fun addViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - weatherView = - layoutInflater.inflate(R.layout.keyguard_weather_area, null, false) as WeatherInfoView - constraintLayout.addView(weatherView) - } - - override fun bindData(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - weatherView.init() - } - - override fun applyConstraints(constraintSet: ConstraintSet) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintSet.apply { - connect( - R.id.keyguard_weather_area, - ConstraintSet.START, - ConstraintSet.PARENT_ID, - ConstraintSet.START, - context.resources.getDimensionPixelSize(clocksR.dimen.clock_padding_start) + - context.resources.getDimensionPixelSize(clocksR.dimen.status_view_margin_horizontal), - ) - connect( - R.id.keyguard_weather_area, - ConstraintSet.END, - ConstraintSet.PARENT_ID, - ConstraintSet.END - ) - constrainHeight(R.id.keyguard_weather_area, ConstraintSet.WRAP_CONTENT) - - connect( - R.id.keyguard_weather_area, - ConstraintSet.TOP, - R.id.keyguard_slice_view, - ConstraintSet.BOTTOM - ) - - // UNIFIED BARRIER - Include ALL status area elements - createUnifiedBarrierAndNotificationConstraints(constraintSet) - } - } - - private fun createUnifiedBarrierAndNotificationConstraints(constraintSet: ConstraintSet) { - constraintSet.apply { - // UNIFIED BARRIER - Include ALL status area elements - createBarrier( - R.id.smart_space_barrier_bottom, - Barrier.BOTTOM, - 0, - *intArrayOf( - R.id.keyguard_slice_view, - R.id.keyguard_weather_area, - R.id.clock_ls, - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - ) - ) - } - } - - override fun removeViews(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return - - constraintLayout.findViewById(R.id.keyguard_weather_area)?.let { weatherArea -> - weatherArea.cleanup() - constraintLayout.removeView(weatherArea) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt new file mode 100644 index 0000000000000..4326e5d1ce213 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.view.layout.sections + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.constraintlayout.widget.Barrier +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.lockscreen.KeyguardWidgetHostController +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.plugins.keyguard.ui.clocks.ClockViewIds +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ConfigurationController +import javax.inject.Inject + +class KeyguardWidgetViewSection +@Inject +constructor( + private val context: Context, + private val layoutInflater: LayoutInflater, + private val activityStarter: ActivityStarter, + private val configurationController: ConfigurationController, + private val broadcastDispatcher: BroadcastDispatcher, + private val keyguardClockViewModel: KeyguardClockViewModel, +) : KeyguardSection() { + private lateinit var widgetsView: ViewGroup + private lateinit var controller: KeyguardWidgetHostController + + private val isSplitShade: Boolean + get() = context.resources.getBoolean(R.bool.config_use_split_notification_shade) + + override fun addViews(constraintLayout: ConstraintLayout) { + widgetsView = + layoutInflater.inflate(R.layout.keyguard_widgets_area, null, false) as ViewGroup + constraintLayout.addView(widgetsView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + controller = KeyguardWidgetHostController(context, widgetsView, activityStarter, configurationController, broadcastDispatcher) + controller.init() + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + connect( + R.id.keyguard_widgets_area, + ConstraintSet.TOP, + ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, + ConstraintSet.BOTTOM, + ) + connect( + R.id.keyguard_widgets_area, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START, + ) + connect( + R.id.keyguard_widgets_area, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END, + ) + constrainWidth(R.id.keyguard_widgets_area, ConstraintSet.MATCH_CONSTRAINT) + constrainMaxWidth( + R.id.keyguard_widgets_area, + context.resources.getDimensionPixelSize(R.dimen.shade_panel_width), + ) + val centered = !isSplitShade || keyguardClockViewModel.clockShouldBeCentered.value + setHorizontalBias(R.id.keyguard_widgets_area, if (centered) 0.5f else 0f) + if (::controller.isInitialized) { + controller.clockCentered = centered + } + constrainHeight(R.id.keyguard_widgets_area, ConstraintSet.WRAP_CONTENT) + val density = context.resources.displayMetrics.density + constrainMaxHeight( + R.id.keyguard_widgets_area, + (KeyguardWidgetHostController.MAX_CONTAINER_HEIGHT_DP * density).toInt(), + ) + + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf(R.id.keyguard_widgets_area) + ) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + controller.dispose() + } + +} + diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt index 34e3d9364c12f..381881fb29f06 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeMediaSection.kt @@ -70,11 +70,7 @@ constructor( constraintSet.apply { constrainWidth(mediaContainerId, MATCH_CONSTRAINT) constrainHeight(mediaContainerId, WRAP_CONTENT) - if (smartspaceViewModel.isSmartspaceEnabled) { - connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) - } else { - connect(mediaContainerId, TOP, R.id.keyguard_slice_view, BOTTOM) - } + connect(mediaContainerId, TOP, R.id.smart_space_barrier_bottom, BOTTOM) connect(mediaContainerId, START, PARENT_ID, START) connect(mediaContainerId, END, R.id.split_shade_guideline, END) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt index 87e48ff27a3c9..0a28968131694 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt @@ -84,7 +84,6 @@ class ClockSizeTransition( val parent = view.parent as View val targetSSView = parent.findViewById(sharedR.id.bc_smartspace_view) - ?: parent.findViewById(R.id.keyguard_slice_view) if (targetSSView == null) { logger.e({ "Failed to find smartspace equivalent target under $str1" }) { str1 = "$parent" @@ -419,6 +418,7 @@ class ClockSizeTransition( // Notifications normally and media on split shade needs to be moved addTarget(R.id.aod_notification_icon_container) addTarget(R.id.status_view_media_container) + addTarget(R.id.keyguard_widgets_area) } override fun initTargets(from: Target, to: Target) { diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt new file mode 100644 index 0000000000000..145212b3e7127 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHost.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lockscreen + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.os.DeadObjectException +import android.os.Looper +import android.os.TransactionTooLargeException +import android.util.Log +import android.widget.RemoteViews + +class KeyguardAppWidgetHost( + context: Context, + hostId: Int, + private val interactionHandler: RemoteViews.InteractionHandler, +) : AppWidgetHost(context, hostId, interactionHandler, Looper.getMainLooper()) { + + var onWidgetRemovedListener: ((Int) -> Unit)? = null + + override fun onCreateView( + context: Context, + appWidgetId: Int, + appWidget: AppWidgetProviderInfo?, + ): AppWidgetHostView { + return KeyguardAppWidgetHostView(context, interactionHandler) + } + + override fun onAppWidgetRemoved(appWidgetId: Int) { + Log.i(TAG, "Widget removed from system: id=$appWidgetId") + onWidgetRemovedListener?.invoke(appWidgetId) + } + + override fun startListening() { + try { + super.startListening() + } catch (e: Exception) { + if (!e.isBinderSizeError()) { + Log.e(TAG, "Error starting widget host listening", e) + } + } + } + + override fun stopListening() { + try { + super.stopListening() + } catch (e: Exception) { + Log.e(TAG, "Error stopping widget host listening", e) + } + } + + companion object { + private const val TAG = "KeyguardAppWidgetHost" + const val HOST_ID = 1027 + + private fun Exception.isBinderSizeError(): Boolean { + return cause is TransactionTooLargeException || cause is DeadObjectException + } + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt new file mode 100644 index 0000000000000..c10d771128489 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardAppWidgetHostView.kt @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lockscreen + +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetProviderInfo +import android.content.Context +import android.graphics.Outline +import android.view.MotionEvent +import android.view.View +import android.view.ViewOutlineProvider +import android.widget.RemoteViews +import com.android.systemui.util.ScrimUtils + +class KeyguardAppWidgetHostView( + context: Context, + interactionHandler: RemoteViews.InteractionHandler, +) : AppWidgetHostView(context, interactionHandler) { + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + if (event.actionMasked == MotionEvent.ACTION_DOWN) { + parent?.requestDisallowInterceptTouchEvent(true) + } + return super.dispatchTouchEvent(event) + } + + var cornerRadius: Float = 0f + set(value) { + field = value + if (value > 0f) { + outlineProvider = roundedOutlineProvider + clipToOutline = true + } else { + outlineProvider = ViewOutlineProvider.BACKGROUND + clipToOutline = false + } + invalidateOutline() + } + + private val roundedOutlineProvider = + object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect(0, 0, view.width, view.height, cornerRadius) + } + } + + override fun setAppWidget(appWidgetId: Int, info: AppWidgetProviderInfo?) { + super.setAppWidget(appWidgetId, info) + super.setPadding(0, 0, 0, 0) + } + + override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) { + + super.setPadding(0, 0, 0, 0) + } + + override fun updateAppWidget(remoteViews: RemoteViews?) { + super.updateAppWidget(remoteViews) + super.setPadding(0, 0, 0, 0) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt new file mode 100644 index 0000000000000..0734f3418d157 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetConfigActivity.kt @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2025-2026 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lockscreen + +import android.app.Activity +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Intent +import android.os.Bundle +import android.os.UserHandle +import android.provider.Settings +import android.util.Log +import org.json.JSONArray +import org.json.JSONObject + +class KeyguardWidgetConfigActivity : Activity() { + + private var widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val providerStr = intent.getStringExtra(EXTRA_PROVIDER) + if (providerStr.isNullOrEmpty()) { + finish() + return + } + + val cn = ComponentName.unflattenFromString(providerStr) + if (cn == null) { + finish() + return + } + + if (savedInstanceState != null) { + widgetId = + savedInstanceState.getInt(STATE_WIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) + return + } + + val host = AppWidgetHost(this, KeyguardAppWidgetHost.HOST_ID) + widgetId = host.allocateAppWidgetId() + + val options = + Bundle().apply { + putInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, + ) + } + val bound = + AppWidgetManager.getInstance(this).bindAppWidgetIdIfAllowed(widgetId, cn, options) + + if (!bound) { + Log.w(TAG, "Failed to bind widget $cn, id=$widgetId") + host.deleteAppWidgetId(widgetId) + widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + finish() + return + } + + val info = AppWidgetManager.getInstance(this).getAppWidgetInfo(widgetId) + if (info != null && requiresConfiguration(info)) { + val configIntent = + Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE).apply { + component = info.configure + putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) + } + try { + startActivityForResult(configIntent, REQUEST_CONFIGURE) + } catch (e: Exception) { + Log.e(TAG, "Failed to start configure activity for widget $widgetId", e) + cleanup() + finish() + } + } else { + + saveWidgetToSettings() + finish() + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putInt(STATE_WIDGET_ID, widgetId) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == REQUEST_CONFIGURE) { + if (resultCode == RESULT_OK) { + saveWidgetToSettings() + } else { + cleanup() + } + finish() + } + } + + private fun saveWidgetToSettings() { + try { + val json = + Settings.System.getStringForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_CONFIG, + UserHandle.USER_CURRENT, + ) ?: "" + + val arr = if (json.isNotBlank()) JSONArray(json) else JSONArray() + arr.put( + JSONObject().apply { + put("appWidgetId", widgetId) + put("provider", intent.getStringExtra(EXTRA_PROVIDER) ?: "") + put("cellX", intent.getIntExtra(EXTRA_CELL_X, 0)) + put("cellY", intent.getIntExtra(EXTRA_CELL_Y, 0)) + put("spanX", intent.getIntExtra(EXTRA_SPAN_X, 1)) + put("spanY", intent.getIntExtra(EXTRA_SPAN_Y, 1)) + } + ) + + Settings.System.putStringForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_CONFIG, + arr.toString(), + UserHandle.USER_CURRENT, + ) + + Settings.System.putIntForUser( + contentResolver, + KeyguardWidgetHostController.SETTING_ENABLED, + 1, + UserHandle.USER_CURRENT, + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to save widget to settings", e) + } + } + + private fun cleanup() { + if (widgetId != AppWidgetManager.INVALID_APPWIDGET_ID) { + try { + AppWidgetHost(this, KeyguardAppWidgetHost.HOST_ID).deleteAppWidgetId(widgetId) + } catch (e: Exception) { + Log.e(TAG, "Failed to delete widget ID $widgetId", e) + } + widgetId = AppWidgetManager.INVALID_APPWIDGET_ID + } + } + + private fun requiresConfiguration(info: AppWidgetProviderInfo): Boolean { + if (info.configure == null) return false + val features = info.widgetFeatures + val configOptional = + (features and AppWidgetProviderInfo.WIDGET_FEATURE_CONFIGURATION_OPTIONAL != 0) && + (features and AppWidgetProviderInfo.WIDGET_FEATURE_RECONFIGURABLE != 0) + return !configOptional + } + + companion object { + private const val TAG = "KgWidgetConfigActivity" + const val EXTRA_PROVIDER = "extra_provider" + const val EXTRA_CELL_X = "extra_cell_x" + const val EXTRA_CELL_Y = "extra_cell_y" + const val EXTRA_SPAN_X = "extra_span_x" + const val EXTRA_SPAN_Y = "extra_span_y" + private const val STATE_WIDGET_ID = "state_widget_id" + private const val REQUEST_CONFIGURE = 1 + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt new file mode 100644 index 0000000000000..f14328ded2d60 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetHostController.kt @@ -0,0 +1,654 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lockscreen + +import android.appwidget.AppWidgetHostView +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.database.ContentObserver +import android.net.Uri +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.os.UserManager +import android.provider.Settings +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import com.android.axion.compose.host.AxComposeView +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate +import com.android.systemui.broadcast.BroadcastDispatcher +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.ConfigurationController +import com.android.systemui.util.ScrimUtils +import kotlin.math.min +import org.json.JSONArray +import org.json.JSONObject + +data class WidgetEntry( + val appWidgetId: Int, + val provider: ComponentName?, + val cellX: Int, + val cellY: Int, + val spanX: Int, + val spanY: Int, + var hostView: AppWidgetHostView? = null, +) + +class KeyguardWidgetHostController( + private val context: Context, + private val container: ViewGroup, + private val activityStarter: ActivityStarter, + private val configurationController: ConfigurationController, + private val broadcastDispatcher: BroadcastDispatcher, +) { + private val appWidgetManager = AppWidgetManager.getInstance(context) + private val interactionHandler = KeyguardWidgetInteractionHandler(activityStarter) + private val widgetHost = + KeyguardAppWidgetHost(context, KeyguardAppWidgetHost.HOST_ID, interactionHandler) + + private val contentResolver: ContentResolver = context.contentResolver + private val userManager = context.getSystemService(UserManager::class.java)!! + private val handler = Handler(Looper.getMainLooper()) + + private val _widgets = mutableStateListOf() + private val _enabled = mutableStateOf(false) + private val _viewGeneration = mutableStateOf(0) + private val _clockCentered = mutableStateOf(false) + + private var listening = false + private var hostListening = false + private var initialized = false + private var pendingSelfUpdates = 0 + private var configDirty = false + + var clockCentered: Boolean + get() = _clockCentered.value + set(value) { _clockCentered.value = value } + + private val cornerRadius: Float + get() = context.resources.getDimension(R.dimen.kg_widget_corner_radius) + + private val sidePadding: Int + get() = context.resources.getDimensionPixelSize(R.dimen.kg_widget_side_padding) + + private val cellGap: Int + get() = context.resources.getDimensionPixelSize(R.dimen.kg_widget_cell_gap) + + private val isSplitShade: Boolean + get() { + val resId = + context.resources.getIdentifier( + "config_use_split_notification_shade", + "bool", + "com.android.systemui", + ) + return if (resId != 0) context.resources.getBoolean(resId) else false + } + + val cellSize: Int + get() { + val dm = context.resources.displayMetrics + val maxWidth = context.resources.getDimensionPixelSize(R.dimen.shade_panel_width) + val baseWidth = + if (isSplitShade) { + min(dm.widthPixels / 2, maxWidth) + } else { + min(min(dm.widthPixels, dm.heightPixels), maxWidth) + } + val widthCell = (baseWidth - 2 * sidePadding - (GRID_COLUMNS - 1) * cellGap) / GRID_COLUMNS + val density = dm.density + val maxContainerHeight = (MAX_CONTAINER_HEIGHT_DP * density).toInt() + val composePadding = (COMPOSE_TOP_PADDING_DP * density).toInt() + val availableHeight = maxContainerHeight - composePadding + val heightCell = (availableHeight - (MAX_ROWS - 1) * cellGap) / MAX_ROWS + return min(widthCell, heightCell) + } + + val cellHeight: Int + get() = cellSize + + private val hostView by lazy { WidgetHostFrameLayout(context) } + + private inner class WidgetHostFrameLayout(context: Context) : + FrameLayout(context), LaunchableView { + private val composeView = + AxComposeView(context).apply { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ) + setContent { WidgetGridContent() } + } + + init { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ) + addView(composeView) + } + + private val delegate = + LaunchableViewDelegate(this, superSetVisibility = { super.setVisibility(it) }) + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + delegate.setVisibility(visibility) + } + + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + composeView.onTouchEvent(event) + return super.onTouchEvent(event) + } + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + if (!ScrimUtils.get().isKeyguardShowing()) return false + composeView.onInterceptTouchEvent(event) + return super.onInterceptTouchEvent(event) + } + } + + private val settingsObserver = + object : ContentObserver(handler) { + override fun onChange(selfChange: Boolean) { + if (pendingSelfUpdates > 0) { + pendingSelfUpdates-- + return + } + loadConfig() + } + } + + private val scrimListener = + object : ScrimUtils.ScrimEventListener { + override fun onKeyguardShowingChanged(showing: Boolean) { + if (showing) { + startHostListening() + } else { + stopHostListening() + hostView.visibility = View.GONE + container.visibility = View.GONE + } + } + } + + private val userUnlockReceiver = + object : BroadcastReceiver() { + override fun onReceive(ctx: Context, intent: Intent) { + if (intent.action == Intent.ACTION_USER_UNLOCKED && !initialized) { + initWidgets() + } + } + } + + private val configListener = + object : ConfigurationController.ConfigurationListener { + override fun onUiModeChanged() { + scheduleResetWidgets() + } + + override fun onThemeChanged() { + scheduleResetWidgets() + } + + override fun onConfigChanged(newConfig: Configuration) { + updateWidgetSizes() + } + } + + private var resetPending = false + + private fun scheduleResetWidgets() { + if (resetPending) return + resetPending = true + handler.postDelayed( + { + resetPending = false + if (hostListening) { + performFullReset() + } else { + configDirty = true + } + }, + RESET_DELAY_MS, + ) + } + + private fun performFullReset() { + configDirty = false + + cleanupWidgetViews() + _widgets.clear() + loadConfig() + } + + fun init() { + container.addView(hostView) + ScrimUtils.get().addListener(scrimListener) + startObserving() + configurationController.addCallback(configListener) + widgetHost.onWidgetRemovedListener = { appWidgetId -> onWidgetRemoved(appWidgetId) } + + if (userManager.isUserUnlocked) { + initWidgets() + } else { + broadcastDispatcher.registerReceiver( + userUnlockReceiver, + IntentFilter(Intent.ACTION_USER_UNLOCKED), + ) + } + } + + private fun initWidgets() { + if (initialized) return + initialized = true + broadcastDispatcher.unregisterReceiver(userUnlockReceiver) + startHostListening() + loadConfig() + } + + fun dispose() { + widgetHost.onWidgetRemovedListener = null + ScrimUtils.get().removeListener(scrimListener) + configurationController.removeCallback(configListener) + handler.removeCallbacksAndMessages(null) + broadcastDispatcher.unregisterReceiver(userUnlockReceiver) + stopObserving() + stopHostListening() + cleanupWidgetViews() + container.removeAllViews() + initialized = false + } + + private fun startObserving() { + if (listening) return + contentResolver.registerContentObserver( + ENABLED_URI, + false, + settingsObserver, + UserHandle.USER_CURRENT, + ) + contentResolver.registerContentObserver( + CONFIG_URI, + false, + settingsObserver, + UserHandle.USER_CURRENT, + ) + listening = true + } + + private fun stopObserving() { + if (!listening) return + contentResolver.unregisterContentObserver(settingsObserver) + listening = false + } + + private fun startHostListening() { + if (hostListening) return + widgetHost.startListening() + hostListening = true + + if (configDirty) { + performFullReset() + return + } + + var needsSave = false + _widgets.forEachIndexed { idx, entry -> + if (entry.hostView == null && entry.provider != null) { + val resolved = resolveWidget(entry) + if (resolved != null) { + if (resolved.appWidgetId != entry.appWidgetId) { + needsSave = true + } + _widgets[idx] = resolved + } + } + } + if (needsSave) saveConfig() + updateVisibility() + } + + private fun stopHostListening() { + if (!hostListening) return + widgetHost.stopListening() + hostListening = false + } + + private fun loadConfig() { + if (!userManager.isUserUnlocked) return + + val isEnabled = + Settings.System.getIntForUser( + contentResolver, + SETTING_ENABLED, + 0, + UserHandle.USER_CURRENT, + ) == 1 + + val configJson = + Settings.System.getStringForUser( + contentResolver, + SETTING_CONFIG, + UserHandle.USER_CURRENT, + ) ?: "" + + _enabled.value = isEnabled && configJson.isNotEmpty() + + val entries = parseConfig(configJson) + updateWidgetEntries(entries) + updateVisibility() + } + + private fun parseConfig(json: String): List { + if (json.isBlank()) return emptyList() + return try { + val arr = JSONArray(json) + (0 until arr.length()).mapNotNull { i -> + val obj = arr.getJSONObject(i) + val providerStr = obj.optString("provider", "") + val provider = + if (providerStr.isNotEmpty()) { + ComponentName.unflattenFromString(providerStr) + } else null + + WidgetEntry( + appWidgetId = obj.optInt("appWidgetId", -1), + provider = provider, + cellX = obj.optInt("cellX", 0), + cellY = obj.optInt("cellY", 0), + spanX = obj.optInt("spanX", 1).coerceIn(1, GRID_COLUMNS), + spanY = obj.optInt("spanY", 1).coerceIn(1, MAX_ROWS), + ) + } + } catch (e: Exception) { + Log.e(TAG, "Failed to parse widget config", e) + emptyList() + } + } + + private fun updateWidgetEntries(entries: List) { + + _widgets.forEach { existing -> + if (existing.appWidgetId >= 0 && existing.provider != null) { + val stillPresent = + entries.any { + it.appWidgetId == existing.appWidgetId || + (it.provider?.flattenToString() == + existing.provider.flattenToString() && + it.cellX == existing.cellX && + it.cellY == existing.cellY) + } + if (!stillPresent) { + widgetHost.deleteAppWidgetId(existing.appWidgetId) + } + } + } + + cleanupWidgetViews() + _widgets.clear() + + if (!hostListening) { + _widgets.addAll(entries) + return + } + + var needsSave = false + entries.forEach { entry -> + val resolved = resolveWidget(entry) + if (resolved != null) { + if (resolved.appWidgetId != entry.appWidgetId) { + needsSave = true + } + _widgets.add(resolved) + } + } + if (needsSave) saveConfig() + + _viewGeneration.value++ + } + + private fun resolveWidget(entry: WidgetEntry): WidgetEntry? { + var widgetId = entry.appWidgetId + val provider = entry.provider ?: return null + + if (widgetId < 0) { + widgetId = allocateAndBind(provider) ?: return null + } else { + val info = appWidgetManager.getAppWidgetInfo(widgetId) + if (info == null) { + widgetHost.deleteAppWidgetId(widgetId) + widgetId = allocateAndBind(provider) ?: return null + } + } + + val hostView = + try { + widgetHost.createView( + context, + widgetId, + appWidgetManager.getAppWidgetInfo(widgetId), + ) + } catch (e: Exception) { + Log.e(TAG, "Failed to create widget view for id=$widgetId", e) + return null + } + + (hostView as? KeyguardAppWidgetHostView)?.cornerRadius = cornerRadius + + val cs = cellSize + val ch = cellHeight + val gap = cellGap + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + hostView.updateAppWidgetSize(Bundle(), pxToDp(w), pxToDp(h), pxToDp(w), pxToDp(h)) + + return entry.copy(appWidgetId = widgetId, hostView = hostView) + } + + private fun allocateAndBind(provider: ComponentName): Int? { + val widgetId = widgetHost.allocateAppWidgetId() + val options = + Bundle().apply { + putInt( + AppWidgetManager.OPTION_APPWIDGET_HOST_CATEGORY, + AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, + ) + } + val bound = appWidgetManager.bindAppWidgetIdIfAllowed(widgetId, provider, options) + if (!bound) { + Log.w(TAG, "Failed to bind widget $provider, id=$widgetId") + widgetHost.deleteAppWidgetId(widgetId) + return null + } + return widgetId + } + + private fun cleanupWidgetViews() { + _widgets.forEach { + it.hostView?.let { view -> (view.parent as? ViewGroup)?.removeView(view) } + } + } + + private fun saveConfig() { + val arr = JSONArray() + _widgets.forEach { entry -> + arr.put( + JSONObject().apply { + put("appWidgetId", entry.appWidgetId) + put("provider", entry.provider?.flattenToString() ?: "") + put("cellX", entry.cellX) + put("cellY", entry.cellY) + put("spanX", entry.spanX) + put("spanY", entry.spanY) + } + ) + } + pendingSelfUpdates++ + Settings.System.putStringForUser( + contentResolver, + SETTING_CONFIG, + arr.toString(), + UserHandle.USER_CURRENT, + ) + } + + private fun updateWidgetSizes() { + val cs = cellSize + val ch = cellHeight + val gap = cellGap + _widgets.forEach { entry -> + val view = entry.hostView ?: return@forEach + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + view.updateAppWidgetSize(Bundle(), pxToDp(w), pxToDp(h), pxToDp(w), pxToDp(h)) + } + _viewGeneration.value++ + } + + private fun updateVisibility() { + val hasViews = _enabled.value && _widgets.any { it.hostView != null } + val vis = if (hasViews) View.VISIBLE else View.GONE + hostView.visibility = vis + container.visibility = vis + } + + private fun onWidgetRemoved(appWidgetId: Int) { + val idx = _widgets.indexOfFirst { it.appWidgetId == appWidgetId } + if (idx < 0) return + val entry = _widgets[idx] + entry.hostView?.let { view -> (view.parent as? ViewGroup)?.removeView(view) } + _widgets.removeAt(idx) + saveConfig() + updateVisibility() + _viewGeneration.value++ + } + + private fun pxToDp(px: Int): Int { + return (px / context.resources.displayMetrics.density).toInt() + } + + @Composable + private fun WidgetGridContent() { + val widgets = _widgets.toList() + val density = LocalDensity.current + val gen = _viewGeneration.value + + if (widgets.isEmpty()) return + + val cs = cellSize + val ch = cellHeight + val gap = cellGap + val maxRow = widgets.maxOf { it.cellY + it.spanY } + val gridHeight = maxRow * ch + (maxRow - 1).coerceAtLeast(0) * gap + val gridWidth = GRID_COLUMNS * cs + (GRID_COLUMNS - 1) * gap + + Box( + modifier = + Modifier.fillMaxWidth() + .wrapContentHeight() + .padding( + start = with(density) { sidePadding.toDp() }, + end = with(density) { sidePadding.toDp() }, + top = 8.dp, + ), + contentAlignment = if (isSplitShade && !_clockCentered.value) Alignment.TopStart else Alignment.TopCenter, + ) { + Box( + modifier = + Modifier.size( + width = with(density) { gridWidth.toDp() }, + height = with(density) { gridHeight.toDp() }, + ) + ) { + widgets.forEach { entry -> + val view = entry.hostView ?: return@forEach + val x = entry.cellX * (cs + gap) + val y = entry.cellY * (ch + gap) + val w = entry.spanX * cs + (entry.spanX - 1).coerceAtLeast(0) * gap + val h = entry.spanY * ch + (entry.spanY - 1).coerceAtLeast(0) * gap + + key(entry.appWidgetId, gen) { + AndroidView( + factory = { _ -> + (view.parent as? ViewGroup)?.removeView(view) + view + }, + modifier = + Modifier.offset { IntOffset(x, y) } + .size( + width = with(density) { w.toDp() }, + height = with(density) { h.toDp() }, + ), + ) + } + } + } + } + } + + companion object { + private const val TAG = "KeyguardWidgetHostCtrl" + private const val RESET_DELAY_MS = 300L + const val MAX_CONTAINER_HEIGHT_DP = 220 + private const val COMPOSE_TOP_PADDING_DP = 8 + const val GRID_COLUMNS = 4 + const val MAX_ROWS = 2 + + const val SETTING_ENABLED = "lockscreen_widgets_enabled" + const val SETTING_CONFIG = "lockscreen_widgets_config" + + private val ENABLED_URI: Uri = Settings.System.getUriFor(SETTING_ENABLED) + private val CONFIG_URI: Uri = Settings.System.getUriFor(SETTING_CONFIG) + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt new file mode 100644 index 0000000000000..55836f94febca --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/KeyguardWidgetInteractionHandler.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2025 AxionOS Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.lockscreen + +import android.app.PendingIntent +import android.view.View +import android.widget.RemoteViews +import com.android.systemui.plugins.ActivityStarter + +class KeyguardWidgetInteractionHandler(private val activityStarter: ActivityStarter) : + RemoteViews.InteractionHandler { + + override fun onInteraction( + view: View, + pendingIntent: PendingIntent, + response: RemoteViews.RemoteResponse, + ): Boolean { + val launchOptions = response.getLaunchOptions(view) + if (pendingIntent.isActivity) { + activityStarter.startPendingIntentMaybeDismissingKeyguard( + pendingIntent, + false, + null, + null, + launchOptions.first, + launchOptions.second?.toBundle(), + null, + ) + } else { + + RemoteViews.startPendingIntent(view, pendingIntent, launchOptions) + } + return true + } +} + diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt index 857a061a17677..18e1f964e9e01 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/controller/KeyguardMediaController.kt @@ -27,6 +27,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.Dumpable +import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.classifier.Classifier import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -84,6 +85,7 @@ constructor( private val mediaViewModelFactory: MediaViewModel.Factory, private val mediaCarouselInteractor: MediaCarouselInteractor, private val falsingSystem: MediaFalsingSystem, + private val keyguardClockRepository: KeyguardClockRepository, ) : Dumpable { private var lastUsedStatusBarState = -1 @@ -177,6 +179,7 @@ constructor( private fun updateResources() { useSplitShade = splitShadeStateController.shouldUseSplitNotificationShade(context.resources) + && !keyguardClockRepository.areLockscreenWidgetsEnabled } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index cef410908b74d..7222a5ca7e1e3 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -17,11 +17,13 @@ package com.android.systemui.power.domain.interactor +import android.graphics.Point import android.os.PowerManager import com.android.systemui.camera.CameraGestureHelper import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -50,6 +52,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val statusBarStateController: StatusBarStateController, private val cameraGestureHelper: Provider, + private val keyguardRepository: KeyguardRepository, ) { /** Whether the screen is on or off. */ val isInteractive: StateFlow = repository.isInteractive @@ -93,6 +96,10 @@ constructor( fun onUserTouch(noChangeLights: Boolean = false) = repository.userTouch(noChangeLights = noChangeLights) + fun setLastTouchToSleepPosition(x: Float, y: Float) { + keyguardRepository.setLastTouchToSleepPosition(Point(x.toInt(), y.toInt())) + } + /** * Wakes up the device if the device was dozing. * diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index c57b53bab4422..cd390cffd85cc 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -60,7 +60,9 @@ enum class WakeSleepReason( FOLD(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD), /** Device goes to sleep because it timed out. */ - TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT); + TIMEOUT(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_TIMEOUT), + + TOUCH_TO_SLEEP(isTouch = true, PowerManager.GO_TO_SLEEP_REASON_TOUCH); companion object { fun fromPowerManagerWakeReason(reason: Int): WakeSleepReason { @@ -84,6 +86,7 @@ enum class WakeSleepReason( PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON -> SLEEP_BUTTON PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD + PowerManager.GO_TO_SLEEP_REASON_TOUCH -> TOUCH_TO_SLEEP else -> OTHER } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java index 91b5b7a7831ed..452ee763a574d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WeatherTile.java @@ -1,6 +1,7 @@ /* * Copyright (C) 2017 The OmniROM project - * Copyright (C) 2022-2025 crDroid Android project + * Copyright (C) 2022-2026 crDroid Android project + * Copyright (C) 2026 AxionOS Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +18,11 @@ package com.android.systemui.qs.tiles; -import android.content.Context; -import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.service.quicksettings.Tile; @@ -32,42 +32,60 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; -import com.android.internal.util.alpha.OmniJawsClient; -import com.android.internal.util.alpha.Utils; +import com.android.systemui.alpha.tiles.dialog.WeatherDialogManager; import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData; +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData; import com.android.systemui.plugins.qs.QSTile.BooleanState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.qs.tileimpl.QSTileImpl; +import com.android.systemui.quicklook.QuickLookClient; import com.android.systemui.res.R; +import com.android.systemui.shared.clocks.WeatherUtils; import javax.inject.Inject; -public class WeatherTile extends QSTileImpl implements OmniJawsClient.OmniJawsObserver { +/** + * Quick Settings weather tile. Data comes from the AxQuickLook privileged service via {@link + * QuickLookClient} (same pipeline as the Axion lockscreen clock), including OmniJaws vs Google + * weather resolution inside AxQuickLook. + */ +public class WeatherTile extends QSTileImpl { public static final String TILE_SPEC = "weather"; - private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; + private static final String AX_QUICK_LOOK_PACKAGE = "com.android.axion.quicklook"; + private static final String AX_QUICK_LOOK_SETTINGS_ACTION = + "com.android.axion.quicklook.SETTINGS"; private static final String TAG = "WeatherTile"; - private static final boolean DEBUG = false; private Drawable mWeatherImage; - private OmniJawsClient.WeatherInfo mWeatherData; - private boolean mEnabled; - private final ActivityStarter mActivityStarter; - private String mFormattedCondition; + private static final boolean DEBUG = false; + + private static final int WEATHER_ICON_DP = 32; - private static final String[] ALTERNATIVE_WEATHER_APPS = { - "cz.martykan.forecastie", - "com.accuweather.android", - "com.wunderground.android.weather", - "com.samruston.weather", - "jp.miyavi.androiod.gnws", - }; + private final ActivityStarter mActivityStarter; + private final QuickLookClient mQuickLookClient; + private final WeatherDialogManager mWeatherDialogManager; + private final Handler mMainHandler; + + /** Non-null only when AxQuickLook reports usable weather. */ + @Nullable private ClockWeatherData mWeather; + + private final QuickLookClient.Callback mQuickLookCallback = + new QuickLookClient.Callback() { + @Override + public void onClockDataChanged(ClockData data) { + ClockWeatherData w = data.getWeather(); + mWeather = isMeaningfulWeather(w) ? w : null; + refreshState(); + } + }; @Inject public WeatherTile( @@ -79,12 +97,22 @@ public WeatherTile( MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, - QSLogger qsLogger - ) { + QSLogger qsLogger, + QuickLookClient quickLookClient, + WeatherDialogManager weatherDialogManager) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); mActivityStarter = activityStarter; + mQuickLookClient = quickLookClient; + mWeatherDialogManager = weatherDialogManager; + mMainHandler = mainHandler; + } + + private static boolean isMeaningfulWeather(@Nullable ClockWeatherData w) { + if (w == null) return false; + boolean hasTemp = w.getTemp() != null && !w.getTemp().isEmpty(); + boolean hasCondition = w.getCondition() != null && !w.getCondition().isEmpty(); + return hasTemp || hasCondition || w.getConditionCode() != 0; } @Override @@ -100,138 +128,102 @@ public BooleanState newTileState() { @Override public void handleSetListening(boolean listening) { if (DEBUG) Log.d(TAG, "setListening " + listening); - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); - if (listening) { - OmniJawsClient.get().addObserver(mContext, this); - queryAndUpdateWeather(); + mQuickLookClient.addCallback(mQuickLookCallback); } else { - OmniJawsClient.get().removeObserver(mContext, this); - } - } - - @Override - public void weatherUpdated() { - if (DEBUG) Log.d(TAG, "weatherUpdated"); - queryAndUpdateWeather(); - } - - @Override - public void weatherError(int errorReason) { - if (DEBUG) Log.d(TAG, "weatherError " + errorReason); - if (errorReason != OmniJawsClient.EXTRA_ERROR_DISABLED) { - mWeatherData = null; - refreshState(); + mQuickLookClient.removeCallback(mQuickLookCallback); } } @Override protected void handleDestroy() { - OmniJawsClient.get().removeObserver(mContext, this); + mQuickLookClient.removeCallback(mQuickLookCallback); super.handleDestroy(); } @Override public boolean isAvailable() { - return OmniJawsClient.get().isOmniJawsServiceInstalled(mContext); + try { + mContext.getPackageManager().getPackageInfo(AX_QUICK_LOOK_PACKAGE, 0); + return true; + } catch (PackageManager.NameNotFoundException e) { + return false; + } } @Override protected void handleClick(@Nullable Expandable expandable) { - if (DEBUG) Log.d(TAG, "handleClick"); - if (!mState.value || mWeatherData == null) { - mActivityStarter.postStartActivityDismissingKeyguard( - OmniJawsClient.get().getSettingsIntent(), 0); - } else { - PackageManager pm = mContext.getPackageManager(); - for (String app: ALTERNATIVE_WEATHER_APPS) { - if (Utils.isPackageInstalled(mContext, app)) { - Intent intent = pm.getLaunchIntentForPackage(app); - if (intent != null) { - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } - } - } - if (Utils.isPackageInstalled(mContext, "com.google.android.googlequicksearchbox")) { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("dynact://velour/weather/ProxyActivity")); - intent.setComponent(new ComponentName("com.google.android.googlequicksearchbox", - "com.google.android.apps.gsa.velour.DynamicActivityTrampoline")); - mActivityStarter.postStartActivityDismissingKeyguard(intent, 0); - } else { - final Intent weatherActivityIntent = new Intent(); - weatherActivityIntent.setAction(Intent.ACTION_MAIN); - weatherActivityIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); - mActivityStarter.postStartActivityDismissingKeyguard(weatherActivityIntent, 0); - } - } - mEnabled = OmniJawsClient.get().isOmniJawsEnabled(mContext); - refreshState(); + // handleClick runs on the background looper; dialog must be shown on the main thread. + mMainHandler.post(() -> mWeatherDialogManager.create(expandable)); } @Override public Intent getLongClickIntent() { - if (DEBUG) Log.d(TAG, "getLongClickIntent"); - return OmniJawsClient.get().getSettingsIntent(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName("com.android.settings", + "com.android.settings.Settings$LockScreenSettingsActivity"); + return intent; } @Override protected void handleUpdateState(BooleanState state, Object arg) { - if (DEBUG) Log.d(TAG, "handleUpdateState " + mEnabled); - state.value = mEnabled; - state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; + boolean hasWeather = mWeather != null; + state.value = hasWeather; + state.state = hasWeather ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE; state.icon = ResourceIcon.get(R.drawable.ic_qs_weather); state.label = mContext.getResources().getString(R.string.omnijaws_label_default); state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_unknown); - if (mEnabled) { - if (mWeatherData == null || mWeatherImage == null) { - state.label = mContext.getResources().getString(R.string.omnijaws_label_default); - state.secondaryLabel = mContext.getResources().getString(R.string.omnijaws_service_error); - } else { - state.icon = new DrawableIcon(mWeatherImage); - state.label = mWeatherData.city; - state.secondaryLabel = mWeatherData.temp + mWeatherData.tempUnits + - " · " + mFormattedCondition; - } + + if (!hasWeather || mWeather == null) { + return; + } + + int iconPx = (int) (mContext.getResources().getDisplayMetrics().density * WEATHER_ICON_DP + 0.5f); + Bitmap bmp = WeatherUtils.INSTANCE.resolveWeatherBitmap(mContext, mWeather, iconPx); + if (bmp != null) { + Drawable d = new BitmapDrawable(mContext.getResources(), bmp); + state.icon = new DrawableIcon(d); + } + + String city = mWeather.getCity(); + if (city != null && !city.isEmpty()) { + state.label = city; + } + + String tempPart = mWeather.getFormattedTemp(); + String cond = localizeCondition(mWeather.getCondition()); + state.secondaryLabel = tempPart + " · " + cond; + } + + /** + * Maps English-ish OmniJaws/Google condition phrases to localized summaries where we have + * matching cr_strings (legacy OmniJaws tile behavior). + */ + private String localizeCondition(String condition) { + if (condition == null || condition.isEmpty()) { + return ""; + } + String lower = condition.toLowerCase(); + if (lower.contains("cloud")) { + return mContext.getResources().getString(R.string.weather_condition_clouds); + } else if (lower.contains("rain")) { + return mContext.getResources().getString(R.string.weather_condition_rain); + } else if (lower.contains("clear")) { + return mContext.getResources().getString(R.string.weather_condition_clear); + } else if (lower.contains("storm")) { + return mContext.getResources().getString(R.string.weather_condition_storm); + } else if (lower.contains("snow")) { + return mContext.getResources().getString(R.string.weather_condition_snow); + } else if (lower.contains("wind")) { + return mContext.getResources().getString(R.string.weather_condition_wind); + } else if (lower.contains("mist")) { + return mContext.getResources().getString(R.string.weather_condition_mist); } + return condition; } @Override public CharSequence getTileLabel() { return mContext.getResources().getString(R.string.omnijaws_label_default); } - - private void queryAndUpdateWeather() { - if (DEBUG) Log.d(TAG, "queryAndUpdateWeather " + mEnabled); - try { - mWeatherData = null; - if (mEnabled) { - OmniJawsClient.get().queryWeather(mContext); - mWeatherData = OmniJawsClient.get().getWeatherInfo(); - mFormattedCondition = mWeatherData.condition; - if (mFormattedCondition.toLowerCase().contains("clouds")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clouds); - } else if (mFormattedCondition.toLowerCase().contains("rain")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_rain); - } else if (mFormattedCondition.toLowerCase().contains("clear")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_clear); - } else if (mFormattedCondition.toLowerCase().contains("storm")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_storm); - } else if (mFormattedCondition.toLowerCase().contains("snow")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_snow); - } else if (mFormattedCondition.toLowerCase().contains("wind")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_wind); - } else if (mFormattedCondition.toLowerCase().contains("mist")) { - mFormattedCondition = mContext.getResources().getString(R.string.weather_condition_mist); - } - if (mWeatherData != null) { - mWeatherImage = OmniJawsClient.get().getWeatherConditionImage(mContext, mWeatherData.conditionCode); - mWeatherImage = mWeatherImage.mutate(); - } - } - } catch(Exception e) { - // Do nothing - } - refreshState(); - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt index bc2bfd7c5de6a..add624fa497e6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/cell/domain/interactor/MobileDataTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.base.domain.model.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.cell.domain.model.MobileDataTileIcon import com.android.systemui.qs.tiles.impl.cell.domain.model.MobileDataTileModel import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import javax.inject.Inject @@ -73,17 +74,28 @@ constructor( } else { combine(it.isDataEnabled, it.signalLevelIcon) { isDataEnabled, signalLevelIcon -> + val useStaticIcon = + ThemeIconController.usesStaticQsCellularTileIcon(context) val icon = if (isDataEnabled) { when (signalLevelIcon) { is SignalIconModel.Cellular -> { - val signalState = - SignalDrawable.getState( - signalLevelIcon.level, - signalLevelIcon.numberOfLevels, - signalLevelIcon.showExclamationMark, + if (useStaticIcon) { + MobileDataTileIcon.ResourceIcon( + Icon.Resource( + ThemeIconController.qsTileFullCellularIconResId(), + ContentDescription.Loaded(mobileDataLabel), + ) ) - MobileDataTileIcon.SignalIcon(signalState) + } else { + val signalState = + SignalDrawable.getState( + signalLevelIcon.level, + signalLevelIcon.numberOfLevels, + signalLevelIcon.showExclamationMark, + ) + MobileDataTileIcon.SignalIcon(signalState) + } } is SignalIconModel.Satellite -> { @@ -98,7 +110,11 @@ constructor( } else { MobileDataTileIcon.ResourceIcon( Icon.Resource( - R.drawable.ic_signal_mobile_data_off, + if (useStaticIcon) { + ThemeIconController.qsTileFullCellularIconResId() + } else { + R.drawable.ic_signal_mobile_data_off + }, ContentDescription.Loaded(mobileDataLabel), ) ) @@ -117,7 +133,14 @@ constructor( icon = MobileDataTileIcon.ResourceIcon( Icon.Resource( - R.drawable.ic_signal_mobile_data_off, + if (ThemeIconController.usesStaticQsCellularTileIcon( + context + ) + ) { + ThemeIconController.qsTileFullCellularIconResId() + } else { + R.drawable.ic_signal_mobile_data_off + }, ContentDescription.Loaded(mobileDataLabel), ) ), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index 01b6a0e6cefd6..43f0a49f024ae 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileMode import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor @@ -72,10 +73,16 @@ constructor( val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { val secondary = removeDoubleQuotes(it.ssid) + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + wifiIcon.icon.resId + } flowOf( InternetTileModel.Active( secondaryTitle = secondary, - icon = InternetTileIconModel.ResourceId(wifiIcon.icon.resId), + icon = InternetTileIconModel.ResourceId(iconResId), stateDescription = wifiIcon.contentDescription, contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"), ) @@ -128,9 +135,21 @@ constructor( dataContentDescription, ) + val icon = + if (ThemeIconController.usesStaticQsCellularTileIcon( + context + ) + ) { + InternetTileIconModel.ResourceId( + ThemeIconController.qsTileFullCellularIconResId() + ) + } else { + InternetTileIconModel.Cellular(signalIcon.level) + } + InternetTileModel.Active( secondaryTitle = secondary, - icon = InternetTileIconModel.Cellular(signalIcon.level), + icon = icon, stateDescription = ContentDescription.Loaded(secondary.toString()), contentDescription = ContentDescription.Loaded(internetLabel), diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt index 6967212568854..7994d69165600 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/wifi/domain/interactor/WifiTileDataInteractor.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.base.domain.model.DataUpdateTrigger import com.android.systemui.qs.tiles.impl.wifi.domain.model.WifiTileModel import com.android.systemui.res.R import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.statusbar.connectivity.ThemeIconController import com.android.systemui.statusbar.connectivity.WifiIcons import com.android.systemui.statusbar.connectivity.ui.MobileContextProvider import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository @@ -70,16 +71,28 @@ constructor( val wifiIcon = WifiIcon.fromModel(it, context, showHotspotInfo = true) if (it is WifiNetworkModel.Active && wifiIcon is WifiIcon.Visible) { val secondary = removeDoubleQuotes(it.ssid) + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + wifiIcon.icon.resId + } flowOf( WifiTileModel.Active( - icon = WifiTileIconModel(wifiIcon.icon.resId), + icon = WifiTileIconModel(iconResId), secondaryLabel = secondary, ) ) } else { + val iconResId = + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } flowOf( WifiTileModel.Inactive( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel(iconResId), secondaryLabel = null, ) ) @@ -235,12 +248,24 @@ constructor( if (toggleState == WifiToggleState.Pausing) { return@combine WifiTileModel.Inactive( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel( + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } + ), secondaryLabel = notConnectedDescription, ) } else if (toggleState == WifiToggleState.Scanning) { return@combine WifiTileModel.Active( - icon = WifiTileIconModel(WifiIcons.WIFI_NO_SIGNAL), + icon = WifiTileIconModel( + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else { + WifiIcons.WIFI_NO_SIGNAL + } + ), secondaryLabel = context.getString(R.string.quick_settings_scanning_for_wifi), ) } @@ -255,7 +280,9 @@ constructor( WifiTileModel.Inactive( icon = WifiTileIconModel( - if (isEnabled) { + if (ThemeIconController.usesStaticQsWifiTileIcon(context)) { + ThemeIconController.qsTileFullWifiIconResId() + } else if (isEnabled) { WifiIcons.WIFI_NO_SIGNAL } else { R.drawable.ic_signal_wifi_off diff --git a/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt index 271c35717b5be..07e3b7eb50d96 100644 --- a/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt +++ b/packages/SystemUI/src/com/android/systemui/quicklook/QuickLookClient.kt @@ -28,9 +28,17 @@ import com.android.axion.quicklook.IAxQuickLookService import com.android.axion.quicklook.IQuickLookCallback import com.android.axion.quicklook.QuickLookTarget import com.android.axion.quicklook.SportsData +import com.android.axion.quicklook.calendarData +import com.android.axion.quicklook.mediaData import com.android.axion.quicklook.nowPlayingData +import com.android.axion.quicklook.smartspaceData import com.android.axion.quicklook.sportsData +import com.android.axion.quicklook.weatherData import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.plugins.keyguard.ui.clocks.CalendarSimpleData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockData +import com.android.systemui.plugins.keyguard.ui.clocks.ClockWeatherData +import com.android.systemui.plugins.keyguard.ui.clocks.SmartspaceData import com.android.systemui.util.WeakListenerManager import javax.inject.Inject @@ -40,6 +48,9 @@ class QuickLookClient @Inject constructor( ) { interface Callback { + fun onClockDataChanged(data: ClockData) {} + fun onQLPlaybackStateChanged(play: Boolean) {} + fun onQLMetadataChanged(track: String, artist: String, packageName: String) {} fun onNowPlayingUpdate(nowPlayingText: String, tapAction: PendingIntent?) {} fun onSportsUpdate(sports: List) {} } @@ -49,6 +60,11 @@ class QuickLookClient @Inject constructor( private var isBound = false private var rebindAttempts = 0 + private var cachedClockData: ClockData = ClockData.EMPTY + private var cachedMediaPlaying: Boolean = false + private var cachedTrack: String = "" + private var cachedArtist: String = "" + private var cachedMediaPackage: String = "" private var cachedNowPlaying: String = "" private var cachedNowPlayingAction: PendingIntent? = null private var cachedSports: List = emptyList() @@ -97,6 +113,13 @@ class QuickLookClient @Inject constructor( } private fun deliverCachedData(callback: Callback) { + if (cachedClockData != ClockData.EMPTY) { + callback.onClockDataChanged(cachedClockData) + } + callback.onQLPlaybackStateChanged(cachedMediaPlaying) + if (cachedTrack.isNotEmpty() || cachedArtist.isNotEmpty()) { + callback.onQLMetadataChanged(cachedTrack, cachedArtist, cachedMediaPackage) + } if (cachedNowPlaying.isNotEmpty()) { callback.onNowPlayingUpdate(cachedNowPlaying, cachedNowPlayingAction) } @@ -106,11 +129,63 @@ class QuickLookClient @Inject constructor( } private fun processTargets(targets: List) { + var weather: ClockWeatherData = ClockWeatherData.EMPTY + var calendar: CalendarSimpleData = CalendarSimpleData.EMPTY + var hasMedia = false var hasNowPlaying = false + val smartspaceTargets = mutableListOf() val sportsTargets = mutableListOf() for (target in targets) { when (target.targetType) { + QuickLookTarget.TYPE_WEATHER -> { + val w = target.weatherData ?: continue + weather = ClockWeatherData( + temp = w.temp, + condition = w.condition, + conditionCode = w.conditionCode, + city = w.city, + humidity = w.humidity, + wind = w.wind, + windDirection = w.windDirection, + tempUnit = w.tempUnit, + windUnit = w.windUnit, + pinWheel = w.pinWheel, + timestamp = w.timestamp, + iconBytes = w.iconBytes, + tintIcon = w.iconBytes == null, + tapAction = target.primaryAction?.pendingIntent, + ) + } + QuickLookTarget.TYPE_CALENDAR -> { + val c = target.calendarData ?: continue + calendar = CalendarSimpleData( + id = c.id, + title = c.title, + startTime = c.startTime, + endTime = c.endTime, + location = c.location, + description = c.description, + formattedTime = c.formattedTime, + eventStatus = c.eventStatus, + tapAction = target.primaryAction?.pendingIntent, + ) + } + QuickLookTarget.TYPE_MEDIA -> { + hasMedia = true + val m = target.mediaData ?: continue + + if (m.isPlaying != cachedMediaPlaying) { + cachedMediaPlaying = m.isPlaying + callbacks.notify { it.onQLPlaybackStateChanged(m.isPlaying) } + } + if (m.track != cachedTrack || m.artist != cachedArtist || m.packageName != cachedMediaPackage) { + cachedTrack = m.track + cachedArtist = m.artist + cachedMediaPackage = m.packageName ?: "" + callbacks.notify { it.onQLMetadataChanged(m.track, m.artist, cachedMediaPackage) } + } + } QuickLookTarget.TYPE_NOW_PLAYING -> { hasNowPlaying = true val np = target.nowPlayingData ?: continue @@ -131,15 +206,65 @@ class QuickLookClient @Inject constructor( "${s.team1Name} vs ${s.team2Name}" else -> target.title ?: "" } + if (sportTitle.isNotEmpty()) { + smartspaceTargets.add(SmartspaceData( + id = target.id, + title = sportTitle, + subtitle = s.statusDetail, + featureType = 9, + iconBytes = target.iconBytes, + componentName = null, + isSensitive = false, + sourceType = QuickLookTarget.TYPE_SPORTS, + creationTime = target.creationTime, + score = target.score, + tapAction = target.primaryAction?.pendingIntent, + )) + } + } + QuickLookTarget.TYPE_SMARTSPACER, + QuickLookTarget.TYPE_GOOGLE_SMARTSPACE -> { + val s = target.smartspaceData ?: continue + smartspaceTargets.add(SmartspaceData( + id = s.id, + title = s.title, + subtitle = s.subtitle, + featureType = s.featureType, + iconBytes = s.iconBytes, + componentName = s.componentName, + isSensitive = s.isSensitive, + sourceType = s.sourceType, + creationTime = s.creationTime, + score = s.score, + tapAction = target.primaryAction?.pendingIntent, + )) } } } + val newClockData = ClockData(weather, calendar, smartspaceTargets) + if (newClockData != cachedClockData) { + cachedClockData = newClockData + callbacks.notify { it.onClockDataChanged(newClockData) } + } + if (sportsTargets != cachedSports) { cachedSports = sportsTargets callbacks.notify { it.onSportsUpdate(sportsTargets) } } + if (!hasMedia) { + if (cachedMediaPlaying) { + cachedMediaPlaying = false + callbacks.notify { it.onQLPlaybackStateChanged(false) } + } + if (cachedTrack.isNotEmpty() || cachedArtist.isNotEmpty()) { + cachedTrack = "" + cachedArtist = "" + cachedMediaPackage = "" + callbacks.notify { it.onQLMetadataChanged("", "", "") } + } + } if (!hasNowPlaying && cachedNowPlaying.isNotEmpty()) { cachedNowPlaying = "" cachedNowPlayingAction = null @@ -191,3 +316,4 @@ class QuickLookClient @Inject constructor( private const val MAX_REBIND_DELAY_MS = 30000L } } + diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 2eb937d5c2617..db9428a3a4aed 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -71,6 +71,7 @@ import android.os.Handler; import android.os.PowerManager; import android.os.Trace; +import android.os.UserHandle; import android.provider.Settings; import android.util.IndentingPrintWriter; import android.util.Log; @@ -829,7 +830,9 @@ public void onViewDetachedFromWindow(View v) {} @Override public boolean onDoubleTap(MotionEvent e) { if (mPowerManager != null) { - mPowerManager.goToSleep(e.getEventTime()); + mPowerInteractor.setLastTouchToSleepPosition(e.getX(), e.getY()); + mPowerManager.goToSleep(e.getEventTime(), + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0); } return true; } @@ -1252,13 +1255,25 @@ private ClockSize computeDesiredClockSize() { } private ClockSize computeDesiredClockSizeForSingleShade() { - if (hasVisibleNotifications()) { + if (hasVisibleNotifications() || areLockscreenWidgetsEnabled()) { return ClockSize.SMALL; } return ClockSize.LARGE; } + private boolean areLockscreenWidgetsEnabled() { + int enabled = Settings.System.getIntForUser( + mContentResolver, "lockscreen_widgets_enabled", 0, + UserHandle.USER_CURRENT); + String config = Settings.System.getStringForUser( + mContentResolver, "lockscreen_widgets_config", + UserHandle.USER_CURRENT); + return enabled == 1 && config != null && !config.isEmpty(); + } private ClockSize computeDesiredClockSizeForSplitShade() { + if (areLockscreenWidgetsEnabled()) { + return ClockSize.SMALL; + } // Media is not visible to the user on AOD. boolean isMediaVisibleToUser = mMediaDataManager.hasActiveMedia() && !isOnAod(); @@ -3338,7 +3353,6 @@ private void updateVisibility() { mView.setVisibility(shouldPanelBeVisible() ? VISIBLE : INVISIBLE); } - @Override public void updateExpansionAndVisibility() { if (!SceneContainerFlag.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt index fc53866cef7de..996058524d5ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt @@ -121,10 +121,10 @@ constructor( return false } - return onDoubleTapEvent() + return onDoubleTapEvent(e.x, e.y) } - fun onDoubleTapEvent(): Boolean { + fun onDoubleTapEvent(x: Float = 0f, y: Float = 0f): Boolean { // React to the [MotionEvent.ACTION_UP] event after double tap is detected. Falsing // checks MUST be on the ACTION_UP event. if ( @@ -133,6 +133,7 @@ constructor( !falsingManager.isProximityNear && !falsingManager.isFalseDoubleTap ) { + dozeInteractor.setLastTapToWakePosition(Point(x.toInt(), y.toInt())) powerInteractor.wakeUpIfDozing("PULSING_DOUBLE_TAP", PowerManager.WAKE_REASON_TAP) return true } diff --git a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt index 002aeb31629f0..2126e512defa1 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/QQSGestureListener.kt @@ -26,9 +26,10 @@ import android.view.MotionEvent import com.android.systemui.dagger.SysUISingleton import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import javax.inject.Inject @SysUISingleton @@ -39,6 +40,7 @@ class QQSGestureListener @Inject constructor( private val statusBarStateController: StatusBarStateController, private val selectedUserInteractor: SelectedUserInteractor, private val centralSurfaces: CentralSurfaces, + private val powerInteractor: PowerInteractor, ) : GestureDetector.SimpleOnGestureListener() { private var doubleTapToSleepEnabled = false @@ -80,7 +82,9 @@ class QQSGestureListener @Inject constructor( e.getY() < quickQsOffsetHeight && !falsingManager.isFalseDoubleTap ) { - powerManager.goToSleep(e.getEventTime()) + powerInteractor.setLastTouchToSleepPosition(e.x, e.y) + powerManager.goToSleep(e.eventTime, + PowerManager.GO_TO_SLEEP_REASON_TOUCH, 0) return true } else if (!statusBarStateController.isDozing && lockscreenDT2SEnabled && diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index 39a95756b66cb..670cc06107e5e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -1007,6 +1007,10 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto mClipRect.set(0, -height, getWidth(), height); if (mShelfIcons != null) { mShelfIcons.setClipBounds(mClipRect); + if (mAmbientState.isDozing() && mShelfIcons.shouldCenterIcons()) { + mShelfIcons.calculateIconXTranslations(); + mShelfIcons.applyIconStates(); + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt index 7402e7cd7ebfc..be66557eaedf9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ThemeIconController.kt @@ -36,6 +36,12 @@ object ThemeIconController { private const val NEW_WIFI_WIDTH_DP = 17f private const val NEW_WIFI_HEIGHT_DP = 12.58f + /** + * Applied when rendering ThemeEngine wifi/signal bitmaps (dedicated RROs and icon-pack + * fallbacks). Overlay vectors often use tighter padding than stock status-bar art. + */ + private const val THEME_ICON_SIZE_SCALE = 1.1f + private val SIGNAL_4BAR_NAMES = arrayOf( "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", @@ -106,6 +112,57 @@ object ThemeIconController { return engine.isTargetedResource(SIGNAL_4BAR_NAMES[0]) } + /** + * Returns true when a dedicated wifi RRO or an icon pack with wifi art is active. + * + * Business rules for QS wifi tile icon selection: + * 1. Dedicated wifi RRO active → render themed wifi icon (this gate). + * 2. Icon pack active but no dedicated wifi RRO → render icon-pack wifi art (this gate, + * because icon packs register ic_wifi_signal_* in the android overlay via ThemeEngine). + * 3. Neither active → AOSP path (gate returns false). + * + * Checked independently from [usesStaticQsCellularTileIcon] — a wifi RRO being active + * must not affect the cellular tile, and vice versa. + */ + @JvmStatic + fun usesStaticQsWifiTileIcon(context: Context): Boolean { + return ThemeEngine.getInstance(context)?.isTargetedResource("ic_wifi_signal_4") == true + } + + /** + * Returns true when a dedicated signal RRO or an icon pack with signal art is active. + * + * Business rules for QS cellular tile icon selection: + * 1. Dedicated signal RRO active → render themed cellular icon (this gate). + * 2. Icon pack active but no dedicated signal RRO → render icon-pack signal art (this gate, + * because icon packs register ic_signal_cellular_* in the android overlay via ThemeEngine). + * 3. Neither active → AOSP path (gate returns false). + * + * Checked independently from [usesStaticQsWifiTileIcon] — a signal RRO being active + * must not affect the wifi tile, and vice versa. + */ + @JvmStatic + fun usesStaticQsCellularTileIcon(context: Context): Boolean { + return hasThemedSignalIcons(context) + } + + /** + * Resource ID of the full-strength QS wifi glyph (last entry of [WifiIcons.WIFI_FULL_ICONS]). + * Used by QS interactors when [usesStaticQsWifiTileIcon] is true. + */ + @JvmStatic + fun qsTileFullWifiIconResId(): Int = + com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS[ + com.android.systemui.statusbar.connectivity.WifiIcons.WIFI_FULL_ICONS.size - 1] + + /** + * Resource ID of the full-strength QS cellular static glyph. + * Used by QS interactors when [usesStaticQsCellularTileIcon] is true. + */ + @JvmStatic + fun qsTileFullCellularIconResId(): Int = + com.android.settingslib.R.drawable.ic_mobile_4_4_bar + @JvmStatic fun getThemedWifiIcon(context: Context, resId: Int): Drawable? { val level = mapWifiResIdToLevel(context, resId) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 83e4601829a75..22a86bd29f594 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -34,8 +34,6 @@ import android.os.UserHandle import android.provider.Settings import android.provider.Settings.Secure.LOCK_SCREEN_ALLOW_PRIVATE_NOTIFICATIONS import android.provider.Settings.Secure.LOCK_SCREEN_SHOW_NOTIFICATIONS -import android.provider.Settings.Secure.LOCKSCREEN_SMARTSPACE_ENABLED -import android.provider.Settings.System.LOCKSCREEN_WEATHER_ENABLED import android.util.Log import android.view.ContextThemeWrapper import android.view.View @@ -307,26 +305,29 @@ constructor( get() = datePlugin != null && weatherPlugin != null val isWeatherEnabled: Boolean - get() { - val showWeather = - secureSettings.getIntForUser(LOCKSCREEN_SMARTSPACE_ENABLED, 1, userTracker.userId) == - 1 - return showWeather - } + get() = false val isOmniWeatherEnabled: Boolean - get() { - val showCustomWeather = - systemSettings.getIntForUser( - LOCKSCREEN_WEATHER_ENABLED, - 0, - userTracker.userId, - ) == 1 - return showCustomWeather && !isWeatherEnabled && !isCustomClockEnabled - } - + get() = false + + /** + * BC smartspace is permanently disabled. Axion clocks render their own date/weather/ + * alarm/now-playing via AxQuickLook — the BC pipeline (date_smartspace_view, + * weather_smartspace_view, bc_smartspace_view) would always duplicate that content. + * See axion-integration-plan.md §12. + */ val isEnabled: Boolean - get() = plugin != null && isWeatherEnabled + get() = false + + /** + * Whether some path is responsible for surfacing smartspace-style content (next alarm, + * now-playing, calendar, weather) on the lockscreen — either the BC plugin (source = + * GOOGLE) or the Axion clock + AxQuickLook bridge (source = AXION). Used by view + * sections (e.g. the legacy keyguard slice) that should not draw redundant content + * while either of those paths is active. + */ + val isSmartspaceContentActive: Boolean + get() = true private fun updateBypassEnabled() { val bypassEnabled = bypassController.bypassEnabled diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 8bd771de901a2..94ddbcc60c8e4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -89,7 +89,6 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.SystemBarUtils; import com.android.keyguard.BouncerPanelExpansionCalculator; -import com.android.keyguard.KeyguardSliceView; import com.android.systemui.Dependency; import com.android.systemui.Dumpable; import com.android.systemui.ExpandHelper; @@ -390,6 +389,13 @@ public boolean onPreDraw() { // Rect of QsHeader. Kept as a field just to avoid creating a new one each time. private final Rect mQsHeaderBound = new Rect(); private boolean mContinuousShadowUpdate; + + private float mLastAvoidOverlapScrollY = Float.NaN; + private float mLastAvoidOverlapExpansionFraction = Float.NaN; + private int mLastAvoidOverlapChildCount = -1; + private static final float AVOID_OVERLAP_SCROLL_EPSILON = 0.5f; + private static final float AVOID_OVERLAP_FRACTION_EPSILON = 1e-3f; + private final ViewTreeObserver.OnPreDrawListener mShadowUpdater = () -> { updateViewShadows(); return true; @@ -1504,6 +1510,25 @@ void avoidNotificationOverlaps() { if (!physicalNotificationMovement()) { return; } + + float scrollY = getOwnScrollY(); + float expansionFraction = mAmbientState.getExpansionFraction(); + int childCount = getChildCount(); + // Epsilon comparisons defeat FPU jitter on otherwise-identical interpolated + // inputs during a steady fling. Tracking childCount as a cache key catches + // silent mutations (add/remove paths that fail to call requestChildrenUpdate()). + if (Math.abs(mLastAvoidOverlapScrollY - scrollY) < AVOID_OVERLAP_SCROLL_EPSILON + && Math.abs(mLastAvoidOverlapExpansionFraction - expansionFraction) + < AVOID_OVERLAP_FRACTION_EPSILON + && mLastAvoidOverlapChildCount == childCount + && !mChildrenUpdateRequested + && !isCurrentlyAnimating()) { + return; + } + mLastAvoidOverlapScrollY = scrollY; + mLastAvoidOverlapExpansionFraction = expansionFraction; + mLastAvoidOverlapChildCount = childCount; + createSortedNotificationLists(mTmpSortedChildren, mTmpNonOverlapChildren); // Now lets update the overlaps for the views, ensuring that we set the values for every // view, otherwise they might get stuck. @@ -1590,6 +1615,18 @@ void avoidNotificationOverlaps() { * getLocationOnScreen[1] however this also works for transientViews. */ private float getRelativePosition(ExpandableView expandableView) { + ViewParent viewParent = expandableView.getParent(); + if (viewParent == this && expandableView.hasIdentityMatrix()) { + // Common steady-state case: direct child with no in-flight transform. + // The layout Y equals the visual Y, so we can skip both the matrix + // mapPoints() call and the parent-chain walk below. The identity-matrix + // guard is required because rows use setTranslationY() heavily during + // appear/disappear, swipe-dismiss, and heads-up entry animations; without + // it the overlap clipping would be computed from the layout position + // instead of the animated visual position. + return expandableView.getTop(); + } + mTempFloat2[0] = 0f; mTempFloat2[1] = 0f; if (!expandableView.hasIdentityMatrix()) { @@ -1598,7 +1635,6 @@ private float getRelativePosition(ExpandableView expandableView) { mTempFloat2[1] += expandableView.getTop(); - ViewParent viewParent = expandableView.getParent(); while (viewParent instanceof View && viewParent != this) { final View view = (View) viewParent; @@ -3904,7 +3940,7 @@ private void generateTopPaddingEvent() { if (mAmbientState.isDozing()) { event = new AnimationEvent(null /* view */, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED, - KeyguardSliceView.DEFAULT_ANIM_DURATION); + 550); } else { event = new AnimationEvent(null /* view */, AnimationEvent.ANIMATION_TYPE_TOP_PADDING_CHANGED); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java index 8006657eb9e67..667cb93f7ce7c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ViewState.java @@ -25,8 +25,7 @@ import android.animation.ObjectAnimator; import android.animation.PropertyValuesHolder; import android.animation.ValueAnimator; -import android.os.Handler; -import android.os.Looper; + import android.util.Log; import android.util.Property; import android.view.View; @@ -50,8 +49,7 @@ import kotlin.jvm.functions.Function1; import java.io.PrintWriter; -import java.util.Map; -import java.util.WeakHashMap; + import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -62,18 +60,7 @@ */ public class ViewState implements Dumpable { - private static final Handler sLayerHandler = new Handler(Looper.getMainLooper()); - private static final long LAYER_DEBOUNCE_MS = 150; - private static final Map sPendingLayerChanges = new WeakHashMap<>(); - private static void debouncedSetFaded(View view, Runnable action) { - Runnable prev = sPendingLayerChanges.get(view); - if (prev != null) { - sLayerHandler.removeCallbacks(prev); - } - sPendingLayerChanges.put(view, action); - sLayerHandler.postDelayed(action, LAYER_DEBOUNCE_MS); - } public ViewState() { this(physicalNotificationMovement()); @@ -354,7 +341,7 @@ public void applyToView(View view) { FadeOptimizedNotification fadeOptimizedView = (FadeOptimizedNotification) view; boolean isFaded = fadeOptimizedView.isNotificationFaded(); if (isFaded != becomesFaded) { - debouncedSetFaded(view, () -> fadeOptimizedView.setNotificationFaded(becomesFaded)); + fadeOptimizedView.setNotificationFaded(becomesFaded); } } else { boolean newLayerTypeIsHardware = becomesFaded && view.hasOverlappingRendering(); @@ -362,7 +349,7 @@ public void applyToView(View view) { int newLayerType = newLayerTypeIsHardware ? View.LAYER_TYPE_HARDWARE : View.LAYER_TYPE_NONE; if (layerType != newLayerType) { - debouncedSetFaded(view, () -> view.setLayerType(newLayerType, null)); + view.setLayerType(newLayerType, null); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 28271e21e23f9..0d9dd0753c47d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -29,6 +29,7 @@ import android.graphics.Rect; import android.graphics.drawable.Icon; import android.os.Handler; +import android.os.Looper; import android.os.UserHandle; import android.provider.Settings; import android.util.AttributeSet; @@ -46,6 +47,7 @@ import com.android.internal.statusbar.StatusBarIcon; import com.android.settingslib.Utils; import com.android.systemui.res.R; +import com.android.systemui.shared.clocks.ClockSettingsRepository; import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.headsup.shared.StatusBarNoHunBehavior; import com.android.systemui.statusbar.notification.stack.AnimationFilter; @@ -170,6 +172,8 @@ public AnimationFilter getAnimationFilter() { private int mThemedTextColorPrimaryInverse; @Nullable private Runnable mIsolatedIconAnimationEndRunnable; private boolean mUseIncreasedIconScale; + private boolean mShouldCenterIcons = false; + private final ContentObserver mClockSettingsObserver; private SettingsObserver mSettingsObserver; @@ -177,6 +181,12 @@ public NotificationIconContainer(Context context, AttributeSet attrs) { super(context, attrs); initResources(); setWillNotDraw(!(DEBUG || DEBUG_OVERFLOW)); + mClockSettingsObserver = new ContentObserver(new Handler(Looper.getMainLooper())) { + @Override + public void onChange(boolean selfChange) { + updateShouldCenterIcons(); + } + }; } private void initResources() { @@ -258,6 +268,18 @@ protected void onAttachedToWindow() { super.onAttachedToWindow(); mSettingsObserver = new SettingsObserver(new Handler()); mSettingsObserver.register(); + ClockSettingsRepository.init(getContext()); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.clockFaceUri, + false, + mClockSettingsObserver + ); + getContext().getContentResolver().registerContentObserver( + ClockSettingsRepository.alignmentUri, + false, + mClockSettingsObserver + ); + updateShouldCenterIcons(); } @Override @@ -266,6 +288,7 @@ protected void onDetachedFromWindow() { if (mSettingsObserver != null) { mSettingsObserver.unregister(); } + getContext().getContentResolver().unregisterContentObserver(mClockSettingsObserver); } @Override @@ -632,7 +655,23 @@ protected float getRightBound() { * @return The left boundary (not the RTL compatible start) of the area that icons can be added. */ protected float getLeftBound() { - return getActualPaddingStart(); + float basePadding = getActualPaddingStart(); + if (mShouldCenterIcons && getChildCount() > 0) { + float iconsWidth = 0; + int childCount = getChildCount(); + int maxVisibleIcons = mMaxIcons; + int showCount = Math.min(childCount, maxVisibleIcons); + for (int i = 0; i < showCount; i++) { + View child = getChildAt(i); + iconsWidth += child.getWidth() * getDrawingScale(child); + } + if (childCount > maxVisibleIcons) { + iconsWidth += mStaticDotDiameter + mDotPadding; + } + float centeredPadding = Math.max(0, (getWidth() - iconsWidth) / 2); + return centeredPadding; + } + return basePadding; } protected float getActualPaddingEnd() { @@ -945,4 +984,14 @@ public void initFrom(View view) { } } } + private void updateShouldCenterIcons() { + final boolean shouldCenter = ClockSettingsRepository.shouldCenterIcons(getContext()); + if (mShouldCenterIcons != shouldCenter) { + mShouldCenterIcons = shouldCenter; + requestLayout(); + } + } + public boolean shouldCenterIcons() { + return mShouldCenterIcons; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 75dc42ba4b4ee..642fb231a9622 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -23,6 +23,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.domain.interactor.DisplayStateInteractor import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.res.R import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor import com.android.systemui.shade.domain.interactor.ShadeLockscreenInteractor @@ -31,6 +32,7 @@ import com.android.systemui.statusbar.CircleReveal import com.android.systemui.statusbar.LiftReveal import com.android.systemui.statusbar.LightRevealEffect import com.android.systemui.statusbar.LightRevealScrim +import com.android.systemui.statusbar.PowerButtonReveal import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.StatusBarStateControllerImpl @@ -132,10 +134,12 @@ constructor( } override fun onAnimationStart(animation: Animator) { - if (dozeParameters.get().isMinModeActive()) { - lightRevealScrim.revealEffect = LiftReveal - } else { - lightRevealScrim.revealEffect = revealEffect + if (!ambientAod()) { + if (dozeParameters.get().isMinModeActive()) { + lightRevealScrim.revealEffect = LiftReveal + } else { + lightRevealScrim.revealEffect = revealEffect + } } interactionJankMonitor.begin( notifShadeWindowControllerLazy.get().windowRootView, @@ -298,6 +302,15 @@ constructor( lightRevealAnimator.setDuration(LIGHT_REVEAL_ANIMATION_DURATION_MINMODE) } + if (wakefulnessLifecycle.lastSleepReason == PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON) { + val powerButtonY = context.resources.getDimensionPixelSize( + R.dimen.physical_power_button_center_screen_location_y + ).toFloat() + revealEffect = PowerButtonReveal(powerButtonY) + } else { + revealEffect = LiftReveal + } + // Start the animation on the next frame. startAnimation() is called after // PhoneWindowManager makes a binder call to System UI on // IKeyguardService#onStartedGoingToSleep(). By the time we get here, system_server is diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index 309217ce52bff..627814622907a 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -190,7 +190,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, OVERLAY_CATEGORY_SYSTEM_PALETTE, OVERLAY_CATEGORY_ACCENT_COLOR, OVERLAY_CATEGORY_DYNAMIC_COLOR, OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, - OVERLAY_CATEGORY_ICON_ANDROID)); + OVERLAY_CATEGORY_ICON_ANDROID, + OVERLAY_CATEGORY_ICON_SIGNAL, OVERLAY_CATEGORY_ICON_WIFI)); mTargetPackageToCategories.put(SYSUI_PACKAGE, Sets.newHashSet(OVERLAY_CATEGORY_ICON_SYSUI, OVERLAY_CATEGORY_BACK_GESTURE, OVERLAY_CATEGORY_CHARGING_ANIMATION, OVERLAY_CATEGORY_BATTERY_STYLE)); @@ -206,8 +207,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, mCategoryToTargetPackage.put(OVERLAY_CATEGORY_CHARGING_ANIMATION, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_BATTERY_STYLE, SYSUI_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SETTINGS, SETTINGS_PACKAGE); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SIGNAL, SYSUI_PACKAGE); - mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_WIFI, SYSUI_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SIGNAL, ANDROID_PACKAGE); + mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_WIFI, ANDROID_PACKAGE); dumpManager.registerDumpable(TAG, this); } diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 3f996c35e41f3..e91dc419d85e9 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -68,6 +68,8 @@ import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import com.android.internal.graphics.ColorUtils; +import com.android.internal.graphics.cam.Cam; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -168,6 +170,8 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { protected int mMainWallpaperColor = Color.TRANSPARENT; // UI contrast as reported by UiModeManager private double mContrast = 0.0; + private double mChromaBoost = 0.0; + private boolean mIsFidelityEnabled = true; // Theme variant: Vibrant, Tonal, Expressive, etc @VisibleForTesting @ThemeStyle.Type @@ -682,6 +686,7 @@ private void reevaluateSystemTheme(boolean forceReload) { mMainWallpaperColor = mainColor; if (mIsMonetEnabled) { + fetchCustomThemeSettings(); mThemeStyle = fetchThemeStyleFromSetting(); mMonetParams = fetchMonetParamsFromSetting(); createOverlays(mMainWallpaperColor); @@ -696,6 +701,29 @@ private void reevaluateSystemTheme(boolean forceReload) { updateThemeOverlays(); } + private void fetchCustomThemeSettings() { + final String overlayPackageJson = mSecureSettings.getStringForUser( + Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, + mUserTracker.getUserId()); + if (!TextUtils.isEmpty(overlayPackageJson)) { + try { + JSONObject object = new JSONObject(overlayPackageJson); + mContrast = object.optDouble("_contrast_level", 0.0); + mChromaBoost = object.optDouble("_chroma_boost", 0.0); + mIsFidelityEnabled = object.has(OVERLAY_FIDELITY) + ? object.optInt(OVERLAY_FIDELITY, 0) == 1 + : object.optBoolean("_fidelity_enabled", false); + if (DEBUG) { + Log.d(TAG, "Custom theme settings: contrast=" + mContrast + + " chromaBoost=" + mChromaBoost + + " fidelity=" + mIsFidelityEnabled); + } + } catch (JSONException e) { + Log.w(TAG, "Failed to parse custom theme settings.", e); + } + } + } + /** * Return the main theme color from a given {@link WallpaperColors} instance. */ @@ -725,19 +753,23 @@ protected boolean isPrivateProfile(UserHandle userHandle) { } private void createOverlays(int color) { + int style = mThemeStyle; + if (mIsFidelityEnabled) { + style = ThemeStyle.CONTENT; + } // Keep SystemUI resilient: if custom Monet params trigger an unexpected runtime issue, // fall back to default params instead of crashing the process. try { - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast, + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast, mMonetParams); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast, + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast, mMonetParams); } catch (RuntimeException e) { Log.w(TAG, "Failed to build ColorScheme with custom MonetParams, falling back", e); mMonetParams = MonetParams.DEFAULT; - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast, + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast, mMonetParams); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast, + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast, mMonetParams); } mColorScheme = isNightMode() ? mDarkColorScheme : mLightColorScheme; @@ -759,20 +791,46 @@ private void createOverlays(int color) { private void assignColorsToOverlay(FabricatedOverlay overlay, List> colors, Boolean isFixed) { - colors.forEach(p -> { + for (Pair p : colors) { + String prefix = "android:color/system_" + p.first; if (isFixed) { - overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - return; + int original = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int boosted = boostChroma(original); + overlay.setResourceValue(prefix, TYPE_INT_COLOR_ARGB8, boosted, null); + continue; } - overlay.setResourceValue(prefix + "_light", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mLightColorScheme.getMaterialScheme()), null); - overlay.setResourceValue(prefix + "_dark", TYPE_INT_COLOR_ARGB8, - p.second.getArgb(mDarkColorScheme.getMaterialScheme()), null); - }); + int lightOriginal = p.second.getArgb(mLightColorScheme.getMaterialScheme()); + int darkOriginal = p.second.getArgb(mDarkColorScheme.getMaterialScheme()); + + int boostedLight = boostChroma(lightOriginal); + int boostedDark = boostChroma(darkOriginal); + + overlay.setResourceValue(prefix + "_light", + TYPE_INT_COLOR_ARGB8, boostedLight, null); + + overlay.setResourceValue(prefix + "_dark", + TYPE_INT_COLOR_ARGB8, boostedDark, null); + } + } + + private int boostChroma(int argb) { + Cam cam = Cam.fromInt(argb); + + final float chromaBoost = (float) mChromaBoost; + + float boostedChroma = cam.getChroma() * (1f + chromaBoost/ 100f); + boostedChroma = Math.min(boostedChroma, 150f); + + int boosted = ColorUtils.CAMToColor( + cam.getHue(), + boostedChroma, + cam.getJ() + ); + + return ColorUtils.setAlphaComponent(boosted, Color.alpha(argb)); } /** @@ -980,7 +1038,7 @@ private int fetchThemeStyleFromSetting() { try { JSONObject object = new JSONObject(overlayPackageJson); style = ThemeStyle.valueOf( - object.getString(OVERLAY_CATEGORY_THEME_STYLE)); + object.optString(OVERLAY_CATEGORY_THEME_STYLE, ThemeStyle.name(ThemeStyle.TONAL_SPOT))); if (!validStyles.contains(style)) { style = ThemeStyle.TONAL_SPOT; } diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt deleted file mode 100644 index 96cc77ceecb5b..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2023-2024 risingOS Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.weather - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.view.View.MeasureSpec -import android.widget.ImageView -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherImageView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : ImageView(context, attrs, defStyle) { - - private val maxSizePx: Int = - context.resources.getDimension(R.dimen.weather_image_max_size).toInt() - - private val weatherViewController: WeatherViewController - - init { - visibility = View.GONE - - val stubText = TextView(context) - - weatherViewController = WeatherViewController( - context = context, - weatherIcon = this, - weatherTemp = stubText, - weatherInfoView = this, - ) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - weatherViewController.init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - weatherViewController.removeObserver() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val width = maxSizePx.coerceAtMost(MeasureSpec.getSize(widthMeasureSpec)) - val height = maxSizePx.coerceAtMost(MeasureSpec.getSize(heightMeasureSpec)) - setMeasuredDimension(width, height) - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt deleted file mode 100644 index 531c2e486ad15..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherInfoView.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2025 the AxionAOSP Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.weather - -import android.content.Context -import android.util.AttributeSet -import androidx.constraintlayout.widget.ConstraintLayout -import android.widget.ImageView -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherInfoView @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyle: Int = 0 -) : ConstraintLayout(context, attrs, defStyle) { - - private lateinit var weatherIcon: ImageView - private lateinit var weatherTemp: TextView - private lateinit var controller: WeatherViewController - private var initialized = false - - fun init() { - if (initialized) return - initialized = true - - weatherIcon = findViewById(R.id.weather_icon) - weatherTemp = findViewById(R.id.weather_temp) - - controller = WeatherViewController(context, weatherIcon, weatherTemp, this) - controller.init() - } - - fun cleanup() { - if (::controller.isInitialized) controller.removeObserver() - initialized = false - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - if (!initialized) init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - cleanup() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt deleted file mode 100644 index 21d43d0e8aa5f..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2023-2024 risingOS Android Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.weather - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import android.widget.TextView -import com.android.systemui.res.R - -class WeatherTextView @JvmOverloads constructor( - context: Context, - attrs: AttributeSet? = null, - defStyle: Int = 0 -) : TextView(context, attrs, defStyle) { - - private val mWeatherViewController: WeatherViewController - - init { - visibility = View.GONE - - val stubIcon = android.widget.ImageView(context) - - mWeatherViewController = WeatherViewController( - context = context, - weatherIcon = stubIcon, - weatherTemp = this, - weatherInfoView = this, - ) - } - - override fun onAttachedToWindow() { - super.onAttachedToWindow() - mWeatherViewController.init() - } - - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() - mWeatherViewController.removeObserver() - } -} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt deleted file mode 100644 index fcb2e1ae84de4..0000000000000 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ /dev/null @@ -1,213 +0,0 @@ -/* - * SPDX-FileCopyrightText: The AxionAOSP Project - * SPDX-FileCopyrightText: crDroid Android Project - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.android.systemui.weather - -import android.content.Context -import android.database.ContentObserver -import android.net.Uri -import android.os.Handler -import android.os.Looper -import android.os.UserHandle -import android.provider.Settings -import android.view.View -import android.widget.ImageView -import android.widget.TextView - -import com.android.internal.util.alpha.OmniJawsClient - -import com.android.systemui.res.R - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collectLatest - -class WeatherViewController( - private val context: Context, - private val weatherIcon: ImageView, - private val weatherTemp: TextView, - private val weatherInfoView: View, -) : OmniJawsClient.OmniJawsObserver { - - private var weatherInfo: OmniJawsClient.WeatherInfo? = null - private val scope = CoroutineScope(Dispatchers.Main.immediate + SupervisorJob()) - - private val weatherSettingsFlow = MutableStateFlow(getWeatherSettings()) - - private val settingsObserver = object : ContentObserver(Handler(Looper.getMainLooper())) { - override fun onChange(selfChange: Boolean, uri: Uri?) { - weatherSettingsFlow.value = getWeatherSettings() - } - } - - fun init() { - registerSettingsObservers() - scope.launch { - weatherSettingsFlow.collectLatest { applyWeatherSettings(it) } - } - } - - private fun registerSettingsObservers() { - val resolver = context.contentResolver - WEATHER_SETTING_KEYS.forEach { key -> - resolver.registerContentObserver( - Settings.System.getUriFor(key), - false, - settingsObserver, - UserHandle.USER_ALL - ) - } - } - - private fun unregisterSettingsObservers() { - context.contentResolver.unregisterContentObserver(settingsObserver) - } - - private fun getConditionText(condition: String): String { - val locale = context.resources.configuration.locales[0] - val isEnglish = locale.language.startsWith("en", ignoreCase = true) - - if (!isEnglish) { - for ((key, value) in WEATHER_CONDITIONS) { - if (condition.contains(key)) { - return context.resources.getString(value) - } - } - } - return condition.split(" ").joinToString(" ") { it.replaceFirstChar { char -> char.uppercaseChar() } } - } - - private fun getWeatherSettings() = WeatherSettings( - weatherEnabled = getSystemSetting(LOCKSCREEN_WEATHER_ENABLED), - showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), - showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), - showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), - showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO) - ) - - private fun getSystemSetting(setting: String, defaultValue: Int = 0) = - Settings.System.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 - - private fun applyWeatherSettings(settings: WeatherSettings) { - if (!settings.weatherEnabled) { - hideAllViews() - OmniJawsClient.get().removeObserver(context, this@WeatherViewController) - } else { - OmniJawsClient.get().addObserver(context, this@WeatherViewController) - updateWeather() - showAllViews() - } - } - - override fun weatherUpdated() = updateWeather() - - private fun updateWeather() { - if (!weatherSettingsFlow.value.weatherEnabled) { - hideAllViews() - return - } - - try { - OmniJawsClient.get().queryWeather(context) - weatherInfo = OmniJawsClient.get().weatherInfo - weatherInfo?.let { info -> - weatherIcon.setImageDrawable( - OmniJawsClient.get().getWeatherConditionImage(context, - info.conditionCode)) - weatherTemp.text = buildWeatherText(info) - weatherTemp.isSelected = true - } - } catch (e: Exception) {} - } - - private fun hideAllViews() { - scope.launch { - listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { - updateViewVisibility(it, false) - } - } - } - - private fun showAllViews() { - scope.launch { - listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { - updateViewVisibility(it, true) - } - } - } - - private fun buildWeatherText(info: OmniJawsClient.WeatherInfo): String { - val settings = weatherSettingsFlow.value - val conditionText = getConditionText(info.condition.lowercase()) - - val locationText = if (settings.showWeatherLocation) " • ${info.city}" else "" - val conditionDisplay = if (settings.showWeatherText) " • $conditionText" else "" - val windDisplay = if (settings.showWindInfo) " • ${info.windSpeed} ${info.windUnits} ${info.pinWheel}" else "" - val humidityDisplay = if (settings.showHumidityInfo) " • ${info.humidity}" else "" - - return "${info.temp}${info.tempUnits}$locationText$conditionDisplay$windDisplay$humidityDisplay" - } - - override fun weatherError(errorReason: Int) { - if (errorReason == OmniJawsClient.EXTRA_ERROR_DISABLED) { - weatherInfo = null - weatherIcon.setImageDrawable(null) - weatherTemp.text = "" - hideAllViews() - } - } - - fun removeObserver() { - unregisterSettingsObservers() - scope.cancel() - OmniJawsClient.get().removeObserver(context, this) - } - - private suspend fun updateViewVisibility(view: View, visible: Boolean) { - withContext(Dispatchers.Main) { - view.visibility = if (visible) View.VISIBLE else View.GONE - } - } - - data class WeatherSettings( - val weatherEnabled: Boolean, - val showWeatherLocation: Boolean, - val showWeatherText: Boolean, - val showWindInfo: Boolean, - val showHumidityInfo: Boolean - ) - - companion object { - private const val LOCKSCREEN_WEATHER_ENABLED = "lockscreen_weather_enabled" - private const val LOCKSCREEN_WEATHER_LOCATION = "lockscreen_weather_location" - private const val LOCKSCREEN_WEATHER_TEXT = "lockscreen_weather_text" - private const val LOCKSCREEN_WEATHER_WIND_INFO = "lockscreen_weather_wind_info" - private const val LOCKSCREEN_WEATHER_HUMIDITY_INFO = "lockscreen_weather_humidity_info" - - private val WEATHER_SETTING_KEYS = listOf( - LOCKSCREEN_WEATHER_ENABLED, - LOCKSCREEN_WEATHER_LOCATION, - LOCKSCREEN_WEATHER_TEXT, - LOCKSCREEN_WEATHER_WIND_INFO, - LOCKSCREEN_WEATHER_HUMIDITY_INFO - ) - - private val WEATHER_CONDITIONS = mapOf( - "clouds" to R.string.weather_condition_clouds, - "rain" to R.string.weather_condition_rain, - "clear" to R.string.weather_condition_clear, - "storm" to R.string.weather_condition_storm, - "snow" to R.string.weather_condition_snow, - "wind" to R.string.weather_condition_wind, - "mist" to R.string.weather_condition_mist - ) - } -} diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java index 41cefca4a59d7..919bbc227f2d0 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/BcSmartspaceDataProvider.java @@ -90,6 +90,7 @@ public void onTargetsAvailable(List list) { mSmartspaceTargets = list.stream() .filter(target -> target.getFeatureType() != 15) + .filter(target -> target.getFeatureType() != 25) .collect(Collectors.toList()); mSmartspaceTargetListeners.forEach( diff --git a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java index 5fe31341541d2..4e7c286bbef0a 100644 --- a/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java +++ b/packages/SystemUI/src/com/google/android/systemui/smartspace/CardRecyclerViewAdapter.java @@ -731,22 +731,15 @@ public final void updateTargetVisibility(Runnable runnable, boolean z) { smartspaceTargets.stream().collect(Collectors.toList()), runnable); } hasAodLockscreenTransition = targets != lockscreenTargets; - if (configProvider.isDefaultDateWeatherDisabled() - || BcSmartspaceDataPlugin.UI_SURFACE_HOME_SCREEN.equals(uiSurface)) {} BcSmartspaceTemplateDataUtils.updateVisibility( root, smartspaceTargets.isEmpty() ? View.GONE : View.VISIBLE); return; } } - z2 = false; - if (smartspaceTargets == lockscreenTargets) {} - if (!z2) {} - if (!z) {} viewHolders.clear(); resetListIfNeeded(); mDiffer.submitList(smartspaceTargets.stream().collect(Collectors.toList()), runnable); hasAodLockscreenTransition = targets != lockscreenTargets; - if (configProvider.isDefaultDateWeatherDisabled()) {} } public final void setTargets(List list, Runnable runnable) { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt index 9144de92514cc..df4c540858775 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardClockRepository.kt @@ -52,6 +52,7 @@ class FakeKeyguardClockRepository() : KeyguardClockRepository { override val clockEventController: ClockEventController = mock() + override val areLockscreenWidgetsEnabled: Boolean = false override fun setClockSize(size: ClockSize) { _clockSize.value = size _forcedClockSize.value = size diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index b8fda0504b9a5..6063a0830cae2 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -5965,6 +5965,9 @@ public boolean canAccessAppWidget(Widget widget, int uid, String packageName) { && isDifferentPackageFromProvider(widget.provider, packageName) && widget.host != null && isDifferentPackageFromHost(widget.host, packageName)) { + if (widget.host.id != null && widget.host.id.uid == uid) { + return true; + } // An AppWidget can only be accessed by either // 1. The package that provides the AppWidget. // 2. The package that hosts the AppWidget. diff --git a/services/core/java/com/android/server/alpha/QuickSwitchService.java b/services/core/java/com/android/server/alpha/QuickSwitchService.java index 27f4d9ff094e2..4d17f66fc8be2 100644 --- a/services/core/java/com/android/server/alpha/QuickSwitchService.java +++ b/services/core/java/com/android/server/alpha/QuickSwitchService.java @@ -55,22 +55,21 @@ public final class QuickSwitchService extends SystemService { private static final String LAWNCHAIR_OVERLAY = "app.lawnchair.overlay"; private static final List WALLPAPER_PICKER_PACKAGES = List.of( - "com.android.customization.themes", - "com.android.wallpaper" + "com.android.alpha.themepicker" ); private static final List WALLPAPER_PICKER_GOOGLE_PACKAGES = List.of( - "com.google.android.aicore", - "com.google.android.apps.aiwallpapers", + // "com.google.android.aicore", + // "com.google.android.apps.aiwallpapers", "com.google.android.apps.customization", "com.google.android.apps.customization.pixel", - "com.google.android.apps.emojiwallpaper", - "com.google.android.apps.wallpaper", + // "com.google.android.apps.emojiwallpaper", + // "com.google.android.apps.wallpaper", "com.google.android.apps.wallpaper.overlay.android", - "com.google.android.apps.wallpaper.overlay.launcher", - "com.google.android.apps.wallpaper.pixel", - "com.google.android.wallpaper.effects", - "com.google.pixel.livewallpaper" + "com.google.android.apps.wallpaper.overlay.launcher" + // "com.google.android.apps.wallpaper.pixel", + // "com.google.android.wallpaper.effects", + // "com.google.pixel.livewallpaper" ); private static final List LAUNCHER3_OVERLAYS = List.of( diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index e3c3c8ccd503e..d4a9a761c12e0 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -1579,6 +1579,7 @@ private String checkContentProviderPermission(ProviderInfo cpi, int callingPid, return "ContentProvider access not allowed from sdk sandbox UID. " + "ProviderInfo: " + cpi.toString(); } + if ("com.android.systemui".equals(appName)) return null; boolean checkedGrants = false; if (checkUser) { // Looking for cross-user grants before enforcing the typical cross-users permissions diff --git a/services/core/java/com/android/server/power/Notifier.java b/services/core/java/com/android/server/power/Notifier.java index 2b153f8ca6fe8..b21ac2847de5f 100644 --- a/services/core/java/com/android/server/power/Notifier.java +++ b/services/core/java/com/android/server/power/Notifier.java @@ -211,7 +211,7 @@ private static class Interactivity { private int mBroadcastedInteractiveState; private boolean mBroadcastInProgress; private long mBroadcastStartTime; - private static final long USER_ACTIVITY_MIN_INTERVAL_MS = 500; + private static final long USER_ACTIVITY_MIN_INTERVAL_MS = 100; private long mLastUserActivityTimeMs; // True if a user activity message should be sent. diff --git a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java index be14c0d91f8eb..40fd913837133 100644 --- a/services/core/java/com/android/server/theme/ThemeEngineManagerService.java +++ b/services/core/java/com/android/server/theme/ThemeEngineManagerService.java @@ -82,6 +82,29 @@ public class ThemeEngineManagerService extends SystemService { private static final String TARGET_ARRAY_ANDROID = "target_android"; private static final String TARGET_ARRAY_SETTINGS = "target_settings"; + /** Overlay-manager category prefix for built-in icon pack RROs (Theme Store path). */ + private static final String ICON_PACK_OVERLAY_PREFIX = + "android.theme.customization.icon_pack."; + + private static final String OVERLAY_CATEGORY_WIFI_ICON = + "android.theme.customization.wifi_icon"; + private static final String OVERLAY_CATEGORY_SIGNAL_ICON = + "android.theme.customization.signal_icon"; + + private static final String[] STATUSBAR_THEMED_DRAWABLE_NAMES = { + "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", + "ic_signal_cellular_2_4_bar", "ic_signal_cellular_3_4_bar", + "ic_signal_cellular_4_4_bar", + "ic_signal_cellular_0_5_bar", "ic_signal_cellular_1_5_bar", + "ic_signal_cellular_2_5_bar", "ic_signal_cellular_3_5_bar", + "ic_signal_cellular_4_5_bar", "ic_signal_cellular_5_5_bar", + "ic_wifi_signal_0", "ic_wifi_signal_1", "ic_wifi_signal_2", + "ic_wifi_signal_3", "ic_wifi_signal_4", + }; + + private static final String ENGINE_KEY_WIFI = "wifi"; + private static final String ENGINE_KEY_SIGNAL = "signal"; + private static final int BITMAP_CACHE_SIZE = 5 * 1024 * 1024; private final Context mContext; @@ -98,6 +121,8 @@ public class ThemeEngineManagerService extends SystemService { private volatile String mActiveSystemThemeIcons = null; private volatile String mIconPackPackage = null; + /** All icon-pack overlay APKs active in categoryThemes (android, systemui, …). */ + private final Set mIconPackOverlayPackages = new HashSet<>(); private final Map mIconPackMap = new ConcurrentHashMap<>(); private final List mIconBackList = new CopyOnWriteArrayList<>(); @@ -116,6 +141,9 @@ public class ThemeEngineManagerService extends SystemService { private boolean mTextColorPrimaryCached = false; private ContentObserver mSettingsObserver; + /** Guards against re-entrant beginBroadcast() (settings observer + explicit notify). */ + private boolean mBroadcasting = false; + private static final long PERSIST_DEBOUNCE_MS = 500; private final Runnable mPersistPerAppRunnable = this::persistPerAppIconPacksNow; @@ -203,6 +231,7 @@ private synchronized void loadThemeConfig() { mActiveSystemThemeIcons = null; mSystemThemeIconTargets.clear(); mCategoryThemes.clear(); + mIconPackOverlayPackages.clear(); mBitmapCache.evictAll(); mPerAppIconPacks.clear(); mPerAppIconPackMaps.clear(); @@ -255,9 +284,28 @@ private synchronized void loadThemeConfig() { while (catKeys.hasNext()) { String category = catKeys.next(); String pkgName = categoryThemes.optString(category); + if (pkgName == null || pkgName.isEmpty()) { + continue; + } + if (isIconPackOverlayCategory(category)) { + // Theme Store enables icon packs via categoryThemes only (no themes.icon_pack). + mIconPackOverlayPackages.add(pkgName); + mCategoryThemes.put(category, pkgName); + loadTargetArrays(pkgName); + continue; + } + // Dedicated statusbar wifi/signal RROs use overlay-manager category ids; + // themes.wifi uses the short key "wifi" — always register when present in + // categoryThemes so getThemePackageForResource can prefer them over iconpack. + if (OVERLAY_CATEGORY_WIFI_ICON.equals(category) + || OVERLAY_CATEGORY_SIGNAL_ICON.equals(category)) { + mCategoryThemes.put(category, pkgName); + loadTargetArrays(pkgName); + continue; + } boolean isCategoryEnabled = mEnabledThemes.containsKey(category) || mSystemThemeIconTargets.contains(category); - if (pkgName != null && !pkgName.isEmpty() && isCategoryEnabled) { + if (isCategoryEnabled) { mCategoryThemes.put(category, pkgName); loadTargetArrays(pkgName); } @@ -277,9 +325,18 @@ private synchronized void loadThemeConfig() { } String iconPackPkg = mEnabledThemes.get(CATEGORY_ICON_PACK); + if (iconPackPkg == null && !mIconPackOverlayPackages.isEmpty()) { + iconPackPkg = resolvePrimaryIconPackPackage(); + } if (iconPackPkg != null && (!iconPackPkg.equals(mIconPackPackage) || mIconPackMap.isEmpty())) { loadIconPack(iconPackPkg); + // Register the iconpack's overlaid resources so getThemePackageForResource + // can fall back to it when no per-category statusbar theme (wifi/signal) is + // active. This lets an iconpack contribute its bundled ic_wifi_signal_* / + // ic_signal_cellular_* drawables when the user has not picked a dedicated + // statusbar overlay. + loadTargetArrays(iconPackPkg); } else if (iconPackPkg == null && mIconPackPackage != null) { mIconPackPackage = null; mIconPackMap.clear(); @@ -323,42 +380,149 @@ private void loadTargetArrays(@NonNull String packageName) { } } - if (allTargets.isEmpty()) { - String overlayCategory = null; - try { - android.content.pm.PackageInfo pi = mContext.getPackageManager() - .getPackageInfo(packageName, 0); - overlayCategory = pi.overlayCategory; - } catch (Exception e) { - Slog.d(TAG, "Failed to get overlay category for " + packageName, e); - } + registerStatusbarDrawableTargets(themeResources, packageName, allTargets); + + mTargetArrayCache.put(packageName, allTargets); + } - String resCategory = null; - if ("android.theme.customization.signal_icon".equals(overlayCategory)) { - resCategory = "signal"; - } else if ("android.theme.customization.wifi_icon".equals(overlayCategory)) { - resCategory = "wifi"; + private static boolean isIconPackOverlayCategory(@NonNull String category) { + return category.startsWith(ICON_PACK_OVERLAY_PREFIX); + } + + /** + * Prefer the android overlay (appfilter + framework wifi/signal drawables), else any member. + */ + @Nullable + private String resolvePrimaryIconPackPackage() { + for (String pkg : mIconPackOverlayPackages) { + if (pkg.endsWith(".android")) { + return pkg; } + } + for (String pkg : mIconPackOverlayPackages) { + return pkg; + } + return null; + } - String[] knownNames = { - "ic_signal_cellular_0_4_bar", "ic_signal_cellular_1_4_bar", - "ic_signal_cellular_2_4_bar", "ic_signal_cellular_3_4_bar", - "ic_signal_cellular_4_4_bar", - "ic_signal_cellular_0_5_bar", "ic_signal_cellular_1_5_bar", - "ic_signal_cellular_2_5_bar", "ic_signal_cellular_3_5_bar", - "ic_signal_cellular_4_5_bar", "ic_signal_cellular_5_5_bar", - "ic_wifi_signal_0", "ic_wifi_signal_1", "ic_wifi_signal_2", - "ic_wifi_signal_3", "ic_wifi_signal_4", - }; - for (String name : knownNames) { - if (themeResources.getIdentifier(name, "drawable", packageName) != 0) { - allTargets.add(name); - if (resCategory != null) mResourceCategoryCache.put(name, resCategory); + /** + * Probe for wifi/signal drawables shipped in icon-pack overlays. Runs even when target_* arrays + * are present so statusbar icons are registered for ThemeEngine fallback. + */ + private void registerStatusbarDrawableTargets(@NonNull Resources themeResources, + @NonNull String packageName, @NonNull Set allTargets) { + String overlayCategory = null; + try { + android.content.pm.PackageInfo pi = mContext.getPackageManager() + .getPackageInfo(packageName, 0); + overlayCategory = pi.overlayCategory; + } catch (Exception e) { + Slog.d(TAG, "Failed to get overlay category for " + packageName, e); + } + + String resCategory = null; + if ("android.theme.customization.signal_icon".equals(overlayCategory)) { + resCategory = CATEGORY_STATUSBAR_SIGNAL; + } else if ("android.theme.customization.wifi_icon".equals(overlayCategory)) { + resCategory = CATEGORY_STATUSBAR_WIFI; + } + + for (String name : STATUSBAR_THEMED_DRAWABLE_NAMES) { + if (themeResources.getIdentifier(name, "drawable", packageName) != 0) { + allTargets.add(name); + if (resCategory != null) { + mResourceCategoryCache.put(name, resCategory); } } } + } - mTargetArrayCache.put(packageName, allTargets); + private boolean packageTargetsResource(@Nullable String packageName, + @NonNull String resourceName) { + if (packageName == null) return false; + Set targets = mTargetArrayCache.get(packageName); + return targets != null && targets.contains(resourceName); + } + + private boolean hasDedicatedWifiOverlay() { + return mCategoryThemes.containsKey(OVERLAY_CATEGORY_WIFI_ICON) + || mCategoryThemes.containsKey(ENGINE_KEY_WIFI) + || mCategoryThemes.containsKey(CATEGORY_STATUSBAR_WIFI); + } + + private boolean hasDedicatedSignalOverlay() { + return mCategoryThemes.containsKey(OVERLAY_CATEGORY_SIGNAL_ICON) + || mCategoryThemes.containsKey(ENGINE_KEY_SIGNAL) + || mCategoryThemes.containsKey(CATEGORY_STATUSBAR_SIGNAL); + } + + @Nullable + private String getDedicatedStatusbarPackage(@Nullable String category) { + if (category == null) return null; + if (CATEGORY_STATUSBAR_WIFI.equals(category)) { + return firstCategoryThemePackage( + OVERLAY_CATEGORY_WIFI_ICON, + ENGINE_KEY_WIFI, + CATEGORY_STATUSBAR_WIFI); + } + if (CATEGORY_STATUSBAR_SIGNAL.equals(category)) { + return firstCategoryThemePackage( + OVERLAY_CATEGORY_SIGNAL_ICON, + ENGINE_KEY_SIGNAL, + CATEGORY_STATUSBAR_SIGNAL); + } + return null; + } + + @Nullable + private String firstCategoryThemePackage(String... categories) { + for (String category : categories) { + String pkg = mCategoryThemes.get(category); + if (pkg != null && !pkg.isEmpty()) { + return pkg; + } + } + return null; + } + + private static boolean isWifiStatusbarResource(@NonNull String resourceName) { + return resourceName.startsWith("ic_wifi_signal_"); + } + + private static boolean isCellularStatusbarResource(@NonNull String resourceName) { + return resourceName.startsWith("ic_signal_cellular_"); + } + + @Nullable + private String resolveIconPackPackageForResource(@NonNull String resourceName) { + // Iconpack wifi/signal art is fallback only — never override a dedicated statusbar RRO. + if (isWifiStatusbarResource(resourceName) && hasDedicatedWifiOverlay()) { + return null; + } + if (isCellularStatusbarResource(resourceName) && hasDedicatedSignalOverlay()) { + return null; + } + for (String pkg : mIconPackOverlayPackages) { + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } + } + String primary = mIconPackPackage; + if (primary != null && packageTargetsResource(primary, resourceName)) { + return primary; + } + return null; + } + + private boolean shouldUseActiveSystemThemeIcons(@Nullable String category) { + if (mActiveSystemThemeIcons == null) return false; + if (category == null || mSystemThemeIconTargets.isEmpty()) { + return true; + } + if (mSystemThemeIconTargets.contains(category)) { + return true; + } + return mSystemThemeIconTargets.contains("statusbar_" + category); } private void loadIconPack(String packageName) { @@ -468,28 +632,39 @@ private String getThemePackageForResource(@NonNull String resourceName) { String category = mResourceCategoryCache.get(resourceName); if (category != null && mCategoryThemes.containsKey(category)) { - return mCategoryThemes.get(category); + String pkg = mCategoryThemes.get(category); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } } if (category != null) { - String aliasCategory = "statusbar_" + category; - if (mCategoryThemes.containsKey(aliasCategory)) { - return mCategoryThemes.get(aliasCategory); + if (!category.startsWith("statusbar_")) { + String aliasCategory = "statusbar_" + category; + if (mCategoryThemes.containsKey(aliasCategory)) { + String pkg = mCategoryThemes.get(aliasCategory); + if (packageTargetsResource(pkg, resourceName)) { + return pkg; + } + } + } + String dedicatedPkg = getDedicatedStatusbarPackage(category); + if (dedicatedPkg != null && packageTargetsResource(dedicatedPkg, resourceName)) { + return dedicatedPkg; } } - if (mActiveSystemThemeIcons != null) { - if (category == null || mSystemThemeIconTargets.isEmpty() - || mSystemThemeIconTargets.contains(category)) { - return mActiveSystemThemeIcons; - } - if (category != null - && mSystemThemeIconTargets.contains("statusbar_" + category)) { - return mActiveSystemThemeIcons; - } + if (shouldUseActiveSystemThemeIcons(category) + && packageTargetsResource(mActiveSystemThemeIcons, resourceName)) { + return mActiveSystemThemeIcons; } - return null; + // Fall back to the active iconpack when a statusbar resource (wifi/signal) + // has no dedicated category theme. Iconpacks that ship ic_wifi_signal_* / + // ic_signal_cellular_* drawables get registered in mTargetArrayCache via + // loadTargetArrays at iconpack load time; consult that set so we don't + // serve from an iconpack that does not actually theme this resource. + return resolveIconPackPackageForResource(resourceName); } @NonNull @@ -544,7 +719,15 @@ private int getTextColorPrimary() { } private void notifyThemeChangedInternal(@Nullable String category) { + if (mBroadcasting) { + // theme_engine_data can change while callbacks run (e.g. AlphaVisuals sync + + // notifyThemeChanged); defer so RemoteCallbackList is not re-entered. + mHandler.post(() -> notifyThemeChangedInternal(category)); + return; + } + final long ident = Binder.clearCallingIdentity(); + mBroadcasting = true; try { loadThemeConfig(); @@ -565,6 +748,7 @@ private void notifyThemeChangedInternal(@Nullable String category) { mContext.sendBroadcastAsUser(intent, UserHandle.ALL); } } finally { + mBroadcasting = false; Binder.restoreCallingIdentity(ident); } } @@ -755,24 +939,7 @@ public Bitmap getSystemThemeIconDrawable(String resourceName, int density) { public boolean isTargetedResource(String resourceName) { if (resourceName == null) return false; synchronized (ThemeEngineManagerService.this) { - String category = mResourceCategoryCache.get(resourceName); - - if (category != null && mCategoryThemes.containsKey(category)) { - String pkgName = mCategoryThemes.get(category); - Set targets = mTargetArrayCache.get(pkgName); - return targets != null && targets.contains(resourceName); - } - - if (mActiveSystemThemeIcons == null) return false; - - Set targets = mTargetArrayCache.get(mActiveSystemThemeIcons); - if (targets == null || !targets.contains(resourceName)) return false; - - if (category != null && !mSystemThemeIconTargets.isEmpty()) { - return mSystemThemeIconTargets.contains(category); - } - - return !mSystemThemeIconTargets.isEmpty(); + return getThemePackageForResource(resourceName) != null; } }