From c100ef6e45c1d94191934925f8a07888fa0bd972 Mon Sep 17 00:00:00 2001 From: Jesse Chan Date: Wed, 24 Jun 2020 05:41:39 +0800 Subject: [PATCH 001/190] VolumeDialog: Display default row when active row is notification Commit "frameworks: Add unlinked ringtone and notification volumes" introduced STREAM_NOTIFICATION. However, this stream type, although marked as important, was not added to the list in shouldBeVisibleH. As a result, the volume panel behavior of STREAM_NOTIFICATION deviates from other user-facing streams like media, call and ring. This change adds STREAM_NOTIFICATION so the behavior becomes consistent. Change-Id: I092c5bf0ae8cbee85af6adfa0da308dfdb60e66a Signed-off-by: Jesse Chan Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 5a8d9e0110aa..89887ebcda6f 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -23,6 +23,7 @@ import static android.media.AudioManager.STREAM_ACCESSIBILITY; import static android.media.AudioManager.STREAM_ALARM; import static android.media.AudioManager.STREAM_MUSIC; +import static android.media.AudioManager.STREAM_NOTIFICATION; import static android.media.AudioManager.STREAM_RING; import static android.media.AudioManager.STREAM_VOICE_CALL; import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE; @@ -1734,6 +1735,7 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { if (row.defaultStream) { return activeRow.stream == STREAM_RING + || activeRow.stream == STREAM_NOTIFICATION || activeRow.stream == STREAM_ALARM || activeRow.stream == STREAM_VOICE_CALL || activeRow.stream == STREAM_ACCESSIBILITY From 0596bb8591c9613bf0fe31218d10a7db8a3ddb31 Mon Sep 17 00:00:00 2001 From: Arian Date: Sun, 30 Jan 2022 22:30:12 +0100 Subject: [PATCH 002/190] VolumeDialogImpl: Don't hide the default stream when adjusting the music stream If there are two visible rows and the user touches the default (music) row, the other row disappears immediately. Avoid this behaviour by tracking the row with which this panel was created and keep showing that row if the user adjusts the music stream. Test: While receiving a call both rows are touchable and the additional row does not disappear when adjusting the music stream. Change-Id: I4e14a0ea50c5cc41cb279c6fbfc8a7d6e6d1ba61 Signed-off-by: Ghosuto --- .../android/systemui/volume/VolumeDialogImpl.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 89887ebcda6f..2e71bf6b935c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -313,6 +313,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mWindowGravity; + // Variable to track the default row with which the panel is initially shown + private VolumeRow mDefaultRow = null; + @VisibleForTesting final int mVolumeRingerIconDrawableId = R.drawable.ic_legacy_speaker_on; @VisibleForTesting @@ -1568,6 +1571,10 @@ private void showH(int reason, boolean keyguardLocked, int lockTaskModeState) { mConfigChanged = false; } + if (mDefaultRow == null) { + mDefaultRow = getActiveRow(); + } + initSettingsH(lockTaskModeState); initAppVolumeH(); mShowing = true; @@ -1680,6 +1687,7 @@ protected void dismissH(int reason) { mDialog.dismiss(); } tryToRemoveCaptionsTooltip(); + mDefaultRow = null; mIsAnimatingDismiss = false; hideRingerDrawer(); @@ -1733,11 +1741,14 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { return true; } - if (row.defaultStream) { + // if the row is the default stream or the row with which this panel was created, + // show it additonally to the active row if it is one of the following streams + if (row.defaultStream || mDefaultRow == row) { return activeRow.stream == STREAM_RING || activeRow.stream == STREAM_NOTIFICATION || activeRow.stream == STREAM_ALARM || activeRow.stream == STREAM_VOICE_CALL + || activeRow.stream == STREAM_MUSIC || activeRow.stream == STREAM_ACCESSIBILITY || mDynamic.get(activeRow.stream); } From 9c86e4c397836ca724268a5bf96a255ccad36532 Mon Sep 17 00:00:00 2001 From: LuK1337 Date: Mon, 22 Mar 2021 19:04:08 +0100 Subject: [PATCH 003/190] VolumeDialogImpl: Don't vibrate when volume dialog is not visible We shouldn't be playing volume dialog specific haptic feedback when the dialog is not visible. Test: Call am.setRingerModeInternal(AudioManager.RINGER_MODE_VIBRATE) from external application, observe that there's no vibration. Change-Id: I10ad1e0259092c2297d96f083161395275467781 Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 2e71bf6b935c..1b86d396925b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1933,7 +1933,7 @@ private void trimObsoleteH() { protected void onStateChangedH(State state) { if (D.BUG) Log.d(TAG, "onStateChangedH() state: " + state.toString()); - if (mState != null && state != null + if (mShowing && mState != null && state != null && mState.ringerModeInternal != -1 && mState.ringerModeInternal != state.ringerModeInternal && state.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE) { From 908559384501aa08074e6e941b3968b0998f02ce Mon Sep 17 00:00:00 2001 From: jhenrique09 Date: Thu, 7 Apr 2022 18:24:11 +0000 Subject: [PATCH 004/190] VolumeDialogImpl: Fix cut layout when on setup or lock task mode Change-Id: I20aacbda4e1a90cad3120ed6f4a65c8c884521cf Signed-off-by: Ghosuto --- .../res/layout-land/volume_dialog_legacy.xml | 12 ++++++++++++ .../SystemUI/res/layout/volume_dialog_legacy.xml | 12 ++++++++++++ .../android/systemui/volume/VolumeDialogImpl.java | 13 ++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml index ebad1db11851..98e7a81c3cce 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml @@ -126,6 +126,18 @@ android:src="@drawable/horizontal_ellipsis" android:tint="?androidprv:attr/colorAccent" /> + + + diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml index aa1a4f2b1b7f..05242e326f91 100644 --- a/packages/SystemUI/res/layout/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml @@ -123,6 +123,18 @@ android:tint="?androidprv:attr/colorAccent" android:soundEffectsEnabled="false" /> + + + diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 1b86d396925b..a93184204b94 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -100,6 +100,7 @@ import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; @@ -316,6 +317,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, // Variable to track the default row with which the panel is initially shown private VolumeRow mDefaultRow = null; + private FrameLayout mRoundedBorderBottom; + @VisibleForTesting final int mVolumeRingerIconDrawableId = R.drawable.ic_legacy_speaker_on; @VisibleForTesting @@ -710,6 +713,7 @@ public void onViewDetachedFromWindow(View v) { mAppVolumeView = mDialog.findViewById(R.id.app_volume_container); mAppVolumeIcon = mDialog.findViewById(R.id.app_volume); + mRoundedBorderBottom = mDialog.findViewById(R.id.rounded_border_bottom); if (mRows.isEmpty()) { if (!AudioSystem.isSingleVolume(mContext)) { @@ -1248,10 +1252,13 @@ private void updateSelectedRingerContainerDescription(boolean open) { } private void initSettingsH(int lockTaskModeState) { + final boolean showSettings = mDeviceProvisionedController.isCurrentUserSetup() + && lockTaskModeState == LOCK_TASK_MODE_NONE; + if (mRoundedBorderBottom != null) { + mRoundedBorderBottom.setVisibility(!showSettings ? VISIBLE : GONE); + } if (mSettingsView != null) { - mSettingsView.setVisibility( - mDeviceProvisionedController.isCurrentUserSetup() && - lockTaskModeState == LOCK_TASK_MODE_NONE ? VISIBLE : GONE); + mSettingsView.setVisibility(showSettings ? VISIBLE : GONE); } if (mSettingsIcon != null) { mSettingsIcon.setOnClickListener(v -> { From b331998e8f3fd78ab45a95ec460bfde4c16d0cbc Mon Sep 17 00:00:00 2001 From: Arian Date: Tue, 7 Nov 2023 10:52:08 +0100 Subject: [PATCH 005/190] VolumeDialogImpl: Drop unnecessary layout gravity defines and handle left These layouts are already in layouts which set gravity to the same value, making the layout gravity set here redundant. Additionally, invert the gravities when the gravitiy is set to left by R.integer.volume_dialog_gravity. Change-Id: Ia989ab507512443949b3a7994166d56f97dde9df Signed-off-by: Ghosuto --- .../res/layout-land/volume_dialog_legacy.xml | 7 +--- .../res/layout/volume_dialog_legacy.xml | 7 +--- .../systemui/volume/VolumeDialogImpl.java | 38 +++++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml index 98e7a81c3cce..408cddbd9b11 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml @@ -19,7 +19,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:background="@android:color/transparent" android:theme="@style/volume_dialog_theme"> @@ -29,7 +28,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" android:orientation="vertical" android:clipToPadding="false" @@ -53,7 +51,6 @@ android:layout_height="@dimen/volume_dialog_ringer_size" android:layout_marginBottom="@dimen/volume_dialog_spacer" android:gravity="right" - android:layout_gravity="right" android:translationZ="@dimen/volume_dialog_elevation" android:clipToPadding="false" android:background="@drawable/rounded_bg_full"> @@ -75,7 +72,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:orientation="vertical" android:clipChildren="false" android:clipToPadding="false" > @@ -148,7 +144,6 @@ android:layout_height="@dimen/volume_dialog_caption_size" android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" android:gravity="right" - android:layout_gravity="right" android:clipToPadding="false" android:clipToOutline="true" android:background="@drawable/volume_row_rounded_background"> @@ -170,7 +165,7 @@ android:layout="@layout/volume_tool_tip_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="bottom | right" + android:layout_gravity="bottom" android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> \ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml index 05242e326f91..ec6ef659dcfa 100644 --- a/packages/SystemUI/res/layout/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml @@ -21,7 +21,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:clipToPadding="false" android:theme="@style/volume_dialog_theme"> @@ -31,7 +30,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" android:orientation="vertical" android:clipToPadding="false" @@ -54,7 +52,6 @@ android:layout_height="@dimen/volume_dialog_ringer_size" android:layout_marginBottom="@dimen/volume_dialog_spacer" android:gravity="right" - android:layout_gravity="right" android:translationZ="@dimen/volume_dialog_elevation" android:clipToPadding="false" android:background="@drawable/rounded_bg_full"> @@ -76,7 +73,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_gravity="right" android:orientation="vertical" android:clipChildren="false" android:clipToPadding="false" > @@ -145,7 +141,6 @@ android:layout_height="@dimen/volume_dialog_caption_size" android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom" android:gravity="right" - android:layout_gravity="right" android:clipToPadding="false" android:clipToOutline="true" android:background="@drawable/volume_row_rounded_background"> @@ -167,7 +162,7 @@ android:layout="@layout/volume_tool_tip_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="bottom | right" + android:layout_gravity="bottom" android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index a93184204b94..9c4a91762b1c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -524,6 +524,25 @@ private void unionViewBoundstoTouchableRegion(final View view) { Region.Op.UNION); } + // Helper to set gravity. + private void setGravity(ViewGroup viewGroup, int gravity) { + if (viewGroup instanceof LinearLayout) { + ((LinearLayout) viewGroup).setGravity(gravity); + } + } + + // Helper to set layout gravity. + private void setLayoutGravity(ViewGroup viewGroup, int gravity) { + if (viewGroup != null) { + Object obj = viewGroup.getLayoutParams(); + if (obj instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) obj).gravity = gravity; + } else if (obj instanceof LinearLayout.LayoutParams) { + ((LinearLayout.LayoutParams) obj).gravity = gravity; + } + } + } + private void initDialog(int lockTaskModeState) { Log.d(TAG, "initDialog: called!"); mDialog = new CustomDialog(mContext); @@ -715,6 +734,25 @@ public void onViewDetachedFromWindow(View v) { mAppVolumeIcon = mDialog.findViewById(R.id.app_volume); mRoundedBorderBottom = mDialog.findViewById(R.id.rounded_border_bottom); + if (isWindowGravityLeft()) { + ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container); + setGravity(container, Gravity.LEFT); + + setGravity(mDialogView, Gravity.LEFT); + + setGravity((ViewGroup) mTopContainer, Gravity.LEFT); + + setLayoutGravity(mRingerDrawerNewSelectionBg, Gravity.BOTTOM | Gravity.LEFT); + + setLayoutGravity(mSelectedRingerContainer, Gravity.BOTTOM | Gravity.LEFT); + + setGravity(mRinger, Gravity.LEFT); + + setGravity(mDialogRowsViewContainer, Gravity.LEFT); + + setGravity(mODICaptionsView, Gravity.LEFT); + } + if (mRows.isEmpty()) { if (!AudioSystem.isSingleVolume(mContext)) { addRow(STREAM_ACCESSIBILITY, R.drawable.ic_volume_accessibility, From 3545d2fb2f2604951064a45b3277fd078ff3b0b9 Mon Sep 17 00:00:00 2001 From: Arian Date: Tue, 7 Nov 2023 11:23:40 +0100 Subject: [PATCH 006/190] VolumeDialogImpl: Set touchable region properly for left gravity Change-Id: I025ae38963d7ca929e01cec26c209d86718ce0e7 Signed-off-by: Ghosuto --- .../systemui/volume/VolumeDialogImpl.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 9c4a91762b1c..a7e90237d0b2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -501,8 +501,8 @@ private void unionViewBoundstoTouchableRegion(final View view) { final int[] locInWindow = new int[2]; view.getLocationInWindow(locInWindow); - float x = locInWindow[0]; - float y = locInWindow[1]; + float xExtraSize = 0; + float yExtraSize = 0; // The ringer and rows container has extra height at the top to fit the expanded ringer // drawer. This area should not be touchable unless the ringer drawer is open. @@ -510,18 +510,27 @@ private void unionViewBoundstoTouchableRegion(final View view) { // are multiple rows they are touchable. if (view == mTopContainer && !mIsRingerDrawerOpen) { if (!isLandscape()) { - y += getRingerDrawerOpenExtraSize(); + yExtraSize = getRingerDrawerOpenExtraSize(); } else if (getRingerDrawerOpenExtraSize() > getVisibleRowsExtraSize()) { - x += (getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize()); + xExtraSize = (getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize()); } } - mTouchableRegion.op( - (int) x, - (int) y, - locInWindow[0] + view.getWidth(), - locInWindow[1] + view.getHeight(), - Region.Op.UNION); + if (isWindowGravityLeft()) { + mTouchableRegion.op( + locInWindow[0], + locInWindow[1] + (int) yExtraSize, + locInWindow[0] + view.getWidth() - (int) xExtraSize, + locInWindow[1] + view.getHeight(), + Region.Op.UNION); + } else { + mTouchableRegion.op( + locInWindow[0] + (int) xExtraSize, + locInWindow[1] + (int) yExtraSize, + locInWindow[0] + view.getWidth(), + locInWindow[1] + view.getHeight(), + Region.Op.UNION); + } } // Helper to set gravity. From f56b755a0dfcabeba07e43afd5568df711b550c9 Mon Sep 17 00:00:00 2001 From: Arian Date: Tue, 7 Nov 2023 11:25:39 +0100 Subject: [PATCH 007/190] VolumeDialogImpl: Respect left gravity in ringer drawer Change-Id: I2cc18aea9bf4afb51c5faee7af6437f6c047416b Signed-off-by: Ghosuto --- .../systemui/volume/VolumeDialogImpl.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index a7e90237d0b2..48c55b96ddd4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1046,6 +1046,12 @@ private void setupRingerDrawer() { mDialogView.getPaddingTop(), mDialogView.getPaddingRight(), mDialogView.getPaddingBottom() + getRingerDrawerOpenExtraSize()); + } else if (isWindowGravityLeft()) { + mDialogView.setPadding( + mDialogView.getPaddingLeft(), + mDialogView.getPaddingTop(), + mDialogView.getPaddingRight() + getRingerDrawerOpenExtraSize(), + mDialogView.getPaddingBottom()); } else { mDialogView.setPadding( mDialogView.getPaddingLeft() + getRingerDrawerOpenExtraSize(), @@ -1116,15 +1122,16 @@ private ImageView getDrawerIconViewForMode(int mode) { } /** - * Translation to apply form the origin (either top or left) to overlap the selection background - * with the given mode in the drawer. + * Translation to apply form the origin (either top or left/right) to overlap the selection + * background with the given mode in the drawer. */ private float getTranslationInDrawerForRingerMode(int mode) { - return mode == RINGER_MODE_VIBRATE - ? -mRingerDrawerItemSize * 2 - : mode == RINGER_MODE_SILENT - ? -mRingerDrawerItemSize - : 0; + final int distantRinger = ((isLandscape() && isWindowGravityLeft()) ? RINGER_MODE_NORMAL + : RINGER_MODE_VIBRATE); + return (mode == distantRinger ? mRingerDrawerItemSize * 2 + : mode == RINGER_MODE_SILENT ? mRingerDrawerItemSize + : 0) + * ((isLandscape() && isWindowGravityLeft()) ? 1 : -1); } @VisibleForTesting String getSelectedRingerContainerDescription() { @@ -1168,12 +1175,13 @@ private void showRingerDrawer() { getTranslationInDrawerForRingerMode(mState.ringerModeInternal)); } - // Move the drawer so that the top/rightmost ringer choice overlaps with the selected ringer + // Move the drawer so that the top/outmost ringer choice overlaps with the selected ringer // icon. if (!isLandscape()) { mRingerDrawerContainer.setTranslationY(mRingerDrawerItemSize * (mRingerCount - 1)); } else { - mRingerDrawerContainer.setTranslationX(mRingerDrawerItemSize * (mRingerCount - 1)); + mRingerDrawerContainer.setTranslationX( + (isWindowGravityLeft() ? -1 : 1) * mRingerDrawerItemSize * (mRingerCount - 1)); } mRingerDrawerContainer.setAlpha(0f); mRingerDrawerContainer.setVisibility(VISIBLE); @@ -1252,7 +1260,7 @@ private void hideRingerDrawer() { .start(); } else { mRingerDrawerContainer.animate() - .translationX(mRingerDrawerItemSize * 2) + .translationX((isWindowGravityLeft() ? -1 : 1) * mRingerDrawerItemSize * 2) .start(); } From 35e043d01725d45795741c8c1bee92f22c7d0feb Mon Sep 17 00:00:00 2001 From: Arian Date: Tue, 7 Nov 2023 11:27:45 +0100 Subject: [PATCH 008/190] VolumeDialogImpl: Handle the outmost row with respect to left gravity Change-Id: I23ffc447f512446688a20bad5639f1a1719f7660 Signed-off-by: Ghosuto --- .../systemui/volume/VolumeDialogImpl.java | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 48c55b96ddd4..7289891e71f3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1831,8 +1831,10 @@ private void updateRowsH(final VolumeRow activeRow) { trimObsoleteH(); } + boolean isOutmostIndexMax = isWindowGravityLeft() ? isRtl() : !isRtl(); + // Index of the last row that is actually visible. - int rightmostVisibleRowIndex = !isRtl() ? -1 : Short.MAX_VALUE; + int outmostVisibleRowIndex = isOutmostIndexMax ? -1 : Short.MAX_VALUE; // apply changes to all rows for (final VolumeRow row : mRows) { @@ -1841,14 +1843,11 @@ private void updateRowsH(final VolumeRow activeRow) { Util.setVisOrGone(row.view, shouldBeVisible); if (shouldBeVisible && mRingerAndDrawerContainerBackground != null) { - // For RTL, the rightmost row has the lowest index since child views are laid out + // For RTL, the outmost row has the lowest index since child views are laid out // from right to left. - rightmostVisibleRowIndex = - !isRtl() - ? Math.max(rightmostVisibleRowIndex, - mDialogRowsView.indexOfChild(row.view)) - : Math.min(rightmostVisibleRowIndex, - mDialogRowsView.indexOfChild(row.view)); + outmostVisibleRowIndex = isOutmostIndexMax + ? Math.max(outmostVisibleRowIndex, mDialogRowsView.indexOfChild(row.view)) + : Math.min(outmostVisibleRowIndex, mDialogRowsView.indexOfChild(row.view)); // Add spacing between each of the visible rows - we'll remove the spacing from the // last row after the loop. @@ -1856,7 +1855,7 @@ private void updateRowsH(final VolumeRow activeRow) { if (layoutParams instanceof LinearLayout.LayoutParams) { final LinearLayout.LayoutParams linearLayoutParams = ((LinearLayout.LayoutParams) layoutParams); - if (!isRtl()) { + if (isOutmostIndexMax) { linearLayoutParams.setMarginEnd(mRingerRowsPadding); } else { linearLayoutParams.setMarginStart(mRingerRowsPadding); @@ -1874,8 +1873,8 @@ private void updateRowsH(final VolumeRow activeRow) { } } - if (rightmostVisibleRowIndex > -1 && rightmostVisibleRowIndex < Short.MAX_VALUE) { - final View lastVisibleChild = mDialogRowsView.getChildAt(rightmostVisibleRowIndex); + if (outmostVisibleRowIndex > -1 && outmostVisibleRowIndex < Short.MAX_VALUE) { + final View lastVisibleChild = mDialogRowsView.getChildAt(outmostVisibleRowIndex); final ViewGroup.LayoutParams layoutParams = lastVisibleChild.getLayoutParams(); // Remove the spacing on the last row, and remove its background since the container is // drawing a background for this row. @@ -2446,6 +2445,9 @@ private void updateBackgroundForDrawerClosedAmount() { final Rect bounds = mRingerAndDrawerContainerBackground.copyBounds(); if (!isLandscape()) { bounds.top = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize()); + } else if (isWindowGravityLeft()) { + bounds.right = (int) ((mDialogCornerRadius / 2) + mRingerDrawerItemSize + + (1f - mRingerDrawerClosedAmount) * getRingerDrawerOpenExtraSize()); } else { bounds.left = (int) (mRingerDrawerClosedAmount * getRingerDrawerOpenExtraSize()); } @@ -2453,7 +2455,7 @@ private void updateBackgroundForDrawerClosedAmount() { } /* - * The top container is responsible for drawing the solid color background behind the rightmost + * The top container is responsible for drawing the solid color background behind the outmost * (primary) volume row. This is because the volume drawer animates in from below, initially * overlapping the primary row. We need the drawer to draw below the row's SeekBar, since it * looks strange to overlap it, but above the row's background color, since otherwise it will be @@ -2508,8 +2510,9 @@ private void setTopContainerBackgroundDrawable() { ? mDialogRowsViewContainer.getTop() : mDialogRowsViewContainer.getTop() - mDialogCornerRadius); - // Set gravity to top-right, since additional rows will be added on the left. - background.setLayerGravity(0, Gravity.TOP | Gravity.RIGHT); + // Set gravity to top and opposite side where additional rows will be added. + background.setLayerGravity(0, + isWindowGravityLeft() ? Gravity.TOP | Gravity.LEFT : Gravity.TOP | Gravity.RIGHT); // In landscape, the ringer drawer animates out to the left (instead of down). Since the // drawer comes from the right (beyond the bounds of the dialog), we should clip it so it From 610ade9111329eb407c20ca672f0c5a809ed30e7 Mon Sep 17 00:00:00 2001 From: Arian Date: Tue, 7 Nov 2023 11:34:40 +0100 Subject: [PATCH 009/190] SystemUI: volume dialog: Align padding/margin for left and right Change-Id: I8159e029f14896e1bae3bd1047618ffa69724ba9 Signed-off-by: Ghosuto --- packages/SystemUI/res/layout-land/volume_dialog_legacy.xml | 6 ++++-- packages/SystemUI/res/layout/volume_dialog_legacy.xml | 6 ++++-- packages/SystemUI/res/values/dimens.xml | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml index 408cddbd9b11..d418028fbf8a 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml @@ -28,7 +28,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:layout_marginLeft="@dimen/volume_dialog_panel_transparent_padding_horizontal" + android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_horizontal" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> @@ -166,6 +167,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + android:layout_marginLeft="@dimen/volume_tool_tip_horizontal_margin" + android:layout_marginRight="@dimen/volume_tool_tip_horizontal_margin"/> \ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml index ec6ef659dcfa..1c85ddc04c70 100644 --- a/packages/SystemUI/res/layout/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml @@ -30,7 +30,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="right" - android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right" + android:layout_marginLeft="@dimen/volume_dialog_panel_transparent_padding_horizontal" + android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_horizontal" android:orientation="vertical" android:clipToPadding="false" android:clipChildren="false"> @@ -163,6 +164,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom" - android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/> + android:layout_marginLeft="@dimen/volume_tool_tip_horizontal_margin" + android:layout_marginRight="@dimen/volume_tool_tip_horizontal_margin"/> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 761ab3e3d7c9..ef78257f7d17 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -650,7 +650,7 @@ 48dp - 8dp + 8dp 20dp @@ -689,7 +689,7 @@ 0dp - 76dp + 76dp 2dp From a0375d90dabd2f716e8edf890f1df22f1bfaeac9 Mon Sep 17 00:00:00 2001 From: "a.derendyaev" Date: Wed, 19 Dec 2018 21:57:45 +0800 Subject: [PATCH 010/190] SystemUI: runtime configurable audio panel location Co-authored-by: Alex Cruz Co-authored-by: Arian Co-authored-by: Bruno Martins Co-authored-by: LuK1337 Co-authored-by: programminghoch10 Co-authored-by: Sam Mortimer Change-Id: If22c97eb1ce4cfb13396d182d90786f4b7acaee7 Signed-off-by: Ghosuto --- .../systemui/volume/VolumeDialogImpl.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 7289891e71f3..1e120b68c448 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -55,6 +55,7 @@ import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.database.ContentObserver; import android.graphics.Color; import android.graphics.Outline; import android.graphics.PixelFormat; @@ -152,6 +153,7 @@ import com.google.android.msdl.domain.MSDLPlayer; import dagger.Lazy; +import lineageos.providers.LineageSettings; import java.io.PrintWriter; import java.util.ArrayList; @@ -307,6 +309,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ViewStub mODICaptionsTooltipViewStub; @VisibleForTesting View mODICaptionsTooltipView = null; + // Volume panel placement left or right + private boolean mVolumePanelOnLeft; + private final boolean mUseBackgroundBlur; private Consumer mCrossWindowBlurEnabledListener; private BackgroundBlurDrawable mDialogRowsViewBackground; @@ -411,6 +416,25 @@ public VolumeDialogImpl( }; } + if (!mIsTv) { + ContentObserver volumePanelOnLeftObserver = new ContentObserver(null) { + @Override + public void onChange(boolean selfChange) { + final boolean volumePanelOnLeft = LineageSettings.Secure.getInt( + mContext.getContentResolver(), + LineageSettings.Secure.VOLUME_PANEL_ON_LEFT, 0) != 0; + if (mVolumePanelOnLeft != volumePanelOnLeft) { + mVolumePanelOnLeft = volumePanelOnLeft; + mHandler.post(mControllerCallbackH::onConfigurationChanged); + } + } + }; + mContext.getContentResolver().registerContentObserver( + LineageSettings.Secure.getUriFor(LineageSettings.Secure.VOLUME_PANEL_ON_LEFT), + false, volumePanelOnLeftObserver); + volumePanelOnLeftObserver.onChange(true); + } + initDimens(); mOrientation = mContext.getResources().getConfiguration().orientation; @@ -579,6 +603,12 @@ private void initDialog(int lockTaskModeState) { lp.windowAnimations = -1; mOriginalGravity = mContext.getResources().getInteger(R.integer.volume_dialog_gravity); + if (!mIsTv) { + // Clear the pre-defined gravity for left or right, + // this is handled by mVolumePanelOnLeft + mOriginalGravity &= ~(Gravity.LEFT | Gravity.RIGHT); + mOriginalGravity |= mVolumePanelOnLeft ? Gravity.LEFT : Gravity.RIGHT; + } mWindowGravity = Gravity.getAbsoluteGravity(mOriginalGravity, mContext.getResources().getConfiguration().getLayoutDirection()); lp.gravity = mWindowGravity; From 35ca40109e679885f0bce821e898723c8b164927 Mon Sep 17 00:00:00 2001 From: Arian Date: Sun, 30 Jan 2022 20:59:18 +0100 Subject: [PATCH 011/190] SystemUI: Make the volume dialog expandable Co-authored-by: Christian Hoffmann Change-Id: I780a9851eaa209204b6be2ad26506e4eab4dc84e Signed-off-by: Ghosuto --- packages/SystemUI/proguard.flags | 4 + .../res/drawable/ic_speaker_group_24dp.xml | 22 ++ .../res/layout-land/volume_dialog_legacy.xml | 29 +- .../res/layout/volume_dialog_legacy.xml | 29 +- .../systemui/volume/VolumeDialogImpl.java | 292 +++++++++++++++++- 5 files changed, 353 insertions(+), 23 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_speaker_group_24dp.xml diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags index 4c8c898150e0..7e9cbc36a2a1 100644 --- a/packages/SystemUI/proguard.flags +++ b/packages/SystemUI/proguard.flags @@ -6,3 +6,7 @@ -keep,allowoptimization,allowaccessmodification class com.android.systemui.dagger.DaggerReferenceGlobalRootComponent** { !synthetic *; } -keep class com.google.** { *; } + +-keep class com.android.systemui.statusbar.phone.ExpandableIndicator { + *; +} diff --git a/packages/SystemUI/res/drawable/ic_speaker_group_24dp.xml b/packages/SystemUI/res/drawable/ic_speaker_group_24dp.xml new file mode 100644 index 000000000000..37475e52010a --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_speaker_group_24dp.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml index d418028fbf8a..394c7946b121 100644 --- a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml +++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml @@ -23,7 +23,7 @@ android:theme="@style/volume_dialog_theme"> - + + + - + - + android:background="@drawable/volume_background"> + + + - + mRows = new ArrayList<>(); private ConfigurableTexts mConfigurableTexts; private final SparseBooleanArray mDynamic = new SparseBooleanArray(); @@ -324,6 +334,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private FrameLayout mRoundedBorderBottom; + // Volume panel expand state + private boolean mExpanded; + + // Number of animating rows + private int mAnimatingRows = 0; + @VisibleForTesting final int mVolumeRingerIconDrawableId = R.drawable.ic_legacy_speaker_on; @VisibleForTesting @@ -532,11 +548,31 @@ private void unionViewBoundstoTouchableRegion(final View view) { // drawer. This area should not be touchable unless the ringer drawer is open. // In landscape the ringer expands to the left and it has to be ensured that if there // are multiple rows they are touchable. - if (view == mTopContainer && !mIsRingerDrawerOpen) { + // The invisible expandable rows reserve space if the panel is not expanded, this space + // needs to be touchable. + if (view == mTopContainer) { if (!isLandscape()) { - yExtraSize = getRingerDrawerOpenExtraSize(); - } else if (getRingerDrawerOpenExtraSize() > getVisibleRowsExtraSize()) { - xExtraSize = (getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize()); + if (!mIsRingerDrawerOpen) { + yExtraSize = getRingerDrawerOpenExtraSize(); + } + if (!mExpanded) { + xExtraSize = getExpandableRowsExtraSize(); + } + } else { + if (!mIsRingerDrawerOpen && !mExpanded) { + xExtraSize = + Math.max(getRingerDrawerOpenExtraSize(), getExpandableRowsExtraSize()); + } else if (!mIsRingerDrawerOpen) { + if (getRingerDrawerOpenExtraSize() > getVisibleRowsExtraSize()) { + xExtraSize = getRingerDrawerOpenExtraSize() - getVisibleRowsExtraSize(); + } + } else if (!mExpanded) { + if ((getVisibleRowsExtraSize() + getExpandableRowsExtraSize()) + > getRingerDrawerOpenExtraSize()) { + xExtraSize = (getVisibleRowsExtraSize() + getExpandableRowsExtraSize()) + - getRingerDrawerOpenExtraSize(); + } + } } } @@ -584,6 +620,7 @@ private void initDialog(int lockTaskModeState) { mConfigurableTexts = new ConfigurableTexts(mContext); mHovering = false; mShowing = false; + mExpanded = false; mWindow = mDialog.getWindow(); mWindow.requestFeature(Window.FEATURE_NO_TITLE); mWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); @@ -618,6 +655,8 @@ private void initDialog(int lockTaskModeState) { mDialog.setContentView(R.layout.volume_dialog_legacy); mDialogView = mDialog.findViewById(R.id.volume_dialog); mDialogView.setAlpha(0); + mDialogView.setLayoutDirection( + mVolumePanelOnLeft ? LAYOUT_DIRECTION_LTR : LAYOUT_DIRECTION_RTL); mDialogTimeoutMillis = mSecureSettings.get().getInt( Settings.Secure.VOLUME_DIALOG_DISMISS_TIMEOUT, DIALOG_TIMEOUT_MILLIS); mDialog.setCanceledOnTouchOutside(true); @@ -721,6 +760,9 @@ public void onViewDetachedFromWindow(View v) { updateBackgroundForDrawerClosedAmount(); setTopContainerBackgroundDrawable(); + + // Rows need to be updated after mRingerAndDrawerContainerBackground is set + updateRowsH(getActiveRow()); } }); } @@ -773,6 +815,9 @@ public void onViewDetachedFromWindow(View v) { mAppVolumeIcon = mDialog.findViewById(R.id.app_volume); mRoundedBorderBottom = mDialog.findViewById(R.id.rounded_border_bottom); + mExpandRowsView = mDialog.findViewById(R.id.expandable_indicator_container); + mExpandRows = mDialog.findViewById(R.id.expandable_indicator); + if (isWindowGravityLeft()) { ViewGroup container = mDialog.findViewById(R.id.volume_dialog_container); setGravity(container, Gravity.LEFT); @@ -790,6 +835,8 @@ public void onViewDetachedFromWindow(View v) { setGravity(mDialogRowsViewContainer, Gravity.LEFT); setGravity(mODICaptionsView, Gravity.LEFT); + + mExpandRows.setRotation(-90); } if (mRows.isEmpty()) { @@ -864,8 +911,7 @@ private boolean isLandscape() { } private boolean isRtl() { - return mContext.getResources().getConfiguration().getLayoutDirection() - == LAYOUT_DIRECTION_RTL; + return mDialogView.getLayoutDirection() == LAYOUT_DIRECTION_RTL; } public void setStreamImportant(int stream, boolean important) { @@ -1336,6 +1382,71 @@ private void updateSelectedRingerContainerDescription(boolean open) { mSelectedRingerContainer.setContentDescription(currentMode + tapToSelect); } + /** + * Returns a {@link MediaController} that state is playing and type is local playback, + * and also have active sessions. + */ + @Nullable + private MediaController getActiveLocalMediaController() { + MediaSessionManager mediaSessionManager = + mContext.getSystemService(MediaSessionManager.class); + MediaController localController = null; + final List remoteMediaSessionLists = new ArrayList<>(); + for (MediaController controller : mediaSessionManager.getActiveSessions(null)) { + final MediaController.PlaybackInfo pi = controller.getPlaybackInfo(); + if (pi == null) { + // do nothing + continue; + } + final PlaybackState playbackState = controller.getPlaybackState(); + if (playbackState == null) { + // do nothing + continue; + } + if (D.BUG) { + Log.d(TAG, + "getActiveLocalMediaController() package name : " + + controller.getPackageName() + + ", play back type : " + pi.getPlaybackType() + + ", play back state : " + playbackState.getState()); + } + if (playbackState.getState() != PlaybackState.STATE_PLAYING) { + // do nothing + continue; + } + if (pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (localController != null + && TextUtils.equals( + localController.getPackageName(), controller.getPackageName())) { + localController = null; + } + if (!remoteMediaSessionLists.contains(controller.getPackageName())) { + remoteMediaSessionLists.add(controller.getPackageName()); + } + continue; + } + if (pi.getPlaybackType() == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL) { + if (localController == null + && !remoteMediaSessionLists.contains(controller.getPackageName())) { + localController = controller; + } + } + } + return localController; + } + + private boolean isBluetoothA2dpConnected() { + final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled() + && mBluetoothAdapter.getProfileConnectionState(BluetoothProfile.A2DP) + == BluetoothProfile.STATE_CONNECTED; + } + + private boolean isMediaControllerAvailable() { + final MediaController mediaController = getActiveLocalMediaController(); + return mediaController != null && !TextUtils.isEmpty(mediaController.getPackageName()); + } + private void initSettingsH(int lockTaskModeState) { final boolean showSettings = mDeviceProvisionedController.isCurrentUserSetup() && lockTaskModeState == LOCK_TASK_MODE_NONE; @@ -1343,15 +1454,30 @@ private void initSettingsH(int lockTaskModeState) { mRoundedBorderBottom.setVisibility(!showSettings ? VISIBLE : GONE); } if (mSettingsView != null) { - mSettingsView.setVisibility(showSettings ? VISIBLE : GONE); + mSettingsView.setVisibility( + showSettings && (isMediaControllerAvailable() || isBluetoothA2dpConnected()) + ? VISIBLE + : GONE); } if (mSettingsIcon != null) { mSettingsIcon.setOnClickListener(v -> { Events.writeEvent(Events.EVENT_SETTINGS_CLICK); + String packageName = isMediaControllerAvailable() + ? getActiveLocalMediaController().getPackageName() + : ""; + mMediaOutputDialogManager.createAndShow(packageName, true, mDialogView, null, null); dismissH(DISMISS_REASON_SETTINGS_CLICKED); - mMediaOutputDialogManager.dismiss(); - mVolumeNavigator.openVolumePanel( - mVolumePanelNavigationInteractor.getVolumePanelRoute()); + }); + } + + if (mExpandRowsView != null) { + mExpandRowsView.setVisibility(showSettings ? VISIBLE : GONE); + } + if (mExpandRows != null) { + mExpandRows.setOnClickListener(v -> { + mExpanded = !mExpanded; + updateRowsH(mDefaultRow, true); + mExpandRows.setExpanded(mExpanded); }); } } @@ -1779,6 +1905,11 @@ protected void dismissH(int reason) { mDialog.dismiss(); } tryToRemoveCaptionsTooltip(); + mExpanded = false; + if (mExpandRows != null) { + mExpandRows.setExpanded(mExpanded); + } + mAnimatingRows = 0; mDefaultRow = null; mIsAnimatingDismiss = false; @@ -1807,6 +1938,12 @@ private boolean isTv() { || mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEVISION); } + private boolean isExpandableRowH(VolumeRow row) { + return row != null && row != mDefaultRow && !row.defaultStream + && (row.stream == STREAM_RING + || row.stream == STREAM_ALARM + || row.stream == STREAM_MUSIC); + } private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { boolean isActive = row.stream == activeRow.stream; @@ -1826,6 +1963,10 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { return true; } + if (mExpanded && isExpandableRowH(row)) { + return true; + } + // Always show the stream for audio sharing if it exists. if (row.ss != null && mContext.getString(R.string.volume_dialog_guest_device_volume_description) @@ -1856,6 +1997,10 @@ private boolean shouldBeVisibleH(VolumeRow row, VolumeRow activeRow) { private void updateRowsH(final VolumeRow activeRow) { Trace.beginSection("VolumeDialogImpl#updateRowsH"); + updateRowsH(activeRow, false); + } + + private void updateRowsH(final VolumeRow activeRow, boolean animate) { if (D.BUG) Log.d(TAG, "updateRowsH"); if (!mShowing) { trimObsoleteH(); @@ -1869,10 +2014,17 @@ private void updateRowsH(final VolumeRow activeRow) { // apply changes to all rows for (final VolumeRow row : mRows) { final boolean isActive = row == activeRow; + final boolean isExpandableRow = isExpandableRowH(row); final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); - Util.setVisOrGone(row.view, shouldBeVisible); - if (shouldBeVisible && mRingerAndDrawerContainerBackground != null) { + if (!isExpandableRow) { + Util.setVisOrGone(row.view, shouldBeVisible); + } else if (!mExpanded) { + row.view.setVisibility(View.INVISIBLE); + } + + if ((shouldBeVisible || isExpandableRow) + && mRingerAndDrawerContainerBackground != null) { // For RTL, the outmost row has the lowest index since child views are laid out // from right to left. outmostVisibleRowIndex = isOutmostIndexMax @@ -1891,6 +2043,7 @@ private void updateRowsH(final VolumeRow activeRow) { linearLayoutParams.setMarginStart(mRingerRowsPadding); } } + row.view.setLayoutParams(layoutParams); // Set the background on each of the rows. We'll remove this from the last row after // the loop, since the last row's background is drawn by the main volume container. @@ -1898,7 +2051,7 @@ private void updateRowsH(final VolumeRow activeRow) { mContext.getDrawable(R.drawable.volume_row_rounded_background)); } - if (row.view.isShown()) { + if (row.view.isShown() || isExpandableRow) { updateVolumeRowTintH(row, isActive); } } @@ -1913,8 +2066,106 @@ private void updateRowsH(final VolumeRow activeRow) { ((LinearLayout.LayoutParams) layoutParams); linearLayoutParams.setMarginStart(0); linearLayoutParams.setMarginEnd(0); + lastVisibleChild.setLayoutParams(linearLayoutParams); lastVisibleChild.setBackgroundColor(Color.TRANSPARENT); } + + int elevationCount = 0; + if (animate) { + // Increase the elevation of the outmost row so that other rows animate behind it. + lastVisibleChild.setElevation(1f / ++elevationCount); + + // Add a solid background to the outmost row temporary so that other rows animate + // behind it + lastVisibleChild.setBackgroundDrawable( + mContext.getDrawable(R.drawable.volume_background)); + } + + int[] lastVisibleChildLocation = new int[2]; + lastVisibleChild.getLocationInWindow(lastVisibleChildLocation); + + // Track previous rows to calculate translations + int visibleRowsCount = 0; + int hiddenRowsCount = 0; + int rowWidth = mDialogWidth + mRingerRowsPadding; + + for (final VolumeRow row : mRows) { + final boolean isExpandableRow = isExpandableRowH(row); + final boolean shouldBeVisible = shouldBeVisibleH(row, activeRow); + + if (shouldBeVisible) { + visibleRowsCount++; + } else if (isExpandableRow) { + hiddenRowsCount++; + } + + // Only move rows that are either expandable or visible and not the default stream + if ((!isExpandableRow && !shouldBeVisible) || row.defaultStream) { + continue; + } + + // Cancel any ongoing animations + row.view.animate().cancel(); + + // Expandable rows need to animate behind the last visible row, an additional + // always visible stream animates next to the default stream + float translationX = (isWindowGravityLeft() ? -1 : 1) * hiddenRowsCount * rowWidth; + + if (animate) { + // If the translation is not set and the panel is expanding start the + // animation behind the main row + if (isExpandableRow && mExpanded && row.view.getTranslationX() == 0f) { + row.view.setTranslationX( + (isWindowGravityLeft() ? -1 : 1) * visibleRowsCount * rowWidth); + } + + // The elevation should decrease from the outmost row to the inner rows, so that + // every row animates behind the outer rows + row.view.setElevation(1f / ++elevationCount); + + row.view.setVisibility(View.VISIBLE); + + // Track how many rows are animating to avoid running animation end actions + // if there is still a row animating + mAnimatingRows++; + row.view.animate() + .translationX(translationX) + .setDuration(mExpanded ? mDialogShowAnimationDurationMs + : mDialogHideAnimationDurationMs) + .setInterpolator(mExpanded + ? new SystemUIInterpolators.LogDecelerateInterpolator() + : new SystemUIInterpolators.LogAccelerateInterpolator()) + .setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + mAnimatingRows--; + animation.removeAllListeners(); + } + + @Override + public void onAnimationEnd(Animator animation) { + row.view.setElevation(0); + if (!shouldBeVisible) { + row.view.setVisibility(View.INVISIBLE); + } + mAnimatingRows--; + if (mAnimatingRows == 0) { + // Restore the elevation and background + lastVisibleChild.setElevation(0); + lastVisibleChild.setBackgroundColor(Color.TRANSPARENT); + // Set the active stream to ensure the volume keys change + // the volume of the tinted row. The tint was set before + // already, but setting the active row cancels ongoing + // animations. + mController.setActiveStream(activeRow.stream, false); + } + } + }); + } else { + row.view.setTranslationX(translationX); + row.view.setVisibility(shouldBeVisible ? View.VISIBLE : View.INVISIBLE); + } + } } updateBackgroundForDrawerClosedAmount(); @@ -2467,6 +2718,21 @@ private int getVisibleRowsExtraSize() { return (visibleRows - 1) * (mDialogWidth + mRingerRowsPadding); } + /** + * Return the size of invisible rows. + * Expandable rows are invisible while the panel is not expanded. + */ + private int getExpandableRowsExtraSize() { + VolumeRow activeRow = getActiveRow(); + int expandableRows = 0; + for (final VolumeRow row : mRows) { + if (isExpandableRowH(row)) { + expandableRows++; + } + } + return expandableRows * (mDialogWidth + mRingerRowsPadding); + } + private void updateBackgroundForDrawerClosedAmount() { if (mRingerAndDrawerContainerBackground == null) { return; From 233941f92c59790633c6195b4053d79ce7898779 Mon Sep 17 00:00:00 2001 From: LuK1337 Date: Sat, 5 Aug 2023 13:55:52 +0200 Subject: [PATCH 012/190] VolumeDialogImpl: Add STREAM_NOTIFICATION row Change-Id: Ia851c70871ef561398203b358e8c4b32995f30cd Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 9d2d449e6573..d961717c70e9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -1941,6 +1941,7 @@ private boolean isTv() { private boolean isExpandableRowH(VolumeRow row) { return row != null && row != mDefaultRow && !row.defaultStream && (row.stream == STREAM_RING + || row.stream == STREAM_NOTIFICATION || row.stream == STREAM_ALARM || row.stream == STREAM_MUSIC); } @@ -2346,6 +2347,7 @@ private void updateVolumeRowH(VolumeRow row) { final boolean isVoiceCallStream = row.stream == AudioManager.STREAM_VOICE_CALL; final boolean isA11yStream = row.stream == STREAM_ACCESSIBILITY; final boolean isRingStream = row.stream == AudioManager.STREAM_RING; + final boolean isNotificationStream = row.stream == AudioManager.STREAM_NOTIFICATION; final boolean isSystemStream = row.stream == AudioManager.STREAM_SYSTEM; final boolean isAlarmStream = row.stream == STREAM_ALARM; final boolean isMusicStream = row.stream == AudioManager.STREAM_MUSIC; @@ -2353,6 +2355,7 @@ private void updateVolumeRowH(VolumeRow row) { && mState.ringerModeInternal == AudioManager.RINGER_MODE_VIBRATE; final boolean isRingSilent = isRingStream && mState.ringerModeInternal == AudioManager.RINGER_MODE_SILENT; + final boolean isNotificationMuted = isNotificationStream && ss.muted; final boolean isZenPriorityOnly = mState.zenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; final boolean isZenAlarms = mState.zenMode == Global.ZEN_MODE_ALARMS; final boolean isZenNone = mState.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS; @@ -2388,7 +2391,7 @@ private void updateVolumeRowH(VolumeRow row) { iconRes = com.android.internal.R.drawable.ic_qs_dnd; } else if (isRingVibrate) { iconRes = R.drawable.ic_legacy_volume_ringer_vibrate; - } else if (isRingSilent) { + } else if (isRingSilent || isNotificationMuted) { iconRes = row.iconMuteRes; } else if (ss.routedToBluetooth) { if (isVoiceCallStream) { From 01243bb95bc57e3b7a6afd3948229692398202af Mon Sep 17 00:00:00 2001 From: danielml Date: Sat, 27 Jan 2024 19:07:00 +0100 Subject: [PATCH 013/190] VolumeDialogImpl: Ignore external layout direction changes To simplify the handling of expanded volume rows with the dialog on the left and right side, we use the layout direction to ensure the first row is the outmost one. However, when the system layout direction was set again but did not change its actual value after initDialog, this setting was overriden and since no config change was indicated (mConfigChanged), the system layout direction was used when opening the dialog the next time. This lead to the first row not being the outmost one. To solve that issue, just ignore system layout direction changes. Test: 1. Open volume dialog 2. Eject and insert SIM card 3. Enter PIN, wait a second until the locale is set 4. Open the dialog and verify the the main row is the outmost one. Change-Id: I0b9f15972d0416647ba045b7776bed8e6250d57f Signed-off-by: danielml Signed-off-by: Arian Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index d961717c70e9..8d67d8ecccfe 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -2860,7 +2860,6 @@ public void onStateChanged(State state) { @Override public void onLayoutDirectionChanged(int layoutDirection) { - mDialogView.setLayoutDirection(layoutDirection); } @Override From 5cf75ad4b011dda44f16b4fcd1b05d4cdda11230 Mon Sep 17 00:00:00 2001 From: Arian Date: Sat, 5 Oct 2024 23:14:02 +0200 Subject: [PATCH 014/190] VolumeDialogImpl: Update rows when adding one while the panel is showing When a row is added while the dialog was open, for example when a remote stream is started, the row is added as the innermost row. Due to the hidden expandable rows we need to set the x-translation for the new row for it to not be in the middle of the screen. Test: 1. Start a remote stream in spotify 2. Force close spotify 3. Open spotify and immediately open the volume dialog by pressing a volume key 4. Ensure the row is placed next to the default row Change-Id: I8d5731acf85cad9b50a00c0bed116fcb1cff4f92 Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 8d67d8ecccfe..2d87ded67603 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -942,6 +942,9 @@ private void addRow(int stream, int iconRes, int iconMuteRes, boolean important, initRow(row, stream, iconRes, iconMuteRes, important, defaultStream); mDialogRowsView.addView(row.view); mRows.add(row); + if (mShowing) { + updateRowsH(getActiveRow()); + } } private void addExistingRows() { From 57843c1e8aeb2728a9238dcbc00a9982a554bb93 Mon Sep 17 00:00:00 2001 From: Dhina17 Date: Fri, 3 Jan 2025 16:47:35 +0530 Subject: [PATCH 015/190] Revert "Remove Live Captions button from the Volume Dialog when the new Volmue Panel is enabled" This reverts commit 0e0bc102630761aedfb92b970705e82afd320970. Reason: * Live caption is now shown in the new volume panel but we don't use it so it's not available to the user. * So bring it back as like 14 on the bottom of the volume dialog. Change-Id: Ia54cccf50c2b9b625d376a0a0165417f0c665184 Signed-off-by: Ghosuto --- .../src/com/android/systemui/volume/VolumeDialogImpl.java | 7 ------- .../com/android/systemui/volume/dagger/VolumeModule.java | 3 --- .../com/android/systemui/volume/VolumeDialogImplTest.java | 4 ---- 3 files changed, 14 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 2d87ded67603..c913a3d6934b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -155,7 +155,6 @@ import com.android.systemui.util.settings.SecureSettings; import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; @@ -355,7 +354,6 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final VibratorHelper mVibratorHelper; private final MSDLPlayer mMSDLPlayer; private final com.android.systemui.util.time.SystemClock mSystemClock; - private final VolumePanelFlag mVolumePanelFlag; private final VolumeDialogInteractor mInteractor; // Optional actions for soundDose private Optional> @@ -375,7 +373,6 @@ public VolumeDialogImpl( CsdWarningDialog.Factory csdWarningDialogFactory, DevicePostureController devicePostureController, Looper looper, - VolumePanelFlag volumePanelFlag, DumpManager dumpManager, Lazy secureSettings, VibratorHelper vibratorHelper, @@ -415,7 +412,6 @@ public VolumeDialogImpl( mVolumeNavigator = volumeNavigator; mSecureSettings = secureSettings; mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS; - mVolumePanelFlag = volumePanelFlag; mInteractor = interactor; dumpManager.registerDumpable("VolumeDialogImpl", this); @@ -1645,9 +1641,6 @@ protected void tryToRemoveCaptionsTooltip() { } private void updateODICaptionsH(boolean isServiceComponentEnabled, boolean fromTooltip) { - // don't show captions view when the new volume panel is enabled. - isServiceComponentEnabled = - isServiceComponentEnabled && !mVolumePanelFlag.canUseNewVolumePanel(); if (mODICaptionsView != null) { mODICaptionsView.setVisibility(isServiceComponentEnabled ? VISIBLE : GONE); } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 7cb666ea6fbf..4a68de885188 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -51,7 +51,6 @@ import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; import com.android.systemui.volume.panel.dagger.VolumePanelComponent; import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; @@ -130,7 +129,6 @@ static VolumeDialog provideVolumeDialog( VolumeNavigator volumeNavigator, CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, - VolumePanelFlag volumePanelFlag, DumpManager dumpManager, Lazy secureSettings, VibratorHelper vibratorHelper, @@ -162,7 +160,6 @@ static VolumeDialog provideVolumeDialog( csdFactory, devicePostureController, Looper.getMainLooper(), - volumePanelFlag, dumpManager, secureSettings, vibratorHelper, diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 847827609404..79731097ceff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -87,7 +87,6 @@ import com.android.systemui.util.time.FakeSystemClock; import com.android.systemui.volume.domain.interactor.VolumeDialogInteractor; import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; -import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag; import com.android.systemui.volume.ui.navigation.VolumeNavigator; import com.google.android.msdl.domain.MSDLPlayer; @@ -155,8 +154,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Mock private VolumeNavigator mVolumeNavigator; @Mock - private VolumePanelFlag mVolumePanelFlag; - @Mock private VolumeDialogInteractor mVolumeDialogInteractor; private final CsdWarningDialog.Factory mCsdWarningDialogFactory = @@ -222,7 +219,6 @@ public void setup() throws Exception { mCsdWarningDialogFactory, mPostureController, mTestableLooper.getLooper(), - mVolumePanelFlag, mDumpManager, mLazySecureSettings, mVibratorHelper, From 6dff96d4faa5fd102cfc1c73c345318055e23c95 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 8 Feb 2026 10:31:00 +0000 Subject: [PATCH 016/190] SettingsLib: Always display mobile signal as 4/4 bars - in combined data 5/5 looks good but not in qs or non-combined Signed-off-by: Ghosuto --- .../res/drawable/ic_mobile_level_list.xml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/SettingsLib/res/drawable/ic_mobile_level_list.xml b/packages/SettingsLib/res/drawable/ic_mobile_level_list.xml index 6ec6793ea233..5ff7e5fcb6be 100644 --- a/packages/SettingsLib/res/drawable/ic_mobile_level_list.xml +++ b/packages/SettingsLib/res/drawable/ic_mobile_level_list.xml @@ -23,12 +23,12 @@ SignalDrawable.java for usage. --> - - - - - - + + + + + + @@ -37,10 +37,10 @@ SignalDrawable.java for usage. --> - - - - - - + + + + + + From 37096afd6cf4dd372625d412ca3e94f1744cb83d Mon Sep 17 00:00:00 2001 From: LuK1337 Date: Sat, 31 Jan 2026 17:43:19 +0100 Subject: [PATCH 017/190] fixup! webkit: SystemImpl: Filter out unavailable providers Allows non-factory packages to be used. Fixes: https://gitlab.com/LineageOS/issues/android/-/issues/8764 Change-Id: I318ae9402c7d55ecf2d400a22050d0cb4053ed2b Signed-off-by: Ghosuto --- .../core/java/com/android/server/webkit/SystemImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index 3877f6358828..c7f8185549f7 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -143,7 +143,7 @@ public class SystemImpl implements SystemInterface { @Override public WebViewProviderInfo[] getWebViewPackages() { return Arrays.stream(mWebViewProviderPackages) - .filter(x -> isProviderAvailable(x.packageName)) + .filter(this::isProviderAvailable) .toArray(WebViewProviderInfo[]::new); } @@ -153,9 +153,9 @@ public long getFactoryPackageVersion(String packageName) throws NameNotFoundExce .getLongVersionCode(); } - private boolean isProviderAvailable(String packageName) { + private boolean isProviderAvailable(WebViewProviderInfo configInfo) { try { - getFactoryPackageVersion(packageName); + getPackageInfoForProvider(configInfo); return true; } catch (NameNotFoundException e) { return false; From 0fdddc7588f565debec80bbca7c38238680800cb Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 8 Feb 2026 12:50:32 +0000 Subject: [PATCH 018/190] SystemUI: Allow disable statusbar combined signal - Thanks crdroid for icon Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 5 +++ packages/SystemUI/res/drawable/ic_signal.xml | 26 +++++++++++++ .../SystemUI/res/values/lunaris_strings.xml | 3 ++ .../SystemUI/res/xml/status_bar_prefs.xml | 6 +++ .../connectivity/MobileSignalController.java | 9 +++++ .../domain/interactor/MobileIconInteractor.kt | 28 ++++++++++++++ .../interactor/MobileIconInteractorKairos.kt | 26 +++++++++++++ .../MobileIconInteractorKairosAdapter.kt | 7 ++++ .../interactor/MobileIconsInteractor.kt | 37 ++++++++++--------- .../interactor/MobileIconsInteractorKairos.kt | 13 +++++-- .../MobileIconsInteractorKairosAdapter.kt | 3 ++ .../viewmodel/MobileIconsViewModelKairos.kt | 1 + 12 files changed, 144 insertions(+), 20 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_signal.xml diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index fbffe0743a74..5369463c1510 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7170,6 +7170,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String SHOW_FOURG_ICON = "show_fourg_icon"; + /** + * @hide + */ + public static final String DISABLE_STACKED_MOBILE_ICONS = "disable_stacked_mobile_icons"; + /** * @hide */ diff --git a/packages/SystemUI/res/drawable/ic_signal.xml b/packages/SystemUI/res/drawable/ic_signal.xml new file mode 100644 index 000000000000..4e26d9af9407 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_signal.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index f248b2473038..13a5614f710a 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -25,6 +25,9 @@ Data disabled icon + + Disable combined data icon + 4G icon diff --git a/packages/SystemUI/res/xml/status_bar_prefs.xml b/packages/SystemUI/res/xml/status_bar_prefs.xml index beb9c17177aa..452b7d3aaf8b 100644 --- a/packages/SystemUI/res/xml/status_bar_prefs.xml +++ b/packages/SystemUI/res/xml/status_bar_prefs.xml @@ -134,6 +134,12 @@ android:title="@string/fourg_icon_title" sysui:defValue="false" /> + + val shouldShowFourgIcon: StateFlow + + val disableStackedMobileIcons: Flow } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ @@ -214,6 +216,9 @@ class MobileIconInteractorImpl( private final val SHOW_FOURG_ICON: String = "system:" + Settings.System.SHOW_FOURG_ICON; + private final val DISABLE_STACKED_MOBILE_ICONS: String = + "system:" + Settings.System.DISABLE_STACKED_MOBILE_ICONS + override val shouldShowFourgIcon: StateFlow = conflatedCallbackFlow { val callback = @@ -235,6 +240,29 @@ class MobileIconInteractorImpl( true ) + private val _disableStackedMobileIcons: StateFlow = + conflatedCallbackFlow { + val callback = + object : TunerService.Tunable { + override fun onTuningChanged(key: String, newValue: String?) { + when (key) { + DISABLE_STACKED_MOBILE_ICONS -> + trySend(TunerService.parseIntegerSwitch(newValue, false)) + } + } + } + Dependency.get(TunerService::class.java).addTunable(callback, DISABLE_STACKED_MOBILE_ICONS) + + awaitClose { Dependency.get(TunerService::class.java).removeTunable(callback) } + } + .stateIn( + scope, + started = SharingStarted.WhileSubscribed(), + false + ) + + override val disableStackedMobileIcons: Flow = _disableStackedMobileIcons + // True if there exists _any_ icon override for this carrierId. Note that overrides can include // any or none of the icon groups defined in MobileMappings, so we still need to check on a // per-network-type basis whether or not the given icon group is overridden diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt index bc23e6781732..99386e015134 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairos.kt @@ -152,6 +152,9 @@ interface MobileIconInteractorKairos { /** Whether to show the 4G icon instead of LTE. */ val shouldShowFourgIcon: State + + /** True if stacked mobile icons should be disabled */ + val disableStackedMobileIcons: State } /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */ @@ -419,6 +422,9 @@ class MobileIconInteractorKairosImpl( private val SHOW_FOURG_ICON: String = "system:" + Settings.System.SHOW_FOURG_ICON + private final val DISABLE_STACKED_MOBILE_ICONS: String = + "system:" + Settings.System.DISABLE_STACKED_MOBILE_ICONS + override val shouldShowFourgIcon: State = buildState { callbackFlow { val callback = @@ -436,4 +442,24 @@ class MobileIconInteractorKairosImpl( } .toState(initialValue = false) } + + private val _disableStackedMobileIcons: State = buildState { + callbackFlow { + val callback = + object : TunerService.Tunable { + override fun onTuningChanged(key: String, newValue: String?) { + when (key) { + DISABLE_STACKED_MOBILE_ICONS -> + trySend(TunerService.parseIntegerSwitch(newValue, false)) + } + } + } + Dependency.get(TunerService::class.java).addTunable(callback, DISABLE_STACKED_MOBILE_ICONS) + + awaitClose { Dependency.get(TunerService::class.java).removeTunable(callback) } + } + .toState(initialValue = false) + } + + override val disableStackedMobileIcons: State = _disableStackedMobileIcons } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt index 1eef307d527b..08616b219278 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorKairosAdapter.kt @@ -178,6 +178,12 @@ fun BuildScope.MobileIconInteractorKairosAdapter( "MobileIconInteractorKairosAdapter(subId=$subscriptionId).shouldShowFourgIcon" } ), + disableStackedMobileIcons = + disableStackedMobileIcons.toStateFlow( + nameTag { + "MobileIconInteractorKairosAdapter(subId=$subscriptionId).disableStackedMobileIcons" + } + ), ) } private class MobileIconInteractorKairosAdapter( @@ -207,4 +213,5 @@ private class MobileIconInteractorKairosAdapter( override val isAllowedDuringAirplaneMode: StateFlow, override val carrierNetworkChangeActive: StateFlow, override val shouldShowFourgIcon: StateFlow, + override val disableStackedMobileIcons: StateFlow, ) : MobileIconInteractor diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index 658ee5094f1d..5fe7abcd758e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -318,26 +318,29 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) - override val isStackable = - if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { - icons.flatMapLatest { icons -> - if (icons.isEmpty()) { - flowOf(false) - } else { - combine(icons.map { it.signalLevelIcon }) { signalLevelIcons -> - // These are only stackable if: - // - They are cellular - // - There's exactly two - // - They have the same number of levels - signalLevelIcons.filterIsInstance().let { - it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels - } + override val isStackable: StateFlow = + if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + icons.flatMapLatest { iconsList -> + when { + iconsList.isEmpty() -> flowOf(false) + iconsList.size != 2 -> flowOf(false) + else -> { + combine( + iconsList[0].disableStackedMobileIcons, + iconsList[0].signalLevelIcon, + iconsList[1].signalLevelIcon + ) { disableStacking, icon0, icon1 -> + !disableStacking && + icon0 is SignalIconModel.Cellular && + icon1 is SignalIconModel.Cellular && + icon0.numberOfLevels == icon1.numberOfLevels } } } - } else { - flowOf(false) - } + }.stateIn(scope, SharingStarted.WhileSubscribed(), false) + } else { + flowOf(false).stateIn(scope, SharingStarted.WhileSubscribed(), false) + } /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt index c5d15609fd0f..e0940a6eef99 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairos.kt @@ -306,7 +306,8 @@ constructor( } override val isStackable: State = - if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + if (NewStatusBarIcons.isEnabled && StatusBarRootModernization.isEnabled) { + combine( icons.flatMap { iconsBySubId: Map -> iconsBySubId.values .map { it.signalLevelIcon } @@ -319,10 +320,16 @@ constructor( it.size == 2 && it[0].numberOfLevels == it[1].numberOfLevels } } + }, + icons.flatMap { iconsBySubId -> + iconsBySubId.values.firstOrNull()?.disableStackedMobileIcons ?: stateOf(false) } - } else { - stateOf(false) + ) { shouldStack, disableStacking -> + shouldStack && !disableStacking } + } else { + stateOf(false) + } override val activeDataIconInteractor: State = combine(mobileConnectionsRepo.activeMobileDataSubscriptionId, icons) { activeSubId, icons -> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt index fedf421dbc08..9d4aafc77a22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorKairosAdapter.kt @@ -263,6 +263,9 @@ constructor( override val shouldShowFourgIcon: StateFlow = latest { shouldShowFourgIcon } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val disableStackedMobileIcons: StateFlow = latest { disableStackedMobileIcons } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private fun latest(block: MobileIconInteractor.() -> Flow): Flow = interactorsBySubId.flatMapLatestConflated { it[subId]?.block() ?: emptyFlow() } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt index c17696aed69c..2d1cb6e2f322 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt @@ -175,6 +175,7 @@ constructor( override val isVoWifi: State = latest(false) { isVoWifi } override val isVoWifiForceHidden: State = latest(true) { isVoWifiForceHidden } override val shouldShowFourgIcon: State = latest(false) { shouldShowFourgIcon } + override val disableStackedMobileIcons: State = latest(false) { disableStackedMobileIcons } } private fun trackedCommonViewModel(subId: Int) = From dca28aa81efabdc62b330227007aff1ce04d8f20 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 8 Feb 2026 17:12:38 +0000 Subject: [PATCH 019/190] SystemUI: Hide nowplaying view when bouncer is showing Signed-off-by: Ghosuto --- .../nowplaying/NowPlayingViewController.kt | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt b/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt index 7aa8e8e5ac43..e0d7abd76e3e 100644 --- a/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/nowplaying/NowPlayingViewController.kt @@ -45,6 +45,7 @@ constructor( private val mediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! private var activeController: MediaController? = null + private var bouncerShowingOrKeyguardDismissing = false private var currentTrackTitle: String = "" private var currentArtist: String = "" private var currentPackageName: String = "" @@ -175,6 +176,7 @@ constructor( val shouldShow = when { !isPlaying || currentTrackTitle.isEmpty() -> false + bouncerShowingOrKeyguardDismissing -> false !isPanelCollapsed -> false isDozing -> currentSettings.showOnAod isKeyguardShowing -> currentSettings.showOnLockscreen @@ -193,6 +195,33 @@ constructor( updateVisibility() } + override fun onPrimaryBouncerShowingChanged(showing: Boolean) { + bouncerShowingOrKeyguardDismissing = showing + if (showing) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + + override fun onKeyguardGoingAwayChanged(goingAway: Boolean) { + bouncerShowingOrKeyguardDismissing = goingAway + if (goingAway) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + + override fun onKeyguardFadingAwayChanged(fadingAway: Boolean) { + bouncerShowingOrKeyguardDismissing = fadingAway + if (fadingAway) { + nowPlayingView.hide() + } else { + updateVisibility() + } + } + override fun onDozingChanged(dozing: Boolean) { try { isDozing = ScrimUtils.get()?.isDozing() ?: false From 703758c2a869761cff9cfd4aea975d3f37feeff7 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Fri, 23 Jan 2026 04:07:14 +0000 Subject: [PATCH 020/190] SettingsLib: shrink 4g+ icon Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml index 1272ea7a30e1..32b7819b6232 100644 --- a/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml +++ b/packages/SettingsLib/res/drawable/ic_4g_plus_mobiledata.xml @@ -14,8 +14,8 @@ limitations under the License. --> Date: Tue, 20 Jan 2026 00:22:29 +0000 Subject: [PATCH 021/190] fwb: stop performance hint spam Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- native/android/performance_hint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index 1e48c1f6d7bb..97da7b857750 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -443,7 +443,7 @@ int APerformanceHintManager::createSessionUsingConfig(ASessionCreationConfig* se sessionCreationConfig->layerTokens.clear(); if (!ret.isOk() || !returnValue.session) { - ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage()); + //ALOGE("%s: PerformanceHint cannot create session. %s", __FUNCTION__, ret.getMessage()); switch (ret.getExceptionCode()) { case binder::Status::EX_UNSUPPORTED_OPERATION: return ENOTSUP; From 9b5e0729cade2ddc48411aeb30afba05e0c69131 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Wed, 4 Feb 2026 05:29:39 +0000 Subject: [PATCH 022/190] SystemUI: Fix roaming icon after google changes in QPR2 Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- .../mobile/ui/binder/MobileIconBinder.kt | 9 ++++++++ .../ui/viewmodel/MobileIconViewModel.kt | 14 +++++++++++++ .../ui/viewmodel/MobileIconViewModelKairos.kt | 21 +++++++++++++++++++ .../viewmodel/MobileIconsViewModelKairos.kt | 1 + .../viewmodel/StackedMobileIconViewModel.kt | 20 ++++++++++++++++-- .../StackedMobileIconViewModelKairos.kt | 11 +++++++++- .../shared/ui/composable/StackedMobileIcon.kt | 2 +- 7 files changed, 74 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index d834ddf58058..ad6f86564e62 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -72,6 +72,7 @@ object MobileIconBinder { val mobileDrawable = SignalDrawable(view.context) val mobileHdView = view.requireViewById(R.id.mobile_hd) val mobileHdSpace = view.requireViewById(R.id.mobile_hd_space) + val endSideRoamingView = view.requireViewById(R.id.mobile_roaming_updated) val dotView = view.requireViewById(R.id.status_bar_dot) view.isVisible = viewModel.isVisible.value @@ -230,6 +231,13 @@ object MobileIconBinder { } } + // Set the roaming indicator (single SIM - end side) + launch { + viewModel.isRoamingVisible.distinctUntilChanged().collect { isRoaming -> + endSideRoamingView.isVisible = isRoaming + } + } + if (statusBarStaticInoutIndicators()) { // Set the opacity of the activity indicators launch { @@ -276,6 +284,7 @@ object MobileIconBinder { networkTypeView.imageTintList = tint } + endSideRoamingView.imageTintList = tint mobileHdView.imageTintList = tint activityIn.imageTintList = tint activityOut.imageTintList = tint diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 521bfd58f9e6..3a5f5a10d2c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -51,6 +51,7 @@ interface MobileIconViewModelCommon { val icon: Flow val contentDescription: Flow val roaming: Flow + val isRoamingVisible: Flow /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: Flow /** The slice attribution. Drawn as a background layer */ @@ -127,6 +128,8 @@ class MobileIconViewModel( override val roaming: Flow = vmProvider.flatMapLatest { it.roaming } + override val isRoamingVisible: Flow = vmProvider.flatMapLatest { it.isRoamingVisible } + override val networkTypeIcon: Flow = vmProvider.flatMapLatest { it.networkTypeIcon } @@ -165,6 +168,7 @@ private class CarrierBasedSatelliteViewModelImpl( /** These fields are not used for satellite icons currently */ override val roaming: Flow = flowOf(false) + override val isRoamingVisible: Flow = flowOf(false) override val networkTypeIcon: Flow = flowOf(null) override val networkTypeBackground: StateFlow = MutableStateFlow(null) override val activityInVisible: Flow = flowOf(false) @@ -339,6 +343,16 @@ private class CellularIconViewModel( ) .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override val isRoamingVisible: StateFlow = + combine( + roaming, + iconInteractor.isRoamingForceHidden + ) { isRoaming, isHidden -> + isRoaming && !isHidden + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + private val activity: Flow = if (!constants.shouldShowActivityConfig) { flowOf(null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt index 968c6acc6f1e..65b1645f3f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelKairos.kt @@ -50,6 +50,7 @@ interface MobileIconViewModelKairosCommon { val icon: KairosState val contentDescription: KairosState val roaming: KairosState + val isRoamingVisible: KairosState /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */ val networkTypeIcon: KairosState /** The slice attribution. Drawn as a background layer */ @@ -117,6 +118,8 @@ class MobileIconViewModelKairos( override val roaming: KairosState = vmProvider.flatMap { it.roaming } + override val isRoamingVisible: KairosState = vmProvider.flatMap { it.isRoamingVisible } + override val networkTypeIcon: KairosState = vmProvider.flatMap { it.networkTypeIcon } @@ -148,6 +151,7 @@ private class CarrierBasedSatelliteViewModelKairosImpl( /** These fields are not used for satellite icons currently */ override val roaming: KairosState = stateOf(false) + override val isRoamingVisible: KairosState = stateOf(false) override val networkTypeIcon: KairosState = stateOf(null) override val networkTypeBackground: KairosState = stateOf(null) override val activityInVisible: KairosState = stateOf(false) @@ -304,6 +308,23 @@ private class CellularIconViewModelKairos( } } + override val isRoamingVisible: KairosState = + combine( + iconInteractor.isRoaming, + iconInteractor.isRoamingForceHidden + ) { isRoaming, isHidden -> + isRoaming && !isHidden + }.also { + onActivated { + logDiffsForTable( + name = nameTag { "CellularIconViewModelKairos(subId=$subscriptionId).isRoamingVisible" }, + it, + iconInteractor.tableLogBuffer, + columnName = "roamingVisible", + ) + } + } + private val activity: KairosState = if (!constants.shouldShowActivityConfig) { stateOf(null) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt index 2d1cb6e2f322..fbef7850e368 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelKairos.kt @@ -194,6 +194,7 @@ constructor( override val contentDescription: State = latest(null) { contentDescription } override val roaming: State = latest(false) { roaming } + override val isRoamingVisible: KairosState = stateOf(false) override val networkTypeIcon: State = latest(null) { networkTypeIcon } override val networkTypeBackground: State = latest(null) { networkTypeBackground } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt index 532dafef8bd7..8a86a6f2ee3a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt @@ -48,7 +48,7 @@ interface StackedMobileIconViewModel { val activityContainerVisible: Boolean /** [Context] to use when loading the [networkTypeIcon] */ val mobileContext: Context? - val roaming: Boolean + val isRoamingVisible: Boolean val isIconVisible: Boolean } @@ -190,7 +190,7 @@ constructor( initialValue = null, ) - override val roaming: Boolean by + private val roaming: Boolean by hydrator.hydratedStateOf( traceName = "isRoaming", source = @@ -211,6 +211,21 @@ constructor( initialValue = false, ) + override val isRoamingVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "isRoamingVisible", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + viewModels.firstOrNull()?.isRoamingVisible ?: flowOf(false) + } + .logDiffsForTable( + tableLogBuffer = tableLogger, + columnName = COL_ROAMING_VISIBLE, + initialValue = false, + ), + initialValue = false, + ) + override val isIconVisible: Boolean by hydrator.hydratedStateOf( traceName = "isIconVisible", @@ -245,5 +260,6 @@ constructor( private companion object { const val COL_IS_ICON_VISIBLE = "isIconVisible" const val COL_ROAMING = "roam" + const val COL_ROAMING_VISIBLE = "roamVisible" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt index 0cac207beffa..5b41b0106f23 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModelKairos.kt @@ -132,13 +132,22 @@ constructor( initialValue = null, ) - override val roaming: Boolean by + private val roaming: Boolean by hydratedComposeStateOf( name = "roaming", source = iconList.flatMap { icons -> icons.firstOrNull()?.roaming ?: stateOf(false) }, initialValue = false, ) + override val isRoamingVisible: Boolean by + hydratedComposeStateOf( + name = "isRoamingVisible", + source = iconList.flatMap { icons -> + icons.firstOrNull()?.isRoamingVisible ?: stateOf(false) + }, + initialValue = false, + ) + override val isIconVisible: Boolean get() = isStackable && dualSim != null diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt index 1e93ee0d4f64..1f35ef14b21d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt @@ -123,7 +123,7 @@ fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier contentDescription = viewModel.contentDescription, ) - if (viewModel.roaming) { + if (viewModel.isRoamingVisible) { val height = with(LocalDensity.current) { RoamingIconHeightSp.toDp() } val paddingTop = with(LocalDensity.current) { RoamingIconPaddingTopSp.toDp() } Image( From 3375669009065d99316cce4a5e2e8a3608690405 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 8 Jan 2026 09:38:27 +0800 Subject: [PATCH 023/190] SystemUI: Skipping screen off animation controller for fast animation settings Change-Id: I329d640625642cf99da168f431d9e9dadd926ae3 Signed-off-by: Ghosuto --- .../statusbar/phone/UnlockedScreenOffAnimationController.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 75dc42ba4b4e..aeab291699d1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -354,8 +354,8 @@ constructor( return false } - // If animations are disabled system-wide, don't play this one either. - if (globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) == 0f) { + // If animations are sped up, don't play this one either to avoid flickering. + if (globalSettings.getFloat(Settings.Global.ANIMATOR_DURATION_SCALE, 1f) != 1f) { return false } From dab01dd46559163b5047fb083f105f474713310c Mon Sep 17 00:00:00 2001 From: Alex Cruz Date: Sat, 19 Jan 2019 17:30:24 -0600 Subject: [PATCH 024/190] SystemUI: [SQUASHED] Ambient display show battery & cpu temperature [1/2] - Allow to hide battery in doze - Add a method to determine CPU temperature, battery temp Co-authored-by: Aston-Martinn Co-authored-by: eyosen Change-Id: I2c55f607a9ad1a9693618d5f23ff65a2a203efa1 Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 16 ++++ .../internal/util/lunaris/ThemeUtils.java | 57 ++++++++++++++ core/res/res/values/lunaris_config.xml | 5 ++ core/res/res/values/lunaris_symbols.xml | 5 ++ .../res/drawable/ic_ambient_battery.xml | 10 +++ .../SystemUI/res/drawable/ic_ambient_cpu.xml | 10 +++ .../res/drawable/ic_ambient_temperature.xml | 10 +++ .../KeyguardIndicationController.java | 78 ++++++++++++++++++- 8 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_ambient_battery.xml create mode 100644 packages/SystemUI/res/drawable/ic_ambient_cpu.xml create mode 100644 packages/SystemUI/res/drawable/ic_ambient_temperature.xml diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 5369463c1510..9d7ffae5c5b4 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7762,6 +7762,22 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean @Readable public static final String BLOCK_WALLPAPER_DIMMING = "block_wallpaper_dimming"; + /** + * What to show at the bottom of the Ambient display + * 0: Nothing. + * 1: Battery Percentage. + * 2: Battery Temperature. + * 3: Battery Percentage & Temperature Together. + * @hide + */ + public static final String AMBIENT_SHOW_SETTINGS = "ambient_show_settings"; + + /** + * Ambient settings show icons + * @hide + */ + public static final String AMBIENT_SHOW_SETTINGS_ICONS = "ambient_show_settings_icons"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/com/android/internal/util/lunaris/ThemeUtils.java b/core/java/com/android/internal/util/lunaris/ThemeUtils.java index 0a4d229c4888..fadabfbe1e49 100644 --- a/core/java/com/android/internal/util/lunaris/ThemeUtils.java +++ b/core/java/com/android/internal/util/lunaris/ThemeUtils.java @@ -22,6 +22,8 @@ import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.om.IOverlayManager; import android.content.om.OverlayInfo; import android.content.pm.PackageManager; @@ -38,6 +40,7 @@ import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.PathShape; import android.net.Uri; +import android.os.BatteryManager; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -48,6 +51,12 @@ import org.json.JSONException; import org.json.JSONObject; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.IOException; + import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -244,4 +253,52 @@ public boolean isDefaultOverlay(String category) { return getOverlayPackagesForCategory(category).stream() .noneMatch(pkg -> isOverlayEnabled(pkg)); } + + public static String batteryTemperature(Context context, Boolean ForC) { + Intent intent = context.registerReceiver(null, new IntentFilter( + Intent.ACTION_BATTERY_CHANGED)); + float temp = ((float) (intent != null ? intent.getIntExtra( + BatteryManager.EXTRA_TEMPERATURE, 0) : 0)) / 10; + // Round up to nearest number + int c = (int) ((temp) + 0.5f); + float n = temp + 0.5f; + // Use boolean to determine celsius or fahrenheit + return String.valueOf((n - c) % 2 == 0 ? (int) temp : + ForC ? c * 9/5 + 32 + "°F" :c + "°C"); + } + + public static String getCPUTemp(Context context) { + String value; + if (fileExists(context.getResources().getString( + com.android.internal.R.string.config_cpu_temp_path))) { + value = readOneLine(context.getResources().getString( + com.android.internal.R.string.config_cpu_temp_path)); + } else { + value = "Error"; + } + int cpuTempMultiplier = context.getResources().getInteger( + com.android.internal.R.integer.config_sysCPUTempMultiplier); + return value == "Error" ? "N/A" : String.format("%s", Integer.parseInt(value) / cpuTempMultiplier) + "°C"; + } + public static boolean fileExists(String filename) { + if (filename == null) { + return false; + } + return new File(filename).exists(); + } + public static String readOneLine(String fname) { + BufferedReader br; + String line = null; + try { + br = new BufferedReader(new FileReader(fname), 512); + try { + line = br.readLine(); + } finally { + br.close(); + } + } catch (Exception e) { + return null; + } + return line; + } } diff --git a/core/res/res/values/lunaris_config.xml b/core/res/res/values/lunaris_config.xml index 2b5d8553965c..516b9d79d2ef 100644 --- a/core/res/res/values/lunaris_config.xml +++ b/core/res/res/values/lunaris_config.xml @@ -105,4 +105,9 @@ false + + + /sys/class/thermal/thermal_zone0/temp + 1 + 1 diff --git a/core/res/res/values/lunaris_symbols.xml b/core/res/res/values/lunaris_symbols.xml index 2ab46a7ca652..f9059d7d27e2 100644 --- a/core/res/res/values/lunaris_symbols.xml +++ b/core/res/res/values/lunaris_symbols.xml @@ -99,4 +99,9 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/ic_ambient_battery.xml b/packages/SystemUI/res/drawable/ic_ambient_battery.xml new file mode 100644 index 000000000000..85372b6a6e70 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_ambient_battery.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_ambient_cpu.xml b/packages/SystemUI/res/drawable/ic_ambient_cpu.xml new file mode 100644 index 000000000000..117f3a6724ec --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_ambient_cpu.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_ambient_temperature.xml b/packages/SystemUI/res/drawable/ic_ambient_temperature.xml new file mode 100644 index 000000000000..4e6ec6489eaf --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_ambient_temperature.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 63420902eb65..31d1beda1eb1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -62,6 +62,7 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.hardware.biometrics.BiometricSourceType; import android.os.BatteryManager; import android.os.Handler; @@ -71,8 +72,10 @@ import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; +import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.format.Formatter; +import android.text.style.ImageSpan; import android.util.Pair; import android.view.View; import android.view.ViewGroup; @@ -1181,7 +1184,7 @@ protected final void updateDeviceEntryIndication(boolean animate) { boolean useMisalignmentColor = false; mLockScreenIndicationView.setVisibility(View.GONE); mTopIndicationView.setVisibility(VISIBLE); - CharSequence newIndication; + CharSequence newIndication = ""; if (!TextUtils.isEmpty(mBiometricMessage)) { newIndication = mBiometricMessage; // note: doesn't show mBiometricMessageFollowUp } else if (!TextUtils.isEmpty(mTransientIndication)) { @@ -1200,8 +1203,55 @@ protected final void updateDeviceEntryIndication(boolean animate) { } else if (mPowerPluggedIn || mEnableBatteryDefender) { newIndication = computePowerIndication(); } else { - newIndication = NumberFormat.getPercentInstance() - .format(mBatteryLevel / 100f); + String batteryLevel = NumberFormat.getPercentInstance().format(mBatteryLevel / 100f); + String batteryTemp = com.android.internal.util.lunaris.ThemeUtils.batteryTemperature(mContext, false); + String cpuTemp = com.android.internal.util.lunaris.ThemeUtils.getCPUTemp(mContext); + + Drawable batteryIcon = mContext.getDrawable(R.drawable.ic_ambient_battery); + Drawable cpuIcon = mContext.getDrawable(R.drawable.ic_ambient_cpu); + Drawable temperatureIcon = mContext.getDrawable(R.drawable.ic_ambient_temperature); + + if (batteryIcon != null) { + batteryIcon.setBounds(0, 0, batteryIcon.getIntrinsicWidth(), batteryIcon.getIntrinsicHeight()); + } + + if (cpuIcon != null) { + cpuIcon.setBounds(0, 0, cpuIcon.getIntrinsicWidth(), cpuIcon.getIntrinsicHeight()); + } + + if (temperatureIcon != null) { + temperatureIcon.setBounds(0, 0, temperatureIcon.getIntrinsicWidth(), temperatureIcon.getIntrinsicHeight()); + } + + SpannableStringBuilder indicationBuilder = new SpannableStringBuilder(); + + switch (getAmbientShowSettings()) { + case 2: // Battery level & battery temperature + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, batteryTemp, temperatureIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + + case 3: // Battery level, battery temperature & CPU temperature + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, batteryTemp, temperatureIcon, ambientShowSettingsIcon()); + appendWithSeparator(indicationBuilder, " | "); + appendIcons(indicationBuilder, cpuTemp, cpuIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + + case 0: // Hidden + newIndication = ""; + break; + + case 1: // Show battery level + default: + appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); + newIndication = indicationBuilder; + break; + } } if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) { @@ -1230,6 +1280,28 @@ protected final void updateDeviceEntryIndication(boolean animate) { updateLockScreenIndications(animate, getCurrentUser()); } + private int getAmbientShowSettings() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.AMBIENT_SHOW_SETTINGS, 0, UserHandle.USER_CURRENT); + } + + private boolean ambientShowSettingsIcon() { + return Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.AMBIENT_SHOW_SETTINGS_ICONS, 0, UserHandle.USER_CURRENT) != 0; + } + + private void appendIcons(SpannableStringBuilder builder, String text, Drawable icon, boolean showIcon) { + if (showIcon && icon != null) { + builder.append(" "); + builder.setSpan(new ImageSpan(icon), builder.length() - 1, builder.length(), builder.SPAN_INCLUSIVE_EXCLUSIVE); + } + builder.append(text); + } + + private void appendWithSeparator(SpannableStringBuilder builder, String separator) { + builder.append(separator); + } + /** * Assumption: device is charging */ From b126ec02bb5a7d908f4e26dc94d44cb2bdcb5c46 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 8 Oct 2025 15:54:02 +0000 Subject: [PATCH 025/190] SystemUI: Show username in ambient indicator [1/2] Change-Id: I879dc943394d7c1f11170619ca07c356fbda7852 Signed-off-by: Ghosuto --- .../SystemUI/res/values/lunaris_strings.xml | 2 ++ .../KeyguardIndicationController.java | 22 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index 13a5614f710a..8d3fe15b74c9 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -203,4 +203,6 @@ Re-evaluating system theme.. Please wait for a few seconds + + Lunaris AOSP diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 31d1beda1eb1..cc84aa57dcc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1223,6 +1223,19 @@ protected final void updateDeviceEntryIndication(boolean animate) { temperatureIcon.setBounds(0, 0, temperatureIcon.getIntrinsicWidth(), temperatureIcon.getIntrinsicHeight()); } + UserManager userManager = mContext.getSystemService(UserManager.class); + String userName = null; + if (userManager != null) { + UserHandle currentUser = android.os.Process.myUserHandle(); + UserInfo userInfo = userManager.getUserInfo(currentUser.getIdentifier()); + if (userInfo != null) { + userName = userInfo.name; + } + } + if (TextUtils.isEmpty(userName)) { + userName = mContext.getString(R.string.default_user_name); + } + SpannableStringBuilder indicationBuilder = new SpannableStringBuilder(); switch (getAmbientShowSettings()) { @@ -1242,10 +1255,15 @@ protected final void updateDeviceEntryIndication(boolean animate) { newIndication = indicationBuilder; break; - case 0: // Hidden - newIndication = ""; + case 4: // Hidden → show current user name + newIndication = userName; break; + case 0: // Hidden (safe) + SpannableStringBuilder hiddenIndication = new SpannableStringBuilder(""); + newIndication = hiddenIndication; + break; + case 1: // Show battery level default: appendIcons(indicationBuilder, batteryLevel, batteryIcon, ambientShowSettingsIcon()); From 2463ffe0eb55054e7e0a1ad19d8dbf4770d7f24b Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:36:07 +0800 Subject: [PATCH 026/190] SystemUI: Show Battery Defender indication only when battery level reaches charging threshold Change-Id: I40011190d3d9ad5264afdde5bd0720bb60ee38fe Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../systemui/statusbar/KeyguardIndicationController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index cc84aa57dcc7..731c48c0077e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1588,7 +1588,7 @@ public void onRefreshBatteryInfo(BatteryStatus status) { mBatteryLevel = status.level; mBatteryPresent = status.present; mTemperature = status.temperature; - mBatteryDefender = isBatteryDefender(status); + mBatteryDefender = isBatteryDefender(status) && mBatteryLevel >= 80; mBatteryDead = status.isDead(); // when the battery is overheated, device doesn't charge so only guard on pluggedIn: mEnableBatteryDefender = mBatteryDefender && status.isPluggedIn(); From 1ff228e92077b2995064749a8059acbb8e9e67af Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Sun, 14 Sep 2025 11:00:25 +0300 Subject: [PATCH 027/190] SystemUI: Allow disabling time to full on keyguard [1/2] Change-Id: I6888160583757474cde6c18b1992ac0498ab0c4b Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 7 +++++++ .../android/provider/settings/backup/SystemSettings.java | 1 + .../settings/validators/SystemSettingsValidators.java | 1 + .../systemui/statusbar/KeyguardIndicationController.java | 4 +++- 4 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 9d7ffae5c5b4..74b6b595fdd5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7120,6 +7120,13 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean public static final String STATUSBAR_BATTERY_BAR_BLEND_COLOR_REVERSE = "statusbar_battery_bar_blend_color_reverse"; + /** + * Whether to show remaining charging time on the lockscreen while charging + * @hide + */ + @Readable + public static final String LOCKSCREEN_CHARGING_TIME = "lockscreen_charging_time"; + /** * Enable/disable Bluetooth Battery bar * @hide diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index f0c7c367715d..50ffc53d614f 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -76,6 +76,7 @@ private static String[] getSettingsToBackUp() { Settings.System.POWER_SOUNDS_ENABLED, // moved to global Settings.System.DOCK_SOUNDS_ENABLED, // moved to global Settings.System.LOCKSCREEN_SOUNDS_ENABLED, + Settings.System.LOCKSCREEN_CHARGING_TIME, Settings.System.SHOW_WEB_SUGGESTIONS, Settings.System.SIP_CALL_OPTIONS, Settings.System.SIP_RECEIVE_CALLS, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index c8bbc8e9696b..e44f1ee66da0 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -210,6 +210,7 @@ public boolean validate(String value) { VALIDATORS.put(System.WINDOW_ORIENTATION_LISTENER_LOG, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCKSCREEN_SOUNDS_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCKSCREEN_DISABLED, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.LOCKSCREEN_CHARGING_TIME, BOOLEAN_VALIDATOR); VALIDATORS.put(System.SIP_RECEIVE_CALLS, BOOLEAN_VALIDATOR); VALIDATORS.put( System.SIP_CALL_OPTIONS, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 731c48c0077e..28ef4f738b4d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1347,7 +1347,9 @@ protected String computePowerChargingStringIndication() { return mContext.getResources().getString(R.string.keyguard_plugged_in, percentage); } - final boolean hasChargingTime = mChargingTimeRemaining > 0; + final boolean chargingTimeEnabled = Settings.System.getIntForUser(mContext.getContentResolver(), + Settings.System.LOCKSCREEN_CHARGING_TIME, 1, UserHandle.USER_CURRENT) == 1; + final boolean hasChargingTime = mChargingTimeRemaining > 0 && chargingTimeEnabled; int chargingId; if (mPowerPluggedInWired) { switch (mChargingSpeed) { From 502e9f5bc187d6bca0af88f87d13a7f035de05f1 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Tue, 4 Nov 2025 17:09:54 +0000 Subject: [PATCH 028/190] SystemUI: prevent crash on empty dozing indication Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- .../KeyguardIndicationController.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index 28ef4f738b4d..048822a52c19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -1274,19 +1274,22 @@ protected final void updateDeviceEntryIndication(boolean animate) { if (!TextUtils.equals(mTopIndicationView.getText(), newIndication)) { mWakeLock.setAcquired(true); - final KeyguardIndication.Builder builder = new KeyguardIndication.Builder() - .setMessage(newIndication) - .setTextColor(ColorStateList.valueOf( - useMisalignmentColor - ? mContext.getColor(R.color.misalignment_text_color) - : Color.WHITE)); - if (mBiometricMessage != null && newIndication == mBiometricMessage) { - builder.setForceAccessibilityLiveRegionAssertive(); + KeyguardIndication indication = null; + if (!TextUtils.isEmpty(newIndication)) { + final KeyguardIndication.Builder builder = new KeyguardIndication.Builder() + .setMessage(newIndication) + .setTextColor(ColorStateList.valueOf( + useMisalignmentColor + ? mContext.getColor(R.color.misalignment_text_color) + : Color.WHITE)); + if (mBiometricMessage != null && newIndication == mBiometricMessage) { + builder.setForceAccessibilityLiveRegionAssertive(); + } + indication = builder.build(); } - mTopIndicationView.switchIndication(newIndication, - builder.build(), - animate, () -> mWakeLock.setAcquired(false)); + mTopIndicationView.switchIndication(newIndication, indication, animate, + () -> mWakeLock.setAcquired(false)); } return; } From d3dbbef0dbd84057df0e438793fb47e8a8b3ca3a Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:12:22 +0800 Subject: [PATCH 029/190] core: Optimizing overscroller Change-Id: Ib9034fd8f14ffb149c2255c93c0b6de695314f7c Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- core/java/android/widget/OverScroller.java | 370 ++++++++++++++++++++- 1 file changed, 357 insertions(+), 13 deletions(-) diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index df209fb876ba..88671693a623 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -24,8 +24,120 @@ import android.util.Log; import android.view.ViewConfiguration; import android.view.animation.AnimationUtils; +import android.view.animation.BaseInterpolator; import android.view.animation.Interpolator; +/** + * @hide + */ +class AXUIInterpolator extends BaseInterpolator { + private static final double DEFAULT_STIFFNESS = 40.0; + private static final double DEFAULT_DAMPING_RATIO = 1.15; + private static final float DEFAULT_MAX_VELOCITY = 15000.0f; + + private final double mOmega; + private final double mDampingRatio; + private final double mInitialVelocityNorm; + private final float mDurationScale; + private final double mDampedFreq; + private final double mCoefficient; + private float mNormalizationFactor = -1.0f; + + public AXUIInterpolator(double stiffness, double dampingRatio, double velocity, + float durationScale, float maxVelocity) { + mOmega = Math.sqrt(stiffness <= 0.0 ? DEFAULT_STIFFNESS : stiffness); + mDampingRatio = dampingRatio <= 0.0 ? DEFAULT_DAMPING_RATIO : dampingRatio; + mInitialVelocityNorm = Math.abs(velocity) / (maxVelocity <= 0.0f ? DEFAULT_MAX_VELOCITY : maxVelocity); + mDurationScale = durationScale <= 0.0f ? 1.0f : durationScale; + + if (mDampingRatio < 1.0) { + // Underdamped + mDampedFreq = Math.sqrt(1.0 - (mDampingRatio * mDampingRatio)) * mOmega; + mCoefficient = ((mDampingRatio * mOmega) - mInitialVelocityNorm) / mDampedFreq; + } else if (Double.compare(1.0, mDampingRatio) == 0) { + // Critically damped + mDampedFreq = 0.0; + mCoefficient = (-mInitialVelocityNorm) + mOmega; + } else { + // Overdamped + mDampedFreq = 0.0; + mCoefficient = (-mInitialVelocityNorm) + (mDampingRatio * mOmega); + } + } + + private float calculatePosition(float t) { + if (t < 0.0f) t = 0.0f; + double time = t * mDurationScale; + double expTerm = Math.exp((-mDampingRatio) * mOmega * time); + double displacement; + + if (mDampingRatio < 1.0) { + // Underdamped oscillation + displacement = (Math.cos(mDampedFreq * time) + + (mCoefficient * Math.sin(mDampedFreq * time))) * expTerm; + } else if (Double.compare(1.0, mDampingRatio) == 0) { + // Critically damped + displacement = ((mCoefficient * time) + 1.0) * Math.exp((-mOmega) * time); + } else { + // Overdamped + double sqrtTerm = mOmega * Math.sqrt((mDampingRatio * mDampingRatio) - 1.0); + displacement = (expTerm / sqrtTerm) * + (((-mInitialVelocityNorm + (mOmega * mDampingRatio)) * + Math.sinh(mDampingRatio * time)) + + (Math.cosh(mDampingRatio * time) * sqrtTerm)); + } + return (float) (1.0 - displacement); + } + + @Override + public float getInterpolation(float input) { + if (mNormalizationFactor == -1.0f) { + float endValue = calculatePosition(1.0f); + mNormalizationFactor = endValue != 0.0f ? endValue : 1.0f; + } + return calculatePosition(input) / mNormalizationFactor; + } + + /** + */ + public float getVelocityRatio(float t) { + double time = t >= 0.0f ? t : 0.0f; + double expTerm = Math.exp((-mDurationScale) * mDampingRatio * mOmega * time); + double velocityMagnitude; + + if (mDampingRatio < 1.0) { + double sinTerm = Math.sin(mDurationScale * mDampedFreq * time); + double cosTerm = Math.cos(mDurationScale * mDampedFreq * time); + velocityMagnitude = Math.abs( + ((sinTerm * (-mDurationScale) * + ((mCoefficient * mDampingRatio * mOmega) + mDampedFreq)) + + (cosTerm * mDurationScale * + ((mCoefficient * mDampedFreq) - (mDampingRatio * mOmega)))) * expTerm); + } else if (Double.compare(1.0, mDampingRatio) != 0) { + // Overdamped + double sqrtTerm = mOmega * Math.sqrt((mDampingRatio * mDampingRatio) - 1.0); + double sqrtTermSq = sqrtTerm * sqrtTerm; + velocityMagnitude = Math.abs((expTerm / sqrtTerm) * + ((Math.sinh(mDurationScale * mDampingRatio * time) * mDurationScale * + ((sqrtTermSq + (mInitialVelocityNorm * mDampingRatio * mOmega)) - + ((mDampingRatio * mDampingRatio) * mOmega * mOmega))) + + (Math.cosh(mDurationScale * mDampingRatio * time) * mDurationScale * + mDampingRatio * (((mDampingRatio * mOmega) - mInitialVelocityNorm) - + (mOmega * sqrtTerm))))); + } else { + // Critically damped + velocityMagnitude = Math.abs(mDurationScale * + ((mCoefficient - mOmega) - (((mCoefficient * mDurationScale) * mOmega) * time)) * + Math.exp((-mDurationScale) * mOmega * time)); + } + return (float) velocityMagnitude; + } + + public float getDurationScale() { + return mDurationScale; + } +} + /** * This class encapsulates scrolling with the ability to overshoot the bounds * of a scrolling operation. This class is a drop-in replacement for @@ -599,6 +711,101 @@ static class SplineOverScroller { private static final int CUBIC = 1; private static final int BALLISTIC = 2; + private static final double AX_STIFFNESS = 40.0; + private static final double AX_DAMPING_RATIO = 1.15; + private static final float AX_MAX_VELOCITY = 15000.0f; + private static final double AX_FRICTION_NORMAL = 14.0; + private static final double AX_FRICTION_SLOW = 12.0; + private static final double AX_FRICTION_MID = 16.0; + + private static final double VELOCITY_THRESHOLD_LOW = 5000.0; + private static final double VELOCITY_THRESHOLD_HIGH = 8000.0; + private static final double FRICTION_COEF_1 = 0.35; + private static final double FRICTION_COEF_2 = 0.2; + private static final double FRICTION_COEF_3 = 0.3; + + private static final double MIN_VELOCITY_ADJUST_FRICTION = 1000.0; + private static final double MID_VELOCITY_ADJUST_FRICTION = 4000.0; + private static final double MAX_VELOCITY_ADJUST_FRICTION = 10000.0; + + private static final float SCALE_FACTOR_HIGH = 1.2f; + private static final float SCALE_FACTOR_MID = 0.8f; + private static final float SCALE_FACTOR_LOW = 0.5f; + private static final float VELOCITY_6K = 6000.0f; + private static final float VELOCITY_10K = 10000.0f; + private static final float VELOCITY_20K = 20000.0f; + + private AXUIInterpolator mSpringInterpolator; + private boolean mUseSpringPhysics = true; + private int mSpringSplineDuration; + private int mSpringDistanceOld; + private int mSpringDistanceNew; + private double mMinSplineDelta = FRICTION_COEF_2; + private int mOvershootDistance; + private int mIterationCount = 0; + private boolean mFinishedEarly = false; + private float mDeltaTime; + private int mPhysicsIterations = 0; + private long mLastUpdateTime; + + private FrictionConfig mCurrentFriction; + private final FrictionConfig mPrimaryFriction = new FrictionConfig(AX_FRICTION_NORMAL, 0.0); + private final FrictionConfig mSpringFriction = new FrictionConfig(AX_FRICTION_NORMAL, AX_STIFFNESS); + + private final CurrentState mPhysicsState = new CurrentState(); + private final CurrentState mPhysicsState2 = new CurrentState(); + + /** + */ + private static class FrictionConfig { + double mDamping; + double mStiffness; + + FrictionConfig(double damping, double stiffness) { + mDamping = calculateDamping((float) damping); + mStiffness = calculateStiffness((float) stiffness); + } + + void setDampingFromVelocity(double value) { + mDamping = calculateDamping((float) value); + } + + void setStiffnessFromValue(double value) { + mStiffness = calculateStiffness((float) value); + } + + static float calculateDamping(float f) { + if (f == 0.0f) { + return 0.0f; + } + return ((f - 8.0f) * 3.0f) + 25.0f; + } + + static double calculateStiffness(float f) { + if (f == 0.0f) { + return 0.0; + } + return ((f - 30.0f) * 3.62f) + 194.0f; + } + } + + /** + */ + private static class CurrentState { + double mPosition; + double mVelocity; + + void reset() { + mPosition = 0.0; + mVelocity = 0.0; + } + + void set(double position, double velocity) { + mPosition = position; + mVelocity = velocity; + } + } + static { float x_min = 0.0f; float y_min = 0.0f; @@ -681,6 +888,117 @@ private void adjustDuration(int start, int oldFinal, int newFinal) { } } + + /** + */ + private double getVelocityFrictionCoef(float velocity) { + double absVel = Math.abs(velocity); + if (absVel <= VELOCITY_THRESHOLD_LOW) { + return FRICTION_COEF_2; + } else if (absVel > VELOCITY_THRESHOLD_HIGH) { + return FRICTION_COEF_1; + } else { + return FRICTION_COEF_3; + } + } + + /** + */ + private float getDurationScaleFactor(float velocity) { + float absVel = Math.abs(velocity); + if (absVel <= 2000.0f) { + return SCALE_FACTOR_HIGH; + } else if (absVel <= VELOCITY_6K) { + float ratio = (absVel - 2000.0f) / 4000.0f; + return SCALE_FACTOR_HIGH - (ratio * 0.4f); + } else if (absVel <= VELOCITY_10K) { + return SCALE_FACTOR_MID; + } else if (absVel > VELOCITY_20K) { + return SCALE_FACTOR_LOW; + } else { + float ratio = (absVel - VELOCITY_10K) / VELOCITY_10K; + return SCALE_FACTOR_MID - (ratio * 0.3f); + } + } + + /** + */ + private void initSpringPhysics(int position, int velocity) { + mPhysicsIterations = 1; + mFinishedEarly = false; + mPrimaryFriction.setDampingFromVelocity(AX_FRICTION_NORMAL); + mPrimaryFriction.setStiffnessFromValue(0.0); + mCurrentFriction = mPrimaryFriction; + mPhysicsState.set(position, velocity); + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mLastUpdateTime = mStartTime; + } + + /** + */ + private void updateSpringPhysics() { + double pos = mPhysicsState.mPosition; + double vel = mPhysicsState.mVelocity; + double pos2 = mPhysicsState2.mPosition; + + adjustFrictionForVelocity(); + + double stiffness = mCurrentFriction.mStiffness; + double target = mFinal; + double force = (target - pos2) * stiffness; + float dt = mDeltaTime; + + double midPos = pos + (dt * vel) / 2.0; + double midVel = vel + (dt * force) / 2.0; + double damping = mCurrentFriction.mDamping; + double midForce = ((target - midPos) * stiffness) - (damping * midVel); + double midVel2 = vel + (dt * midForce) / 2.0; + double midForce2 = ((target - (pos + (dt * midVel) / 2.0)) * stiffness) - (damping * midVel2); + double finalPos = pos + (dt * midVel2); + double finalVel = vel + (dt * midForce2); + double finalForce = (stiffness * (target - finalPos)) - (damping * finalVel); + + double avgVel = ((midVel + midVel2) * 2.0 + vel + finalVel) * 0.167; + mPhysicsState2.mVelocity = finalVel; + mPhysicsState2.mPosition = finalPos; + mPhysicsState.mVelocity = vel + (dt * (force + ((midForce + midForce2) * 2.0) + finalForce) * 0.167); + mPhysicsState.mPosition = pos + (dt * avgVel); + mPhysicsIterations++; + } + + /** + */ + private void adjustFrictionForVelocity() { + if (mPhysicsIterations != 1) { + return; + } + double absVel = Math.abs(mPhysicsState.mVelocity); + if (absVel > MID_VELOCITY_ADJUST_FRICTION && absVel < MAX_VELOCITY_ADJUST_FRICTION) { + mCurrentFriction.mDamping = FrictionConfig.calculateDamping((float) AX_FRICTION_MID); + } else if (absVel >= MAX_VELOCITY_ADJUST_FRICTION) { + mCurrentFriction.setDampingFromVelocity(AX_FRICTION_NORMAL); + } else { + mCurrentFriction.mDamping = FrictionConfig.calculateDamping((float) AX_FRICTION_SLOW); + } + } + + /** + */ + private boolean hasLostVelocity() { + return Math.abs(mPhysicsState.mVelocity) < 5.0; + } + + /** + */ + private void updateWithSpringInterpolator(float progress) { + if (mSpringInterpolator == null) return; + float posRatio = mSpringInterpolator.getInterpolation(progress); + float velRatio = mSpringInterpolator.getVelocityRatio(progress); + mPhysicsState.mPosition = (mSpringDistanceOld * posRatio) + mStart; + mPhysicsState.mVelocity = ((mSpringDistanceOld * velRatio) / mSpringSplineDuration) * 1000.0f; + mPhysicsIterations++; + } + void startScroll(int start, int distance, int duration) { mFinished = false; @@ -782,6 +1100,25 @@ void fling(int start, int velocity, int min, int max, int over) { adjustDuration(mStart, mFinal, max); mFinal = max; } + + if (mUseSpringPhysics && velocity != 0) { + mMinSplineDelta = getVelocityFrictionCoef(Math.abs(velocity)); + mOvershootDistance = over; + mIterationCount = 0; + mFinishedEarly = false; + + float durationScale = getDurationScaleFactor(Math.abs(velocity)); + mSpringInterpolator = new AXUIInterpolator( + AX_STIFFNESS, AX_DAMPING_RATIO, + velocity, durationScale, AX_MAX_VELOCITY); + + mSpringDistanceOld = mSplineDistance; + mSpringDistanceNew = mFinal - start; + mSpringSplineDuration = mSplineDuration; + + initSpringPhysics(start, velocity); + mPhysicsState2.reset(); + } } private double getSplineDeceleration(int velocity) { @@ -924,20 +1261,27 @@ boolean update() { switch (mState) { case SPLINE: { final float t = (float) currentTime / mSplineDuration; - final int index = (int) (NB_SAMPLES * t); - float distanceCoef = 1.f; - float velocityCoef = 0.f; - if (index < NB_SAMPLES) { - final float t_inf = (float) index / NB_SAMPLES; - final float t_sup = (float) (index + 1) / NB_SAMPLES; - final float d_inf = SPLINE_POSITION[index]; - final float d_sup = SPLINE_POSITION[index + 1]; - velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); - distanceCoef = d_inf + (t - t_inf) * velocityCoef; - } - distance = distanceCoef * mSplineDistance; - mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; + if (mUseSpringPhysics && mSpringInterpolator != null && mSpringSplineDuration > 0) { + float posRatio = mSpringInterpolator.getInterpolation(t); + float velRatio = mSpringInterpolator.getVelocityRatio(t); + distance = posRatio * mSpringDistanceOld; + mCurrVelocity = ((mSpringDistanceOld * velRatio) / mSpringSplineDuration) * 1000.0f; + } else { + final int index = (int) (NB_SAMPLES * t); + float distanceCoef = 1.f; + float velocityCoef = 0.f; + if (index < NB_SAMPLES) { + final float t_inf = (float) index / NB_SAMPLES; + final float t_sup = (float) (index + 1) / NB_SAMPLES; + final float d_inf = SPLINE_POSITION[index]; + final float d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + distance = distanceCoef * mSplineDistance; + mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f; + } break; } From a2be107d3331ecc75f832fafa0c9933119669f73 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 27 Sep 2025 12:21:31 +0800 Subject: [PATCH 030/190] core: Adding scroll optimizer port Change-Id: I9e4b3f33e58377afb8f4114c2429bdcf4fdc6da4 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- core/java/android/view/Choreographer.java | 16 +- core/java/android/view/ViewRootImpl.java | 7 + .../android/view/WindowManagerGlobal.java | 49 ++ core/java/android/widget/AbsListView.java | 8 +- core/java/android/widget/OverScroller.java | 23 +- .../internal/util/ScrollOptimizer.java | 482 ++++++++++++++++++ .../jni/android_graphics_BLASTBufferQueue.cpp | 14 + .../android/graphics/BLASTBufferQueue.java | 16 + 8 files changed, 608 insertions(+), 7 deletions(-) create mode 100644 core/java/com/android/internal/util/ScrollOptimizer.java diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java index 37782ac7f2f2..3393f3d14963 100644 --- a/core/java/android/view/Choreographer.java +++ b/core/java/android/view/Choreographer.java @@ -44,6 +44,8 @@ import android.util.TimeUtils; import android.view.animation.AnimationUtils; +import com.android.internal.util.ScrollOptimizer; + import java.io.PrintWriter; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; @@ -367,6 +369,8 @@ private Choreographer(Looper looper, int vsyncSource, long layerHandle) { mLastFrameTimeNanos = Long.MIN_VALUE; mFrameIntervalNanos = (long)(1000000000 / getRefreshRate()); + + ScrollOptimizer.setFrameInterval(mFrameIntervalNanos); mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1]; for (int i = 0; i <= CALLBACK_LAST; i++) { @@ -872,7 +876,7 @@ public long getLatestExpectedPresentTimeNanos() { private void scheduleFrameLocked(long now) { if (!mFrameScheduled) { mFrameScheduled = true; - if (USE_VSYNC) { + if (ScrollOptimizer.shouldUseVsync()) { if (DEBUG_FRAMES) { Log.d(TAG, "Scheduling next frame on vsync."); } @@ -888,6 +892,7 @@ private void scheduleFrameLocked(long now) { mHandler.sendMessageAtFrontOfQueue(msg); } } else { + sFrameDelay = ScrollOptimizer.getFrameDelay(); final long nextFrameTime = Math.max( mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now); if (DEBUG_FRAMES) { @@ -1144,6 +1149,13 @@ void doFrame(long frameTimeNanos, int frame, mLastVsyncEventData.copyFrom(vsyncEventData); } + if (frameIntervalNanos > 0 && (Math.abs(frameIntervalNanos - mFrameIntervalNanos) + > TimeUtils.NANOS_PER_MS)) { + mFrameIntervalNanos = frameIntervalNanos; + ScrollOptimizer.setFrameInterval(mFrameIntervalNanos); + } + ScrollOptimizer.setUITaskStatus(true); + if (resynced && Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { String message = String.format("Choreographer#doFrame - resynced to %d in %.1fms", timeline.mVsyncId, (timeline.mDeadlineNanos - startNanos) * 0.000001f); @@ -1164,6 +1176,7 @@ void doFrame(long frameTimeNanos, int frame, doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameIntervalNanos); doCallbacks(Choreographer.CALLBACK_COMMIT, frameIntervalNanos); + ScrollOptimizer.setUITaskStatus(false); } finally { AnimationUtils.unlockAnimationClock(); mInDoFrameCallback = false; @@ -1599,6 +1612,7 @@ public void onVsync(long timestampNanos, long physicalDisplayId, int frame, mTimestampNanos = timestampNanos; mFrame = frame; mLastVsyncEventData.copyFrom(vsyncEventData); + ScrollOptimizer.setVsyncTime(mTimestampNanos); Message msg = Message.obtain(mHandler, this); msg.setAsynchronous(true); mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS); diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index c0e9e15f3431..e7251622facd 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -297,6 +297,7 @@ import com.android.internal.policy.PhoneFallbackEventHandler; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.ScrollOptimizer; import com.android.internal.view.BaseSurfaceHolder; import com.android.internal.view.RootViewSurfaceTaker; import com.android.internal.view.SurfaceCallbackHelper; @@ -2864,6 +2865,7 @@ void updateBlastSurfaceIfNeeded() { mBlastBufferQueue.destroy(); } mBlastBufferQueue = new BLASTBufferQueue(mTag, true /* updateDestinationFrame */); + ScrollOptimizer.setBLASTBufferQueue(mBlastBufferQueue); // If we create and destroy BBQ without recreating the SurfaceControl, we can end up // queuing buffers on multiple apply tokens causing out of order buffer submissions. We // fix this by setting the same apply token on all BBQs created by this VRI. @@ -10644,6 +10646,7 @@ private void scheduleProcessInputEvents() { } void doProcessInputEvents() { + ScrollOptimizer.setBLASTBufferQueue(mBlastBufferQueue); // Deliver all pending input events in the queue. while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; @@ -10658,6 +10661,10 @@ void doProcessInputEvents() { mPendingInputEventCount); mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent)); + + if (q.mEvent instanceof MotionEvent) { + ScrollOptimizer.setMotionType(((MotionEvent)q.mEvent).getActionMasked()); + } deliverInputEvent(q); } diff --git a/core/java/android/view/WindowManagerGlobal.java b/core/java/android/view/WindowManagerGlobal.java index e563d1781b45..e46cd9b9a3ee 100644 --- a/core/java/android/view/WindowManagerGlobal.java +++ b/core/java/android/view/WindowManagerGlobal.java @@ -59,6 +59,7 @@ import com.android.internal.os.ApplicationSharedMemory; import com.android.internal.policy.PhoneWindow; import com.android.internal.util.FastPrintWriter; +import com.android.internal.util.ScrollOptimizer; import java.io.FileDescriptor; import java.io.FileOutputStream; @@ -385,6 +386,36 @@ public View getRootView(String name) { return null; } + private int getVisibleRootCount (ArrayList roots) { + int visibleRootCount = 0; + int lastLeft = -1; + int lastTop = -1; + int lastWidth = 0; + int lastHeight = 0; + for (int i = roots.size() - 1; i >= 0; --i) { + View root_view = roots.get(i).getView(); + if (root_view != null && root_view.getVisibility() == View.VISIBLE) { + int left = root_view.getLeft(); + int top = root_view.getTop(); + int width = root_view.getRight() - root_view.getLeft() ; + int height = root_view.getBottom() - root_view.getTop() ; + // Filter the invalid visible views. + if (width != 0 && height != 0) { + // Filter the overwritten visible views. + if (lastWidth != width || lastHeight != height || + lastLeft != left || lastTop != top ) { + visibleRootCount++; + } + lastLeft = left; + lastTop = top; + lastWidth = width; + lastHeight = height; + } + } + } + return visibleRootCount; + } + public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow, int userId) { if (view == null) { @@ -491,6 +522,12 @@ public void addView(View view, ViewGroup.LayoutParams params, view.setLayoutParams(wparams); + int visibleRootCount = 0; + visibleRootCount = getVisibleRootCount(mRoots); + if (visibleRootCount > 1) { + ScrollOptimizer.disableOptimizer(true); + } + mViews.add(view); mRoots.add(root); mParams.add(wparams); @@ -623,6 +660,18 @@ void doRemoveView(ViewRootImpl root) { final View view = mViews.remove(index); mDyingViews.remove(view); } + // The visibleRootCount more than one means multi-layer, and multi-layer rendering + // can result in unexpected pending between UI thread and render thread with + // pre-rendering enabled. Need to disable pre-rendering for multi-layer cases. + int visibleRootCount = 0; + visibleRootCount = getVisibleRootCount(mRoots); + + if (visibleRootCount > 1) { + ScrollOptimizer.disableOptimizer(true); + } else if (visibleRootCount == 1) { + ScrollOptimizer.disableOptimizer(false); + } + allViewsRemoved = mRoots.isEmpty(); mWindowViewsListenerGroup.accept(getWindowViews()); diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 61ade5fdad8b..406961490d2e 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -4278,6 +4278,10 @@ private void onTouchUp(MotionEvent ev) { } mSelector.setHotspot(x, ev.getY()); } + if (!mDataChanged && !mIsDetaching + && isAttachedToWindow()) { + performClick.run(); + } if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } @@ -4288,10 +4292,6 @@ public void run() { mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); - if (!mDataChanged && !mIsDetaching - && isAttachedToWindow()) { - performClick.run(); - } } }; postDelayed(mTouchModeReset, diff --git a/core/java/android/widget/OverScroller.java b/core/java/android/widget/OverScroller.java index 88671693a623..6f31d4186da8 100644 --- a/core/java/android/widget/OverScroller.java +++ b/core/java/android/widget/OverScroller.java @@ -27,6 +27,8 @@ import android.view.animation.BaseInterpolator; import android.view.animation.Interpolator; +import com.android.internal.util.ScrollOptimizer; + /** * @hide */ @@ -275,6 +277,9 @@ public final boolean isFinished() { */ public final void forceFinished(boolean finished) { mScrollerX.mFinished = mScrollerY.mFinished = finished; + if (finished && mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } } /** @@ -399,6 +404,9 @@ public void setFinalY(int newY) { */ public boolean computeScrollOffset() { if (isFinished()) { + if (mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } return false; } @@ -438,6 +446,10 @@ public boolean computeScrollOffset() { } } + if (isFinished()) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } + break; } @@ -476,6 +488,7 @@ public void startScroll(int startX, int startY, int dx, int dy) { * @param duration Duration of the scroll in milliseconds. */ public void startScroll(int startX, int startY, int dx, int dy, int duration) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); mMode = SCROLL_MODE; mScrollerX.startScroll(startX, dx, duration); mScrollerY.startScroll(startY, dy, duration); @@ -546,6 +559,8 @@ public void fling(int startX, int startY, int velocityX, int velocityY, velocityY += oldVelocityY; } } + + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_START); mMode = FLING_MODE; mScrollerX.fling(startX, velocityX, minX, maxX, overX); @@ -614,6 +629,9 @@ public boolean isOverScrolled() { * @see #forceFinished(boolean) */ public void abortAnimation() { + if (mMode == FLING_MODE) { + ScrollOptimizer.setFlingFlag(ScrollOptimizer.FLING_END); + } mScrollerX.finish(); mScrollerY.finish(); } @@ -1247,9 +1265,10 @@ boolean continueWhenFinished() { */ boolean update() { final long time = AnimationUtils.currentAnimationTimeMillis(); - final long currentTime = time - mStartTime; + final long adjustedTime = ScrollOptimizer.getAdjustedAnimationClock(time); + final long currentTime = adjustedTime - mStartTime; - if (currentTime == 0) { + if (currentTime <= 0) { // Skip work but report that we're still going if we have a nonzero duration. return mDuration > 0; } diff --git a/core/java/com/android/internal/util/ScrollOptimizer.java b/core/java/com/android/internal/util/ScrollOptimizer.java new file mode 100644 index 000000000000..1a0ec2026a8d --- /dev/null +++ b/core/java/com/android/internal/util/ScrollOptimizer.java @@ -0,0 +1,482 @@ +/* + * Copyright (C) 2025 AxionOS + * + * 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.internal.util; + +import android.graphics.BLASTBufferQueue; +import android.os.Process; +import android.os.StrictMode; +import android.os.SystemClock; +import android.os.SystemProperties; +import android.os.Trace; +import android.util.Log; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Locale; + +public class ScrollOptimizer { + + /** @hide */ + public static final int FLING_START = 1; + /** @hide */ + public static final int FLING_END = 0; + + private static final String TAG = "AxPerf"; + + private static final String PROP_SCROLL_OPT = "persist.sys.perf.scroll_opt"; + private static final String PROP_SCROLL_OPT_HEAVY_APP = "persist.sys.perf.scroll_opt.heavy_app"; + private static final String PROP_DEBUG = "persist.sys.perf.scroll_opt_debug"; + + private static final String TIMER_SLACK_CONTENT = "50000"; + + private static final long FRAME_INTERVAL_THRESHOLD_NS = 10_000_000L; + private static final long DEFAULT_FRAME_DELAY_MS = 10L; + private static final long OPTIMIZED_FRAME_DELAY_MS = 3L; + private static final long FLING_END_TIMEOUT_MS = 3000L; + + private static int sInitialUndequeued = 4; + private static int sFallbackUndequeued = 3; + + private static final int PROP_UNSET = -1; + private static final int MOTION_TOUCH = 0; + private static final int MOTION_FLING = 1; + private static final int MOTION_SCROLL = 2; + private static final int APP_TYPE_NORMAL = 1; + private static final int APP_TYPE_HEAVY = 2; + + private static boolean sPrevUseVsync = true; + private static boolean sAdjustCalled = false; + private static boolean sTimerSlackUpdated = false; + private static boolean sPreRenderDone = false; + private static boolean sNeedUpdateBuffer = false; + + private static long sFrameIntervalNs = -1; + private static long sFrameIntervalMs = -1; + private static long sHalfFrameIntervalNs = -1; + private static long sHeavyFrameThresholdNs = -1; + private static long sLastVsyncTimeNs = -1; + private static long sLastAdjustedTimeNs = -1; + private static long sLastFlingStartMs = -1; + private static long sLastUIStartNs = -1; + private static long sLastUIEndNs = -1; + + private static int sPid = -1; + private static int sHeavyAppProp = -1; + private static int sHeavyApp = 0; + private static int sHeavyFrameCount = 0; + private static int sAppType = APP_TYPE_NORMAL; + private static int sMotionType = PROP_UNSET; + private static int mLastFlingFlg = 0; + + private static int sActualUndequeued = 0; + private static int sExpectedUndequeued = 0; + + private static BLASTBufferQueue sBlastQueue = null; + private static Method sSetUndequeuedMethod = null; + private static Method sGetUndequeuedMethod = null; + + private static FileOutputStream sTimerSlackStream = null; + + private static boolean sDebugEnabled = false; + private static boolean sInitCalled = false; + private static boolean sFeatureEnabled = false; + private static boolean sTemporarilyDisabled = false; + private static boolean sLastUseVsync = true; + + private static void logger(String msg) { + if (sDebugEnabled) { + Log.d(TAG, msg); + } + } + + private static int getUndequeuedBufferCount() { + int undequeued = 0; + if (sBlastQueue == null) { + logger("sBlastBufferQueue is null."); + sFeatureEnabled = false; + return 0; + } + try { + Object res = sGetUndequeuedMethod.invoke(sBlastQueue); + undequeued = res instanceof Integer ? (Integer) res : 0; + logger("undequeuedBufferCount: " + undequeued); + } catch (Exception e) { + undequeued = 0; + } + return undequeued; + } + + private static void initIfNeeded() { + try { + sFeatureEnabled = SystemProperties.getBoolean(PROP_SCROLL_OPT, true); + int prop = SystemProperties.getInt(PROP_SCROLL_OPT_HEAVY_APP, 2); + sHeavyAppProp = prop; + sHeavyApp = prop; + sDebugEnabled = SystemProperties.getBoolean(PROP_DEBUG, false); + + Class clazz = Class.forName("android.graphics.BLASTBufferQueue"); + sSetUndequeuedMethod = clazz.getMethod("setUndequeuedBufferCount", Integer.TYPE); + sGetUndequeuedMethod = clazz.getMethod("getUndequeuedBufferCount"); + + sPid = Process.myPid(); + sTimerSlackStream = new FileOutputStream( + String.format(Locale.US, "/proc/%d/timerslack_ns", sPid)); + + if (Process.myUid() == 1000) { + logger("Disable for system_server"); + sFeatureEnabled = false; + } + sInitCalled = true; + } catch (Exception e) { + Log.e(TAG, "Couldn't load BLASTBufferQueue Class"); + sInitCalled = true; + sFeatureEnabled = false; + } + + if (sHeavyApp == 1) { + logger("Heavy app detection is enabled."); + } + if (sSetUndequeuedMethod == null || sGetUndequeuedMethod == null) { + Log.e(TAG, "Couldn't find UndequeuedBufferCount functions"); + sFeatureEnabled = false; + } + } + + public static void disableOptimizer(boolean disable) { + boolean wasTemporarilyDisabled = sTemporarilyDisabled; + if (wasTemporarilyDisabled == disable) return; + if (wasTemporarilyDisabled) { + sFeatureEnabled = true; + sTemporarilyDisabled = false; + logger("enable ScrollOptimizer again."); + } else if (sFeatureEnabled) { + sFeatureEnabled = false; + sTemporarilyDisabled = true; + logger("disable ScrollOptimizer temperarily."); + } + } + + private static void resetFlingState() { + sLastUseVsync = true; + sAdjustCalled = false; + sAppType = APP_TYPE_NORMAL; + sHeavyFrameCount = 0; + sMotionType = PROP_UNSET; + mLastFlingFlg = 0; + sTimerSlackUpdated = false; + } + + private static void updateExpectedFromPreRender() { + if (sPreRenderDone) { + sPreRenderDone = false; + int val = getUndequeuedBufferCount(); + sActualUndequeued = val; + sExpectedUndequeued = val; + if (val > 1) { + sExpectedUndequeued = val - 1; + } else if (val < 1) { + sExpectedUndequeued = 1; + } + } + } + + public static long getAdjustedAnimationClock(long originalTimeNs) { + if (!sFeatureEnabled || Process.myTid() != sPid) { + return originalTimeNs; + } + if (sAdjustCalled) { + logger("unnecessary adjustClock is called!"); + if (originalTimeNs > sLastAdjustedTimeNs) { + sLastAdjustedTimeNs = originalTimeNs; + } + return sLastAdjustedTimeNs; + } + sAdjustCalled = true; + long candidate = sLastAdjustedTimeNs + sFrameIntervalMs; + if (mLastFlingFlg != 1) { + if (originalTimeNs >= candidate || + SystemClock.uptimeMillis() >= sLastFlingStartMs + FLING_END_TIMEOUT_MS) { + sLastAdjustedTimeNs = originalTimeNs; + return originalTimeNs; + } + logger("extended adjustedTime: " + candidate + ", originTime: " + originalTimeNs); + logger("extend clock adjustion"); + sLastAdjustedTimeNs = candidate; + return candidate; + } + if (candidate < originalTimeNs) { + candidate = originalTimeNs; + } else if (sPrevUseVsync) { + long offset = candidate - originalTimeNs; + if (offset > 0 && sFrameIntervalMs > 0) { + long rounds = Math.round((double) offset / (double) sFrameIntervalMs); + candidate = (sFrameIntervalMs * rounds) + originalTimeNs; + } + } + logger("adjustedTime: " + candidate + ", originTime: " + originalTimeNs); + sLastAdjustedTimeNs = candidate; + return candidate; + } + + public static long getFrameDelay() { + if (!sFeatureEnabled) { + return DEFAULT_FRAME_DELAY_MS; + } + return OPTIMIZED_FRAME_DELAY_MS; + } + + private static void setUndequeuedBufferCount(int count) { + if (sBlastQueue == null) { + logger("sBlastBufferQueue is null."); + sFeatureEnabled = false; + return; + } + try { + sSetUndequeuedMethod.invoke(sBlastQueue, count); + logger("setUndequeuedBufferCount: " + count); + } catch (Exception e) { + e.printStackTrace(); + sFeatureEnabled = false; + } + } + + private static void writeTimerSlack() { + if (sTimerSlackStream == null) { + sFeatureEnabled = false; + return; + } + StrictMode.ThreadPolicy old = StrictMode.allowThreadViolations(); + try { + try { + sTimerSlackStream.write(TIMER_SLACK_CONTENT.getBytes()); + sTimerSlackUpdated = true; + } catch (IOException e) { + e.printStackTrace(); + Log.w(TAG, "Failed to update timer slack!"); + sFeatureEnabled = false; + } + } finally { + StrictMode.setThreadPolicy(old); + } + } + + public static void setBLASTBufferQueue(BLASTBufferQueue queue) { + if (sFeatureEnabled && Process.myTid() == sPid && sBlastQueue != queue) { + sBlastQueue = queue; + sNeedUpdateBuffer = false; + setUndequeuedBufferCount(sInitialUndequeued); + } + } + + public static void setFlingFlag(int flingFlg) { + if (sFeatureEnabled && Process.myTid() == sPid) { + logger("setFlingFlag: " + flingFlg); + if (flingFlg != 1) { + if (flingFlg < 0) { + logger("Fling quit for unknown."); + } + if (mLastFlingFlg == 1) { + resetFlingState(); + logger("Fling end."); + } + return; + } + if (mLastFlingFlg == 1) { + resetFlingState(); + logger("avoid concurrent fling"); + return; + } + if (sMotionType == MOTION_FLING) { + mLastFlingFlg = flingFlg; + if (!sTimerSlackUpdated) { + writeTimerSlack(); + } + sNeedUpdateBuffer = false; + sLastFlingStartMs = SystemClock.uptimeMillis(); + logger("Fling start."); + } else { + logger("Fling without touch"); + } + sMotionType = PROP_UNSET; + } + } + + public static void setFrameInterval(long nanos) { + if (!sInitCalled) { + initIfNeeded(); + } + logger("frameIntervalNanos: " + nanos); + sFrameIntervalNs = nanos; + sFrameIntervalMs = nanos / 1_000_000; + long half = nanos / 2; + sHalfFrameIntervalNs = half; + sHeavyFrameThresholdNs = half * OPTIMIZED_FRAME_DELAY_MS; + if (nanos > FRAME_INTERVAL_THRESHOLD_NS) { + sInitialUndequeued = 3; + sFallbackUndequeued = 2; + } else { + sInitialUndequeued = 4; + sFallbackUndequeued = 3; + } + if (sHeavyAppProp == PROP_UNSET) { + if (nanos > FRAME_INTERVAL_THRESHOLD_NS) { + sHeavyApp = 0; + } else { + sHeavyApp = 1; + } + } + } + + public static void setMotionType(int motion) { + if (sFeatureEnabled && Process.myTid() == sPid) { + if (motion == MOTION_TOUCH) { + boolean wasFling = (mLastFlingFlg == 1); + resetFlingState(); + int curUndequeued = getUndequeuedBufferCount(); + sActualUndequeued = curUndequeued; + if (sNeedUpdateBuffer && curUndequeued != sFallbackUndequeued) { + if (curUndequeued > sFallbackUndequeued || + System.nanoTime() - sLastUIEndNs > (sFrameIntervalNs * 2) + 1_000_000) { + setUndequeuedBufferCount(sFallbackUndequeued); + sExpectedUndequeued = sFallbackUndequeued; + } + } else if (wasFling || sActualUndequeued > 0) { + sExpectedUndequeued = sActualUndequeued; + } else { + sExpectedUndequeued = 1; + } + } else if (motion == MOTION_SCROLL) { + sNeedUpdateBuffer = true; + int cur = getUndequeuedBufferCount(); + sActualUndequeued = cur; + if (sExpectedUndequeued > cur && cur > 0) { + sExpectedUndequeued = cur; + } + } + sMotionType = motion; + logger("setMotionType: " + motion); + } + } + + public static void setUITaskStatus(boolean running) { + if (sFeatureEnabled && Process.myTid() == sPid) { + long nowNs = System.nanoTime(); + long uiDurationNs; + if (running) { + sAdjustCalled = false; + if (mLastFlingFlg == 1) { + long durSinceUIStart = nowNs - sLastUIStartNs; + if (durSinceUIStart > (sFrameIntervalNs * 2) - 1_000_000) { + updateExpectedFromPreRender(); + } + long hf = sHalfFrameIntervalNs; + if (durSinceUIStart > (OPTIMIZED_FRAME_DELAY_MS * hf) - 1_000_000 && + nowNs - sLastUIEndNs > hf) { + sHeavyFrameCount++; + } + } + sLastUIStartNs = nowNs; + uiDurationNs = 0; + } else { + sLastUIEndNs = nowNs; + sPrevUseVsync = sLastUseVsync; + uiDurationNs = nowNs - sLastUIStartNs; + if (mLastFlingFlg == 1 && uiDurationNs > sFrameIntervalNs * 2) { + updateExpectedFromPreRender(); + } + } + int mode = sHeavyApp; + if (mode == 0) return; + if (mode == 2) { + sAppType = APP_TYPE_HEAVY; + return; + } + if (running) return; + if (sMotionType == MOTION_SCROLL || mLastFlingFlg == 1) { + if (uiDurationNs > sFrameIntervalNs) { + sHeavyFrameCount++; + } + if ((sHeavyFrameCount > 1 || uiDurationNs > sHeavyFrameThresholdNs) && + sAppType != APP_TYPE_HEAVY) { + sAppType = APP_TYPE_HEAVY; + logger("App type: heavy app"); + } + } + logger("UI duration: " + uiDurationNs); + } + } + + public static void setVsyncTime(long vsyncTimeNs) { + if (sFeatureEnabled) { + sLastVsyncTimeNs = vsyncTimeNs; + logger("setVsyncTime: " + sLastVsyncTimeNs); + } + } + + public static boolean shouldUseVsync() { + boolean result = true; + if (sFeatureEnabled && Process.myTid() == sPid) { + if (mLastFlingFlg != 1) { + sLastUseVsync = true; + return true; + } + if (sAppType == APP_TYPE_HEAVY) { + sLastUseVsync = false; + return false; + } + if (sPreRenderDone) { + logger("pre-render done"); + sLastUseVsync = true; + return true; + } + long interval = sFrameIntervalNs; + long timeToNext = interval - ((sLastUIStartNs - sLastVsyncTimeNs) % interval); + if (timeToNext < 3_000_000L) { + logger("too close to next vsync"); + sLastUseVsync = false; + if (sExpectedUndequeued > 0) { + sExpectedUndequeued = sExpectedUndequeued - 1; + } + return false; + } + if (!sLastUseVsync) { + logger("use vsync as last frame not use vsync"); + sLastUseVsync = true; + return true; + } + int undequeued = getUndequeuedBufferCount(); + sActualUndequeued = undequeued; + int expected = sExpectedUndequeued; + if (undequeued > expected) { + logger("align undequeued: " + sActualUndequeued + " with expected: " + sExpectedUndequeued); + sActualUndequeued = expected; + } else if (undequeued < 1 && expected > 0) { + sActualUndequeued = 1; + } + if (sActualUndequeued > 0) { + sExpectedUndequeued = sExpectedUndequeued - 1; + result = false; + } else { + sPreRenderDone = true; + logger("pre-render done"); + result = true; + } + sLastUseVsync = result; + } + return result; + } +} diff --git a/core/jni/android_graphics_BLASTBufferQueue.cpp b/core/jni/android_graphics_BLASTBufferQueue.cpp index e9e5b7f6c13a..ea4605eb09d9 100644 --- a/core/jni/android_graphics_BLASTBufferQueue.cpp +++ b/core/jni/android_graphics_BLASTBufferQueue.cpp @@ -188,6 +188,18 @@ static jobject nativeGetSurface(JNIEnv* env, jclass clazz, jlong ptr, queue->getSurface(includeSurfaceControlHandle)); } +static void nativeSetUndequeuedBufferCount(JNIEnv* env, jclass clazz, jlong ptr, jint count) { + auto queue = reinterpret_cast(ptr); + if (queue == nullptr) return; + queue->qtiSetUndequeuedBufferCount(count); +} + +static jint nativeGetUndequeuedBufferCount(JNIEnv* env, jclass clazz, jlong ptr) { + auto queue = reinterpret_cast(ptr); + if (queue == nullptr) return -1; + return queue->qtiGetUndequeuedBufferCount(); +} + class JGlobalRefHolder { public: JGlobalRefHolder(JavaVM* vm, jobject object) : mVm(vm), mObject(object) {} @@ -330,6 +342,8 @@ static const JNINativeMethod gMethods[] = { // clang-format off {"nativeCreate", "(Ljava/lang/String;Z)J", (void*)nativeCreate}, {"nativeGetSurface", "(JZ)Landroid/view/Surface;", (void*)nativeGetSurface}, + {"nativeSetUndequeuedBufferCount", "(JI)V", (void*)nativeSetUndequeuedBufferCount}, + {"nativeGetUndequeuedBufferCount", "(J)I", (void*)nativeGetUndequeuedBufferCount}, {"nativeDestroy", "(J)V", (void*)nativeDestroy}, {"nativeSyncNextTransaction", "(JLjava/util/function/Consumer;Z)Z", (void*)nativeSyncNextTransaction}, {"nativeStopContinuousSyncTransaction", "(J)V", (void*)nativeStopContinuousSyncTransaction}, diff --git a/graphics/java/android/graphics/BLASTBufferQueue.java b/graphics/java/android/graphics/BLASTBufferQueue.java index 2f40e7f1a459..44be74194b21 100644 --- a/graphics/java/android/graphics/BLASTBufferQueue.java +++ b/graphics/java/android/graphics/BLASTBufferQueue.java @@ -33,6 +33,8 @@ public final class BLASTBufferQueue { private static native long nativeCreate(String name, boolean updateDestinationFrame); private static native void nativeDestroy(long ptr); + private static native void nativeSetUndequeuedBufferCount(long ptr, int count); + private static native int nativeGetUndequeuedBufferCount(long ptr); private static native Surface nativeGetSurface(long ptr, boolean includeSurfaceControlHandle); private static native boolean nativeSyncNextTransaction(long ptr, Consumer callback, boolean acquireSingleBuffer); @@ -106,6 +108,20 @@ public Surface createSurfaceWithHandle() { return nativeGetSurface(mNativeObject, true /* includeSurfaceControlHandle */); } + /** + * Set undequeued buffer count + */ + public void setUndequeuedBufferCount(int count) { + nativeSetUndequeuedBufferCount(mNativeObject, count); + } + + /** + * @return the count of undequeued buffer + */ + public int getUndequeuedBufferCount() { + return nativeGetUndequeuedBufferCount(mNativeObject); + } + /** * Send a callback that accepts a transaction to BBQ. BBQ will acquire buffers into the a * transaction it created and will eventually send the transaction into the callback From 4f9f28c5a369f5fd5c4c2d1381691de336f46900 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 9 Feb 2026 15:39:23 +0000 Subject: [PATCH 031/190] core: Switch to automatic detection for scroll Optimizer Signed-off-by: Ghosuto --- core/java/com/android/internal/util/ScrollOptimizer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/com/android/internal/util/ScrollOptimizer.java b/core/java/com/android/internal/util/ScrollOptimizer.java index 1a0ec2026a8d..17bb71a771d8 100644 --- a/core/java/com/android/internal/util/ScrollOptimizer.java +++ b/core/java/com/android/internal/util/ScrollOptimizer.java @@ -123,7 +123,7 @@ private static int getUndequeuedBufferCount() { private static void initIfNeeded() { try { sFeatureEnabled = SystemProperties.getBoolean(PROP_SCROLL_OPT, true); - int prop = SystemProperties.getInt(PROP_SCROLL_OPT_HEAVY_APP, 2); + int prop = SystemProperties.getInt(PROP_SCROLL_OPT_HEAVY_APP, 1); sHeavyAppProp = prop; sHeavyApp = prop; sDebugEnabled = SystemProperties.getBoolean(PROP_DEBUG, false); From 3a7e9077cbcc27f101dc7c3a9abcab2454db0760 Mon Sep 17 00:00:00 2001 From: NurKeinNeid Date: Thu, 17 Jul 2025 17:20:41 +0000 Subject: [PATCH 032/190] SystemUI: Fix clock plugin animations during keyguard transitions - Add proper handling for DOZING state transitions in ClockEventController - Ensure smooth animations when transitioning between AOD, DOZING, and LOCKSCREEN states - Fix edge case where clock plugins weren't animating correctly during doze transitions This addresses the same issue as AxionAOSP/android_frameworks_base@079b811a0a6d7a141c294ecc8ff7b7b3b2b9adac but with a more targeted approach that doesn't break other functionality. Signed-off-by: NurKeinNeid Signed-off-by: Ghosuto --- .../com/android/keyguard/ClockEventController.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt index 1ff18673b180..faef1f1c2f26 100644 --- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt +++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt @@ -492,9 +492,11 @@ constructor( zenModeController.addCallback(zenModeCallback) if (SceneContainerFlag.isEnabled) { handleDoze( - when (AOD) { - keyguardTransitionInteractor.getCurrentState() -> 1f - keyguardTransitionInteractor.getStartedState() -> 1f + when { + keyguardTransitionInteractor.getCurrentState() == AOD -> 1f + keyguardTransitionInteractor.getStartedState() == AOD -> 1f + keyguardTransitionInteractor.getCurrentState() == DOZING -> 1f + keyguardTransitionInteractor.getStartedState() == DOZING -> 1f else -> 0f } ) @@ -618,6 +620,10 @@ constructor( it.copy(value = 1f - it.value) }, keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, AOD)), + keyguardTransitionInteractor.transition(Edge.create(DOZING, LOCKSCREEN)).map { + it.copy(value = 1f - it.value) + }, + keyguardTransitionInteractor.transition(Edge.create(LOCKSCREEN, DOZING)), ) .filter { it.transitionState != TransitionState.FINISHED } .collect { handleDoze(it.value) } @@ -646,7 +652,7 @@ constructor( keyguardTransitionInteractor .transition(Edge.create(to = LOCKSCREEN)) .filter { it.transitionState == TransitionState.STARTED } - .filter { it.from != AOD } + .filter { it.from != AOD && it.from != DOZING } .filter { !com.android.systemui.Flags.newDozingKeyguardStates() || it.from != DOZING } From c8562ba1d546ef4a8b322efc0c8e6dc60583b13d Mon Sep 17 00:00:00 2001 From: PMS22 Date: Sat, 30 Nov 2019 06:45:49 +0000 Subject: [PATCH 033/190] AOD: Sleep when proximity is covered for 3 secs - Saves more juice when kept in pocket Change-Id: Ic5fb2afaa0c7a3e6c9e9732c64601638c58a089a Signed-off-by: PMS22 Signed-off-by: NurKeinNeid Signed-off-by: Ghosuto --- .../src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java index 78b742892aac..a7ea719fdb40 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java +++ b/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java @@ -44,7 +44,7 @@ public class AlwaysOnDisplayPolicy { public static final String TAG = "AlwaysOnDisplayPolicy"; - private static final long DEFAULT_PROX_SCREEN_OFF_DELAY_MS = 10 * DateUtils.SECOND_IN_MILLIS; + private static final long DEFAULT_PROX_SCREEN_OFF_DELAY_MS = 3 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_PROX_COOLDOWN_TRIGGER_MS = 2 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_PROX_COOLDOWN_PERIOD_MS = 5 * DateUtils.SECOND_IN_MILLIS; private static final long DEFAULT_WALLPAPER_VISIBILITY_MS = 60 * DateUtils.SECOND_IN_MILLIS; From f2913e313f09135584344bd5f560a23e98fd11ce Mon Sep 17 00:00:00 2001 From: cjh1249131356 Date: Mon, 15 Nov 2021 20:57:33 +0800 Subject: [PATCH 034/190] base: Use EFFECT_DOUBLE_CLICK for camera launch feedback Signed-off-by: cjh1249131356 Signed-off-by: Ghosuto --- .../CentralSurfacesCommandQueueCallbacks.java | 33 +------------------ 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index abc25b7a80bf..927d68cbb732 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -110,7 +110,6 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba private final int mDisplayId; private final UserTracker mUserTracker; private final boolean mVibrateOnOpening; - private final VibrationEffect mCameraLaunchGestureVibrationEffect; private final ActivityStarter mActivityStarter; private final Lazy mCameraLauncherLazy; private final QuickSettingsController mQsController; @@ -192,8 +191,6 @@ enum PowerButtonLaunchGestureTarget { mQSHost = qsHost; mKeyguardInteractor = keyguardInteractor; mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation); - mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect( - mVibratorOptional, resources); mActivityStarter = activityStarter; mEmergencyGestureIntentFactory = emergencyGestureIntentFactory; mWalletController = walletController; @@ -568,35 +565,7 @@ private boolean isWakingUpOrAwake() { private void vibrateForCameraGesture() { mVibratorOptional.ifPresent( - v -> v.vibrate(mCameraLaunchGestureVibrationEffect, - HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES)); - } - - private static VibrationEffect getCameraGestureVibrationEffect( - Optional vibratorOptional, Resources resources) { - if (vibratorOptional.isPresent() && vibratorOptional.get().areAllPrimitivesSupported( - VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, - VibrationEffect.Composition.PRIMITIVE_CLICK)) { - return VibrationEffect.startComposition() - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE) - .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 50) - .compose(); - } - if (vibratorOptional.isPresent() && vibratorOptional.get().hasAmplitudeControl()) { - // Make sure to pass -1 for repeat so VibratorManagerService doesn't stop us when going - // to sleep. - return VibrationEffect.createWaveform( - CentralSurfaces.CAMERA_LAUNCH_GESTURE_VIBRATION_TIMINGS, - CentralSurfaces.CAMERA_LAUNCH_GESTURE_VIBRATION_AMPLITUDES, - /* repeat= */ -1); - } - - int[] pattern = resources.getIntArray(R.array.config_cameraLaunchGestureVibePattern); - long[] timings = new long[pattern.length]; - for (int i = 0; i < pattern.length; i++) { - timings[i] = pattern[i]; - } - return VibrationEffect.createWaveform(timings, /* repeat= */ -1); + v -> v.vibrate(VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK))); } @VisibleForTesting From 5b01d701d8ba3548b5d19efc5fac07e1b13dbffe Mon Sep 17 00:00:00 2001 From: Adithya R Date: Mon, 4 Nov 2024 23:53:59 +0530 Subject: [PATCH 035/190] SystemUI: Show a popup dialog upon usb connection To make switching usb function easier. Partly inspired by moto and coloros. Note that code needs to be kept in sync with Settings UsbDetailsFunctionsController to maintain consistency. Change-Id: I3cfc9b679cb7333c4e98151e383086f98d5135f0 Signed-off-by: Ghosuto --- core/res/res/values/lunaris_config.xml | 3 + core/res/res/values/lunaris_symbols.xml | 3 + packages/SystemUI/AndroidManifest.xml | 9 + .../SystemUI/res/values/lunaris_strings.xml | 9 + .../dagger/DefaultActivityBinder.java | 7 + .../systemui/usb/UsbFunctionActivity.kt | 342 ++++++++++++++++++ .../android/server/usb/UsbDeviceManager.java | 36 ++ 7 files changed, 409 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt diff --git a/core/res/res/values/lunaris_config.xml b/core/res/res/values/lunaris_config.xml index 516b9d79d2ef..389ceb6d77b9 100644 --- a/core/res/res/values/lunaris_config.xml +++ b/core/res/res/values/lunaris_config.xml @@ -110,4 +110,7 @@ /sys/class/thermal/thermal_zone0/temp 1 1 + + + com.android.systemui/com.android.systemui.usb.UsbFunctionActivity diff --git a/core/res/res/values/lunaris_symbols.xml b/core/res/res/values/lunaris_symbols.xml index f9059d7d27e2..3d811080b4d6 100644 --- a/core/res/res/values/lunaris_symbols.xml +++ b/core/res/res/values/lunaris_symbols.xml @@ -104,4 +104,7 @@ + + + diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index e1362951f427..42bfc7b2b604 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -711,6 +711,15 @@ android:excludeFromRecents="true"> + + + + Re-evaluating system theme.. Please wait for a few seconds Lunaris AOSP + + + No data transfer + File Transfer + PTP + Webcam + USB tethering + MIDI + Use USB for diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java index c2e1e33f5318..001f62e89d2a 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultActivityBinder.java @@ -34,6 +34,7 @@ import com.android.systemui.usb.UsbConfirmActivity; import com.android.systemui.usb.UsbDebuggingActivity; import com.android.systemui.usb.UsbDebuggingSecondaryUserActivity; +import com.android.systemui.usb.UsbFunctionActivity; import com.android.systemui.usb.UsbPermissionActivity; import com.android.systemui.user.CreateUserActivity; @@ -102,6 +103,12 @@ public abstract Activity bindUsbDebuggingSecondaryUserActivity( @ClassKey(UsbAccessoryUriActivity.class) public abstract Activity bindUsbAccessoryUriActivity(UsbAccessoryUriActivity activity); + /** Inject into UsbFunctionActivity. */ + @Binds + @IntoMap + @ClassKey(UsbFunctionActivity.class) + public abstract Activity bindUsbFunctionActivity(UsbFunctionActivity activity); + /** Inject into CreateUserActivity. */ @Binds @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt b/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt new file mode 100644 index 000000000000..f297710f5f6e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/usb/UsbFunctionActivity.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2024 Paranoid Android + * + * 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.usb + +import android.app.AlertDialog +import android.content.BroadcastReceiver +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.IntentFilter +import android.content.pm.PackageManager +import android.hardware.usb.UsbManager +import android.net.TetheringManager +import android.os.Bundle +import android.os.Handler +import android.os.HandlerExecutor +import android.os.Looper +import android.os.UserHandle +import android.os.UserManager +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS +import android.widget.ArrayAdapter +import android.widget.CheckedTextView +import com.android.internal.app.AlertActivity +import com.android.internal.app.AlertController.AlertParams +import com.android.systemui.res.R +import com.android.systemui.broadcast.BroadcastDispatcher +import javax.inject.Inject + +class UsbFunctionActivity @Inject constructor( + private val broadcastDispatcher: BroadcastDispatcher, +): AlertActivity(), DialogInterface.OnClickListener { + + private lateinit var usbManager: UsbManager + private lateinit var userManager: UserManager + private lateinit var tetheringManager: TetheringManager + + private var tetheringSupported = false + private var midiSupported = false + private val uvcEnabled = UsbManager.isUvcSupportEnabled() + + private val handler = Handler(Looper.getMainLooper()) + private val executor = HandlerExecutor(handler) + + private var previousFunctions: Long = 0L + private val tetheringCallback = object : TetheringManager.StartTetheringCallback { + override fun onTetheringFailed(error: Int) { + Log.w(TAG, "onTetheringFailed() error : $error") + usbManager.setCurrentFunctions(previousFunctions) + } + } + + private val usbReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action != UsbManager.ACTION_USB_STATE) { + return + } + + val connected = intent.getBooleanExtra(UsbManager.USB_CONNECTED, false) + if (!connected) { + dlog("usb disconnected, goodbye") + finish() + } + } + } + + private lateinit var adapter: UsbFunctionAdapter + private lateinit var supportedFunctions: List + + override fun onCreate(savedInstanceState: Bundle?) { + dlog("onCreate()") + super.onCreate(savedInstanceState) + + window.addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) + + usbManager = getSystemService(UsbManager::class.java) ?: return + userManager = getSystemService(UserManager::class.java) ?: return + tetheringManager = getSystemService(TetheringManager::class.java) ?: return + + tetheringSupported = tetheringManager.isTetheringSupported() + midiSupported = packageManager.hasSystemFeature(PackageManager.FEATURE_MIDI) + + supportedFunctions = getSupportedFunctions() + adapter = UsbFunctionAdapter(this, supportedFunctions) + + mAlertParams.apply { + mAdapter = adapter + mOnClickListener = this@UsbFunctionActivity + mTitle = getString(R.string.usb_use) + mIsSingleChoice = true + mCheckedItem = supportedFunctions.indexOf(getCurrentFunction()) + mPositiveButtonText = getString(com.android.internal.R.string.done_label) + mPositiveButtonListener = this@UsbFunctionActivity + mNeutralButtonText = getString(com.android.internal.R.string.more_item_label) + mNeutralButtonListener = this@UsbFunctionActivity + } + dlog("mCheckedItem=${mAlertParams.mCheckedItem}") + + setupAlert() + mAlert.listView?.requestFocus() + + broadcastDispatcher.registerReceiver( + usbReceiver, + IntentFilter(UsbManager.ACTION_USB_STATE) + ) + } + + override fun onDestroy() { + broadcastDispatcher.unregisterReceiver(usbReceiver) + super.onDestroy() + } + + override fun onClick(dialog: DialogInterface, which: Int) { + dlog("onClick: which = $which") + when (which) { + AlertDialog.BUTTON_POSITIVE -> finish() + AlertDialog.BUTTON_NEUTRAL -> { + val intent = Intent() + .setClassName( + "com.android.settings", + "com.android.settings.Settings\$UsbDetailsActivity" + ) + runCatching { + startActivityAsUser(intent, UserHandle.CURRENT) + finish() + }.onFailure { e -> + Log.e(TAG, "unable to start activity $intent" , e); + } + } + else -> { + adapter.getItem(which)?.let { setCurrentFunction(it) } + // adapter.notifyDataSetChanged() + finish() + } + } + } + + private fun getSupportedFunctions() = ALL_FUNCTIONS + .filter { function -> areFunctionsSupported(function.mask) } + + private fun getCurrentFunction(): UsbFunction { + var currentFunctions = usbManager.getCurrentFunctions().also { + Log.d(TAG, "current usb functions: $it (${UsbManager.usbFunctionsToString(it)})") + } + + if ((currentFunctions and UsbManager.FUNCTION_ACCESSORY) != 0L) { + currentFunctions = UsbManager.FUNCTION_MTP + } else if (currentFunctions == UsbManager.FUNCTION_NCM) { + currentFunctions = UsbManager.FUNCTION_RNDIS + } + + return supportedFunctions + .find { it -> it.mask == currentFunctions } + ?: NONE_FUNCTION + } + + private fun setCurrentFunction(function: UsbFunction) { + if (isClickEventIgnored(function.mask)) { + dlog("setCurrentFunction ignored for $function") + return + } + + dlog("setCurrentFunction: $function") + when (function.mask) { + UsbManager.FUNCTION_RNDIS -> { + previousFunctions = usbManager.getCurrentFunctions() + tetheringManager.startTethering( + TetheringManager.TETHERING_USB, + executor, + tetheringCallback + ) + } + else -> usbManager.setCurrentFunctions(function.mask) + } + } + + // Below functions are replicated from com.android.settings.connecteddevice.usb.UsbBackend + + private fun isClickEventIgnored(function: Long): Boolean { + val currentFunctions = usbManager.getCurrentFunctions() + return (currentFunctions and UsbManager.FUNCTION_ACCESSORY) != 0L + && function == UsbManager.FUNCTION_MTP + } + + private fun areFunctionsSupported(functions: Long): Boolean { + if ((!midiSupported && (functions and UsbManager.FUNCTION_MIDI) != 0L) + || (!tetheringSupported && (functions and UsbManager.FUNCTION_RNDIS) != 0L)) { + return false + } + return !(areFunctionDisallowed(functions) || areFunctionsDisallowedBySystem(functions) + || areFunctionsDisallowedByNonAdminUser(functions)) + } + + private fun isUsbFileTransferRestricted(): Boolean { + return userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER) + } + + private fun isUsbTetheringRestricted(): Boolean { + return userManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_TETHERING) + } + + private fun isUsbFileTransferRestrictedBySystem(): Boolean { + return userManager.hasBaseUserRestriction( + UserManager.DISALLOW_USB_FILE_TRANSFER, + UserHandle.of(UserHandle.myUserId()) + ) + } + + private fun isUsbTetheringRestrictedBySystem(): Boolean { + return userManager.hasBaseUserRestriction( + UserManager.DISALLOW_CONFIG_TETHERING, + UserHandle.of(UserHandle.myUserId()) + ) + } + + private fun areFunctionDisallowed(functions: Long): Boolean { + return (isUsbFileTransferRestricted() && ((functions and UsbManager.FUNCTION_MTP) != 0L + || (functions and UsbManager.FUNCTION_PTP) != 0L)) + || (isUsbTetheringRestricted() && ((functions and UsbManager.FUNCTION_RNDIS) != 0L)) + } + + private fun areFunctionsDisallowedBySystem(functions: Long): Boolean { + return (isUsbFileTransferRestrictedBySystem() && ((functions and UsbManager.FUNCTION_MTP) != 0L + || (functions and UsbManager.FUNCTION_PTP) != 0L)) + || (isUsbTetheringRestrictedBySystem() && ((functions and UsbManager.FUNCTION_RNDIS) != 0L)) + || (!uvcEnabled && ((functions and UsbManager.FUNCTION_UVC) != 0L)) + } + + private fun areFunctionsDisallowedByNonAdminUser(functions: Long): Boolean { + return !userManager.isAdminUser() && (functions and UsbManager.FUNCTION_RNDIS) != 0L + } + + private companion object { + const val TAG = "UsbFunctionActivity" + + val NONE_FUNCTION = UsbFunction( + UsbManager.FUNCTION_NONE, + "", + R.string.usb_use_charging_only + ) + + val ALL_FUNCTIONS = listOf( + UsbFunction( + UsbManager.FUNCTION_MTP, + UsbManager.USB_FUNCTION_MTP, + R.string.usb_use_file_transfers + ), + UsbFunction( + UsbManager.FUNCTION_RNDIS, + UsbManager.USB_FUNCTION_RNDIS, + R.string.usb_use_tethering + ), + UsbFunction( + UsbManager.FUNCTION_MIDI, + UsbManager.USB_FUNCTION_MIDI, + R.string.usb_use_MIDI + ), + UsbFunction( + UsbManager.FUNCTION_PTP, + UsbManager.USB_FUNCTION_PTP, + R.string.usb_use_photo_transfers + ), + UsbFunction( + UsbManager.FUNCTION_UVC, + UsbManager.USB_FUNCTION_UVC, + R.string.usb_use_uvc_webcam + ), + NONE_FUNCTION + ) + + fun dlog(msg: String) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, msg) + } + } + } +} + +private data class UsbFunction( + val mask: Long, + val name: String, + val descriptionResId: Int +) + +private class UsbFunctionAdapter( + private val context: Context, + private val items: List, +) : ArrayAdapter( + context, + com.android.internal.R.layout.select_dialog_singlechoice_material, + items +) { + + private val inflater = LayoutInflater.from(context) + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: inflater.inflate( + com.android.internal.R.layout.select_dialog_singlechoice_material, + parent, + false + ) + + val textView = view as CheckedTextView + val function = getItem(position) + textView.text = context.getString( + function?.descriptionResId ?: com.android.internal.R.string.unknownName + ) + + // required for listview to trigger onclick + view.focusable = View.NOT_FOCUSABLE + view.setClickable(false) + + return view + } + + private companion object { + const val TAG = "UsbFunctionAdapter" + + fun dlog(msg: String) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, msg) + } + } + } +} diff --git a/services/usb/java/com/android/server/usb/UsbDeviceManager.java b/services/usb/java/com/android/server/usb/UsbDeviceManager.java index 9a2183128c3f..208056397c64 100644 --- a/services/usb/java/com/android/server/usb/UsbDeviceManager.java +++ b/services/usb/java/com/android/server/usb/UsbDeviceManager.java @@ -33,6 +33,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.ContentResolver; @@ -699,6 +700,9 @@ abstract static class UsbHandler extends Handler { protected int mCurrentGadgetHalVersion; protected boolean mPendingBootAccessoryHandshakeBroadcast; protected boolean mUserUnlockedAfterBoot; + protected boolean mShowedFunctionDialog; + protected boolean mIsFirstUnlock = true; + /** * The persistent property which stores whether adb is enabled or not. * May also contain vendor-specific default functions for testing purposes. @@ -1289,6 +1293,12 @@ public void handleMessage(Message msg) { } } updateUsbFunctions(); + if (mConnected && !mScreenLocked) { + showFunctionDialog(); + } else if (!mConnected) { + // reset for the next usb connection + mShowedFunctionDialog = false; + } } else { mPendingBootBroadcast = true; } @@ -1452,6 +1462,15 @@ public void handleMessage(Message msg) { // Set the screen unlocked functions if current function is charging. setScreenUnlockedFunctions(operationId); } + if (mIsFirstUnlock) { + // skip showing function dialog at the first unlock + if (mConnected) { + mShowedFunctionDialog = true; + } + mIsFirstUnlock = false; + } else if (mConnected) { + showFunctionDialog(); + } } break; case MSG_UPDATE_USER_RESTRICTIONS: @@ -1781,6 +1800,23 @@ protected void updateUsbNotification(boolean force) { } } + private void showFunctionDialog() { + if (mShowedFunctionDialog) return; + + Intent intent = new Intent(); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(ComponentName.unflattenFromString(mContext.getString( + com.android.internal.R.string.config_usbFunctionActivity))); + + try { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + } catch (ActivityNotFoundException e) { + Slog.e(TAG, "unable to start activity " + intent, e); + } + + mShowedFunctionDialog = true; + } + protected boolean isAdbEnabled() { return LocalServices.getService(AdbManagerInternal.class) .isAdbEnabled(AdbTransportType.USB); From bf2a3202342215e83db5b823314fe94d1ec4fe8a Mon Sep 17 00:00:00 2001 From: Arindam Bhattacharjee Date: Thu, 16 Oct 2025 15:26:29 +0000 Subject: [PATCH 036/190] core: Allow to override Mock Location restriction [1/2] Some apps like PokemonGo & some dating apps uses isFromMockProvider() API to restrict the Mock Location usage. This change will allow to override the Mock Location usage restriction. Change-Id: I73604052a671d29eddf839430cad19cba22f479b Signed-off-by: Arindam Bhattacharjee Signed-off-by: Ghosuto --- core/java/android/location/Location.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/java/android/location/Location.java b/core/java/android/location/Location.java index fd3e5a22e969..fea7bc9315bd 100644 --- a/core/java/android/location/Location.java +++ b/core/java/android/location/Location.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.os.SystemClock; +import android.os.SystemProperties; import android.util.Printer; import android.util.TimeUtils; @@ -805,6 +806,13 @@ public void setIsFromMockProvider(boolean isFromMockProvider) { * @see LocationManager#addTestProvider */ public boolean isMock() { + // Check if mock location override is enabled via Settings + boolean overrideMockDetection = SystemProperties.getBoolean( + "persist.sys.override_mock_location", false); + + if (overrideMockDetection) { + return false; + } return (mFieldsMask & HAS_MOCK_PROVIDER_MASK) != 0; } From 5c6ec0b9a5ead757904c71a0825a62713619a058 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Mon, 28 Oct 2024 07:39:11 +0800 Subject: [PATCH 037/190] AutoAODService: Do not bind observer on the main thread Signed-off-by: Ghosuto --- .../core/java/com/android/server/display/AutoAODService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/display/AutoAODService.java b/services/core/java/com/android/server/display/AutoAODService.java index f30ddc1b7d05..020a4253fc07 100644 --- a/services/core/java/com/android/server/display/AutoAODService.java +++ b/services/core/java/com/android/server/display/AutoAODService.java @@ -78,7 +78,7 @@ public class AutoAODService extends SystemService { private final AlarmManager mAlarmManager; private final Context mContext; - private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Handler mHandler = new Handler(); private TwilightManager mTwilightManager; private TwilightState mTwilightState; private SharedPreferences mSharedPreferences; @@ -234,7 +234,7 @@ public AutoAODService(Context context) { super(context); mContext = context; mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE); - mSettingsObserver = new SettingsObserver(mHandler); + mSettingsObserver = new SettingsObserver(null); mActive = Settings.Secure.getIntForUser( mContext.getContentResolver(), Settings.Secure.DOZE_ALWAYS_ON, 0, From e593f15123583d9ebd18d408db5038804ec618c7 Mon Sep 17 00:00:00 2001 From: 1582130940 <19632674+1582130940@users.noreply.github.com> Date: Sun, 28 Aug 2022 00:56:21 +0800 Subject: [PATCH 038/190] Dialer: Grant permissions * Grant NETWORK_STACK permissions for Dialer Joey Huab * Whitelist CAPTURE_AUDIO_OUTPUT for Dialer Daniel Micay Co-authored-by: Joey Huab Co-authored-by: Daniel Micay Signed-off-by: Ghosuto --- data/etc/com.android.dialer.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/data/etc/com.android.dialer.xml b/data/etc/com.android.dialer.xml index 405279f8b1a4..488b04cd46fd 100644 --- a/data/etc/com.android.dialer.xml +++ b/data/etc/com.android.dialer.xml @@ -26,5 +26,8 @@ + + + From cf43b4dc6691ea80b07e94552f459405080e58e1 Mon Sep 17 00:00:00 2001 From: Oliver Scott Date: Mon, 10 Apr 2023 12:35:40 -0400 Subject: [PATCH 039/190] Hide hidden apps from all apps except system Only generate package info for MATCH_UNINSTALLED_PACKAGES if calling UID is a system application. Also includes squashed change: Author: Wang Han Date: Tue Aug 29 15:21:14 2023 +0000 fixup! Hide hidden apps from all apps except system * getSettingBase() actually calls getSettingLPr(), which returns null for ROOT_UID. So if an application is using root permission when calling package manager api such as getInstalledPackages(), a crash will occur. Fix this by exposing hidden apps to caller with root uid, as hiding everything to root is meaningless. Fixes: https://review.calyxos.org/c/CalyxOS/platform_frameworks_base/+/16571 Change-Id: Ifd7c28cd987b3388de2063fa9bc07824cf01da2b [someone5678] * Move to shouldFilterApplication to deals with more cases * Don't check filterUninstall * Check if the target and caller are the same application * Check if the caller is the current default launcher * Check if the caller is system, root, shell, or system app Issue: calyxos#1107 Change-Id: I8caa9030f4ff44babda5a778d0e0d6849e1e4d1b Signed-off-by: someone5678 <59456192+someone5678@users.noreply.github.com> Signed-off-by: Ghosuto --- .../com/android/server/pm/ComputerEngine.java | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 30c206777254..49acd0e6ec14 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -1548,12 +1548,18 @@ public final PackageInfo generatePackageInfo(PackageStateInternal ps, return null; } - if ((flags & MATCH_UNINSTALLED_PACKAGES) != 0 - && ps.isSystem()) { - flags |= MATCH_ANY_USER; + final PackageUserStateInternal state = ps.getUserStateOrDefault(userId); + if ((flags & MATCH_UNINSTALLED_PACKAGES) != 0) { + final SettingBase callingSetting = + mSettings.getSettingBase(UserHandle.getAppId(callingUid)); + if (state.isHidden() && callingSetting != null && (callingSetting.getFlags() & + ApplicationInfo.FLAG_SYSTEM) != ApplicationInfo.FLAG_SYSTEM) { + return null; + } else if (ps.isSystem()) { + flags |= MATCH_ANY_USER; + } } - final PackageUserStateInternal state = ps.getUserStateOrDefault(userId); AndroidPackage p = ps.getPkg(); if (p != null) { // Compute GIDs only if requested @@ -2578,6 +2584,61 @@ public final boolean isSameProfileGroup(@UserIdInt int callerUserId, } } + private static boolean isBootCompleted() { + return android.os.SystemProperties.getBoolean("sys.boot_completed", false); + } + + /** + * Returns whether caller is home. + */ + private final boolean isCallerHome(int callingUid, int userId) { + final String home = mDefaultAppProvider.getDefaultHome(userId); + if (home == null) return false; + return isCallerSameApp(home, callingUid); + } + + /** + * Returns whether caller is system, root, shell, or updated system app. + */ + private final boolean isCallerSystem(int callingUid) { + if (isSystemOrRootOrShell(callingUid)) { + return true; + } + final SettingBase callingPs = mSettings.getSettingBase(UserHandle.getAppId(callingUid)); + if (callingPs == null) return false; + final int callingFlags = callingPs.getFlags(); + if (((callingFlags & ApplicationInfo.FLAG_SYSTEM) == ApplicationInfo.FLAG_SYSTEM) + || ((callingFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) + == ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) { + return true; + } + return false; + } + + private final boolean shouldFilterApplicationCustom( + @Nullable PackageStateInternal ps, int callingUid, int userId) { + if (!isBootCompleted()) return false; + if (ps == null) return false; + + final String packageName = ps.getPackageName(); + if (packageName == null) return false; + + // if the target and caller are the same application, skip + if (isCallerSameApp(packageName, callingUid) + // if the caller is system, root, shell, or updated system app, skip + || isCallerSystem(callingUid) + // if the caller is the current default home, skip + || isCallerHome(callingUid, userId)) { + return false; + } + // if the target is hidden app, do filter + if (ps.getUserStateOrDefault(userId).isHidden()) { + return true; + } + + return false; + } + /** * Returns whether or not access to the application should be filtered. *

@@ -2605,6 +2666,9 @@ public final boolean shouldFilterApplication(@Nullable PackageStateInternal ps, final boolean callerIsInstantApp = instantAppPkgName != null; final boolean packageArchivedForUser = ps != null && PackageArchiver.isArchived( ps.getUserStateOrDefault(userId)); + if (shouldFilterApplicationCustom(ps, callingUid, userId)) { + return true; + } // Don't treat hiddenUntilInstalled as an uninstalled state, phone app needs to access // these hidden application details to customize carrier apps. Also, allowing the system // caller accessing to application across users. From b1df83d17fca5923bf78af7d88fd6c54ddcca53b Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Sat, 7 Dec 2024 10:14:03 +0900 Subject: [PATCH 040/190] base: Add support for hide applist [1/2] * Based on commit "Hide hidden apps from all apps except system" * Hide user selected apps from applist * The applist includes system apps (including overlays), and user apps Ref: https://review.calyxos.org/c/CalyxOS/platform_frameworks_base/+/16571 https://github.com/TheParasiteProject/frameworks_base/commit/6c41a0fc2c7ba01fd81132ceb0f3c1476099cf25 https://github.com/LineageOS/android_lineage-sdk/blob/lineage-16.0/sdk/src/java/org/lineageos/internal/applications/LongScreen.java https://github.com/yaap/frameworks_base/commit/232260f192effc9fb21a54eb444601179b24a26d Change-Id: If6c94fd345c488edab11927c4f567bc2171f69b2 Signed-off-by: someone5678 <59456192+someone5678@users.noreply.github.com> Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 7 ++ .../util/lunaris/HideAppListUtils.java | 115 ++++++++++++++++++ .../validators/SecureSettingsValidators.java | 1 + .../com/android/server/pm/ComputerEngine.java | 5 + 4 files changed, 128 insertions(+) create mode 100644 core/java/com/android/internal/util/lunaris/HideAppListUtils.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 74b6b595fdd5..daf29386e803 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14718,6 +14718,13 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN = "notification_row_transparency_lockscreen"; + /** + * Control which apps to hide from other user apps. + * @hide + */ + @Readable + public static final String HIDE_APPLIST = "hide_applist"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/core/java/com/android/internal/util/lunaris/HideAppListUtils.java b/core/java/com/android/internal/util/lunaris/HideAppListUtils.java new file mode 100644 index 000000000000..a55b6aae6404 --- /dev/null +++ b/core/java/com/android/internal/util/lunaris/HideAppListUtils.java @@ -0,0 +1,115 @@ +package com.android.internal.util.lunaris; + +import android.content.ContentResolver; +import android.content.Context; +import android.os.SystemProperties; +import android.provider.Settings; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class HideAppListUtils { + enum Action { + ADD, + REMOVE, + SET + } + + private static boolean isBootCompleted() { + return SystemProperties.getBoolean("sys.boot_completed", false); + } + + public static boolean shouldHideAppList(Context context, String packageName) { + return shouldHideAppList(context.getContentResolver(), packageName); + } + + public static boolean shouldHideAppList(ContentResolver cr, String packageName) { + if (cr == null || packageName == null || !isBootCompleted()) { + return false; + } + + Set apps = getApps(cr); + if (apps.isEmpty()) { + return false; + } + + return apps.contains(packageName); + } + + public static Set getApps(Context context) { + if (context == null) { + return new HashSet<>(); + } + + return getApps(context.getContentResolver()); + } + + public static Set getApps(ContentResolver cr) { + if (cr == null) { + return new HashSet<>(); + } + + String apps = ""; + try { + apps = Settings.Secure.getString(cr, Settings.Secure.HIDE_APPLIST); + } catch (IllegalStateException e) { + return new HashSet<>(); + } + if (apps != null && !apps.isEmpty() && !apps.equals(",")) { + return new HashSet<>(Arrays.asList(apps.split(","))); + } + + return new HashSet<>(); + } + + private static void putAppsForUser( + Context context, String packageName, int userId, Action action) { + if (context == null || userId < 0) { + return; + } + + final Set apps = getApps(context); + switch (action) { + case ADD: + apps.add(packageName); + break; + case REMOVE: + apps.remove(packageName); + break; + case SET: + // Don't change + break; + } + + Settings.Secure.putStringForUser( + context.getContentResolver(), + Settings.Secure.HIDE_APPLIST, + String.join(",", apps), + userId); + } + + public void addApp(Context mContext, String packageName, int userId) { + if (mContext == null || packageName == null || userId < 0) { + return; + } + + putAppsForUser(mContext, packageName, userId, Action.ADD); + } + + public void removeApp(Context mContext, String packageName, int userId) { + if (mContext == null || packageName == null || userId < 0) { + return; + } + + putAppsForUser(mContext, packageName, userId, Action.REMOVE); + } + + public void setApps(Context mContext, int userId) { + if (mContext == null || userId < 0) { + return; + } + + putAppsForUser(mContext, null, userId, Action.SET); + } +} diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index a21e47ec8dda..662326dc7369 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -525,5 +525,6 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.DOZE_TAP_GESTURE_VIBRATE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.DOZE_PICK_UP_GESTURE_VIBRATE, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.HIDE_DEVELOPER_STATUS, ANY_STRING_VALIDATOR); + VALIDATORS.put(Secure.HIDE_APPLIST, ANY_STRING_VALIDATOR); } } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 49acd0e6ec14..4bd41f1636bd 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2635,6 +2635,11 @@ private final boolean shouldFilterApplicationCustom( if (ps.getUserStateOrDefault(userId).isHidden()) { return true; } + // if the target is included in Settings.Secure.HIDE_APPLIST, do filter + if (com.android.internal.util.lunaris.HideAppListUtils.shouldHideAppList( + mContext, packageName)) { + return true; + } return false; } From 2fc615e3dbaf33701ca61272f9e90ac2a8cf08f3 Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:25:10 +0900 Subject: [PATCH 041/190] ComputerEngine: Don't hide hidden apps * Since we have Hide Applist support, we don't need to hide it by default Change-Id: I2f49f43043e52ec0ac167e18e6fc90938f30214b Signed-off-by: Ghosuto --- services/core/java/com/android/server/pm/ComputerEngine.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 4bd41f1636bd..202278152fd9 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -2631,10 +2631,6 @@ private final boolean shouldFilterApplicationCustom( || isCallerHome(callingUid, userId)) { return false; } - // if the target is hidden app, do filter - if (ps.getUserStateOrDefault(userId).isHidden()) { - return true; - } // if the target is included in Settings.Secure.HIDE_APPLIST, do filter if (com.android.internal.util.lunaris.HideAppListUtils.shouldHideAppList( mContext, packageName)) { From bb465d2fbd4afef7dcda009a56e43ccd43dd71cb Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 3 Mar 2025 18:29:50 +0800 Subject: [PATCH 042/190] Hide App list: Improve hide app list feature Insprired by: https://github.com/j-hc/zygisk-detach, instead of hooking libbinder, we override the computerengine to hide the app to the caller package. we should hide apps whereever possible except for system apps like launcher,settings, and framework. The main goal of Hide app list is to hide apps from playstore and banking apps, however we don't want to break package installer and system app list. Change-Id: I6d6cb159bde5dbabf37f313363996943ac881623 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../android/server/HideAppListService.java | 84 +++++++++++++++++++ .../com/android/server/pm/ComputerEngine.java | 79 ++++++++++++++++- .../java/com/android/server/SystemServer.java | 2 + 3 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 services/core/java/com/android/server/HideAppListService.java diff --git a/services/core/java/com/android/server/HideAppListService.java b/services/core/java/com/android/server/HideAppListService.java new file mode 100644 index 000000000000..5253424858bc --- /dev/null +++ b/services/core/java/com/android/server/HideAppListService.java @@ -0,0 +1,84 @@ +/* + * 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.server; + +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.Slog; +import com.android.server.SystemService; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class HideAppListService extends SystemService { + private static final String TAG = "HideAppListService"; + + private final Context mContext; + private final Handler mHandler = new Handler(); + + public HideAppListService(Context context) { + super(context); + mContext = context; + } + + @Override + public void onStart() { + Slog.i(TAG, "Starting HideAppListService"); + } + + @Override + public void onBootPhase(int phase) { + if (phase == SystemService.PHASE_BOOT_COMPLETED) { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED); + filter.addDataScheme("package"); + mContext.registerReceiver(new PackageUninstallReceiver(), filter); + } + } + + private class PackageUninstallReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + String packageName = intent.getData().getSchemeSpecificPart(); + if (packageName != null) { + Slog.i(TAG, "Package uninstalled: " + packageName); + removeFromHideAppList(packageName); + } + } + } + + private void removeFromHideAppList(String packageName) { + ContentResolver cr = mContext.getContentResolver(); + String apps = Settings.Secure.getString(cr, Settings.Secure.HIDE_APPLIST); + + if (apps == null || apps.isEmpty() || apps.equals(",")) { + return; + } + + Set appSet = new HashSet<>(Arrays.asList(apps.split(","))); + if (appSet.remove(packageName)) { + Slog.i(TAG, "Removing package due to reason: UNINSTALLED: " + packageName); + Settings.Secure.putString(cr, Settings.Secure.HIDE_APPLIST, String.join(",", appSet)); + } + } +} diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index 202278152fd9..d187df043422 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -183,6 +183,8 @@ import java.util.Set; import java.util.UUID; +import com.android.internal.util.lunaris.HideAppListUtils; + /** * This class contains the implementation of the Computer functions. It * is entirely self-contained - it has no implicit access to @@ -991,6 +993,10 @@ public final ApplicationInfo generateApplicationInfoFromSettings(String packageN public final ApplicationInfo getApplicationInfo(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int userId) { + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } return getApplicationInfoInternal(packageName, flags, Binder.getCallingUid(), userId); } @@ -1004,6 +1010,10 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, int filterCallingUid, int userId) { if (!mUserManager.exists(userId)) return null; + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } flags = updateFlagsForApplication(flags, userId); if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) { @@ -1014,6 +1024,64 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, return getApplicationInfoInternalBody(packageName, flags, filterCallingUid, userId); } + + private boolean canHideApp(int callingUid, String packageName) { + if (!isBootCompleted() || mContext == null || mContext.getPackageManager() == null) { + return false; + } + + String callingPackage = mContext.getPackageManager().getNameForUid(callingUid); + + if (callingPackage == null || TextUtils.isEmpty(callingPackage)) { + return false; + } + + // app can be always hidden if calling package is play store + boolean isFinsky = callingPackage.contains("com.android.vending"); + + if (isFinsky) return true; + + if (packageName == null || TextUtils.isEmpty(packageName)) { + return false; + } + + // the calling package is itself, no need to hide + if (callingPackage.contains(packageName)) return false; + + // we only want to hide these apps from playstore + // to avoid these apps from being updated, so abort if + // calling package is not finsky + if (packageName.contains("youtube") + || packageName.contains("microg") + || packageName.contains("revanced") + || packageName.contains("gms")) { + return false; + } + + // this is for banking apps, but we need to make sure first that + // we arent hiding app infos from sandbox/system processes + return !isCallerSystem(callingUid) + && !Process.isIsolated(callingUid) + && !Process.isSdkSandboxUid(callingUid); + } + + public ParceledListSlice recreatePackageList( + int callingUid, Context context, int userId, ParceledListSlice list) { + List appList = new ArrayList<>(list.getList()); + if (!canHideApp(callingUid, null)) return new ParceledListSlice<>(appList); + Set hiddenApps = HideAppListUtils.getApps(context); + appList.removeIf(info -> hiddenApps.contains(info.packageName)); + return new ParceledListSlice<>(appList); + } + + public List recreateApplicationList( + int callingUid, Context context, int userId, List list) { + List appList = new ArrayList<>(list); + if (!canHideApp(callingUid, null)) return appList; + Set hiddenApps = HideAppListUtils.getApps(context); + appList.removeIf(info -> hiddenApps.contains(info.packageName)); + return appList; + } protected ApplicationInfo getApplicationInfoInternalBody(String packageName, @PackageManager.ApplicationInfoFlagsBits long flags, @@ -1659,6 +1727,10 @@ public final PackageInfo generatePackageInfo(PackageStateInternal ps, public final PackageInfo getPackageInfo(String packageName, @PackageManager.PackageInfoFlagsBits long flags, int userId) { + if (canHideApp(Binder.getCallingUid(), packageName) && + HideAppListUtils.shouldHideAppList(mContext, packageName)) { + return null; + } return getPackageInfoInternal(packageName, PackageManager.VERSION_CODE_HIGHEST, flags, Binder.getCallingUid(), userId); } @@ -1782,7 +1854,8 @@ public final ParceledListSlice getInstalledPackages(long flags, int enforceCrossUserPermission(callingUid, userId, false /* requireFullPermission */, false /* checkShell */, "get installed packages"); - return getInstalledPackagesBody(flags, userId, callingUid); + return recreatePackageList(callingUid, mContext, + userId, getInstalledPackagesBody(flags, userId, callingUid)); } protected ParceledListSlice getInstalledPackagesBody(long flags, int userId, @@ -2632,7 +2705,7 @@ private final boolean shouldFilterApplicationCustom( return false; } // if the target is included in Settings.Secure.HIDE_APPLIST, do filter - if (com.android.internal.util.lunaris.HideAppListUtils.shouldHideAppList( + if (canHideApp(Binder.getCallingUid(), packageName) && HideAppListUtils.shouldHideAppList( mContext, packageName)) { return true; } @@ -4838,7 +4911,7 @@ public List getInstalledApplications( } } - return list; + return recreateApplicationList(callingUid, mContext, userId, list); } @Nullable diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 82d776b86526..5415dcbb871a 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -2926,6 +2926,8 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(CustomDeviceConfigService.class); t.traceEnd(); + mSystemServiceManager.startService(HideAppListService.class); + boolean hbmSupported = SystemProperties.getBoolean("persist.sys.hbmservice_support", false); String hbmFile = SystemProperties.get("persist.sys.hbmservice_file"); if (hbmSupported && hbmFile != null && !hbmFile.isEmpty()) { From 1b59851623082af1078403991aac11e66219e518 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 26 Mar 2024 09:27:10 +0800 Subject: [PATCH 043/190] services: Introduce RisingServicesStarter Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- .../rising/server/RisingServicesStarter.java | 38 +++++++++++++++++++ .../java/com/android/server/SystemServer.java | 5 +++ 2 files changed, 43 insertions(+) create mode 100644 services/core/java/org/rising/server/RisingServicesStarter.java diff --git a/services/core/java/org/rising/server/RisingServicesStarter.java b/services/core/java/org/rising/server/RisingServicesStarter.java new file mode 100644 index 000000000000..63cfb13d6648 --- /dev/null +++ b/services/core/java/org/rising/server/RisingServicesStarter.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023-2024 The 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 org.rising.server; + +import com.android.server.SystemServiceManager; + +public class RisingServicesStarter { + + private final SystemServiceManager mSystemServiceManager; + + public RisingServicesStarter(SystemServiceManager systemServiceManager) { + this.mSystemServiceManager = systemServiceManager; + } + + public void startAllServices() { + } + + private void startService(String serviceClassName) { + try { + mSystemServiceManager.startService(serviceClassName); + } catch (Exception e) {} + } +} + diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 5415dcbb871a..ab4d8af6dc00 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -328,6 +328,8 @@ import dalvik.system.VMDebug; import dalvik.system.VMRuntime; +import org.rising.server.RisingServicesStarter; + import java.io.File; import java.io.FileDescriptor; import java.io.IOException; @@ -3394,6 +3396,9 @@ private void startOtherServices(@NonNull TimingsTraceAndSlog t) { mSystemServiceManager.startService(HEALTHCONNECT_MANAGER_SERVICE_CLASS); t.traceEnd(); + RisingServicesStarter risingServiceStarter = new RisingServicesStarter(mSystemServiceManager); + risingServiceStarter.startAllServices(); + if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_DEVICE_LOCK)) { t.traceBegin("DeviceLockService"); mSystemServiceManager.startServiceFromJar(DEVICE_LOCK_SERVICE_CLASS, From 8b9933c8354a37afae8b6be1d6db1da81f9a11a2 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Wed, 30 Oct 2024 14:53:03 +0800 Subject: [PATCH 044/190] services: Introduce QuickSwitch feature [1/2] * initial feature rework * add support for pixel launcher * adapt for a16 Co-authored-by: Ghosuto Signed-off-by: Ghosuto --- .../policy/ScreenDecorationsUtils.java | 4 + core/res/res/values/lunaris_arrays.xml | 10 - core/res/res/values/quickswitch_arrays.xml | 30 +++ core/res/res/values/quickswitch_symbols.xml | 23 ++ .../shell/common/ExternalInterfaceBinder.java | 2 +- .../shared/system/QuickStepContract.java | 4 + .../systemui/LauncherProxyService.java | 5 +- .../gestural/EdgeBackGestureHandler.java | 5 +- .../server/app/AppLockManagerService.kt | 3 + .../com/android/server/pm/ComputerEngine.java | 12 +- .../android/server/pm/DefaultAppProvider.java | 15 +- .../role/RoleServicePlatformHelperImpl.java | 6 +- .../com/android/server/wm/RecentTasks.java | 4 +- .../org/rising/server/QuickSwitchService.java | 198 ++++++++++++++++++ .../rising/server/RisingServicesStarter.java | 4 + 15 files changed, 301 insertions(+), 24 deletions(-) create mode 100644 core/res/res/values/quickswitch_arrays.xml create mode 100644 core/res/res/values/quickswitch_symbols.xml create mode 100644 services/core/java/org/rising/server/QuickSwitchService.java diff --git a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java index b23515aa51f3..2d358433c6b7 100644 --- a/core/java/com/android/internal/policy/ScreenDecorationsUtils.java +++ b/core/java/com/android/internal/policy/ScreenDecorationsUtils.java @@ -41,6 +41,10 @@ public class ScreenDecorationsUtils { * If the associated display is not internal, will return 0. */ public static float getWindowCornerRadius(Context context) { + String callingPackage = context.getPackageManager().getNameForUid(android.os.Binder.getCallingUid()); + if ("com.google.android.apps.nexuslauncher".equals(callingPackage)) { + return 32f; + } final Resources resources = context.getResources(); if (!supportsRoundedCornersOnWindows(resources)) { return 0f; diff --git a/core/res/res/values/lunaris_arrays.xml b/core/res/res/values/lunaris_arrays.xml index cbc67262316e..384921bc490f 100644 --- a/core/res/res/values/lunaris_arrays.xml +++ b/core/res/res/values/lunaris_arrays.xml @@ -4,16 +4,6 @@ SPDX-License-Identifier: Apache-2.0 --> - - com.google.android.apps.nexuslauncher/com.android.quickstep.RecentsActivity - com.android.launcher3/com.android.quickstep.RecentsActivity - - - - com.google.android.apps.nexuslauncher - com.android.launcher3 - - diff --git a/core/res/res/values/quickswitch_arrays.xml b/core/res/res/values/quickswitch_arrays.xml new file mode 100644 index 000000000000..f38a024a4335 --- /dev/null +++ b/core/res/res/values/quickswitch_arrays.xml @@ -0,0 +1,30 @@ + + + + + com.android.launcher3/com.android.quickstep.RecentsActivity + com.google.android.apps.nexuslauncher/com.android.quickstep.RecentsActivity + + + + com.android.launcher3 + com.google.android.apps.nexuslauncher + + diff --git a/core/res/res/values/quickswitch_symbols.xml b/core/res/res/values/quickswitch_symbols.xml new file mode 100644 index 000000000000..87da280f46f7 --- /dev/null +++ b/core/res/res/values/quickswitch_symbols.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java index 4c3cf8742e59..8d485c649cea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ExternalInterfaceBinder.java @@ -54,7 +54,7 @@ default void executeRemoteCallWithTaskPermission(RemoteCallable controlle default void executeRemoteCallWithTaskPermission(RemoteCallable controllerInstance, String log, Consumer callback, boolean blocking) { if (controllerInstance == null) return; - + final RemoteCallable controller = controllerInstance; if (!com.android.internal.util.lunaris.PixelPropsUtils.shouldBypassManageActivityTaskPermission( controllerInstance.getContext())) { diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 51765fce6874..289485d2300b 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -423,6 +423,10 @@ public static boolean isLegacyMode(int mode) { * @param context A display associated context. */ public static float getWindowCornerRadius(Context context) { + String callingPackage = context.getPackageManager().getNameForUid(android.os.Binder.getCallingUid()); + if ("com.google.android.apps.nexuslauncher".equals(callingPackage)) { + return 0f; + } return ScreenDecorationsUtils.getWindowCornerRadius(context); } diff --git a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java index b25a362fb924..fc25854f3768 100644 --- a/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/LauncherProxyService.java @@ -835,8 +835,9 @@ public LauncherProxyService(Context context, mShadeDisplayPolicy = shadeDisplayPolicy; mUserTracker = userTracker; mConnectionBackoffAttempts = 0; - mRecentsComponentName = ComponentName.unflattenFromString(context.getString( - com.android.internal.R.string.config_recentsComponentName)); + int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); + String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); + mRecentsComponentName = ComponentName.unflattenFromString(launcherComponents[defaultLauncher]); mQuickStepIntent = new Intent(ACTION_QUICKSTEP) .setPackage(mRecentsComponentName.getPackageName()); mPerDisplaySysUiStateRepository = perDisplaySysUiStateRepository; diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 05d48d651f1f..d3e681c8b7f3 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -548,8 +548,9 @@ public interface Factory { mDesktopState = desktopState; mTunerService = tunerService; - ComponentName recentsComponentName = ComponentName.unflattenFromString( - context.getString(com.android.internal.R.string.config_recentsComponentName)); + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + String[] launcherComponents = context.getResources().getStringArray(com.android.internal.R.array.config_launcherComponents); + ComponentName recentsComponentName = ComponentName.unflattenFromString(launcherComponents[defaultLauncher]); if (recentsComponentName != null) { String recentsPackageName = recentsComponentName.getPackageName(); PackageManager manager = context.getPackageManager(); diff --git a/services/applock/java/com/android/server/app/AppLockManagerService.kt b/services/applock/java/com/android/server/app/AppLockManagerService.kt index 0294aa8dcea4..6dad498540bf 100644 --- a/services/applock/java/com/android/server/app/AppLockManagerService.kt +++ b/services/applock/java/com/android/server/app/AppLockManagerService.kt @@ -814,6 +814,9 @@ class AppLockManagerService( } private fun enforceCallingPermission(msg: String) { + if (com.android.internal.util.lunaris.PixelPropsUtils.isSystemLauncher(Binder.getCallingUid())) { + return; + } context.enforceCallingPermission(Manifest.permission.MANAGE_APP_LOCK, msg) } diff --git a/services/core/java/com/android/server/pm/ComputerEngine.java b/services/core/java/com/android/server/pm/ComputerEngine.java index d187df043422..9eddb61bece1 100644 --- a/services/core/java/com/android/server/pm/ComputerEngine.java +++ b/services/core/java/com/android/server/pm/ComputerEngine.java @@ -164,6 +164,8 @@ import com.android.server.utils.WatchedSparseIntArray; import com.android.server.wm.ActivityTaskManagerInternal; +import org.rising.server.QuickSwitchService; + import libcore.util.EmptyArray; import java.io.BufferedOutputStream; @@ -997,6 +999,8 @@ public final ApplicationInfo getApplicationInfo(String packageName, HideAppListUtils.shouldHideAppList(mContext, packageName)) { return null; } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; return getApplicationInfoInternal(packageName, flags, Binder.getCallingUid(), userId); } @@ -1014,6 +1018,8 @@ public final ApplicationInfo getApplicationInfoInternal(String packageName, HideAppListUtils.shouldHideAppList(mContext, packageName)) { return null; } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; flags = updateFlagsForApplication(flags, userId); if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) { @@ -1731,6 +1737,8 @@ public final PackageInfo getPackageInfo(String packageName, HideAppListUtils.shouldHideAppList(mContext, packageName)) { return null; } + if (QuickSwitchService.shouldHide(userId, packageName)) + return null; return getPackageInfoInternal(packageName, PackageManager.VERSION_CODE_HIGHEST, flags, Binder.getCallingUid(), userId); } @@ -1854,7 +1862,7 @@ public final ParceledListSlice getInstalledPackages(long flags, int enforceCrossUserPermission(callingUid, userId, false /* requireFullPermission */, false /* checkShell */, "get installed packages"); - return recreatePackageList(callingUid, mContext, + return QuickSwitchService.recreatePackageList(callingUid, mContext, userId, getInstalledPackagesBody(flags, userId, callingUid)); } @@ -4911,7 +4919,7 @@ public List getInstalledApplications( } } - return recreateApplicationList(callingUid, mContext, userId, list); + return QuickSwitchService.recreateApplicationList(callingUid, mContext, userId, list); } @Nullable diff --git a/services/core/java/com/android/server/pm/DefaultAppProvider.java b/services/core/java/com/android/server/pm/DefaultAppProvider.java index fc61451b0289..1ed23ceea5a6 100644 --- a/services/core/java/com/android/server/pm/DefaultAppProvider.java +++ b/services/core/java/com/android/server/pm/DefaultAppProvider.java @@ -113,9 +113,16 @@ public String getDefaultDialer(@NonNull int userId) { * @return the package name of the default home, or {@code null} if none */ @Nullable - public String getDefaultHome(@NonNull int userId) { - return getRoleHolder(RoleManager.ROLE_HOME, - mUserManagerInternalSupplier.get().getProfileParentId(userId)); + public String getDefaultHome(int userId) { + String currentHome = getRoleHolder( + RoleManager.ROLE_HOME, + mUserManagerInternalSupplier.get().getProfileParentId(userId) + ); + return maybeOverrideDefaultHome(currentHome); + } + + private String maybeOverrideDefaultHome(String packageName) { + return packageName; } /** @@ -135,7 +142,7 @@ public boolean setDefaultHome(@NonNull String packageName, @UserIdInt int userId } final long identity = Binder.clearCallingIdentity(); try { - roleManager.addRoleHolderAsUser(RoleManager.ROLE_HOME, packageName, 0, + roleManager.addRoleHolderAsUser(RoleManager.ROLE_HOME, maybeOverrideDefaultHome(packageName), 0, UserHandle.of(userId), executor, callback); } finally { Binder.restoreCallingIdentity(identity); diff --git a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java index e09ab600a1dc..1678142f61cf 100644 --- a/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java +++ b/services/core/java/com/android/server/policy/role/RoleServicePlatformHelperImpl.java @@ -274,7 +274,7 @@ private Map> readFromLegacySettings(@UserIdInt int userId) { homePackageName = null; } if (homePackageName != null) { - roles.put(RoleManager.ROLE_HOME, Collections.singleton(homePackageName)); + roles.put(RoleManager.ROLE_HOME, Collections.singleton(maybeOverrideDefaultHome(homePackageName))); } // Emergency @@ -286,6 +286,10 @@ private Map> readFromLegacySettings(@UserIdInt int userId) { return roles; } + + private String maybeOverrideDefaultHome(String packageName) { + return packageName; + } private boolean isSettingsApplication(@NonNull String packageName, @UserIdInt int userId) { PackageManager packageManager = mContext.getPackageManager(); diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 6f3f08cf4ed7..a5e977d5a6c4 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -408,8 +408,8 @@ void loadParametersFromResources(Resources res) { * any dependent services (like SystemUI) is started. */ void loadRecentsComponent(Resources res) { - final String rawRecentsComponent = res.getString( - com.android.internal.R.string.config_recentsComponentName); + int defaultLauncher = android.os.SystemProperties.getInt("persist.sys.default_launcher", 0); + final String rawRecentsComponent = res.getStringArray(com.android.internal.R.array.config_launcherComponents)[defaultLauncher]; if (TextUtils.isEmpty(rawRecentsComponent)) { return; } diff --git a/services/core/java/org/rising/server/QuickSwitchService.java b/services/core/java/org/rising/server/QuickSwitchService.java new file mode 100644 index 000000000000..aaa9a11de849 --- /dev/null +++ b/services/core/java/org/rising/server/QuickSwitchService.java @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 The 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 org.rising.server; + +import static android.os.Process.THREAD_PRIORITY_DEFAULT; + +import android.app.ActivityManager; +import android.app.ActivityThread; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.ParceledListSlice; +import android.content.pm.UserInfo; +import android.os.Handler; +import android.os.IUserManager; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.os.SystemProperties; + +import com.android.server.ServiceThread; +import com.android.server.SystemService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public final class QuickSwitchService extends SystemService { + + private static final String TAG = "QuickSwitchService"; + private static final int THREAD_PRIORITY_DEFAULT = android.os.Process.THREAD_PRIORITY_DEFAULT; + + private final Context mContext; + private final IPackageManager mPM; + private final IUserManager mUM; + private final ContentResolver mResolver; + private final String mOpPackageName; + + private ServiceThread mWorker; + private Handler mHandler; + + private static List LAUNCHER_PACKAGES = null; + private static List disabledLaunchersCache = null; + private static int lastDefaultLauncher = -1; + + static { + if (LAUNCHER_PACKAGES == null) { + try { + Context context = ActivityThread.currentApplication() != null ? + ActivityThread.currentApplication().getApplicationContext() : null; + if (context != null) { + String[] launcherPackages = context.getResources().getStringArray(com.android.internal.R.array.config_launcherPackages); + LAUNCHER_PACKAGES = new ArrayList<>(); + for (String packageName : launcherPackages) { + LAUNCHER_PACKAGES.add(packageName); + } + } else { + LAUNCHER_PACKAGES = new ArrayList<>(); + } + } catch (Exception e) { + LAUNCHER_PACKAGES = new ArrayList<>(); + } + } + } + + public static boolean shouldHide(int userId, String packageName) { + return packageName != null && getDisabledDefaultLaunchers().contains(packageName); + } + + public static ParceledListSlice recreatePackageList( + int callingUid, Context context, int userId, ParceledListSlice list) { + List appList = list.getList(); + List disabledLaunchers = getDisabledDefaultLaunchers(); + appList.removeIf(info -> disabledLaunchers.contains(info.packageName)); + return new ParceledListSlice<>(appList); + } + + public static List recreateApplicationList( + int callingUid, Context context, int userId, List list) { + List appList = new ArrayList<>(list); + List disabledLaunchers = getDisabledDefaultLaunchers(); + appList.removeIf(info -> disabledLaunchers.contains(info.packageName)); + return appList; + } + + private void updateStateForUser(int userId) { + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + try { + for (String packageName : LAUNCHER_PACKAGES) { + if (packageName.equals("com.android.launcher3") && defaultLauncher == 2) { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0, userId, mOpPackageName); + } else if (packageName.equals(LAUNCHER_PACKAGES.get(defaultLauncher))) { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, + 0, userId, mOpPackageName); + } else { + mPM.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + 0, userId, mOpPackageName); + } + } + } catch (IllegalArgumentException ignored) {} + catch (RemoteException e) {} + } + + public static List getDisabledDefaultLaunchers() { + int defaultLauncher = SystemProperties.getInt("persist.sys.default_launcher", 0); + if (defaultLauncher != lastDefaultLauncher || disabledLaunchersCache == null) { + lastDefaultLauncher = defaultLauncher; + List disabledDefaultLaunchers = new ArrayList<>(); + for (int i = 0; i < LAUNCHER_PACKAGES.size(); i++) { + if (i != defaultLauncher && !(i == 0 && defaultLauncher == 2)) { + disabledDefaultLaunchers.add(LAUNCHER_PACKAGES.get(i)); + } + } + disabledLaunchersCache = disabledDefaultLaunchers; + } + return disabledLaunchersCache; + } + + private void initForUser(int userId) { + if (userId < 0) + return; + updateStateForUser(userId); + } + + private void init() { + try { + for (UserInfo user : mUM.getUsers(false)) { + initForUser(user.id); + } + } catch (RemoteException e) { + e.rethrowAsRuntimeException(); + } + + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_USER_ADDED); + filter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(new UserReceiver(), filter, + android.Manifest.permission.MANAGE_USERS, mHandler); + } + + @Override + public void onStart() { + mWorker = new ServiceThread(TAG, THREAD_PRIORITY_DEFAULT, false); + mWorker.start(); + mHandler = new Handler(mWorker.getLooper()); + init(); + } + + @Override + public void onBootPhase(int phase) { + super.onBootPhase(phase); + if (phase == SystemService.PHASE_BOOT_COMPLETED) { + IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT); + } + } + + public QuickSwitchService(Context context) { + super(context); + mContext = context; + mResolver = context.getContentResolver(); + mPM = IPackageManager.Stub.asInterface(ServiceManager.getService("package")); + mUM = IUserManager.Stub.asInterface(ServiceManager.getService(Context.USER_SERVICE)); + mOpPackageName = context.getOpPackageName(); + } + + private final class UserReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1); + if (Intent.ACTION_USER_ADDED.equals(intent.getAction())) { + initForUser(userId); + } + } + } +} diff --git a/services/core/java/org/rising/server/RisingServicesStarter.java b/services/core/java/org/rising/server/RisingServicesStarter.java index 63cfb13d6648..1190be4bad0c 100644 --- a/services/core/java/org/rising/server/RisingServicesStarter.java +++ b/services/core/java/org/rising/server/RisingServicesStarter.java @@ -22,11 +22,15 @@ public class RisingServicesStarter { private final SystemServiceManager mSystemServiceManager; + private static final String QUICKSWITCH_SERVICE_CLASS = + "org.rising.server.QuickSwitchService"; + public RisingServicesStarter(SystemServiceManager systemServiceManager) { this.mSystemServiceManager = systemServiceManager; } public void startAllServices() { + startService(QUICKSWITCH_SERVICE_CLASS); } private void startService(String serviceClassName) { From 4a15a8a3cff74f679d4f4668aeb9b36b9a14fa31 Mon Sep 17 00:00:00 2001 From: Anierin Bliss Date: Tue, 10 Feb 2026 08:24:30 +0000 Subject: [PATCH 045/190] Utils: Add util to toggle overlays Signed-off-by: Ghosuto --- .../android/internal/util/lunaris/Utils.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/core/java/com/android/internal/util/lunaris/Utils.java b/core/java/com/android/internal/util/lunaris/Utils.java index 8a77e6949691..bd0f722954ea 100644 --- a/core/java/com/android/internal/util/lunaris/Utils.java +++ b/core/java/com/android/internal/util/lunaris/Utils.java @@ -18,6 +18,10 @@ import android.content.Context; import android.content.Intent; +import android.content.om.OverlayManager; +import android.content.om.OverlayManagerTransaction; +import android.content.om.OverlayIdentifier; +import android.content.om.OverlayInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -27,6 +31,9 @@ import android.os.ServiceManager; import android.os.SystemClock; import android.os.SystemProperties; +import android.os.UserHandle; + +import android.util.Log; import com.android.internal.statusbar.IStatusBarService; @@ -35,6 +42,8 @@ public class Utils { + private static final String TAG = "Utils"; + public static boolean isPackageInstalled(Context context, String packageName, boolean ignoreState) { if (packageName != null) { try { @@ -111,4 +120,45 @@ public static void restartSystemUI() { } catch (RemoteException e) { } } + + public static void toggleOverlay(Context context, String overlayName, boolean enable) { + OverlayManager overlayManager = context.getSystemService(OverlayManager.class); + if (overlayManager == null) { + Log.e(TAG, "OverlayManager is not available"); + return; + } + + OverlayIdentifier overlayId = getOverlayID(overlayManager, overlayName); + if (overlayId == null) { + Log.e(TAG, "Overlay ID not found for " + overlayName); + return; + } + + OverlayManagerTransaction.Builder transaction = new OverlayManagerTransaction.Builder(); + transaction.setEnabled(overlayId, enable, UserHandle.USER_CURRENT); + + try { + overlayManager.commit(transaction.build()); + } catch (Exception e) { + Log.e(TAG, "Error toggling overlay", e); + } + } + + private static OverlayIdentifier getOverlayID(OverlayManager overlayManager, String name) { + try { + if (name.contains(":")) { + String[] parts = name.split(":"); + List infos = overlayManager.getOverlayInfosForTarget(parts[0], UserHandle.CURRENT); + for (OverlayInfo info : infos) { + if (parts[1].equals(info.getOverlayName())) return info.getOverlayIdentifier(); + } + } else { + OverlayInfo info = overlayManager.getOverlayInfo(name, UserHandle.CURRENT); + if (info != null) return info.getOverlayIdentifier(); + } + } catch (Exception e) { + Log.e(TAG, "Error retrieving overlay ID", e); + } + return null; + } } From 8cbd4196e4c58e138cc7361c3a0fd777bce474eb Mon Sep 17 00:00:00 2001 From: Blaster4385 Date: Tue, 10 Feb 2026 11:38:22 +0000 Subject: [PATCH 046/190] base: Add util function to restart any app - Based onn [1] - Nuked uneeded code - Modify code to not restrict function to single app [1] PixysOS/frameworks_base@f479a95 Change-Id: I926236a1f9ef281b827089466285413bd1f7358a Signed-off-by: Ghosuto --- .../android/internal/util/lunaris/Utils.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/core/java/com/android/internal/util/lunaris/Utils.java b/core/java/com/android/internal/util/lunaris/Utils.java index bd0f722954ea..c4131fd8b805 100644 --- a/core/java/com/android/internal/util/lunaris/Utils.java +++ b/core/java/com/android/internal/util/lunaris/Utils.java @@ -16,6 +16,9 @@ package com.android.internal.util.lunaris; +import android.app.ActivityManager; +import android.app.IActivityManager; +import android.content.Context; import android.content.Context; import android.content.Intent; import android.content.om.OverlayManager; @@ -26,6 +29,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.os.AsyncTask; import android.os.PowerManager; import android.os.RemoteException; import android.os.ServiceManager; @@ -44,6 +48,39 @@ public class Utils { private static final String TAG = "Utils"; + public static void restartApp(String appName, Context context) { + new RestartAppTask(appName, context).execute(); + } + + private static class RestartAppTask extends AsyncTask { + private Context mContext; + private String mApp; + + public RestartAppTask(String appName, Context context) { + super(); + mContext = context; + mApp = appName; + } + + @Override + protected Void doInBackground(Void... params) { + try { + ActivityManager am = + (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); + IActivityManager ams = ActivityManager.getService(); + for (ActivityManager.RunningAppProcessInfo app: am.getRunningAppProcesses()) { + if (mApp.equals(app.processName)) { + ams.killApplicationProcess(app.processName, app.uid); + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + public static boolean isPackageInstalled(Context context, String packageName, boolean ignoreState) { if (packageName != null) { try { From a4814e22d17485c70d865935b7c69adacec54227 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 10 Feb 2026 15:04:32 +0000 Subject: [PATCH 047/190] SystemUI: Fix SystemUI crash due to null CPU temperature reading Signed-off-by: Ghosuto --- .../internal/util/lunaris/ThemeUtils.java | 62 +++++++++++++------ 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/core/java/com/android/internal/util/lunaris/ThemeUtils.java b/core/java/com/android/internal/util/lunaris/ThemeUtils.java index fadabfbe1e49..1fa8f310af73 100644 --- a/core/java/com/android/internal/util/lunaris/ThemeUtils.java +++ b/core/java/com/android/internal/util/lunaris/ThemeUtils.java @@ -268,37 +268,61 @@ public static String batteryTemperature(Context context, Boolean ForC) { } public static String getCPUTemp(Context context) { - String value; - if (fileExists(context.getResources().getString( - com.android.internal.R.string.config_cpu_temp_path))) { - value = readOneLine(context.getResources().getString( - com.android.internal.R.string.config_cpu_temp_path)); - } else { - value = "Error"; + String value = null; + String cpuTempPath = context.getResources().getString( + com.android.internal.R.string.config_cpu_temp_path); + + if (fileExists(cpuTempPath)) { + value = readOneLine(cpuTempPath); + } + + if (value == null || value.isEmpty()) { + return "N/A"; + } + + try { + int cpuTempMultiplier = context.getResources().getInteger( + com.android.internal.R.integer.config_sysCPUTempMultiplier); + + if (cpuTempMultiplier == 0) { + cpuTempMultiplier = 1; + } + + int temp = Integer.parseInt(value.trim()) / cpuTempMultiplier; + return String.format("%d°C", temp); + } catch (NumberFormatException | Resources.NotFoundException e) { + Log.w("ThemeUtils", "Failed to parse CPU temperature: " + value, e); + return "N/A"; } - int cpuTempMultiplier = context.getResources().getInteger( - com.android.internal.R.integer.config_sysCPUTempMultiplier); - return value == "Error" ? "N/A" : String.format("%s", Integer.parseInt(value) / cpuTempMultiplier) + "°C"; } + public static boolean fileExists(String filename) { - if (filename == null) { + if (filename == null || filename.isEmpty()) { return false; } return new File(filename).exists(); } + public static String readOneLine(String fname) { - BufferedReader br; - String line = null; + if (fname == null || fname.isEmpty()) { + return null; + } + + BufferedReader br = null; try { br = new BufferedReader(new FileReader(fname), 512); - try { - line = br.readLine(); - } finally { - br.close(); - } + return br.readLine(); } catch (Exception e) { + Log.w("ThemeUtils", "Failed to read file: " + fname, e); return null; + } finally { + if (br != null) { + try { + br.close(); + } catch (IOException e) { + // Ignore close errors + } + } } - return line; } } From 183e2fc426f4a6efe87335bc580d88f8f5e34c80 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 11 Oct 2024 16:49:04 +0800 Subject: [PATCH 048/190] core: Introduce SystemRestartUtils Co-authored-by: spkal01 Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- .../util/lunaris/SystemRestartUtils.java | 169 ++++++++++++++++++ core/res/res/values/lunaris_strings.xml | 16 ++ core/res/res/values/lunaris_symbols.xml | 16 ++ 3 files changed, 201 insertions(+) create mode 100644 core/java/com/android/internal/util/lunaris/SystemRestartUtils.java diff --git a/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java b/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java new file mode 100644 index 000000000000..7c74e33cd1a7 --- /dev/null +++ b/core/java/com/android/internal/util/lunaris/SystemRestartUtils.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2023 Rising OS 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.internal.util.lunaris; + +import android.app.AlertDialog; +import android.app.IActivityManager; +import android.app.ActivityManager; +import android.content.Context; +import android.content.DialogInterface; +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Process; +import android.os.RemoteException; +import android.os.ServiceManager; + +import com.android.internal.R; +import com.android.internal.statusbar.IStatusBarService; + +import java.lang.ref.WeakReference; + +import java.util.List; + +public class SystemRestartUtils { + + private static final int RESTART_TIMEOUT = 1000; + + public static void showSystemRestartDialog(Context context) { + new AlertDialog.Builder(context) + .setTitle(R.string.system_restart_title) + .setMessage(R.string.system_restart_message) + .setPositiveButton(R.string.ok, (dialog, id) -> { + Handler handler = new Handler(); + handler.postDelayed(() -> restartSystem(context), RESTART_TIMEOUT); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public static void powerOffSystem(Context context) { + new PowerOffSystemTask(context).execute(); + } + + private static class PowerOffSystemTask extends AsyncTask { + private final WeakReference mContext; + + PowerOffSystemTask(Context context) { + mContext = new WeakReference<>(context); + } + + @Override + protected Void doInBackground(Void... params) { + try { + IStatusBarService mBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + if (mBarService != null) { + try { + Thread.sleep(RESTART_TIMEOUT); + mBarService.shutdown(); + } catch (RemoteException | InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + + public static void restartSystem(Context context) { + new RestartSystemTask(context).execute(); + } + + private static class RestartSystemTask extends AsyncTask { + private final WeakReference mContext; + + RestartSystemTask(Context context) { + mContext = new WeakReference<>(context); + } + + @Override + protected Void doInBackground(Void... params) { + try { + IStatusBarService mBarService = IStatusBarService.Stub.asInterface( + ServiceManager.getService(Context.STATUS_BAR_SERVICE)); + if (mBarService != null) { + try { + Thread.sleep(RESTART_TIMEOUT); + mBarService.reboot(false, null); + } catch (RemoteException | InterruptedException e) { + e.printStackTrace(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + } + + private static void showRestartDialog(Context context, int title, int message, Runnable action) { + new AlertDialog.Builder(context) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ok, (dialog, id) -> { + Handler handler = new Handler(); + handler.postDelayed(action, RESTART_TIMEOUT); + }) + .setNegativeButton(R.string.cancel, null) + .show(); + } + + public static void restartProcess(Context context, String processName) { + new RestartTask(context, processName).execute(); + } + + private static class RestartTask extends AsyncTask { + private final WeakReference mContext; + private final String mPackageName; + + RestartTask(Context context, String packageName) { + mContext = new WeakReference<>(context); + mPackageName = packageName; + } + + @Override + protected Void doInBackground(Void... params) { + try { + ActivityManager am = (ActivityManager) mContext.get().getSystemService(Context.ACTIVITY_SERVICE); + if (am != null) { + List runningProcesses = am.getRunningAppProcesses(); + for (ActivityManager.RunningAppProcessInfo appProcess : runningProcesses) { + if (appProcess.pkgList != null) { + for (String pkg : appProcess.pkgList) { + if (pkg.equals(mPackageName)) { + Process.killProcess(appProcess.pid); + return null; + } + } + } + } + } + } catch (Exception e) {} + return null; + } + } + + public static void showSettingsRestartDialog(Context context) { + showRestartDialog(context, R.string.settings_restart_title, R.string.settings_restart_message, () -> restartProcess(context, "com.android.settings")); + } + + public static void showSystemUIRestartDialog(Context context) { + showRestartDialog(context, R.string.systemui_restart_title, R.string.systemui_restart_message, () -> restartProcess(context, "com.android.systemui")); + } +} diff --git a/core/res/res/values/lunaris_strings.xml b/core/res/res/values/lunaris_strings.xml index 2c7ee6651d41..8ad829b665bc 100644 --- a/core/res/res/values/lunaris_strings.xml +++ b/core/res/res/values/lunaris_strings.xml @@ -23,4 +23,20 @@ Calls and notifications will ring + + + 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? + + + SystemUI restart required + For all changes to take effect, a SystemUI restart is required. Restart SystemUI now? + + + Launcher restart required + For all changes to take effect, a Launcher restart is required. Restart Launcher now? diff --git a/core/res/res/values/lunaris_symbols.xml b/core/res/res/values/lunaris_symbols.xml index 3d811080b4d6..7cf47b7541b8 100644 --- a/core/res/res/values/lunaris_symbols.xml +++ b/core/res/res/values/lunaris_symbols.xml @@ -107,4 +107,20 @@ + + + + + + + + + + + + + + + + From 560f8a115a3ef132e215c339f0667b02cee6e3e9 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 11 Feb 2026 09:01:39 +0000 Subject: [PATCH 049/190] SystemUI: Apply wifi and signal icon styles last - Ghost: Adapt for qpr1+ Co-authored-by: Ghosuto Signed-off-by: Ghosuto --- .../systemui/theme/ThemeOverlayApplier.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java index caae2c4ce99a..6302df21d8ba 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayApplier.java @@ -113,6 +113,12 @@ public class ThemeOverlayApplier implements Dumpable { @VisibleForTesting static final String OVERLAY_CATEGORY_ICON_THEME_PICKER = "android.theme.customization.icon_pack.themepicker"; + @VisibleForTesting + static final String OVERLAY_CATEGORY_ICON_SIGNAL = + "android.theme.customization.signal_icon"; + @VisibleForTesting + static final String OVERLAY_CATEGORY_ICON_WIFI = + "android.theme.customization.wifi_icon"; /* * All theme customization categories used by the system, in order that they should be applied, @@ -126,7 +132,9 @@ public class ThemeOverlayApplier implements Dumpable { OVERLAY_CATEGORY_DYNAMIC_COLOR, OVERLAY_CATEGORY_ICON_ANDROID, OVERLAY_CATEGORY_ICON_SYSUI, - OVERLAY_CATEGORY_ICON_SETTINGS); + OVERLAY_CATEGORY_ICON_SETTINGS, + OVERLAY_CATEGORY_ICON_SIGNAL, + OVERLAY_CATEGORY_ICON_WIFI); /* Categories that need to be applied to the current user as well as the system user. */ @VisibleForTesting @@ -137,7 +145,9 @@ public class ThemeOverlayApplier implements Dumpable { OVERLAY_CATEGORY_FONT, OVERLAY_CATEGORY_SHAPE, OVERLAY_CATEGORY_ICON_ANDROID, - OVERLAY_CATEGORY_ICON_SYSUI); + OVERLAY_CATEGORY_ICON_SYSUI, + OVERLAY_CATEGORY_ICON_SIGNAL, + OVERLAY_CATEGORY_ICON_WIFI); /* Allowed overlay categories for each target package. */ private final Map> mTargetPackageToCategories = new ArrayMap<>(); @@ -177,6 +187,8 @@ public ThemeOverlayApplier(OverlayManager overlayManager, mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_ANDROID, ANDROID_PACKAGE); mCategoryToTargetPackage.put(OVERLAY_CATEGORY_ICON_SYSUI, 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); dumpManager.registerDumpable(TAG, this); } From 49d020e43451493d6d3a1e43cee81f18aa9899e1 Mon Sep 17 00:00:00 2001 From: Abhay Singh Gill Date: Sat, 13 Sep 2025 21:32:55 +0530 Subject: [PATCH 050/190] SystemUI: Add flashlight strength control The default behaviour is toggling the flashlight since we use secondaryClick as default onClick behaviour on small tiles. Change-Id: Id7122ce581aeb57bd14119cca75062a3fbc6105a Signed-off-by: Abhay Singh Gill Signed-off-by: Ghosuto --- packages/SystemUI/res/values/cm_strings.xml | 5 + .../systemui/qs/tiles/FlashlightTile.java | 193 +++++++++++++++++- .../tiles/dialog/FlashlightDialogDelegate.kt | 176 ++++++++++++++++ 3 files changed, 372 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt diff --git a/packages/SystemUI/res/values/cm_strings.xml b/packages/SystemUI/res/values/cm_strings.xml index 381dc02c9110..6e0c495dc5da 100644 --- a/packages/SystemUI/res/values/cm_strings.xml +++ b/packages/SystemUI/res/values/cm_strings.xml @@ -135,4 +135,9 @@ To unpin this screen, touch & hold Back button + + + Flashlight Strength + Turn off + Turn on diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 2b127d60b2be..45774b9a3d2c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -16,18 +16,30 @@ package com.android.systemui.qs.tiles; +import android.annotation.NonNull; import android.app.ActivityManager; +import android.content.Context; import android.content.Intent; +import android.database.ContentObserver; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.net.Uri; import android.os.Handler; import android.os.Looper; +import android.os.UserHandle; import android.provider.MediaStore; +import android.provider.Settings; import android.service.quicksettings.Tile; import android.widget.Switch; import androidx.annotation.Nullable; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.animation.DialogCuj; +import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.animation.Expandable; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; @@ -38,11 +50,14 @@ import com.android.systemui.qs.QSHost; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.logging.QSLogger; +import com.android.systemui.qs.tiles.dialog.FlashlightDialogDelegate; import com.android.systemui.qs.tileimpl.QSTileImpl; import com.android.systemui.res.R; +import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.FlashlightController; import javax.inject.Inject; +import javax.inject.Provider; /** * Quick settings tile: Control flashlight @@ -51,8 +66,27 @@ public class FlashlightTile extends QSTileImpl implements FlashlightController.FlashlightListener { public static final String TILE_SPEC = "flashlight"; + private static final String FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness"; + private static final String INTERACTION_JANK_TAG = "flashlight_strength"; + private final FlashlightController mFlashlightController; + private final Handler mHandler; + private final Looper mBgLooper; + private final Provider mFlashlightDialogProvider; + private final DialogTransitionAnimator mDialogTransitionAnimator; + private final ContentObserver mBrightnessObserver; + private final boolean mStrengthControlSupported; + + private CameraManager mCameraManager; + + private int mDefaultLevel; + private int mMaxLevel; + private float mCurrentPercent; + private int mCurrentLevel; + + @Nullable private String mCameraId; + @Inject public FlashlightTile( QSHost host, @@ -64,17 +98,45 @@ public FlashlightTile( StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, - FlashlightController flashlightController + FlashlightController flashlightController, + Provider flashlightDialogDelegateProvider, + DialogTransitionAnimator dialogTransitionAnimator ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); + mHandler = mainHandler; + mBgLooper = backgroundLooper; mFlashlightController = flashlightController; mFlashlightController.observe(getLifecycle(), this); + mFlashlightDialogProvider = flashlightDialogDelegateProvider; + mDialogTransitionAnimator = dialogTransitionAnimator; + + mBrightnessObserver = new ContentObserver(new Handler(mBgLooper)) { + @Override + public void onChange(boolean selfChange, @Nullable Uri uri) { + super.onChange(selfChange, uri); + refreshState(); + } + }; + + mStrengthControlSupported = isStrengthControlSupported(); + if (mStrengthControlSupported) { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(FLASHLIGHT_BRIGHTNESS_SETTING), + false, + mBrightnessObserver + ); + getCameraManager().registerTorchCallback(mTorchCallback, new Handler(mBgLooper)); + } } @Override protected void handleDestroy() { super.handleDestroy(); + if (mStrengthControlSupported) { + mContext.getContentResolver().unregisterContentObserver(mBrightnessObserver); + getCameraManager().unregisterTorchCallback(mTorchCallback); + } } @Override @@ -103,9 +165,54 @@ protected void handleClick(@Nullable Expandable expandable) { if (ActivityManager.isUserAMonkey()) { return; } + + if (mStrengthControlSupported) { + Runnable runnable = new Runnable() { + @Override + public void run() { + SystemUIDialog dialog = mFlashlightDialogProvider.get() + .setCameraInfo(mCameraId, mMaxLevel, mDefaultLevel) + .createDialog(); + if (expandable != null) { + DialogTransitionAnimator.Controller controller = + expandable.dialogTransitionController( + new DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + INTERACTION_JANK_TAG)); + if (controller != null) { + mDialogTransitionAnimator.show(dialog, controller); + } else { + dialog.show(); + } + } else { + dialog.show(); + } + } + }; + + mHandler.post(runnable); + } else { + handleSecondaryClick(expandable); + } + } + + @Override + protected void handleSecondaryClick(@Nullable Expandable expandable) { + if (ActivityManager.isUserAMonkey()) { + return; + } boolean newState = !mState.value; refreshState(newState); - mFlashlightController.setFlashlight(newState); + + if (mStrengthControlSupported && newState) { + try { + int level = Math.max((int) (mCurrentPercent * mMaxLevel), 1); + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId, level); + } catch (CameraAccessException e) { + } + } else { + mFlashlightController.setFlashlight(newState); + } } @Override @@ -123,6 +230,7 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); state.secondaryLabel = ""; state.stateDescription = ""; + state.handlesSecondaryClick = mStrengthControlSupported; if (!mFlashlightController.isAvailable()) { state.secondaryLabel = mContext.getString( R.string.quick_settings_flashlight_camera_in_use); @@ -131,6 +239,22 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.icon = maybeLoadResourceIcon(R.drawable.qs_flashlight_icon_off); return; } + if (mStrengthControlSupported) { + boolean enabled = mFlashlightController.isEnabled(); + mCurrentPercent = Settings.System.getFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + (float) mDefaultLevel / (float) mMaxLevel, + UserHandle.USER_CURRENT + ); + + mCurrentPercent = Math.max(0.01f, mCurrentPercent); + + if (enabled) { + state.secondaryLabel = Math.round(mCurrentPercent * 100f) + "%"; + state.stateDescription = state.secondaryLabel; + } + } if (arg instanceof Boolean) { boolean value = (Boolean) arg; if (value == state.value) { @@ -166,4 +290,69 @@ public void onFlashlightError() { public void onFlashlightAvailabilityChanged(boolean available) { refreshState(); } + + private final CameraManager.TorchCallback mTorchCallback = new CameraManager.TorchCallback() { + @Override + public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) { + if (!cameraId.equals(mCameraId)) { + return; + } + + if (mCurrentLevel == newStrengthLevel) { + return; + } + + mCurrentLevel = newStrengthLevel; + mCurrentPercent = Math.max(0.01f, ((float) mCurrentLevel) / ((float) mMaxLevel)); + Settings.System.putFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + mCurrentPercent, + UserHandle.USER_CURRENT); + refreshState(true); + } + }; + + private CameraManager getCameraManager() { + if (mCameraManager == null) { + mCameraManager = (CameraManager) mContext.getApplicationContext() + .getSystemService(Context.CAMERA_SERVICE); + } + return mCameraManager; + } + + private String getCameraId(CameraManager cm) throws CameraAccessException { + String[] ids = cm.getCameraIdList(); + for (String id : ids) { + CameraCharacteristics c = cm.getCameraCharacteristics(id); + Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING); + if (flashAvailable != null + && flashAvailable + && lensFacing != null + && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { + return id; + } + } + return null; + } + + private boolean isStrengthControlSupported() { + CameraManager cm = getCameraManager(); + if (cm == null) return false; + + try { + mCameraId = getCameraId(cm); + if (mCameraId != null) { + CameraCharacteristics c = cm.getCameraCharacteristics(mCameraId); + Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + mDefaultLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL); + mMaxLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL); + if (flashAvailable && mMaxLevel > mDefaultLevel) { + return true; + } + } + } catch (CameraAccessException e) {} + return false; + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt new file mode 100644 index 000000000000..f1b4d494c9dd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2025 Neoteric OS + * + * 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.qs.tiles.dialog + +import android.content.Context +import android.hardware.camera2.CameraAccessException +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.os.Bundle +import android.os.UserHandle +import android.os.VibrationEffect +import android.os.Vibrator +import android.provider.Settings +import android.util.Log +import android.widget.Button +import android.widget.FrameLayout +import android.view.ContextThemeWrapper +import android.view.Gravity +import androidx.annotation.MainThread +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.SystemUIDialog +import com.android.systemui.statusbar.policy.FlashlightController +import com.android.systemui.util.concurrency.DelayableExecutor +import com.google.android.material.slider.Slider +import javax.inject.Inject + +class FlashlightDialogDelegate @Inject constructor( + private val context: Context, + private val systemUIDialogFactory: SystemUIDialog.Factory, + @Main private val mainExecutor: DelayableExecutor, + @Background private val backgroundExecutor: DelayableExecutor, + private val flashlightController: FlashlightController +) : SystemUIDialog.Delegate { + + private lateinit var slider: Slider + + private val cameraManager: CameraManager? + get() = context.getSystemService(Context.CAMERA_SERVICE) as? CameraManager + private var cameraId: String? = null + private var maxLevel: Int = 1 + private var defaultLevel: Int = 1 + private var currentPercent: Float = 1f + + private val vibrator = context.getSystemService(Vibrator::class.java) + private val flashlightMoveHaptic: VibrationEffect = + VibrationEffect.get(VibrationEffect.EFFECT_TICK) + + override fun createDialog(): SystemUIDialog = systemUIDialogFactory.create(this) + + override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + dialog.setTitle(R.string.flashlight_strength_title) + + val container = FrameLayout(dialog.context) + + val themedContext = ContextThemeWrapper(dialog.context, + com.google.android.material.R.style.Theme_Material3_DynamicColors_DayNight) + slider = Slider(themedContext).apply { + layoutParams = FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + Gravity.CENTER + ) + } + + container.addView(slider) + dialog.setView(container) + dialog.setPositiveButton(R.string.quick_settings_done, null, true) + dialog.setNeutralButton( + if (flashlightController.isEnabled()) + R.string.flashlight_strength_turn_off + else + R.string.flashlight_strength_turn_on, + { _, _ -> + val newState = !flashlightController.isEnabled() + if (newState) { + val level = (currentPercent * maxLevel).toInt() + val safeLevel = maxOf(level, 1) + cameraId?.let { id -> + try { + cameraManager?.turnOnTorchWithStrengthLevel(id, safeLevel) + } catch (_: CameraAccessException) {} + } + } else { + flashlightController.setFlashlight(false) + } + + slider.isEnabled = newState + + dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)?.text = + if (newState) + dialog.context.getString(R.string.flashlight_strength_turn_off) + else + dialog.context.getString(R.string.flashlight_strength_turn_on) + }, + false + ) + } + + override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { + val defaultPercent = defaultLevel.toFloat() / maxLevel.toFloat() + currentPercent = Settings.System.getFloatForUser( + dialog.context.contentResolver, + FLASHLIGHT_BRIGHTNESS_SETTING, + defaultPercent, + UserHandle.USER_CURRENT + ) + + slider.isEnabled = flashlightController.isEnabled() + slider.valueFrom = 1f + slider.valueTo = 100f + slider.value = (currentPercent * 100f).coerceAtLeast(1f) + slider.setLabelFormatter { value -> + value.toInt().toString() + } + + var last = -1 + slider.addOnChangeListener { _, value, fromUser -> + if (fromUser) { + val percent = (value / 100f) + val p = Math.round(percent * 100) + if (p != last) { + vibrator?.vibrate(flashlightMoveHaptic) + last = p + } + currentPercent = percent + updateFlashlightStrength() + } + } + } + + @MainThread + private fun updateFlashlightStrength() { + Settings.System.putFloatForUser( + context.contentResolver, + FLASHLIGHT_BRIGHTNESS_SETTING, + currentPercent, + UserHandle.USER_CURRENT + ) + + if (cameraId != null) { + try { + val level = (currentPercent * maxLevel).toInt().coerceAtLeast(1) + cameraManager?.turnOnTorchWithStrengthLevel(cameraId!!, level) + } catch (e: CameraAccessException) { + Log.e(TAG, "Unable to set torch strength", e) + } + } + } + + fun setCameraInfo(camId: String?, maxLvl: Int, defLvl: Int): FlashlightDialogDelegate { + cameraId = camId + maxLevel = maxLvl + defaultLevel = defLvl + return this + } + + companion object { + private const val TAG = "FlashlightDialogDelegate" + private const val FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness" + } +} From 56711e36e825bc49f73ed4d0209fbcfa5bcef248 Mon Sep 17 00:00:00 2001 From: NurKeinNeid Date: Fri, 19 Sep 2025 01:42:17 +0200 Subject: [PATCH 051/190] SystemUI: Fix flashlight strength control detection Change detection logic from 'mMaxLevel > mDefaultLevel' to 'mMaxLevel > 1' to properly detect devices where max and default levels are equal. Fixes flashlight strength control not working on some devices. Signed-off-by: NurKeinNeid Signed-off-by: Ghosuto --- .../src/com/android/systemui/qs/tiles/FlashlightTile.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index 45774b9a3d2c..ffea4ba49547 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -348,7 +348,8 @@ private boolean isStrengthControlSupported() { Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); mDefaultLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL); mMaxLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL); - if (flashAvailable && mMaxLevel > mDefaultLevel) { + // Use the same logic as the old implementation: mMaxLevel > 1 + if (flashAvailable && mMaxLevel > 1) { return true; } } From 0a2da9856693a0ee64d722a36946bd8c1d515922 Mon Sep 17 00:00:00 2001 From: Abhay Singh Gill Date: Fri, 19 Sep 2025 18:26:10 +0530 Subject: [PATCH 052/190] SystemUI: Extend flashlight strength logic to flashlight controller Previously the flashlight strength was only applied when toggling it from the QS tile, now it works from other SystemUI components as well. Signed-off-by: Abhay Singh Gill Signed-off-by: Ghosuto --- .../FlashlightQuickAffordanceConfig.kt | 2 + .../systemui/qs/tiles/FlashlightTile.java | 143 ++-------------- .../tiles/dialog/FlashlightDialogDelegate.kt | 76 +-------- .../FlashlightTileDataInteractor.kt | 1 + .../policy/FlashlightController.java | 10 ++ .../policy/FlashlightControllerImpl.java | 159 +++++++++++++++++- 6 files changed, 189 insertions(+), 202 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt index 683c11a88b89..bfcc47c1dbb6 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfig.kt @@ -116,6 +116,8 @@ constructor( TAG, ) } + + override fun onFlashlightStrengthChanged(level: Int) {} } flashlightController.addCallback(flashlightCallback) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index ffea4ba49547..ec7e200cf2ef 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -16,20 +16,11 @@ package com.android.systemui.qs.tiles; -import android.annotation.NonNull; import android.app.ActivityManager; -import android.content.Context; import android.content.Intent; -import android.database.ContentObserver; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.net.Uri; import android.os.Handler; import android.os.Looper; -import android.os.UserHandle; import android.provider.MediaStore; -import android.provider.Settings; import android.service.quicksettings.Tile; import android.widget.Switch; @@ -72,20 +63,8 @@ public class FlashlightTile extends QSTileImpl implements private final FlashlightController mFlashlightController; private final Handler mHandler; - private final Looper mBgLooper; private final Provider mFlashlightDialogProvider; private final DialogTransitionAnimator mDialogTransitionAnimator; - private final ContentObserver mBrightnessObserver; - private final boolean mStrengthControlSupported; - - private CameraManager mCameraManager; - - private int mDefaultLevel; - private int mMaxLevel; - private float mCurrentPercent; - private int mCurrentLevel; - - @Nullable private String mCameraId; @Inject public FlashlightTile( @@ -105,38 +84,15 @@ public FlashlightTile( super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); mHandler = mainHandler; - mBgLooper = backgroundLooper; mFlashlightController = flashlightController; mFlashlightController.observe(getLifecycle(), this); mFlashlightDialogProvider = flashlightDialogDelegateProvider; mDialogTransitionAnimator = dialogTransitionAnimator; - - mBrightnessObserver = new ContentObserver(new Handler(mBgLooper)) { - @Override - public void onChange(boolean selfChange, @Nullable Uri uri) { - super.onChange(selfChange, uri); - refreshState(); - } - }; - - mStrengthControlSupported = isStrengthControlSupported(); - if (mStrengthControlSupported) { - mContext.getContentResolver().registerContentObserver( - Settings.System.getUriFor(FLASHLIGHT_BRIGHTNESS_SETTING), - false, - mBrightnessObserver - ); - getCameraManager().registerTorchCallback(mTorchCallback, new Handler(mBgLooper)); - } } @Override protected void handleDestroy() { super.handleDestroy(); - if (mStrengthControlSupported) { - mContext.getContentResolver().unregisterContentObserver(mBrightnessObserver); - getCameraManager().unregisterTorchCallback(mTorchCallback); - } } @Override @@ -166,13 +122,11 @@ protected void handleClick(@Nullable Expandable expandable) { return; } - if (mStrengthControlSupported) { + if (mFlashlightController.isStrengthControlSupported()) { Runnable runnable = new Runnable() { @Override public void run() { - SystemUIDialog dialog = mFlashlightDialogProvider.get() - .setCameraInfo(mCameraId, mMaxLevel, mDefaultLevel) - .createDialog(); + SystemUIDialog dialog = mFlashlightDialogProvider.get().createDialog(); if (expandable != null) { DialogTransitionAnimator.Controller controller = expandable.dialogTransitionController( @@ -203,16 +157,7 @@ protected void handleSecondaryClick(@Nullable Expandable expandable) { } boolean newState = !mState.value; refreshState(newState); - - if (mStrengthControlSupported && newState) { - try { - int level = Math.max((int) (mCurrentPercent * mMaxLevel), 1); - mCameraManager.turnOnTorchWithStrengthLevel(mCameraId, level); - } catch (CameraAccessException e) { - } - } else { - mFlashlightController.setFlashlight(newState); - } + mFlashlightController.setFlashlight(newState); } @Override @@ -230,7 +175,7 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label); state.secondaryLabel = ""; state.stateDescription = ""; - state.handlesSecondaryClick = mStrengthControlSupported; + state.handlesSecondaryClick = mFlashlightController.isStrengthControlSupported(); if (!mFlashlightController.isAvailable()) { state.secondaryLabel = mContext.getString( R.string.quick_settings_flashlight_camera_in_use); @@ -239,19 +184,12 @@ protected void handleUpdateState(BooleanState state, Object arg) { state.icon = maybeLoadResourceIcon(R.drawable.qs_flashlight_icon_off); return; } - if (mStrengthControlSupported) { + if (mFlashlightController.isStrengthControlSupported()) { boolean enabled = mFlashlightController.isEnabled(); - mCurrentPercent = Settings.System.getFloatForUser( - mContext.getContentResolver(), - FLASHLIGHT_BRIGHTNESS_SETTING, - (float) mDefaultLevel / (float) mMaxLevel, - UserHandle.USER_CURRENT - ); + float percent = mFlashlightController.getCurrentPercent(); - mCurrentPercent = Math.max(0.01f, mCurrentPercent); - if (enabled) { - state.secondaryLabel = Math.round(mCurrentPercent * 100f) + "%"; + state.secondaryLabel = Math.round(percent * 100f) + "%"; state.stateDescription = state.secondaryLabel; } } @@ -291,69 +229,8 @@ public void onFlashlightAvailabilityChanged(boolean available) { refreshState(); } - private final CameraManager.TorchCallback mTorchCallback = new CameraManager.TorchCallback() { - @Override - public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) { - if (!cameraId.equals(mCameraId)) { - return; - } - - if (mCurrentLevel == newStrengthLevel) { - return; - } - - mCurrentLevel = newStrengthLevel; - mCurrentPercent = Math.max(0.01f, ((float) mCurrentLevel) / ((float) mMaxLevel)); - Settings.System.putFloatForUser( - mContext.getContentResolver(), - FLASHLIGHT_BRIGHTNESS_SETTING, - mCurrentPercent, - UserHandle.USER_CURRENT); - refreshState(true); - } - }; - - private CameraManager getCameraManager() { - if (mCameraManager == null) { - mCameraManager = (CameraManager) mContext.getApplicationContext() - .getSystemService(Context.CAMERA_SERVICE); - } - return mCameraManager; - } - - private String getCameraId(CameraManager cm) throws CameraAccessException { - String[] ids = cm.getCameraIdList(); - for (String id : ids) { - CameraCharacteristics c = cm.getCameraCharacteristics(id); - Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING); - if (flashAvailable != null - && flashAvailable - && lensFacing != null - && lensFacing == CameraCharacteristics.LENS_FACING_BACK) { - return id; - } - } - return null; - } - - private boolean isStrengthControlSupported() { - CameraManager cm = getCameraManager(); - if (cm == null) return false; - - try { - mCameraId = getCameraId(cm); - if (mCameraId != null) { - CameraCharacteristics c = cm.getCameraCharacteristics(mCameraId); - Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); - mDefaultLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL); - mMaxLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL); - // Use the same logic as the old implementation: mMaxLevel > 1 - if (flashAvailable && mMaxLevel > 1) { - return true; - } - } - } catch (CameraAccessException e) {} - return false; + @Override + public void onFlashlightStrengthChanged(int level) { + refreshState(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt index f1b4d494c9dd..630be4b4b9bc 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt @@ -16,15 +16,9 @@ package com.android.systemui.qs.tiles.dialog import android.content.Context -import android.hardware.camera2.CameraAccessException -import android.hardware.camera2.CameraCharacteristics -import android.hardware.camera2.CameraManager import android.os.Bundle -import android.os.UserHandle import android.os.VibrationEffect import android.os.Vibrator -import android.provider.Settings -import android.util.Log import android.widget.Button import android.widget.FrameLayout import android.view.ContextThemeWrapper @@ -49,13 +43,6 @@ class FlashlightDialogDelegate @Inject constructor( private lateinit var slider: Slider - private val cameraManager: CameraManager? - get() = context.getSystemService(Context.CAMERA_SERVICE) as? CameraManager - private var cameraId: String? = null - private var maxLevel: Int = 1 - private var defaultLevel: Int = 1 - private var currentPercent: Float = 1f - private val vibrator = context.getSystemService(Vibrator::class.java) private val flashlightMoveHaptic: VibrationEffect = VibrationEffect.get(VibrationEffect.EFFECT_TICK) @@ -87,20 +74,8 @@ class FlashlightDialogDelegate @Inject constructor( R.string.flashlight_strength_turn_on, { _, _ -> val newState = !flashlightController.isEnabled() - if (newState) { - val level = (currentPercent * maxLevel).toInt() - val safeLevel = maxOf(level, 1) - cameraId?.let { id -> - try { - cameraManager?.turnOnTorchWithStrengthLevel(id, safeLevel) - } catch (_: CameraAccessException) {} - } - } else { - flashlightController.setFlashlight(false) - } - + flashlightController.setFlashlight(newState) slider.isEnabled = newState - dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)?.text = if (newState) dialog.context.getString(R.string.flashlight_strength_turn_off) @@ -112,65 +87,32 @@ class FlashlightDialogDelegate @Inject constructor( } override fun onCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) { - val defaultPercent = defaultLevel.toFloat() / maxLevel.toFloat() - currentPercent = Settings.System.getFloatForUser( - dialog.context.contentResolver, - FLASHLIGHT_BRIGHTNESS_SETTING, - defaultPercent, - UserHandle.USER_CURRENT - ) + val maxLevel = flashlightController.getMaxLevel() + val currentPercent = flashlightController.getCurrentPercent() slider.isEnabled = flashlightController.isEnabled() slider.valueFrom = 1f slider.valueTo = 100f slider.value = (currentPercent * 100f).coerceAtLeast(1f) - slider.setLabelFormatter { value -> - value.toInt().toString() - } + slider.setLabelFormatter { value -> value.toInt().toString() } var last = -1 slider.addOnChangeListener { _, value, fromUser -> if (fromUser) { - val percent = (value / 100f) + val percent = value / 100f val p = Math.round(percent * 100) if (p != last) { vibrator?.vibrate(flashlightMoveHaptic) last = p } - currentPercent = percent - updateFlashlightStrength() + updateFlashlightStrength(percent, maxLevel) } } } @MainThread - private fun updateFlashlightStrength() { - Settings.System.putFloatForUser( - context.contentResolver, - FLASHLIGHT_BRIGHTNESS_SETTING, - currentPercent, - UserHandle.USER_CURRENT - ) - - if (cameraId != null) { - try { - val level = (currentPercent * maxLevel).toInt().coerceAtLeast(1) - cameraManager?.turnOnTorchWithStrengthLevel(cameraId!!, level) - } catch (e: CameraAccessException) { - Log.e(TAG, "Unable to set torch strength", e) - } - } - } - - fun setCameraInfo(camId: String?, maxLvl: Int, defLvl: Int): FlashlightDialogDelegate { - cameraId = camId - maxLevel = maxLvl - defaultLevel = defLvl - return this - } - - companion object { - private const val TAG = "FlashlightDialogDelegate" - private const val FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness" + private fun updateFlashlightStrength(percent: Float, maxLevel: Int) { + val level = (percent * maxLevel).toInt().coerceAtLeast(1) + flashlightController.setFlashlightStrengthLevel(level) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt index 2fd2486a47c3..b907efafbaa3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/flashlight/domain/interactor/FlashlightTileDataInteractor.kt @@ -70,6 +70,7 @@ constructor( else FlashlightModel.Unavailable.Temporarily.CameraInUse ) } + override fun onFlashlightStrengthChanged(level: Int) {} } flashlightController.addCallback(callback) awaitClose { flashlightController.removeCallback(callback) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java index dc1b27cfb1f2..a3746c7cf8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java @@ -25,6 +25,13 @@ public interface FlashlightController extends CallbackController mCameraId; private final AtomicBoolean mInitted = new AtomicBoolean(false); + private static final String FLASHLIGHT_BRIGHTNESS_SETTING = "flashlight_brightness"; + + @GuardedBy("this") + private int mDefaultLevel; + @GuardedBy("this") + private int mMaxLevel; + @GuardedBy("this") + private int mCurrentLevel; + @GuardedBy("this") + private boolean mStrengthControlSupported; + @Inject public FlashlightControllerImpl( + Context context, DumpManager dumpManager, CameraManager cameraManager, + @Main Handler mainHandler, @Background Executor bgExecutor, SecureSettings secureSettings, BroadcastSender broadcastSender, PackageManager packageManager ) { + mContext = context; mCameraManager = cameraManager; + mHandler = mainHandler; mExecutor = bgExecutor; mCameraId = new AtomicReference<>(null); mSecureSettings = secureSettings; @@ -110,7 +132,29 @@ private void init() { private void tryInitCamera() { if (!mHasFlashlight || mCameraId.get() != null) return; try { - mCameraId.set(getCameraId()); + String id = getCameraId(); + if (id != null) { + CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id); + Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + if (flashAvailable != null && flashAvailable) { + mDefaultLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_DEFAULT_LEVEL); + mMaxLevel = c.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL); + mStrengthControlSupported = (mMaxLevel > 1); + mCameraId.set(id); + + if (mStrengthControlSupported) { + float percent = Settings.System.getFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + (float) mDefaultLevel / mMaxLevel, + UserHandle.USER_CURRENT); + synchronized (this) { + mCurrentLevel = Math.max(1, Math.round(percent * mMaxLevel)); + } + if (DEBUG) Log.d(TAG, "Restored torch level: " + mCurrentLevel); + } + } + } } catch (Throwable e) { Log.e(TAG, "Couldn't initialize.", e); return; @@ -131,7 +175,20 @@ public void setFlashlight(boolean enabled) { synchronized (this) { if (mFlashlightEnabled != enabled) { try { - mCameraManager.setTorchMode(mCameraId.get(), enabled); + if (enabled) { + if (mStrengthControlSupported) { + int level = (mCurrentLevel > 0) ? mCurrentLevel : mDefaultLevel; + level = Math.max(1, Math.min(level, mMaxLevel)); + if (DEBUG) Log.d(TAG, "Turning on torch with level " + level); + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), level); + } else { + if (DEBUG) Log.d(TAG, "Turning on torch (no strength support)"); + mCameraManager.setTorchMode(mCameraId.get(), true); + } + } else { + if (DEBUG) Log.d(TAG, "Turning off torch"); + mCameraManager.setTorchMode(mCameraId.get(), false); + } } catch (CameraAccessException e) { Log.e(TAG, "Couldn't set torch mode", e); dispatchError(); @@ -153,6 +210,74 @@ public synchronized boolean isAvailable() { return mTorchAvailable; } + public boolean isStrengthControlSupported() { + return mStrengthControlSupported; + } + + public int getMaxLevel() { + return mMaxLevel; + } + + public int getDefaultLevel() { + return mDefaultLevel; + } + + public synchronized int getCurrentLevel() { + return mCurrentLevel; + } + + public synchronized float getCurrentPercent() { + if (!mStrengthControlSupported || mMaxLevel <= 0) { + return 0f; + } + return Math.max(0.01f, (float) mCurrentLevel / (float) mMaxLevel); + } + + public void setFlashlightStrengthLevel(int level) { + if (!mStrengthControlSupported) { + setFlashlight(level > 0); + return; + } + if (mCameraId.get() == null) { + mExecutor.execute(this::tryInitCamera); + } + + final int requestedLevel = level; + + mHandler.post(() -> { + if (mCameraId.get() == null) return; + + int clampedLevel; + synchronized (this) { + clampedLevel = Math.max(1, Math.min(requestedLevel, mMaxLevel)); + if (mCurrentLevel == clampedLevel) return; + mCurrentLevel = clampedLevel; + } + + try { + if (DEBUG) Log.d(TAG, "Setting torch strength level: " + clampedLevel); + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), clampedLevel); + + dispatchStrengthChanged(clampedLevel); + + mExecutor.execute(() -> { + float percent = Math.max(0.01f, (float) clampedLevel / mMaxLevel); + Settings.System.putFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + percent, + UserHandle.USER_CURRENT + ); + }); + + } catch (CameraAccessException e) { + Log.e(TAG, "Couldn't set torch strength level", e); + dispatchError(); + } + }); + } + + @Override public void addCallback(@NonNull FlashlightListener l) { synchronized (mListeners) { @@ -163,6 +288,7 @@ public void addCallback(@NonNull FlashlightListener l) { mListeners.add(new WeakReference<>(l)); l.onFlashlightAvailabilityChanged(isAvailable()); l.onFlashlightChanged(isEnabled()); + l.onFlashlightStrengthChanged(getCurrentLevel()); } } @@ -204,6 +330,17 @@ private void dispatchAvailabilityChanged(boolean available) { dispatchListeners(DISPATCH_AVAILABILITY_CHANGED, available); } + private void dispatchStrengthChanged(int level) { + synchronized (mListeners) { + for (WeakReference ref : mListeners) { + FlashlightListener l = ref.get(); + if (l != null) { + l.onFlashlightStrengthChanged(level); + } + } + } + } + private void dispatchListeners(int message, boolean argument) { synchronized (mListeners) { final ArrayList> copy = @@ -263,6 +400,24 @@ public void onTorchModeChanged(String cameraId, boolean enabled) { } } + @Override + @WorkerThread + public void onTorchStrengthLevelChanged(@NonNull String cameraId, int newStrengthLevel) { + if (!TextUtils.equals(cameraId, mCameraId.get())) return; + synchronized (FlashlightControllerImpl.this) { + if (mCurrentLevel == newStrengthLevel) return; + mCurrentLevel = newStrengthLevel; + } + float percent = Math.max(0.01f, (float) newStrengthLevel / (float) mMaxLevel); + Settings.System.putFloatForUser( + mContext.getContentResolver(), + FLASHLIGHT_BRIGHTNESS_SETTING, + percent, + UserHandle.USER_CURRENT); + if (DEBUG) Log.d(TAG, "Strength level changed: " + newStrengthLevel + "/" + mMaxLevel); + dispatchStrengthChanged(newStrengthLevel); + } + private void setCameraAvailable(boolean available) { boolean changed; synchronized (FlashlightControllerImpl.this) { From 46233c85abb82cd7e1764ab4cc901de9e30e127d Mon Sep 17 00:00:00 2001 From: Abhay Singh Gill Date: Fri, 19 Sep 2025 22:22:13 +0530 Subject: [PATCH 053/190] SystemUI: Allow long click on flashlight tile Would be useful for adjusting flashlight strength when the tile is small/ icon only. Signed-off-by: Abhay Singh Gill Signed-off-by: Ghosuto --- .../src/com/android/systemui/qs/tiles/FlashlightTile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java index ec7e200cf2ef..454c6768f8a8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java @@ -98,7 +98,7 @@ protected void handleDestroy() { @Override public BooleanState newTileState() { BooleanState state = new BooleanState(); - state.handlesLongClick = false; + state.handlesLongClick = true; return state; } From f8463634303ecc72196efb2ca9680c6b32dc4284 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 22 Dec 2025 07:28:38 +0000 Subject: [PATCH 054/190] SystemUI: Improve flashlight strength control UX Change-Id: I9227b6b5512d813eb7b2ceab5dee7e54e6b95f92 Signed-off-by: Ghosuto --- .../qs/tiles/dialog/FlashlightDialogDelegate.kt | 5 ++--- .../statusbar/policy/FlashlightControllerImpl.java | 12 +++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt index 630be4b4b9bc..c5c02cb3c663 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/FlashlightDialogDelegate.kt @@ -19,7 +19,6 @@ import android.content.Context import android.os.Bundle import android.os.VibrationEffect import android.os.Vibrator -import android.widget.Button import android.widget.FrameLayout import android.view.ContextThemeWrapper import android.view.Gravity @@ -75,7 +74,6 @@ class FlashlightDialogDelegate @Inject constructor( { _, _ -> val newState = !flashlightController.isEnabled() flashlightController.setFlashlight(newState) - slider.isEnabled = newState dialog.getButton(SystemUIDialog.BUTTON_NEUTRAL)?.text = if (newState) dialog.context.getString(R.string.flashlight_strength_turn_off) @@ -90,7 +88,7 @@ class FlashlightDialogDelegate @Inject constructor( val maxLevel = flashlightController.getMaxLevel() val currentPercent = flashlightController.getCurrentPercent() - slider.isEnabled = flashlightController.isEnabled() + slider.isEnabled = true slider.valueFrom = 1f slider.valueTo = 100f slider.value = (currentPercent * 100f).coerceAtLeast(1f) @@ -105,6 +103,7 @@ class FlashlightDialogDelegate @Inject constructor( vibrator?.vibrate(flashlightMoveHaptic) last = p } + updateFlashlightStrength(percent, maxLevel) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java index 0910d0c53764..752a4a1f1315 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java @@ -248,15 +248,21 @@ public void setFlashlightStrengthLevel(int level) { if (mCameraId.get() == null) return; int clampedLevel; + boolean wasEnabled; synchronized (this) { clampedLevel = Math.max(1, Math.min(requestedLevel, mMaxLevel)); - if (mCurrentLevel == clampedLevel) return; + if (mCurrentLevel == clampedLevel && !mFlashlightEnabled) return; mCurrentLevel = clampedLevel; + wasEnabled = mFlashlightEnabled; } try { - if (DEBUG) Log.d(TAG, "Setting torch strength level: " + clampedLevel); - mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), clampedLevel); + if (DEBUG) Log.d(TAG, "Setting torch strength level: " + clampedLevel + + ", wasEnabled: " + wasEnabled); + + if (wasEnabled) { + mCameraManager.turnOnTorchWithStrengthLevel(mCameraId.get(), clampedLevel); + } dispatchStrengthChanged(clampedLevel); From dc3a72420bf5e0690f3c56105a51eef93d34d743 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 11 Feb 2026 11:27:13 +0000 Subject: [PATCH 055/190] SystemUI: Fix redesigning media panel Play/Pause switch size Signed-off-by: Ghosuto --- packages/SystemUI/res/values/dimens.xml | 2 +- packages/SystemUI/res/xml/media_session_expanded.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index ef78257f7d17..44e637247433 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1389,7 +1389,7 @@ 7.7491dp 3.0996dp 12dp - 72dp + 60dp 18.5978dp 9.2989dp 1dp diff --git a/packages/SystemUI/res/xml/media_session_expanded.xml b/packages/SystemUI/res/xml/media_session_expanded.xml index 9d15fcc0493b..20f1248f8e08 100644 --- a/packages/SystemUI/res/xml/media_session_expanded.xml +++ b/packages/SystemUI/res/xml/media_session_expanded.xml @@ -121,7 +121,7 @@ Date: Tue, 20 Jan 2026 00:27:26 +0000 Subject: [PATCH 056/190] AconfigFlags: silence missing packages spam 01-19 10:21:03.147 1736 2037 E AconfigFlags: Failed to load aconfig package android.xr 01-19 10:21:03.147 1736 2037 E AconfigFlags: android.os.flagging.AconfigStorageReadException: ERROR_PACKAGE_NOT_FOUND: package android.xr cannot be found on the device 01-19 10:21:03.147 1736 2037 E AconfigFlags: at android.os.flagging.AconfigPackage.load(AconfigPackage.java:160) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.lambda$getFlagValueFromNewStorage$0(AconfigFlags.java:265) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags$$ExternalSyntheticLambda0.apply(D8$$SyntheticClass:0) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1713) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.getFlagValueFromNewStorage(AconfigFlags.java:263) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.getFlagValue(AconfigFlags.java:229) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.skip(AconfigFlags.java:343) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.skipCurrentElement(AconfigFlags.java:331) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.component.AconfigFlags.skipCurrentElement(AconfigFlags.java:299) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.parsing.ParsingPackageUtils.parseBaseApkTags(ParsingPackageUtils.java:1053) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.parsing.ParsingPackageUtils.parseBaseApk(ParsingPackageUtils.java:786) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.parsing.ParsingPackageUtils.parseBaseApk(ParsingPackageUtils.java:618) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.parsing.ParsingPackageUtils.parseMonolithicPackage(ParsingPackageUtils.java:454) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.pkg.parsing.ParsingPackageUtils.parsePackage(ParsingPackageUtils.java:352) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.pm.parsing.PackageParser2.parsePackage(PackageParser2.java:134) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.server.pm.ParallelPackageParser.parsePackage(go/retraceme c4881300f9a26cec87a4f45b7feae07c123ca539ee52fe36a528a87e7b5f9fc3:4) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.server.pm.ParallelPackageParser$$ExternalSyntheticLambda1.call(go/retraceme c4881300f9a26cec87a4f45b7feae07c123ca539ee52fe36a528a87e7b5f9fc3:47) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at java.util.concurrent.FutureTask.run(FutureTask.java:317) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1154) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:652) 01-19 10:21:03.147 1736 2037 E AconfigFlags: at com.android.internal.util.ConcurrentUtils$1$1.run(ConcurrentUtils.java:65) 01-19 10:21:03.147 1736 2037 E AconfigFlags: Failed to load aconfig package android.xr 01-19 10:21:03.147 1736 2037 E AconfigFlags: android.os.flagging.AconfigStorageReadException: ERROR_PACKAGE_NOT_FOUND: package android.xr cannot be found on the device Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- .../com/android/internal/pm/pkg/component/AconfigFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 7503fb1f9913..7f744b148f18 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -264,7 +264,7 @@ private Boolean getFlagValueFromNewStorage(String flagPackageAndName) { try { return AconfigPackage.load(p); } catch (Exception e) { - Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e); + //Slog.e(LOG_TAG, "Failed to load aconfig package " + p, e); return null; } }); From 3f7c53894a856d840ffbb1dc3cff5289f5d9220e Mon Sep 17 00:00:00 2001 From: Rve27 Date: Mon, 19 Jan 2026 14:18:22 +0000 Subject: [PATCH 057/190] base: Migrate to MaterialExpressiveTheme Change-Id: I7aaf57be5d62337f49a5dbbbfdb5ed0a4f187727 Signed-off-by: Rve27 Signed-off-by: Ghosuto --- .../settingslib/spa/framework/theme/SettingsTheme.kt | 8 +++++++- .../core/src/com/android/compose/theme/PlatformTheme.kt | 3 ++- .../systemui/bouncer/ui/composable/BouncerContent.kt | 7 ++++++- .../compose/animation/scene/SceneTransitionLayoutTest.kt | 3 ++- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index 03e8f7d2a2d8..41471e551d47 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -14,11 +14,16 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.settingslib.spa.framework.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MotionScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory @@ -30,8 +35,9 @@ import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory fun SettingsTheme(content: @Composable () -> Unit) { val isDarkTheme = isSystemInDarkTheme() - MaterialTheme( + MaterialExpressiveTheme( colorScheme = materialColorScheme(isDarkTheme), + motionScheme = MotionScheme.expressive(), typography = rememberSettingsTypography(), ) { CompositionLocalProvider( diff --git a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt index fcaf2c6972cd..59377bb1a06f 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/theme/PlatformTheme.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ColorScheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.dynamicDarkColorScheme @@ -74,7 +75,7 @@ fun PlatformTheme(isDarkTheme: Boolean = isSystemInDarkTheme(), content: @Compos } val windowSizeClass = calculateWindowSizeClass() - MaterialTheme( + MaterialExpressiveTheme( colorScheme = colorScheme, typography = typography, motionScheme = ExpressiveMotionScheme, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt index 000494199e08..4bd8d160ddc1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt @@ -14,6 +14,8 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.systemui.bouncer.ui.composable import android.app.AlertDialog @@ -57,8 +59,10 @@ import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.MotionScheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -1024,12 +1028,13 @@ private fun UserSwitcherDropdownMenu( val context = LocalContext.current // TODO(b/303071855): once the FR is fixed, remove this composition local override. - MaterialTheme( + MaterialExpressiveTheme( colorScheme = MaterialTheme.colorScheme.copy( surface = MaterialTheme.colorScheme.surfaceContainerHighest ), shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(28.dp)), + motionScheme = MotionScheme.expressive() ) { DropdownMenu( expanded = isExpanded, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index fc002f3db3a8..ba26a0994196 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -29,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.Text @@ -565,7 +566,7 @@ class SceneTransitionLayoutTest { } } - MaterialTheme(motionScheme = motionScheme2) { + MaterialExpressiveTheme(motionScheme = motionScheme2) { // Important: we should read this state inside the MaterialTheme composable. state2 = rememberMutableSceneTransitionLayoutState(initialScene = SceneA) SceneTransitionLayout(state2) { From 16eb9e0e6208c3bbe634d7aece45679fa98f92e3 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 11 Feb 2026 18:29:26 +0000 Subject: [PATCH 058/190] SystemUI: Use Surface Bright color for fallback tile also Signed-off-by: Ghosuto --- .../qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt index 8e8f34d880e9..e469783fabb8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CustomColorScheme.kt @@ -46,7 +46,7 @@ class CustomColorScheme(private val context: Context) { else com.android.internal.R.color.surface_effect_1 } else { - com.android.internal.R.color.surface_effect_2 + com.android.internal.R.color.materialColorSurfaceBright } val tileColor = context.resources.getColor(colorRes, context.theme) return Color(tileColor) From 63032da2ed567f35f90989168d0851bdb25ae633 Mon Sep 17 00:00:00 2001 From: SagarMakhar Date: Sat, 28 Aug 2021 14:15:37 +0000 Subject: [PATCH 059/190] base: Allow locking tasks to recents [1/2] Change-Id: Id18af94cf52db46054299a862b91c739e8d204f6 Signed-off-by: SagarMakhar Signed-off-by: Sipun Ku Mahanta Signed-off-by: Dmitrii Signed-off-by: Jis G Jacob Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 5 +++++ services/core/java/com/android/server/wm/RecentTasks.java | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index daf29386e803..c8acda78f10c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7336,6 +7336,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String FP_ERROR_VIBRATE = "fp_error_vibrate"; + /** + * @hide + */ + public static final String RECENTS_LOCKED_TASKS = "recents_locked_tasks"; + /** * Whether to show the carrier name on the lockscreen * @hide diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index a5e977d5a6c4..2e79cb56d6da 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -69,6 +69,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.os.UserHandle; +import android.provider.Settings; import android.text.TextUtils; import android.util.ArraySet; import android.util.IntArray; @@ -768,8 +769,15 @@ void removeTasksByPackageName(String packageName, int userId) { void removeAllVisibleTasks(int userId) { Set profileIds = getProfileIds(userId); + String lockedTasks = Settings.System.getStringForUser( + mService.mContext.getContentResolver(), + Settings.System.RECENTS_LOCKED_TASKS, + userId); for (int i = mTasks.size() - 1; i >= 0; --i) { final Task task = mTasks.get(i); + ComponentName cn = task.intent != null ? task.intent.getComponent() : null; + if (lockedTasks != null && !lockedTasks.isEmpty() && + cn != null && lockedTasks.contains(cn.getPackageName())) continue; if (!profileIds.contains(task.mUserId)) continue; if (isVisibleRecentTask(task)) { mTasks.remove(i); From d9be5aa665a0a9c0d4078f4ec0e8279eb761e3bb Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 12 Feb 2026 06:20:44 +0530 Subject: [PATCH 060/190] SystemUI: Fix edge light, media art, pulse, charging, now playing on ambient display * Insert overlay below the front scrim. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- packages/SystemUI/res/values/ids.xml | 3 + .../statusbar/phone/CentralSurfacesImpl.java | 74 ++++++++++++++++--- 2 files changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 751ee5f23bb0..84e84bd0cc79 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -308,4 +308,7 @@ + + + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index a1cb793751ea..c5aeb3cc1c7d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -83,11 +83,13 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.WindowInsets; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.widget.DateTimeView; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.lifecycle.Lifecycle; @@ -1153,17 +1155,67 @@ public void onHoldStatusBarOpenChange() { (requestTopUi, componentTag) -> mMainExecutor.execute( () -> mTopUiController.setRequestTopUi(requestTopUi, componentTag) ))); - getNotifContainerParentView().addView(mMediaViewController.getMediaArtScrim(), 0); - getNotifContainerParentView().addView(mPulseViewController.getPulseView(), 1); - getNotifContainerParentView().addView(mEdgeLightViewController.getEdgeLightView(), 2); - getNotifContainerParentView().addView(mNowPlayingViewController.getNowPlayingView(), 3); - getNotifContainerParentView().addView(mChargingAnimationViewController.getChargingView(), 4); - } - - private ViewGroup getNotifContainerParentView() { - ViewGroup rootView = (ViewGroup) getNotificationShadeWindowView().findViewById(R.id.scrim_behind).getParent(); - ViewGroup targetView = rootView.findViewById(R.id.notification_container_parent); - return targetView; + attachCustomOverlays(); + } + + private ViewGroup getScrimOverlayContainer() { + ViewGroup root = (ViewGroup) getNotificationShadeWindowView(); + + FrameLayout container = root.findViewById(R.id.custom_overlay_container); + if (container != null) { + return container; + } + + container = new FrameLayout(mContext); + container.setId(R.id.custom_overlay_container); + container.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + View scrimInFront = root.findViewById(R.id.scrim_in_front); + int scrimIndex = Math.max(root.indexOfChild(scrimInFront) - 3, 0); + root.addView(container, scrimIndex); + + return container; + } + + private void attachCustomOverlays() { + ViewGroup overlay = getScrimOverlayContainer(); + + detachFromParent(mMediaViewController.getMediaArtScrim()); + detachFromParent(mPulseViewController.getPulseView()); + detachFromParent(mEdgeLightViewController.getEdgeLightView()); + detachFromParent(mNowPlayingViewController.getNowPlayingView()); + detachFromParent(mChargingAnimationViewController.getChargingView()); + + overlay.addView(mMediaViewController.getMediaArtScrim(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mPulseViewController.getPulseView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mEdgeLightViewController.getEdgeLightView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mNowPlayingViewController.getNowPlayingView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + overlay.addView(mChargingAnimationViewController.getChargingView(), + new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + + private static void detachFromParent(View v) { + if (v == null) return; + final ViewParent p = v.getParent(); + if (p instanceof ViewGroup) { + ((ViewGroup) p).removeView(v); + } } @VisibleForTesting From 3044cb200aa1a845fdc472f531c5746a92c78f89 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 12 Feb 2026 10:51:48 +0000 Subject: [PATCH 061/190] SystemUI: Disable notification translucency when blur off Signed-off-by: Ghosuto --- .../WindowRootViewBlurRepository.kt | 111 +++++++++++------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 0e01ca1588b3..3eacec83124b 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.combine typealias BlurAppliedListener = Consumer @@ -106,58 +107,70 @@ constructor( .stateIn(scope, SharingStarted.WhileSubscribed(), false) override val isTranslucentSupported: StateFlow = - conflatedCallbackFlow { - val sendUpdate = { - trySendWithFailureLogging( - isTranslucentEnabled(), - TAG, - "unable to send notificationRowTransparency state change", - ) - } - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) = sendUpdate() + combine( + conflatedCallbackFlow { + val sendUpdate = { + trySendWithFailureLogging( + isTranslucentEnabledInSettings(), + TAG, + "unable to send notificationRowTransparency state change", + ) } - val resolver = context.contentResolver - resolver.registerContentObserver( - Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY), - true, - observer, - ) - sendUpdate() - awaitClose { resolver.unregisterContentObserver(observer) } + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) = sendUpdate() + } + val resolver = context.contentResolver + resolver.registerContentObserver( + Settings.Secure.getUriFor(Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY), + true, + observer, + ) + sendUpdate() + awaitClose { resolver.unregisterContentObserver(observer) } + }, + isBlurSupported + ) { settingEnabled, blurSupported -> + settingEnabled && blurSupported && isSystemBlurEnabled() } - .stateIn(scope, SharingStarted.WhileSubscribed(), isTranslucentEnabled()) + .stateIn(scope, SharingStarted.WhileSubscribed(), + isTranslucentEnabledInSettings() && isSystemBlurEnabled()) override val isLockscreenTranslucentSupported: StateFlow = - conflatedCallbackFlow { - val sendUpdate = { - trySendWithFailureLogging( - isLockscreenTranslucentEnabled(), - TAG, - "unable to send notificationRowTransparency lockscreen state change", - ) - } - val observer = - object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) = sendUpdate() + combine( + conflatedCallbackFlow { + val sendUpdate = { + trySendWithFailureLogging( + isLockscreenTranslucentEnabledInSettings(), + TAG, + "unable to send notificationRowTransparency lockscreen state change", + ) } - val resolver = context.contentResolver - resolver.registerContentObserver( - Settings.Secure.getUriFor( - Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, - ), - true, - observer, - ) - sendUpdate() - awaitClose { resolver.unregisterContentObserver(observer) } + val observer = + object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) = sendUpdate() + } + val resolver = context.contentResolver + resolver.registerContentObserver( + Settings.Secure.getUriFor( + Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, + ), + true, + observer, + ) + sendUpdate() + awaitClose { resolver.unregisterContentObserver(observer) } + }, + isBlurSupported + ) { settingEnabled, blurSupported -> + settingEnabled && blurSupported && isSystemBlurEnabled() } - .stateIn(scope, SharingStarted.WhileSubscribed(), isLockscreenTranslucentEnabled()) + .stateIn(scope, SharingStarted.WhileSubscribed(), + isLockscreenTranslucentEnabledInSettings() && isSystemBlurEnabled()) override var blurAppliedListener: BlurAppliedListener? = null - private fun isTranslucentEnabled(): Boolean = + private fun isTranslucentEnabledInSettings(): Boolean = Settings.Secure.getIntForUser( context.contentResolver, Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY, @@ -165,12 +178,22 @@ constructor( UserHandle.USER_CURRENT, ) == 1 - private fun isLockscreenTranslucentEnabled(): Boolean = + private fun isLockscreenTranslucentEnabledInSettings(): Boolean = Settings.Secure.getIntForUser( context.contentResolver, Settings.Secure.NOTIFICATION_ROW_TRANSPARENCY_LOCKSCREEN, 1, UserHandle.USER_CURRENT) == 1 + private fun isSystemBlurEnabled(): Boolean { + val blurEnabledByDefault = SystemProperties.getBoolean("ro.custom.blur.enable", false) + val blurEnabled = Settings.Global.getInt( + context.contentResolver, + Settings.Global.DISABLE_WINDOW_BLURS, + if (blurEnabledByDefault) 0 else 1 + ) != 1 + return blurEnabled + } + private fun isBlurAllowed(): Boolean { return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet() } From 08c1d27a0692343f6e8bbc5dc37ccdf722d5c062 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Wed, 27 Aug 2025 18:26:05 +0800 Subject: [PATCH 062/190] core: Add perf activity anim override fix for mtk and low-end devices perf regression caused by fixed dp translate and fade animation Change-Id: Ifee7edcc1fecca034007e9afd9a9cf61704aa869 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../view/animation/AnimationUtils.java | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/core/java/android/view/animation/AnimationUtils.java b/core/java/android/view/animation/AnimationUtils.java index 8ecd57179bad..1ea19e8e6804 100644 --- a/core/java/android/view/animation/AnimationUtils.java +++ b/core/java/android/view/animation/AnimationUtils.java @@ -30,13 +30,17 @@ import android.content.res.Resources.Theme; import android.content.res.XmlResourceParser; import android.os.SystemClock; +import android.os.SystemProperties; import android.ravenwood.annotation.RavenwoodIgnore; import android.ravenwood.annotation.RavenwoodKeepPartialClass; import android.util.AttributeSet; +import android.util.DisplayMetrics; import android.util.TimeUtils; import android.util.Xml; import android.view.InflateException; +import com.android.internal.R; + import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -226,6 +230,20 @@ public static long getExpectedPresentationTimeMillis() { public static Animation loadAnimation(Context context, @AnimRes int id) throws NotFoundException { + if (SystemProperties.getBoolean("persist.sys.activity_anim_perf_override", false)) { + ActivityAnimations.maybeInit(context); + switch (id) { + case R.anim.activity_open_enter: + return ActivityAnimations.getOpenEnter(); + case R.anim.activity_open_exit: + return ActivityAnimations.getOpenExit(); + case R.anim.activity_close_enter: + return ActivityAnimations.getCloseEnter(); + case R.anim.activity_close_exit: + return ActivityAnimations.getCloseExit(); + } + } + XmlResourceParser parser = null; try { parser = context.getResources().getAnimation(id); @@ -506,4 +524,100 @@ private static Interpolator createInterpolatorFromXml( } return interpolator; } + + /** @hide */ + public final class ActivityAnimations { + + private static Animation sOpenEnter; + private static Animation sOpenExit; + private static Animation sCloseEnter; + private static Animation sCloseExit; + + private static Interpolator sFastOutExtraSlowInInterpolator; + + private static final float DISTANCE = 0.1f; + + private ActivityAnimations() {} + + /** @hide */ + public static void maybeInit(Context context) { + if (sFastOutExtraSlowInInterpolator == null) { + sFastOutExtraSlowInInterpolator = AnimationUtils.loadInterpolator( + context, R.interpolator.fast_out_extra_slow_in); + } + } + + private static class ActivityAnimFactory { + private float fromX = 0f, toX = 0f; + private long duration = 200L; + + public ActivityAnimFactory fromX(float ratio) { + this.fromX = ratio; + return this; + } + + public ActivityAnimFactory toX(float ratio) { + this.toX = ratio; + return this; + } + + public Animation build() { + AnimationSet animationSet = new AnimationSet(false); + TranslateAnimation slide = new TranslateAnimation( + Animation.RELATIVE_TO_SELF, fromX, + Animation.RELATIVE_TO_SELF, toX, + Animation.RELATIVE_TO_SELF, 0f, + Animation.RELATIVE_TO_SELF, 0f + ); + slide.setDuration(duration); + slide.setInterpolator(sFastOutExtraSlowInInterpolator); + animationSet.addAnimation(slide); + return animationSet; + } + } + + /** @hide */ + public static Animation getOpenEnter() { + if (sOpenEnter == null) { + sOpenEnter = new ActivityAnimFactory() + .fromX(1.0f) + .toX(0.0f) + .build(); + } + return sOpenEnter; + } + + /** @hide */ + public static Animation getOpenExit() { + if (sOpenExit == null) { + sOpenExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(-DISTANCE) + .build(); + } + return sOpenExit; + } + + /** @hide */ + public static Animation getCloseEnter() { + if (sCloseEnter == null) { + sCloseEnter = new ActivityAnimFactory() + .fromX(-DISTANCE) + .toX(0.0f) + .build(); + } + return sCloseEnter; + } + + /** @hide */ + public static Animation getCloseExit() { + if (sCloseExit == null) { + sCloseExit = new ActivityAnimFactory() + .fromX(0.0f) + .toX(1.0f) + .build(); + } + return sCloseExit; + } + } } From 31df9fd0260ea7366b621d69d70041a29c80a1b7 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 21 Nov 2025 08:55:32 +0800 Subject: [PATCH 063/190] core: Preventing memory leaks from bloating os memory that leads to OOM thank you meta :) million dollar company btw 11-16 12:32:07.427 12602 12602 E ActivityThread: Activity com.facebook.messenger.neue.MainActivity has leaked IntentReceiver X.0Cl@f8f79db that was originally registered here. Are you missing a call to unregisterReceiver()? 11-16 12:32:07.427 12602 12602 E ActivityThread: android.app.IntentReceiverLeaked: Activity com.facebook.messenger.neue.MainActivity has leaked IntentReceiver X.0Cl@f8f79db that was originally registered here. Are you missing a call to unregisterReceiver()? 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.app.LoadedApk$ReceiverDispatcher.(LoadedApk.java:1879) 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.app.LoadedApk.getReceiverDispatcher(LoadedApk.java:1621) 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1912) 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.app.ContextImpl.registerReceiver(ContextImpl.java:1871) 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.app.ContextImpl.registerReceiver(ContextImpl.java:1858) 11-16 12:32:07.427 12602 12602 E ActivityThread: at android.content.ContextWrapper.registerReceiver(ContextWrapper.java:784) Change-Id: I4b529c2876c823dc21e4780e6beaa997f7be0425 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- core/java/android/os/StrictMode.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 12928596efc9..d631bea8158e 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -1559,6 +1559,8 @@ public static void initVmDefaults(ApplicationInfo ai) { if (targetSdkVersion >= Build.VERSION_CODES.N) { builder.detectFileUriExposure(); builder.penaltyDeathOnFileUriExposure(); + builder.detectActivityLeaks(); + builder.detectLeakedRegistrationObjects(); } if (Build.IS_USER || Build.IS_USERDEBUG || DISABLE || SystemProperties.getBoolean(DISABLE_PROPERTY, false)) { @@ -2655,7 +2657,9 @@ private static void clampViolationTimeMap(final @NonNull SparseLongArray violati /** @hide */ public static void onVmPolicyViolation(Violation originStack) { - onVmPolicyViolation(originStack, false); + boolean forceDeath = originStack instanceof IntentReceiverLeakedViolation + || originStack instanceof ServiceConnectionLeakedViolation; + onVmPolicyViolation(originStack, forceDeath); } /** @hide */ From 169a0e7b6ca040ed4aaf9cb702c9b09d70096296 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:51:42 +0800 Subject: [PATCH 064/190] media: disable noisy exifinterface logs Change-Id: I819dee676aca2199fa6d8cda93fd96a773e67971 Signed-off-by: Ghosuto --- media/java/android/media/ExifInterface.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/media/java/android/media/ExifInterface.java b/media/java/android/media/ExifInterface.java index 917640d99347..61ef04d1762b 100644 --- a/media/java/android/media/ExifInterface.java +++ b/media/java/android/media/ExifInterface.java @@ -101,7 +101,7 @@ */ public class ExifInterface { private static final String TAG = "ExifInterface"; - private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private static final boolean DEBUG = false; // The Exif tag names. See Tiff 6.0 Section 3 and Section 8. /** Type is String. */ @@ -2037,10 +2037,12 @@ private void loadAttributes(@NonNull InputStream in) { // Ignore exceptions in order to keep the compatibility with the old versions of // ExifInterface. mIsSupportedFile = false; - Log.d( - TAG, - "Invalid image: ExifInterface got an unsupported or corrupted image file", - e); + if (DEBUG) { + Log.d( + TAG, + "Invalid image: ExifInterface got an unsupported or corrupted image file", + e); + } } finally { addDefaultValuesForCompatibility(); From e900a4afeb59845e205119c6ce5c2558b2e6b197 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:53:46 +0800 Subject: [PATCH 065/190] core: add guard to null ResourcesImpl log spam Change-Id: Icd07fc5b5d64714b5aef10b1237f5ec5a3756462 Signed-off-by: Ghosuto --- core/java/android/app/ResourcesManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 523849e6914c..f2939324afaa 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -1782,7 +1782,7 @@ private void appendLibAssetsLocked(@NonNull SharedLibraryAssets libAssets) { final WeakReference weakImplRef = mResourceImpls.valueAt(i); final ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null; if (impl == null) { - Slog.w(TAG, "Found a null ResourcesImpl, skipped."); + if (DEBUG) Slog.w(TAG, "Found a null ResourcesImpl, skipped."); continue; } From 57462079ec5a2c8c5db1f6887eb67a2dda7be8a5 Mon Sep 17 00:00:00 2001 From: cevente Date: Thu, 12 Feb 2026 09:29:43 +0800 Subject: [PATCH 066/190] Update CE storage handling in StorageManagerService Allow proceeding if directory exists despite locked CE storage. Fixes Android/data folder creation on some apps dues to race condition. 02-12 06:42:13.214 2176 17668 W StorageManagerService: Failed to get storage lifetime 02-12 06:42:13.214 2176 17668 I StorageManagerService: Turn off gc_urgent based on checking lifetime and charge status 02-12 06:42:13.214 2176 17668 I StorageManagerService: Set smart idle maintenance: latest write amount: 244, average write amount: 0, min segment threshold: 512, dirty reclaim rate: 0.5, segment reclaim weight: 2.0, period(min): 60, min gc sleep time(ms): 5000, target dirty ratio: 100 02-12 06:51:02.679 18324 18339 W ContextImpl: at android.os.storage.IStorageManager$Stub$Proxy.mkdirs(IStorageManager.java:1305) 02-12 06:51:02.679 18324 18339 W ContextImpl: at android.os.storage.StorageManager.mkdirs(StorageManager.java:1421) 02-12 06:51:02.679 18324 18339 W ContextImpl: at com.android.server.StorageManagerService.mkdirs(StorageManagerService.java:3776) 02-12 06:51:02.679 18324 18339 W ContextImpl: at android.os.storage.IStorageManager$Stub.onTransact(IStorageManager.java:648) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../java/com/android/server/StorageManagerService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index a2e41ecdd001..7c005db76e79 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3779,9 +3779,14 @@ public void mkdirs(String callingPkg, String appPath) { final int userId = UserHandle.getUserId(callingUid); final String propertyName = "sys.user." + userId + ".ce_available"; - // Ignore requests to create directories while CE storage is locked if (!isCeStorageUnlocked(userId)) { - throw new IllegalStateException("Failed to prepare " + appPath); + // If the directory already exists, the hardware is clearly unlocked. + // We log a warning but allow the code to continue to the vold call. + if (new File(appPath).exists()) { + Slog.w(TAG, "Storage reported locked, but path exists. Proceeding for: " + appPath); + } else { + throw new IllegalStateException("Failed to prepare " + appPath); + } } // Ignore requests to create directories if CE storage is not available From d1524a96079692a0f95d5c9e39e09815951b4fed Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 13 Feb 2026 14:33:13 +0000 Subject: [PATCH 067/190] SystemUI: Redesigned qs tile style [1/2] Co-authored-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 15 ++ .../panels/ui/compose/QuickQuickSettings.kt | 25 +- .../ui/compose/infinitegrid/AxTileStyle.kt | 200 ++++++++++++++ .../ui/compose/infinitegrid/CommonTile.kt | 14 +- .../infinitegrid/InfiniteGridLayout.kt | 25 +- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 248 ++++++++++++++---- 6 files changed, 461 insertions(+), 66 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index c8acda78f10c..f4ff80dbb20b 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7428,6 +7428,21 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_SHAPE = "qs_tile_shape"; + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL = "qs_tile_style_minimal"; + + /** + * @hide + */ + public static final String QS_USE_MODIFIED_TILE_SPACING = "qs_use_modified_tile_spacing"; + + /** + * @hide + */ + public static final String QS_TILE_STYLE_MINIMAL_INVERT = "qs_tile_style_minimal_invert"; + /** * Customize Brightness slider shape. * @hide diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 84869c71eac5..ab97fe28dc51 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -16,7 +16,9 @@ package com.android.systemui.qs.panels.ui.compose +import android.provider.Settings import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -24,6 +26,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -31,6 +34,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.grid.ui.compose.VerticalSpannedGrid import com.android.systemui.qs.composefragment.ui.GridAnchor import com.android.systemui.qs.flags.QSMaterialExpressiveTiles +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults import com.android.systemui.qs.panels.ui.compose.infinitegrid.Tile import com.android.systemui.qs.panels.ui.viewmodel.BounceableTileViewModel import com.android.systemui.qs.panels.ui.viewmodel.QuickQuickSettingsViewModel @@ -48,6 +52,11 @@ fun ContentScope.QuickQuickSettings( val tiles = sizedTiles.fastMap { it.tile } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() val scope = rememberCoroutineScope() + + val context = androidx.compose.ui.platform.LocalContext.current + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } Box(modifier = modifier) { GridAnchor() @@ -80,12 +89,22 @@ fun ContentScope.QuickQuickSettings( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, - modifier = Modifier.sysuiResTag("qqs_tile_layout"), + modifier = Modifier.sysuiResTag("qqs_tile_layout") + .then(if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier), keys = { sizedTiles[it].tile.spec }, ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt new file mode 100644 index 000000000000..60b000d369fd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/AxTileStyle.kt @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2025-2026 AxionOS + * + * 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.qs.panels.ui.compose.infinitegrid + +import android.content.Context +import android.graphics.drawable.Drawable +import android.service.quicksettings.Tile.STATE_ACTIVE +import android.service.quicksettings.Tile.STATE_INACTIVE +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.thenIf +import com.android.systemui.Flags +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabelSettings +import com.android.systemui.qs.panels.ui.viewmodel.AccessibilityUiState +import com.android.systemui.qs.ui.compose.borderOnFocus + +object AxTileDefaults { + val TileCornerRadius = 50.dp + val ActiveTileCornerRadius = 26.dp + + val LargeIconSize = 24.dp + val DividerWidth = 1.dp + val DividerHeight = 16.dp + val IconDividerSpacing = 12.dp + val DividerLabelSpacing = 16.dp + val LargeTileStartPadding = 24.dp + val LargeTileEndPadding = 16.dp + + @Composable + fun dividerColor(state: Int): Color { + return when (state) { + STATE_ACTIVE -> MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.2f) + STATE_INACTIVE -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.15f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f) + } + } + + @Composable + fun animateTileShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = if (invertRadius) TileCornerRadius else ActiveTileCornerRadius, + inactiveCornerRadius = if (invertRadius) ActiveTileCornerRadius else TileCornerRadius, + label = "AxTileCornerRadius", + ) + + @Composable + fun animateIconShapeAsState( + state: Int, + invertRadius: Boolean = false, + ): androidx.compose.runtime.State = animateAxShape( + state = state, + activeCornerRadius = TileCornerRadius, + inactiveCornerRadius = TileCornerRadius, + label = "AxIconCornerRadius", + ) + + @Composable + private fun animateAxShape( + state: Int, + activeCornerRadius: Dp, + inactiveCornerRadius: Dp, + label: String, + ): State { + val radius by animateDpAsState( + targetValue = if (state == STATE_ACTIVE) activeCornerRadius else inactiveCornerRadius, + label = label, + ) + return remember { + mutableStateOf( + RoundedCornerShape( + object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float = + with(density) { radius.toPx() } + } + ) + ) + } + } +} + +@Composable +fun AxLargeTileContent( + label: String, + secondaryLabel: String?, + iconProvider: Context.() -> Icon, + sideDrawable: Drawable?, + colors: TileColors, + squishiness: () -> Float, + tileState: Int, + modifier: Modifier = Modifier, + isVisible: () -> Boolean = { true }, + accessibilityUiState: AccessibilityUiState? = null, + iconShape: RoundedCornerShape = RoundedCornerShape(CommonTileDefaults.InactiveCornerRadius), + textScale: () -> Float = { 1f }, + toggleClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, +) { + val isDualTarget = toggleClick != null + val dividerColor = AxTileDefaults.dividerColor(tileState) + val focusBorderColor = MaterialTheme.colorScheme.secondary + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier.padding( + start = AxTileDefaults.LargeTileStartPadding, + end = AxTileDefaults.LargeTileEndPadding, + ), + ) { + val longPressLabel = longPressLabelSettings().takeIf { onLongClick != null } + + Box( + modifier = Modifier + .fillMaxHeight() + .thenIf(isDualTarget) { + Modifier + .borderOnFocus(color = focusBorderColor, iconShape.topEnd) + .combinedClickable( + onClick = toggleClick!!, + onLongClick = onLongClick, + onLongClickLabel = longPressLabel, + hapticFeedbackEnabled = !Flags.msdlFeedback(), + ) + }, + contentAlignment = Alignment.Center, + ) { + SmallTileContent( + iconProvider = iconProvider, + color = colors.icon, + size = { AxTileDefaults.LargeIconSize }, + modifier = Modifier, + ) + } + + if (isDualTarget) { + Spacer(modifier = Modifier.width(AxTileDefaults.IconDividerSpacing)) + Box( + modifier = Modifier + .width(AxTileDefaults.DividerWidth) + .height(AxTileDefaults.DividerHeight) + .background(dividerColor) + ) + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } else { + Spacer(modifier = Modifier.width(AxTileDefaults.DividerLabelSpacing)) + } + + LargeTileLabels( + label = label, + secondaryLabel = secondaryLabel, + colors = colors, + accessibilityUiState = accessibilityUiState, + isVisible = isVisible, + modifier = Modifier + .weight(1f) + .bounceScale(TransformOrigin(0f, .5f), textScale), + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index c58093da862a..d25fc33befc3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -411,19 +411,27 @@ object TileBounceMotionTestKeys { } object CommonTileDefaults { - val IconSize = 32.dp - val LargeTileIconSize = 28.dp + val IconSize = 24.dp + val LargeTileIconSize = 24.dp + val SideIconWidth = 32.dp val SideIconHeight = 20.dp val ChevronSize = 14.dp val ToggleTargetSize = 56.dp + val TileHeight = 72.dp + val TileStartPadding = 8.dp val TileEndPadding = 12.dp val TileDualTargetEndPadding = 8.dp + val TileArrangementPadding = 6.dp + val TileColumnSpacing = 20.dp + val TileRowSpacing = 20.dp + + val InactiveCornerRadius = 36.dp val ActiveTileCornerRadius = 24.dp - val InactiveCornerRadius = 50.dp + val TileLabelBlurWidth = 32.dp const val TILE_MARQUEE_ITERATIONS = 1 const val TILE_INITIAL_DELAY_MILLIS = 2000 diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 734377e8d216..52ad368b4118 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -16,6 +16,8 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid +import android.provider.Settings +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -28,6 +30,7 @@ import androidx.compose.runtime.toMutableStateList import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastMap import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope @@ -83,11 +86,14 @@ constructor( rememberViewModel(traceName = "InfiniteGridLayout.TileGrid", key = context) { textFeedbackContentViewModelFactory.create(context) } + + val useModifiedSpacing = remember { + Settings.System.getInt(context.contentResolver, Settings.System.QS_USE_MODIFIED_TILE_SPACING, 0) == 1 + } val columns = viewModel.columnsWithMediaViewModel.columns val largeTilesSpan = viewModel.columnsWithMediaViewModel.largeSpan val largeTiles by viewModel.iconTilesViewModel.largeTilesState - // Tiles or largeTiles may be updated while this is composed, so listen to any changes val sizedTiles = remember(tiles, largeTiles, largeTilesSpan) { tiles.map { @@ -124,13 +130,24 @@ constructor( val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val spans by remember(sizedTiles) { derivedStateOf { sizedTiles.fastMap { it.width } } } + VerticalSpannedGrid( columns = columns, - columnSpacing = dimensionResource(R.dimen.qs_tile_margin_horizontal), - rowSpacing = dimensionResource(R.dimen.qs_tile_margin_vertical), + columnSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileColumnSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_horizontal) + }, + rowSpacing = if (useModifiedSpacing) { + CommonTileDefaults.TileRowSpacing + } else { + dimensionResource(R.dimen.qs_tile_margin_vertical) + }, spans = spans, keys = { sizedTiles[it].tile.spec }, - modifier = modifier, + modifier = modifier.then( + if (useModifiedSpacing) Modifier.padding(horizontal = 10.dp) else Modifier + ), ) { spanIndex, column, isFirstInColumn, isLastInColumn -> val it = sizedTiles[spanIndex] diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index d30e2a3e2283..96ba63d1b902 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -205,14 +205,16 @@ fun ContentScope.Tile( tile.state.collect { value = it.toIconProvider() } } - val colors = TileDefaults.getColorForState(uiState, iconOnly) + val useMinimalStyle = rememberAxTileStyle() + + val colors = TileDefaults.getColorForState(uiState, iconOnly, forceMonochrome = useMinimalStyle) val hapticsViewModel: TileHapticsViewModel? = if (rememberTileHaptic()) { rememberViewModel(traceName = "TileHapticsViewModel") { tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) } } else { - null + null } if (tile.spec.spec == "sound" && !iconOnly) { @@ -220,11 +222,18 @@ fun ContentScope.Tile( return@trace } - val shapeMode = rememberTileShapeMode() - val wantCircle = shapeMode == 4 && iconOnly + val shapeMode = if (useMinimalStyle) 0 else rememberTileShapeMode() + val wantCircle = !useMinimalStyle && shapeMode == 4 && iconOnly val tileShape = - if (wantCircle) CircleShape - else TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + if (wantCircle) { + CircleShape + } else if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + AxTileDefaults.animateTileShapeAsState(uiState.state, useMinimalInvert).value + } else { + TileDefaults.animateTileShapeAsState(uiState.state, shapeMode).value + } + val animatedColor by animateColorAsState(colors.background, label = "QSTileBackgroundColor") val isDualTarget = uiState.handlesSecondaryClick @@ -254,8 +263,8 @@ fun ContentScope.Tile( modifier = modifier .then(surfaceRevealModifier) - .thenIf(!wantCircle) { - modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) + .thenIf(!wantCircle) { + Modifier.borderOnFocus(color = focusBorderColor, outerShape.topEnd) } .fillMaxWidth() .height(CommonTileDefaults.TileHeight) @@ -331,6 +340,7 @@ fun ContentScope.Tile( requestToggleTextFeedback(tile.spec) } } + if (wantCircle) { val interaction = remember { MutableInteractionSource() } @@ -384,7 +394,6 @@ fun ContentScope.Tile( }, ) } else { - val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) val secondaryClick: (() -> Unit)? = { hapticsViewModel?.setTileInteractionState( @@ -393,22 +402,43 @@ fun ContentScope.Tile( tile.toggleClick() } .takeIf { isDualTarget } - LargeTileContent( - label = uiState.label, - secondaryLabel = uiState.secondaryLabel, - iconProvider = iconProvider, - sideDrawable = uiState.sideDrawable, - colors = colors, - iconShape = iconShape, - toggleClick = secondaryClick, - onLongClick = longClick, - accessibilityUiState = uiState.accessibilityUiState, - squishiness = squishiness, - isVisible = isVisible, - textScale = { contentBounceable.textBounceScale }, - modifier = - Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), - ) + if (useMinimalStyle) { + val useMinimalInvert = rememberAxMinimalInvert() + val iconShape by AxTileDefaults.animateIconShapeAsState(uiState.state, useMinimalInvert) + AxLargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + tileState = uiState.state, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + ) + } else { + val iconShape by TileDefaults.animateIconShapeAsState(uiState.state, shapeMode) + LargeTileContent( + label = uiState.label, + secondaryLabel = uiState.secondaryLabel, + iconProvider = iconProvider, + sideDrawable = uiState.sideDrawable, + colors = colors, + iconShape = iconShape, + toggleClick = secondaryClick, + onLongClick = longClick, + accessibilityUiState = uiState.accessibilityUiState, + squishiness = squishiness, + isVisible = isVisible, + textScale = { contentBounceable.textBounceScale }, + modifier = + Modifier.largeTilePadding(isDualTarget = uiState.handlesLongClick), + ) + } } } } @@ -550,6 +580,46 @@ data class TileColors( val icon: Color, ) +@Composable +fun rememberAxTileStyle(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readAxStyle(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var axStyle by remember { mutableStateOf(readAxStyle()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + axStyle = readAxStyle() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return axStyle +} + @Composable fun rememberTileShapeMode(): Int { val context = LocalContext.current @@ -590,6 +660,46 @@ fun rememberTileShapeMode(): Int { return shapeMode } +@Composable +fun rememberAxMinimalInvert(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readMinimalInvert(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_STYLE_MINIMAL_INVERT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var minimalInvert by remember { mutableStateOf(readMinimalInvert()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + minimalInvert = readMinimalInvert() + } + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_STYLE_MINIMAL_INVERT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return minimalInvert +} + @Composable fun rememberTileHaptic(): Boolean { val context = LocalContext.current @@ -651,7 +761,7 @@ private object TileDefaults { fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + return if (isSingleToneStyle) { TileColors( background = MaterialTheme.colorScheme.primary, @@ -676,7 +786,7 @@ private object TileDefaults { fun inactiveDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - + return if (isSingleToneStyle) { TileColors( background = CustomColorScheme.current.qsTileColor, @@ -723,11 +833,38 @@ private object TileDefaults { @Composable @ReadOnlyComposable - fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors { + fun activeDualTargetMonochromeTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.primary, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onPrimary, + secondaryLabel = MaterialTheme.colorScheme.onPrimary, + icon = MaterialTheme.colorScheme.onPrimary, + ) + + @Composable + @ReadOnlyComposable + fun inactiveDualTargetMonochromeTileColors(): TileColors = + TileColors( + background = CustomColorScheme.current.qsTileColor, + iconBackground = Color.Transparent, + label = MaterialTheme.colorScheme.onSurface, + secondaryLabel = MaterialTheme.colorScheme.onSurface, + icon = MaterialTheme.colorScheme.onSurface, + ) + + @Composable + @ReadOnlyComposable + fun getColorForState( + uiState: TileUiState, + iconOnly: Boolean, + forceMonochrome: Boolean = false, + ): TileColors { return when (uiState.state) { STATE_ACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - activeDualTargetTileColors() + if (forceMonochrome) activeDualTargetMonochromeTileColors() + else activeDualTargetTileColors() } else { activeTileColors() } @@ -735,12 +872,15 @@ private object TileDefaults { STATE_INACTIVE -> { if (uiState.handlesSecondaryClick && !iconOnly) { - inactiveDualTargetTileColors() + if (forceMonochrome) inactiveDualTargetMonochromeTileColors() + else inactiveDualTargetTileColors() } else { inactiveTileColors() } } + STATE_UNAVAILABLE -> unavailableTileColors() + else -> unavailableTileColors() } } @@ -772,25 +912,23 @@ private object TileDefaults { label: String, shapeMode: Int, ): State { - val animatedCornerRadius by - animateDpAsState( - targetValue = when (shapeMode) { - 1 -> InactiveCornerRadius // Circle-ish - 2 -> activeCornerRadius // Rounded Square - 3 -> 0.dp // Square - 4 -> InactiveCornerRadius // Circle - else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius - }, - label = label, - ) + val animatedCornerRadius by animateDpAsState( + targetValue = when (shapeMode) { + 1 -> InactiveCornerRadius // Circle-ish + 2 -> activeCornerRadius // Rounded Square + 3 -> 0.dp // Square + 4 -> InactiveCornerRadius // Circle + else -> if (state == STATE_ACTIVE) activeCornerRadius else InactiveCornerRadius + }, + label = label, + ) return remember { - val corner = - object : CornerSize { - override fun toPx(shapeSize: Size, density: Density): Float { - return with(density) { animatedCornerRadius.toPx() } - } + val corner = object : CornerSize { + override fun toPx(shapeSize: Size, density: Density): Float { + return with(density) { animatedCornerRadius.toPx() } } + } mutableStateOf(RoundedCornerShape(corner)) } } @@ -819,25 +957,23 @@ enum class DualTargetTileStyle { } object DualTargetTileStyleProvider { - + fun getStyle(context: android.content.Context): DualTargetTileStyle { val value = Settings.System.getInt( context.contentResolver, Settings.System.DUAL_TARGET_TILE_STYLE, 0 ) - + return when (value) { 1 -> DualTargetTileStyle.SINGLE else -> DualTargetTileStyle.DUAL } } - - fun isSingleToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.SINGLE - } - - fun isDualToneStyle(context: android.content.Context): Boolean { - return getStyle(context) == DualTargetTileStyle.DUAL - } + + fun isSingleToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.SINGLE + + fun isDualToneStyle(context: android.content.Context): Boolean = + getStyle(context) == DualTargetTileStyle.DUAL } From c92f9f1cfea80b6b29cd752d4dddf7c1f7b7eca4 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 13 Feb 2026 15:49:05 +0000 Subject: [PATCH 068/190] SystemUI: Redesigned brightness slider style [1/2] Co-authored-by: Ghosuto Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 5 + .../brightness/ui/compose/BrightnessSlider.kt | 169 ++++++++++++++---- 2 files changed, 144 insertions(+), 30 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index f4ff80dbb20b..ebdd60a11ae8 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7449,6 +7449,11 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_BRIGHTNESS_SLIDER_SHAPE = "qs_brightness_slider_shape"; + /** + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_STYLE = "qs_brightness_slider_style"; + /** * Haptic feedback on QS tiles * @hide diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index 1ba29a25f010..cf5d355252a2 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -34,6 +34,7 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -83,6 +84,7 @@ import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.ColorPainter import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInteropFilter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -99,6 +101,8 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.compose.PlatformSlider +import com.android.compose.PlatformSliderDefaults import com.android.compose.modifiers.padding import com.android.compose.modifiers.sliderPercentage import com.android.compose.modifiers.thenIf @@ -156,6 +160,8 @@ fun BrightnessSlider( val cr = context.contentResolver var hapticsEnabled by remember { mutableStateOf(readEnableHaptics(cr)) } + var useAxStyle by remember { mutableStateOf(readUseAxStyle(cr)) } + var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } val shapeMode = rememberSliderShapeMode() val trackCornerDp: Dp = when (shapeMode) { @@ -173,6 +179,7 @@ fun BrightnessSlider( val enabled = !isRestricted val contentDescription = stringResource(R.string.accessibility_brightness) val interactionSource = remember { MutableInteractionSource() } + val hapticsViewModel: SliderHapticsViewModel? = if (hapticsEnabled) { rememberViewModel(traceName = "SliderHapticsViewModel") { @@ -189,6 +196,128 @@ fun BrightnessSlider( } else { null } + + val hasAutoBrightness = context.resources.getBoolean( + com.android.internal.R.bool.config_automatic_brightness_available + ) + + DisposableEffect(Unit) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + context.mainExecutor.execute { + hapticsEnabled = readEnableHaptics(cr) + useAxStyle = readUseAxStyle(cr) + showAutoBrightness = readShowAutoBrightness(cr) + } + } + } + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_STYLE), + false, observer, UserHandle.USER_ALL + ) + + cr.registerContentObserver( + LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + cr.unregisterContentObserver(observer) + } + } + + if (useAxStyle) { + val iconRes by + remember(gammaValue, valueRange) { + derivedStateOf { + val percentage = + (value - valueRange.first) * 100f / (valueRange.last - valueRange.first) + iconResProvider(percentage) + } + } + val axIconRes = if (autoMode) R.drawable.ic_qs_brightness_auto_on else iconRes + val axIconSize = 56.dp + val iconTapScope = rememberCoroutineScope() + val sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors().copy( + trackColor = CustomColorScheme.current.qsTileColor, + ) + + Box(modifier = modifier) { + PlatformSlider( + value = animatedValue, + onValueChange = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChange(it) + value = it.toInt() + onDrag(value) + } + }, + onValueChangeFinished = { + if (enabled && !overriddenByAppState) { + hapticsViewModel?.onValueChangeEnded() + onStop(value) + } + }, + valueRange = floatValueRange, + enabled = enabled, + interactionSource = interactionSource, + colors = sliderColors, + modifier = Modifier + .fillMaxWidth() + .height(axIconSize) + .sysuiResTag("slider") + .semantics(mergeDescendants = true) { + this.text = AnnotatedString(contentDescription) + } + .sliderPercentage { + (value - valueRange.first).toFloat() / (valueRange.last - valueRange.first) + } + .thenIf(isRestricted) { + Modifier.clickable { + if (restriction is PolicyRestriction.Restricted) { + onRestrictedClick(restriction) + } + } + }, + icon = { _ -> + Icon( + painter = painterResource(axIconRes), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + }, + ) + + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .size(axIconSize) + .clip(CircleShape) + .pointerInput(Unit) { + detectTapGestures { + iconTapScope.launch { onIconClick() } + } + } + ) + } + + val currentShowToast by rememberUpdatedState(showToast) + LaunchedEffect(interactionSource, overriddenByAppState) { + interactionSource.interactions.collect { interaction -> + if (interaction is DragInteraction.Start && overriddenByAppState) { + currentShowToast() + } + } + } + return + } + val colors = colors() // The value state is recreated every time gammaValue changes, so we recreate this derivedState @@ -236,36 +365,6 @@ fun BrightnessSlider( } } - val hasAutoBrightness = context.resources.getBoolean( - com.android.internal.R.bool.config_automatic_brightness_available - ) - var showAutoBrightness by remember { mutableStateOf(readShowAutoBrightness(cr)) } - - DisposableEffect(Unit) { - val observer = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - context.mainExecutor.execute { - showAutoBrightness = readShowAutoBrightness(cr) - hapticsEnabled = readEnableHaptics(cr) - } - } - } - - cr.registerContentObserver( - LineageSettings.Secure.getUriFor(LineageSettings.Secure.QS_SHOW_AUTO_BRIGHTNESS), - false, observer, UserHandle.USER_ALL - ) - - cr.registerContentObserver( - Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_HAPTIC), - false, observer, UserHandle.USER_ALL - ) - - onDispose { - cr.unregisterContentObserver(observer) - } - } - Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -499,6 +598,16 @@ private fun readEnableHaptics(cr: ContentResolver): Boolean = false } +private fun readUseAxStyle(cr: ContentResolver): Boolean = + try { + Settings.System.getIntForUser( + cr, Settings.System.QS_BRIGHTNESS_SLIDER_STYLE, + 0, UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + @Composable private fun drawAutoBrightnessButton( autoMode: Boolean, From 2b7e03c09e4cc55527e3b75e7f1cfbaacf8c49b9 Mon Sep 17 00:00:00 2001 From: Wu Shaokang Date: Thu, 5 Feb 2026 17:04:02 +0800 Subject: [PATCH 069/190] Fix NavBar haptic feedback after Bluetooth device disconnect After disconnecting a Bluetooth controller or trackpad, the virtual key haptic feedback for the navigation bar becomes disabled. This causes subsequent presses of the Back and Home buttons to provide no haptic feedback to the user. The issue occurs because when the last trackpad is removed from mTrackpadsConnected, the update() method is called but the state change callback is not triggered, leaving the navigation bar's haptic feedback disabled. This fix ensures that mStateChangeCallback.run() is invoked after update() when the trackpad list becomes empty, properly re-enabling haptic feedback for navigation bar buttons. Test: Connect/disconnect Bluetooth controller, verify navigation bar haptic feedback works after disconnect Change-Id: Icf6e7526f7a06784e0e4d63b85c0879e36c09bc5 Signed-off-by: Ghosuto --- .../navigationbar/gestural/EdgeBackGestureHandler.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index d3e681c8b7f3..b92c812a19d8 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -458,6 +458,9 @@ public void onInputDeviceRemoved(int deviceId) { mTrackpadsConnected.remove(deviceId); if (mTrackpadsConnected.isEmpty()) { update(); + if (mStateChangeCallback != null) { + mStateChangeCallback.run(); + } } }); } From 62eace894e7ce66491d3b2f0ceb8e775652e9f84 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 13 Feb 2026 19:15:28 +0000 Subject: [PATCH 070/190] SystemUI: Simplify WiFi and Mobile Data tiles - Dialog is usless Signed-off-by: Ghosuto --- .../src/com/android/systemui/qs/tiles/MobileDataTile.kt | 8 ++++---- .../src/com/android/systemui/qs/tiles/WifiTile.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt index b9cf3eb8c5e8..fb39db11c5d7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/MobileDataTile.kt @@ -153,11 +153,12 @@ constructor( } override fun handleClick(expandable: Expandable?) { - lifecycle.coroutineScope.launch { userActionInteractor.handleClick(expandable) } + userActionInteractor.handleSecondaryClick(expandable) } override fun handleSecondaryClick(expandable: Expandable?) { - userActionInteractor.handleSecondaryClick(expandable) + // Disabled + handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -200,8 +201,7 @@ constructor( contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName - handlesSecondaryClick = - tileState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK) + handlesSecondaryClick = false handlesLongClick = tileState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt index 6236dc90b790..227091bb3cd6 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt @@ -149,11 +149,12 @@ constructor( } override fun handleClick(expandable: Expandable?) { - lifecycle.coroutineScope.launch { userActionInteractor.handleClick(expandable) } + userActionInteractor.handleSecondaryClick(expandable) } override fun handleSecondaryClick(expandable: Expandable?) { - userActionInteractor.handleSecondaryClick(expandable) + // Disabled + handleClick(expandable) } override fun getLongClickIntent(): Intent = userActionInteractor.longClickIntent @@ -187,8 +188,7 @@ constructor( contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName - handlesSecondaryClick = - tileState.supportedActions.contains(QSTileState.UserAction.TOGGLE_CLICK) + handlesSecondaryClick = false handlesLongClick = tileState.supportedActions.contains(QSTileState.UserAction.LONG_CLICK) } From 92eeb37705e977979b3c4cf5e91e1ed4731f1272 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 29 Sep 2025 04:59:02 +0000 Subject: [PATCH 071/190] SystemUI: Fade the clock insted of scale effect Change-Id: Ia19d47cc9a29792d26f6610a75c41c09b9b67abf Signed-off-by: Ghosuto --- packages/SystemUI/res/xml/combined_qs_header_scene.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/SystemUI/res/xml/combined_qs_header_scene.xml b/packages/SystemUI/res/xml/combined_qs_header_scene.xml index c16725682a82..b7ca14c16641 100644 --- a/packages/SystemUI/res/xml/combined_qs_header_scene.xml +++ b/packages/SystemUI/res/xml/combined_qs_header_scene.xml @@ -52,6 +52,16 @@ app:framePosition="@integer/fade_in_start_frame" android:alpha="0" /> + + Date: Thu, 23 Oct 2025 01:53:41 +0530 Subject: [PATCH 072/190] SystemUI: Fix imageloader spam in BrightnessSliderViewModel * This also fixes brightness icons not loading correctly on moving slider. * Image decoder cannot handle vector XML. Log: 10-23 00:02:52.472 W/ImageLoader(2485): Failed to load source Resource{name=com.android.systemui:drawable/ic_brightness_medium} 10-23 00:02:52.472 W/ImageLoader(2485): android.graphics.ImageDecoder$DecodeException: Failed to create image decoder with message 'unimplemented'Input contained an error. 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder.nCreate(Native Method) 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder.createFromAsset(ImageDecoder.java:548) 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder.-$$Nest$smcreateFromAsset(Unknown Source:0) 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder$ResourceSource.createImageDecoder(ImageDecoder.java:525) 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder.decodeDrawableImpl(ImageDecoder.java:1750) 10-23 00:02:52.472 W/ImageLoader(2485): at android.graphics.ImageDecoder.decodeDrawable(ImageDecoder.java:1742) 10-23 00:02:52.472 W/ImageLoader(2485): at com.android.systemui.graphics.ImageLoader.loadDrawableSync(go/retraceme fa033e5bae99b59d8477124fa92133edcdefa6c9ebb8507171a3414e8dc9df78:3) 10-23 00:02:52.472 W/ImageLoader(2485): at com.android.systemui.graphics.ImageLoader.loadDrawableSync(go/retraceme fa033e5bae99b59d8477124fa92133edcdefa6c9ebb8507171a3414e8dc9df78:19) 10-23 00:02:52.472 W/ImageLoader(2485): at com.android.systemui.graphics.ImageLoader$loadDrawable$4.invokeSuspend(go/retraceme fa033e5bae99b59d8477124fa92133edcdefa6c9ebb8507171a3414e8dc9df78:25) 10-23 00:02:52.472 W/ImageLoader(2485): at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(go/retraceme fa033e5bae99b59d8477124fa92133edcdefa6c9ebb8507171a3414e8dc9df78:8) 10-23 00:02:52.472 W/ImageLoader(2485): at kotlinx.coroutines.DispatchedTask.run(go/retraceme fa033e5bae99b59d8477124fa92133edcdefa6c9ebb8507171a3414e8dc9df78:112) 10-23 00:02:52.472 W/ImageLoader(2485): at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:524) 10-23 00:02:52.472 W/ImageLoader(2485): at java.util.concurrent.FutureTask.run(FutureTask.java:317) 10-23 00:02:52.472 W/ImageLoader(2485): at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:348) 10-23 00:02:52.472 W/ImageLoader(2485): at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1156) 10-23 00:02:52.472 W/ImageLoader(2485): at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:651) 10-23 00:02:52.472 W/ImageLoader(2485): at java.lang.Thread.run(Thread.java:1119) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../ui/viewmodel/BrightnessSliderViewModel.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt index 36efeaa1bca9..e3026ba28a91 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/viewmodel/BrightnessSliderViewModel.kt @@ -21,6 +21,7 @@ import androidx.annotation.DrawableRes import androidx.annotation.FloatRange import androidx.annotation.StringRes import androidx.compose.runtime.getValue +import androidx.core.content.res.ResourcesCompat import com.android.systemui.brightness.domain.interactor.BrightnessPolicyEnforcementInteractor import com.android.systemui.brightness.domain.interactor.ScreenBrightnessInteractor import com.android.systemui.brightness.shared.model.GammaBrightness @@ -28,7 +29,6 @@ import com.android.systemui.classifier.Classifier import com.android.systemui.classifier.domain.interactor.FalsingInteractor import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.asIcon -import com.android.systemui.graphics.ImageLoader import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator @@ -62,7 +62,6 @@ constructor( private val falsingInteractor: FalsingInteractor, @Assisted private val supportsMirroring: Boolean, private val brightnessWarningToast: BrightnessWarningToast, - private val imageLoader: ImageLoader, ) : ExclusiveActivatable() { init { @@ -110,13 +109,13 @@ constructor( suspend fun loadImage(@DrawableRes resId: Int, context: Context): Icon.Loaded? { return withTimeoutOrNull(500L) { - imageLoader - .loadDrawable( - android.graphics.drawable.Icon.createWithResource(context, resId), - maxHeight = 200, - maxWidth = 200, - ) - ?.asIcon(null, resId) + val drawable = ResourcesCompat.getDrawable( + context.resources, + resId, + context.theme + ) ?: return@withTimeoutOrNull null + + drawable.asIcon(null, resId) } } From 5dfec634f0512eec508b52bbee61553d828222ea Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 10 Oct 2024 11:00:37 +0800 Subject: [PATCH 073/190] services: Introduce Shake Gestures [1/2] Co-authored-by: AmeChanRain Change-Id: If2a3c094c2f30e3eb7bf1df811edb482554749bb Signed-off-by: Alvin Francis Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- .../server/policy/PhoneWindowManager.java | 27 ++++ .../rising/server/ShakeGestureService.java | 106 +++++++++++++++ .../org/rising/server/ShakeGestureUtils.java | 128 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 services/core/java/org/rising/server/ShakeGestureService.java create mode 100644 services/core/java/org/rising/server/ShakeGestureUtils.java diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 3c6a3709079e..04f32eee1cfd 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -283,6 +283,8 @@ import org.lineageos.internal.buttons.LineageButtons; import org.lineageos.internal.util.ActionUtils; +import org.rising.server.ShakeGestureService; + import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; @@ -713,6 +715,8 @@ public void onDrawn() { ANBIHandler mANBIHandler; private boolean mANBIEnabled; + private ShakeGestureService mShakeGestures; + // Tracks user-customisable behavior for certain key events private Action mBackLongPressAction; private Action mHomeLongPressAction; @@ -727,6 +731,7 @@ public void onDrawn() { private Action mCornerLongSwipeAction; private Action mEdgeLongSwipeAction; private Action mThreeFingersSwipeAction; + private Action mShakeGestureAction; // support for activating the lock screen while the screen is on private HashSet mAllowLockscreenWhenOnDisplays = new HashSet<>(); @@ -1155,6 +1160,9 @@ void observe() { resolver.registerContentObserver(Settings.System.getUriFor( "doze_trigger_doubletap"), false, this, UserHandle.USER_ALL); + resolver.registerContentObserver(LineageSettings.System.getUriFor( + LineageSettings.System.KEY_SHAKE_GESTURE_ACTION), false, this, + UserHandle.USER_ALL); updateSettings(); } @@ -3502,6 +3510,10 @@ private void updateKeyAssignments() { } } + mShakeGestureAction = Action.fromSettings(resolver, + LineageSettings.System.KEY_SHAKE_GESTURE_ACTION, + Action.NOTHING); + mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_NOTHING; if (mPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)) { mShortPressOnWindowBehavior = SHORT_PRESS_WINDOW_PICTURE_IN_PICTURE; @@ -7100,6 +7112,21 @@ public void systemReady() { if (mVrManagerInternal != null) { mVrManagerInternal.addPersistentVrModeStateListener(mPersistentVrModeListener); } + + mShakeGestures = ShakeGestureService.getInstance(mContext, new ShakeGestureService.ShakeGesturesCallbacks() { + @Override + public void onShake() { + if (mShakeGestureAction == Action.NOTHING) + return; + long now = SystemClock.uptimeMillis(); + KeyEvent event = new KeyEvent(now, now, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SYSRQ, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, + KeyEvent.FLAG_FROM_SYSTEM, InputDevice.SOURCE_TOUCHSCREEN); + performKeyAction(mShakeGestureAction, event); + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS, "Shake Gesture"); + } + }); + mShakeGestures.onStart(); mDockObserverInternal = LocalServices.getService(DockObserverInternal.class); if (mDockObserverInternal != null) { diff --git a/services/core/java/org/rising/server/ShakeGestureService.java b/services/core/java/org/rising/server/ShakeGestureService.java new file mode 100644 index 000000000000..6dce9663f1ec --- /dev/null +++ b/services/core/java/org/rising/server/ShakeGestureService.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2023-2024 The 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 org.rising.server; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.provider.Settings; + +public final class ShakeGestureService { + + private static final String TAG = "ShakeGestureService"; + + private static final String SHAKE_GESTURES_ENABLED = "shake_gestures_enabled"; + private static final String SHAKE_GESTURES_ACTION = "shake_gestures_action"; + private static final int USER_ALL = UserHandle.USER_ALL; + + private final Context mContext; + private ShakeGestureUtils mShakeGestureUtils; + private static volatile ShakeGestureService instance; + private final ShakeGesturesCallbacks mShakeCallbacks; + + private final SettingsObserver mSettingsObserver; + private boolean mShakeServiceEnabled = false; + + private ShakeGestureUtils.OnShakeListener mShakeListener; + + public interface ShakeGesturesCallbacks { + void onShake(); + } + + private ShakeGestureService(Context context, ShakeGesturesCallbacks callback) { + mContext = context; + mShakeCallbacks = callback; + mShakeListener = () -> { + if (mShakeServiceEnabled && mShakeCallbacks != null) { + mShakeCallbacks.onShake(); + } + }; + mSettingsObserver = new SettingsObserver(null); + } + + public static synchronized ShakeGestureService getInstance(Context context, ShakeGesturesCallbacks callback) { + if (instance == null) { + synchronized (ShakeGestureService.class) { + if (instance == null) { + instance = new ShakeGestureService(context, callback); + } + } + } + return instance; + } + + public void onStart() { + if (mShakeGestureUtils == null) { + mShakeGestureUtils = new ShakeGestureUtils(mContext); + } + updateSettings(); + mSettingsObserver.observe(); + if (mShakeServiceEnabled) { + mShakeGestureUtils.registerListener(mShakeListener); + } + } + + private void updateSettings() { + boolean wasShakeServiceEnabled = mShakeServiceEnabled; + mShakeServiceEnabled = Settings.System.getInt(mContext.getContentResolver(), + SHAKE_GESTURES_ENABLED, 0) == 1; + if (mShakeServiceEnabled && !wasShakeServiceEnabled) { + mShakeGestureUtils.registerListener(mShakeListener); + } else if (!mShakeServiceEnabled && wasShakeServiceEnabled) { + mShakeGestureUtils.unregisterListener(mShakeListener); + } + } + + class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { + super(handler); + } + void observe() { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(SHAKE_GESTURES_ENABLED), false, this, USER_ALL); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(SHAKE_GESTURES_ACTION), false, this, USER_ALL); + } + @Override + public void onChange(boolean selfChange) { + updateSettings(); + } + } +} diff --git a/services/core/java/org/rising/server/ShakeGestureUtils.java b/services/core/java/org/rising/server/ShakeGestureUtils.java new file mode 100644 index 000000000000..76e9e02047d7 --- /dev/null +++ b/services/core/java/org/rising/server/ShakeGestureUtils.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023-2024 The 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 org.rising.server; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.os.SystemClock; +import android.provider.Settings; + +import java.util.ArrayList; + +public class ShakeGestureUtils implements SensorEventListener { + + private static final String TAG = "ShakeGestureUtils"; + + private static final String SHAKE_GESTURES_SHAKE_INTENSITY = "shake_gestures_intensity"; + + private Context mContext; + private SensorManager mSensorManager; + private Sensor mAccelerometer; + private ArrayList mListeners = new ArrayList<>(); + private long mLastShakeTime = 0L; + private long mLastUpdateTime = 0L; + private int mShakeCount = 0; + private float mLastX = 0f; + private float mLastY = 0f; + private float mLastZ = 0f; + + public ShakeGestureUtils(Context context) { + mContext = context; + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + if (mSensorManager != null) { + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + } + + public interface OnShakeListener { + void onShake(); + } + + public void registerListener(OnShakeListener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + startListening(); + } + } + + public void unregisterListener(OnShakeListener listener) { + mListeners.remove(listener); + if (mListeners.isEmpty()) { + stopListening(); + } + } + + private void startListening() { + if (mAccelerometer != null) { + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI); + } + } + + private void stopListening() { + mSensorManager.unregisterListener(this); + } + + private int getShakeIntensity() { + return Settings.System.getInt(mContext.getContentResolver(), + SHAKE_GESTURES_SHAKE_INTENSITY, 3); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (event == null) { + return; + } + long curUpdateTime = System.currentTimeMillis(); + long timeInterval = curUpdateTime - mLastUpdateTime; + if (timeInterval < (getShakeIntensity() * 14f)) { + return; + } + if (event.values.length < 3) { + return; + } + mLastUpdateTime = curUpdateTime; + float x = event.values[0]; + float y = event.values[1]; + float z = event.values[2]; + float deltaX = x - mLastX; + float deltaY = y - mLastY; + float deltaZ = z - mLastZ; + mLastX = x; + mLastY = y; + mLastZ = z; + double speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ) * 1000.0 / timeInterval; + if (speed >= getShakeIntensity() * 100f) { + notifyShakeListeners(); + } + } + + private void notifyShakeListeners() { + if (SystemClock.elapsedRealtime() - mLastShakeTime < 1000) { + return; + } + for (OnShakeListener listener : mListeners) { + listener.onShake(); + } + mLastShakeTime = SystemClock.elapsedRealtime(); + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) {} +} From 782cde882df795eb7209a7977713f566b45aace6 Mon Sep 17 00:00:00 2001 From: LuK1337 Date: Sat, 14 Feb 2026 09:49:11 +0100 Subject: [PATCH 074/190] fixup! SystemUI: Bring back good ol' circle battery style once again Color arc red if level <= 20 and charging/powersave isn't active to match 23.0. Fixes: https://gitlab.com/LineageOS/issues/android/-/issues/9866 Change-Id: I40c15fca2413f52609b7184d87a77d1e04b41241 Signed-off-by: Ghosuto --- .../pipeline/battery/ui/composable/UnifiedBattery.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt index d0e9109b92bb..3257eca3720b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/UnifiedBattery.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.battery.ui.composable import android.graphics.Rect import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -427,6 +428,7 @@ fun CircleBatteryBody( modifier: Modifier = Modifier, contentDescription: String = "", ) { + val colorError = MaterialTheme.colorScheme.error val textMeasurer = rememberTextMeasurer() Canvas(modifier = modifier, contentDescription = contentDescription) { @@ -445,7 +447,11 @@ fun CircleBatteryBody( // Draw colored arc representing charge level if (level != null && level > 0) { drawArc( - colors.attribution, + if (level <= 20 && attr !is BatteryGlyph.Bolt && attr !is BatteryGlyph.Plus) { + colorError + } else { + colors.attribution + }, 270f, 3.6f * level, useCenter = false, From 552d5c7f96217e1926a288bb38ff0ee56667363b Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 15 Feb 2026 04:01:47 +0000 Subject: [PATCH 075/190] SystemUI: Start QS header animation only on expanding Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../NotificationPanelViewController.java | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 9dc323228a7b..809e3d4682d2 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -62,6 +62,7 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.RenderEffect; +import android.graphics.drawable.Animatable; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; @@ -4535,15 +4536,51 @@ private void updateHeaderImage() { mShadeHeaderExpansion = shadeHeaderExpansion; mQsHeaderImageView.setImageAlpha( (int) (mShadeHeaderExpansion * (255 - mHeaderImageShadow))); + startHeaderAnimIfPossible(); } } else { mQsHeaderLayout.setVisibility(View.GONE); + stopHeaderAnimIfRunning(); + } + } + + private void startHeaderAnimIfPossible() { + Drawable drawable = mQsHeaderImageView.getDrawable(); + if (drawable == null) return; + + // Only animate when we are actually showing it + if (mQsHeaderLayout.getVisibility() != View.VISIBLE || mQsHeaderImageView.getVisibility() != View.VISIBLE) { + return; + } + + drawable.setVisible(true, true); + if (drawable instanceof Animatable anim && !anim.isRunning()) { + anim.start(); + } + } + + private void stopHeaderAnimIfRunning() { + Drawable drawable = mQsHeaderImageView.getDrawable(); + if (drawable instanceof Animatable anim && anim.isRunning()) { + anim.stop(); + } + if (drawable != null) { + drawable.setVisible(false, false); } } private void setNotificationPanelHeaderBackground(Drawable dw, boolean force) { - if (mQsHeaderImageView.getDrawable() != null && !force) { - Drawable[] layers = new Drawable[]{mQsHeaderImageView.getDrawable(), dw}; + // Stop previous anim, if any, before swapping + stopHeaderAnimIfRunning(); + + if (dw instanceof Animatable) { + mQsHeaderImageView.setImageDrawable(dw); + return; + } + + Drawable current = mQsHeaderImageView.getDrawable(); + if (current != null && !force && !(current instanceof Animatable)) { + Drawable[] layers = new Drawable[]{current, dw}; TransitionDrawable transitionDrawable = new TransitionDrawable(layers); transitionDrawable.setCrossFadeEnabled(true); mQsHeaderImageView.setImageDrawable(transitionDrawable); From 70783e1205568c6df0931cafe0a0b46910bf24b0 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 15 Feb 2026 06:21:29 +0530 Subject: [PATCH 076/190] SystemUI: Move right logo in statusbar to extreme right * This way it doesn't messes with wifi group / mobile group layout. Fixes: https://github.com/crdroidandroid/issue_tracker/issues/890 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- packages/SystemUI/res/layout/status_bar.xml | 14 +++++++++----- packages/SystemUI/res/layout/system_icons.xml | 11 ----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index a94cc917d272..dd73e6cdbce2 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -95,11 +95,8 @@ android:id="@+id/statusbar_logo" android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_gravity="center" - android:paddingStart="@dimen/status_bar_left_clock_starting_padding" - android:paddingEnd="@dimen/status_bar_left_clock_end_padding" - android:gravity="center_vertical|start" - android:scaleType="center" + android:layout_gravity="center_vertical" + android:scaleType="centerInside" android:visibility="gone" /> + + diff --git a/packages/SystemUI/res/layout/system_icons.xml b/packages/SystemUI/res/layout/system_icons.xml index c915d4092255..6397b7788800 100644 --- a/packages/SystemUI/res/layout/system_icons.xml +++ b/packages/SystemUI/res/layout/system_icons.xml @@ -66,16 +66,5 @@ android:visibility="gone" systemui:isStatusBar="true" /> - - From 9bc7f1904af9754a94c320a46b29071844c2c3d7 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 9 Dec 2025 21:33:15 +0530 Subject: [PATCH 077/190] SystemUI: Fix concurrent modification exception on config change * Iterating the mutable listeners list during configuration dispatch causes below crash 12-09 21:15:07.361 D/AndroidRuntime(2214): Shutting down VM 12-09 21:15:07.361 E/AndroidRuntime(2214): FATAL EXCEPTION: main 12-09 21:15:07.361 E/AndroidRuntime(2214): Process: com.android.systemui, PID: 2214 12-09 21:15:07.361 E/AndroidRuntime(2214): java.util.ConcurrentModificationException 12-09 21:15:07.361 E/AndroidRuntime(2214): at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:1111) 12-09 21:15:07.361 E/AndroidRuntime(2214): at java.util.ArrayList$Itr.next(ArrayList.java:1064) 12-09 21:15:07.361 E/AndroidRuntime(2214): at kotlin.collections.CollectionsKt.filterNotNull(Unknown Source:16) 12-09 21:15:07.361 E/AndroidRuntime(2214): at com.android.systemui.statusbar.phone.ConfigurationControllerImpl.onConfigurationChanged(go/retraceme d43d4e7a8dd5b2e7eba70ac5852a31df7ec95eb6ed6791527c5ce29550f31e4c:30) 12-09 21:15:07.361 E/AndroidRuntime(2214): at com.android.systemui.SystemUIApplication.onConfigurationChanged(go/retraceme d43d4e7a8dd5b2e7eba70ac5852a31df7ec95eb6ed6791527c5ce29550f31e4c:36) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ConfigurationController.performConfigurationChanged(ConfigurationController.java:261) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ConfigurationController.handleConfigurationChangedInner(ConfigurationController.java:235) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:154) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:129) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ActivityThread.handleConfigurationChanged(ActivityThread.java:6948) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.servertransaction.ConfigurationChangeItem.execute(ConfigurationChangeItem.java:56) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:133) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:103) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:80) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2831) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.os.Handler.dispatchMessage(Handler.java:110) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.os.Looper.dispatchMessage(Looper.java:315) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.os.Looper.loopOnce(Looper.java:251) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.os.Looper.loop(Looper.java:349) 12-09 21:15:07.361 E/AndroidRuntime(2214): at android.app.ActivityThread.main(ActivityThread.java:9047) 12-09 21:15:07.361 E/AndroidRuntime(2214): at java.lang.reflect.Method.invoke(Native Method) 12-09 21:15:07.361 E/AndroidRuntime(2214): at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:596) 12-09 21:15:07.361 E/AndroidRuntime(2214): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:929) Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../phone/ConfigurationControllerImpl.kt | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index a941b3a4d8b9..3829af6c106e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -61,24 +61,25 @@ constructor(@Assisted private val context: Context) : fontWeightAdjustment = currentConfig.fontWeightAdjustment } - override fun notifyThemeChanged() { + private inline fun forEachListener(block: (ConfigurationListener) -> Unit) { // Avoid concurrent modification exception - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } + val snapshot = synchronized(listeners) { listeners.toList() } + snapshot.forEach(block) + } - listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() } + override fun notifyThemeChanged() { + forEachListener { it.onThemeChanged() } } override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) { - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onMovedToDisplay(newDisplayId, newConfiguration) } } override fun onConfigurationChanged(newConfig: Configuration) { // Avoid concurrent modification exception - val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } - listeners.filterForEach({ this.listeners.contains(it) }) { it.onConfigChanged(newConfig) } + forEachListener { it.onConfigChanged(newConfig) } val fontScale = newConfig.fontScale val density = newConfig.densityDpi val uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK @@ -86,7 +87,7 @@ constructor(@Assisted private val context: Context) : val uiModeChanged = uiMode != this.uiMode if (density != this.density || fontScale != this.fontScale || inCarMode && uiModeChanged || fontWeightAdjustment != this.fontWeightAdjustment) { - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onDensityOrFontScaleChanged() } this.density = density @@ -97,7 +98,7 @@ constructor(@Assisted private val context: Context) : val smallestScreenWidth = newConfig.smallestScreenWidthDp if (smallestScreenWidth != this.smallestScreenWidth) { this.smallestScreenWidth = smallestScreenWidth - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onSmallestScreenWidthChanged() } } @@ -109,13 +110,13 @@ constructor(@Assisted private val context: Context) : // would be a direct reference to windowConfiguration.maxBounds, so the if statement // above would always fail. See b/245799099 for more information. this.maxBounds.set(maxBounds) - listeners.filterForEach({ this.listeners.contains(it) }) { it.onMaxBoundsChanged() } + forEachListener { it.onMaxBoundsChanged() } } val localeList = newConfig.locales if (localeList != this.localeList) { this.localeList = localeList - listeners.filterForEach({ this.listeners.contains(it) }) { it.onLocaleListChanged() } + forEachListener { it.onLocaleListChanged() } } if (uiModeChanged) { @@ -124,24 +125,24 @@ constructor(@Assisted private val context: Context) : context.theme.applyStyle(context.themeResId, true) this.uiMode = uiMode - listeners.filterForEach({ this.listeners.contains(it) }) { it.onUiModeChanged() } + forEachListener { it.onUiModeChanged() } } if (layoutDirection != newConfig.layoutDirection) { layoutDirection = newConfig.layoutDirection - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onLayoutDirectionChanged(layoutDirection == LAYOUT_DIRECTION_RTL) } } if (lastConfig.updateFrom(newConfig) and ActivityInfo.CONFIG_ASSETS_PATHS != 0) { - listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() } + forEachListener { it.onThemeChanged() } } val newOrientation = newConfig.orientation if (orientation != newOrientation) { orientation = newOrientation - listeners.filterForEach({ this.listeners.contains(it) }) { + forEachListener { it.onOrientationChanged(orientation) } } From 4e8bccf97b434b0ece5521b5e9512dd48134643d Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Sun, 15 Dec 2024 11:35:46 +0900 Subject: [PATCH 078/190] SystemUI: ConfigurationControllerImpl: Avoid NullPointerException * As listener could be null, apply filterNotNull to avoid it Log: 12-14 17:58:13.733 3663 3663 E AndroidRuntime: FATAL EXCEPTION: main 12-14 17:58:13.733 3663 3663 E AndroidRuntime: Process: com.android.systemui, PID: 3663 12-14 17:58:13.733 3663 3663 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void com.android.systemui.statusbar.policy.ConfigurationController$ConfigurationListener.onConfigChanged(android.content.res.Configuration)' on a null object reference 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at com.android.systemui.statusbar.phone.ConfigurationControllerImpl.onConfigurationChanged(ConfigurationControllerImpl.kt:74) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at com.android.systemui.SystemUIApplication.onConfigurationChanged(SystemUIApplication.java:449) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ConfigurationController.performConfigurationChanged(ConfigurationController.java:242) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:216) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ConfigurationController.handleConfigurationChanged(ConfigurationController.java:128) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ActivityThread.handleConfigurationChanged(ActivityThread.java:6520) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.servertransaction.ConfigurationChangeItem.execute(ConfigurationChangeItem.java:48) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeNonLifecycleItem(TransactionExecutor.java:231) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.executeTransactionItems(TransactionExecutor.java:152) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:93) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2595) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:107) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.os.Looper.loop(Looper.java:317) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8597) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:583) 12-14 17:58:13.733 3663 3663 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878) neobuddy89: Ensure these checks for all listeners and during callbacks. Change-Id: I1b5197667f7837dcabd67204ae6b764113dbbcce Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../systemui/statusbar/phone/ConfigurationControllerImpl.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index 3829af6c106e..e348f1f2590a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -64,7 +64,7 @@ constructor(@Assisted private val context: Context) : private inline fun forEachListener(block: (ConfigurationListener) -> Unit) { // Avoid concurrent modification exception val snapshot = synchronized(listeners) { listeners.toList() } - snapshot.forEach(block) + snapshot.filterNotNull().forEach(block) } override fun notifyThemeChanged() { @@ -149,11 +149,13 @@ constructor(@Assisted private val context: Context) : } override fun addCallback(listener: ConfigurationListener) { + if (listener == null) return synchronized(listeners) { listeners.add(listener) } listener.onDensityOrFontScaleChanged() } override fun removeCallback(listener: ConfigurationListener) { + if (listener == null) return synchronized(listeners) { listeners.remove(listener) } } From 1b240817f7370bd8ef44af0c34c3def19726d7bd Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Sat, 10 Jan 2026 11:26:31 +0800 Subject: [PATCH 079/190] SystemUI: Fixing dynamic stream crashes Change-Id: I750260172999d2bb9b06b8f6817e189018552e58 Signed-off-by: Ghosuto --- .../volume/repository/VolumeRepository.kt | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt b/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt index 1705e5d57f60..49492405d73b 100644 --- a/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/axion/volume/repository/VolumeRepository.kt @@ -37,6 +37,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.VolumeDialogController +import com.android.systemui.volume.VolumeDialogControllerImpl import javax.inject.Inject import kotlinx.coroutines.* import kotlinx.coroutines.channels.awaitClose @@ -315,7 +316,8 @@ class AxionVolumeRepositoryImpl @Inject constructor( AudioManager.STREAM_VOICE_CALL ) - if (state.activeStream != -1 && state.activeStream !in excludedActiveStreams) { + if (state.activeStream != -1 && state.activeStream !in excludedActiveStreams && + state.states.get(state.activeStream) != null) { streams.add(state.activeStream) } @@ -332,7 +334,13 @@ class AxionVolumeRepositoryImpl @Inject constructor( val max = getMaxVolume(streamType) val min = getMinVolume(streamType) val volume = (min + (level * (max - min))).toInt().coerceIn(min, max) - audioManager.setStreamVolume(streamType, volume, flags) + + if (streamType >= VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX || + streamType == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST) { + controller.setStreamVolume(streamType, volume, false) + } else { + audioManager.setStreamVolume(streamType, volume, flags) + } } override fun setMute(streamType: Int, muted: Boolean) { @@ -365,11 +373,27 @@ class AxionVolumeRepositoryImpl @Inject constructor( controller.userActivity() } - override fun getMaxVolume(streamType: Int): Int = - audioManager.getStreamMaxVolume(streamType) + override fun getMaxVolume(streamType: Int): Int { + val state = controllerState.value + if (state != null) { + val ss = state.states.get(streamType) + if (ss != null) { + return ss.levelMax + } + } + return audioManager.getStreamMaxVolume(streamType) + } - override fun getMinVolume(streamType: Int): Int = - audioManager.getStreamMinVolume(streamType) + override fun getMinVolume(streamType: Int): Int { + val state = controllerState.value + if (state != null) { + val ss = state.states.get(streamType) + if (ss != null) { + return ss.levelMin + } + } + return audioManager.getStreamMinVolume(streamType) + } override fun isStreamMuted(streamType: Int): Boolean = audioManager.isStreamMute(streamType) From fbc2b66ae48714f7448c9888bcc73100e4bbd382 Mon Sep 17 00:00:00 2001 From: Rve27 Date: Mon, 19 Jan 2026 14:18:22 +0000 Subject: [PATCH 080/190] base: Migrate to MaterialExpressiveTheme to volume slider Change-Id: I7aaf57be5d62337f49a5dbbbfdb5ed0a4f187727 Signed-off-by: Rve27 Signed-off-by: Ghosuto --- .../android/systemui/axion/volume/AxionVolumeDialog.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt index 3a4ca954f625..5df70432a563 100644 --- a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3ExpressiveApi::class) + package com.android.systemui.axion.volume import android.content.Context @@ -129,11 +131,13 @@ class AxionVolumeDialog @Inject constructor( } } - MaterialTheme( - colorScheme = if (isSystemInDarkTheme()) + MaterialExpressiveTheme( + colorScheme = if (isSystemInDarkTheme()) { dynamicDarkColorScheme(LocalContext.current) - else + } else { dynamicLightColorScheme(LocalContext.current) + }, + MotionScheme.expressive() ) { Box( modifier = Modifier From 84d2464ac4788cade0ff0c0df4edea44bcfb0b67 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:06:17 +0800 Subject: [PATCH 081/190] SystemUI: Fixing ax volume dialog flags Change-Id: I98eb5622048652c98b2789556709067d16784844 Signed-off-by: Ghosuto --- .../android/systemui/axion/volume/AxionVolumeDialog.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt index 5df70432a563..172943642f7b 100644 --- a/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/axion/volume/AxionVolumeDialog.kt @@ -51,8 +51,14 @@ class AxionVolumeDialog @Inject constructor( init { with(window!!) { + clearFlags( + WindowManager.LayoutParams.FLAG_DIM_BEHIND or + WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR + ) addFlags( - WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH or WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED ) From 13840f6d530fdfaf33df25b45351f2abbf6ca4cc Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 16 Feb 2026 10:28:58 +0000 Subject: [PATCH 082/190] SystemUI: Add charging bolt indicator for text-only battery style - Inspired from https://github.com/Lunaris-AOSP/frameworks_base_old/commit/57f26ed8228a0d3b329ed06b57231595b6370dd7 Signed-off-by: Ghosuto --- .../events/ui/view/BatteryStatusEventComposeChip.kt | 7 +++++++ .../battery/ui/composable/BatteryWithPercent.kt | 9 +++++++++ .../battery/ui/viewmodel/BatteryViewModel.kt | 12 ++++++++++++ 3 files changed, 28 insertions(+) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt index 584498eba13c..9708028c4874 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt @@ -165,5 +165,12 @@ private fun BatteryAndPercentChip( style = MaterialTheme.typography.labelLargeEmphasized, ) } + if (isText) { + Text( + text = "\u26A1", + color = BatteryColors.DarkTheme.Default.fill, + style = MaterialTheme.typography.labelLargeEmphasized, + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt index 36d3ba1e63d6..6a50697119c5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/composable/BatteryWithPercent.kt @@ -95,6 +95,15 @@ fun BatteryWithPercent( } } + if (viewModel.shouldShowBoltInTextMode) { + BasicText( + text = "\u26A1", + color = colorProducer, + style = textStyle, + maxLines = 1, + ) + } + if (showEstimate) { viewModel.batteryTimeRemainingEstimate?.let { BasicText( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt index 73a9a1d600aa..07015abff30e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/ui/viewmodel/BatteryViewModel.kt @@ -94,6 +94,18 @@ sealed class BatteryViewModel( source = interactor.showPercentInsideIcon, ) + val shouldShowBoltInTextMode: Boolean by + hydrator.hydratedStateOf( + traceName = "shouldShowBoltInTextMode", + initialValue = false, + source = combine( + interactor.batteryIconStyle.map { it == BatteryRepository.ICON_STYLE_TEXT }, + interactor.isCharging + ) { isTextMode, charging -> + isTextMode && charging + } + ) + /** A [List] representation of the current [level] */ private val levelGlyphs: Flow> = interactor.level.map { it?.glyphRepresentation() ?: emptyList() } From 17c36c992dc13edf42b599499ce2959505167369 Mon Sep 17 00:00:00 2001 From: Mohammad Hasan Keramat J Date: Thu, 16 Jun 2022 22:32:56 +0430 Subject: [PATCH 083/190] SystemUI: Add VPNTethering tile Signed-off-by: Mohammad Hasan Keramat J Change-Id: I2d8924bc6341e0dafa2a4db69c25e6200bb5c288 Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- .../res/drawable/ic_qs_vpn_tethering.xml | 28 ++++ packages/SystemUI/res/values/config.xml | 2 +- .../SystemUI/res/values/lunaris_strings.xml | 5 + .../android/systemui/lineage/LineageModule.kt | 22 +++ .../systemui/qs/tiles/VPNTetheringTile.java | 153 ++++++++++++++++++ 5 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res/drawable/ic_qs_vpn_tethering.xml create mode 100644 packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java diff --git a/packages/SystemUI/res/drawable/ic_qs_vpn_tethering.xml b/packages/SystemUI/res/drawable/ic_qs_vpn_tethering.xml new file mode 100644 index 000000000000..e732c98f747b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_vpn_tethering.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 78438b6ddfd9..c5866df0c4b6 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -120,7 +120,7 @@ - internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume + internet,wifi,cell,bt,flashlight,dnd,modes_dnd,alarm,airplane,nfc,rotation,battery,controls,wallet,cast,screenrecord,mictoggle,cameratoggle,location,hotspot,inversion,saver,dark,work,night,reverse,reduce_brightness,qr_code_scanner,onehanded,color_correction,dream,font_scaling,record_issue,hearing_devices,notes,desktopeffects,ambient_display,aod,caffeine,heads_up,powershare,profiles,reading_mode,sync,usb_tether,vpn,onthego,sound,cpuinfo,fpsinfo,compass,dataswitch,volume_panel,smartpixels,weather,refresh_rate,screenshot,locale,dns,preferred_network,volume,vpn_tethering diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index f43c8f9cf07e..f26f7312c7fa 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -214,4 +214,9 @@ USB tethering MIDI Use USB for + + + VPN tethering + VPN tethering turned off. + VPN tethering turned on. diff --git a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt index 6a58ed9a8d14..90db8c8f0739 100644 --- a/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt +++ b/packages/SystemUI/src/com/android/systemui/lineage/LineageModule.kt @@ -42,6 +42,7 @@ import com.android.systemui.qs.tiles.SoundTile import com.android.systemui.qs.tiles.SyncTile import com.android.systemui.qs.tiles.UsbTetherTile import com.android.systemui.qs.tiles.VolumeQSTile +import com.android.systemui.qs.tiles.VPNTetheringTile import com.android.systemui.qs.tiles.VolumeTile import com.android.systemui.qs.tiles.VpnTile import com.android.systemui.qs.tiles.WeatherTile @@ -188,6 +189,12 @@ interface LineageModule { @IntoMap @StringKey(VolumeQSTile.TILE_SPEC) fun bindVolumeQSTile(volumeQSTile: VolumeQSTile): QSTileImpl<*> + + /** Inject VPNTetheringTile into tileMap in QSModule */ + @Binds + @IntoMap + @StringKey(VPNTetheringTile.TILE_SPEC) + fun bindVPNTetheringTile(vpnTetheringTile: VPNTetheringTile): QSTileImpl<*> /** Inject VolumeTile into tileMap in QSModule */ @Binds @@ -594,5 +601,20 @@ interface LineageModule { instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.UTILITIES ) + + @Provides + @IntoMap + @StringKey(VPNTetheringTile.TILE_SPEC) + fun provideVPNTetheringTileConfig(uiEventLogger: QsEventLogger): QSTileConfig { + return QSTileConfig( + tileSpec = TileSpec.create(VPNTetheringTile.TILE_SPEC), + uiConfig = QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_vpn_tethering, + labelRes = R.string.vpn_tethering_label + ), + instanceId = uiEventLogger.getNewInstanceId(), + category = TileCategory.CONNECTIVITY + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java new file mode 100644 index 000000000000..7977129dcd74 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/VPNTetheringTile.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2015 The CyanogenMod Project + * Copyright (C) 2017-2021 The LineageOS Project + * Copyright (C) 2022 The LibreMobileOS Foundation + * + * 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.qs.tiles; + +import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN; + +import android.app.ActivityManager; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemProperties; +import android.provider.Settings; +import android.provider.Settings.Secure; +import android.service.quicksettings.Tile; +import android.text.TextUtils; +import com.android.systemui.animation.Expandable; + +import androidx.annotation.Nullable; + +import com.android.internal.logging.MetricsLogger; +import com.android.systemui.res.R; +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.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.qs.UserSettingObserver; +import com.android.systemui.settings.UserTracker; +import com.android.systemui.util.settings.SecureSettings; + +import javax.inject.Inject; + +/** Quick settings tile: VPN Tethering **/ +public class VPNTetheringTile extends QSTileImpl { + + public static final String TILE_SPEC = "vpn_tethering"; + + private final Icon mIcon = ResourceIcon.get(R.drawable.ic_qs_vpn_tethering); + private static final Intent TETHER_SETTINGS = new Intent().setComponent(new ComponentName( + "com.android.settings", "com.android.settings.TetherSettings")); + + private final UserSettingObserver mSetting; + + @Inject + public VPNTetheringTile( + QSHost host, + QsEventLogger uiEventLogger, + @Background Looper backgroundLooper, + @Main Handler mainHandler, + FalsingManager falsingManager, + MetricsLogger metricsLogger, + StatusBarStateController statusBarStateController, + ActivityStarter activityStarter, + QSLogger qsLogger, + UserTracker userTracker, + SecureSettings secureSettings + ) { + super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, + statusBarStateController, activityStarter, qsLogger); + + mSetting = new UserSettingObserver(secureSettings, mHandler, Settings.Secure.TETHERING_ALLOW_VPN_UPSTREAMS, + userTracker.getUserId(), 0) { + @Override + protected void handleValueChanged(int value, boolean observedChange) { + handleRefreshState(value); + } + }; + } + + @Override + protected void handleDestroy() { + super.handleDestroy(); + mSetting.setListening(false); + } + + @Override + public BooleanState newTileState() { + return new BooleanState(); + } + + @Override + public void handleSetListening(boolean listening) { + super.handleSetListening(listening); + mSetting.setListening(listening); + } + + @Override + protected void handleUserSwitch(int newUserId) { + mSetting.setUserId(newUserId); + handleRefreshState(mSetting.getValue()); + } + + @Override + protected void handleClick(@Nullable Expandable expandable) { + mSetting.setValue(mState.value ? 0 : 1); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(TETHER_SETTINGS); + } + + @Override + protected void handleUpdateState(BooleanState state, Object arg) { + final int value = arg instanceof Integer ? (Integer) arg : mSetting.getValue(); + final boolean enable = value != 0; + state.value = enable; + state.label = mContext.getString(R.string.vpn_tethering_label); + state.icon = mIcon; + if (enable) { + state.contentDescription = mContext.getString( + R.string.vpn_tethering_changed_on); + state.state = Tile.STATE_ACTIVE; + } else { + state.contentDescription = mContext.getString( + R.string.vpn_tethering_changed_off); + state.state = Tile.STATE_INACTIVE; + } + } + + @Override + public CharSequence getTileLabel() { + return mContext.getString(R.string.vpn_tethering_label); + } + + @Override + public int getMetricsCategory() { + return VIEW_UNKNOWN; + } +} From 07f811a23cc5b545d8d1c0d864e02f7681a7773f Mon Sep 17 00:00:00 2001 From: someone5678 Date: Sun, 11 Aug 2024 00:24:42 +0900 Subject: [PATCH 084/190] SystemUI: Use system_accent1_200 for monetized privacy indicators * Use system_accent1_200 instead of system_accent1_100 (?android:attr/colorAccent) to emphasize color since these are "indicators". Change-Id: Iced6e3e1acd7ae982bb97edcde9ed38e3a8f0835 Signed-off-by: Ghosuto --- packages/SystemUI/res/values/colors.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 49996c72c73f..47266cc2b332 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -255,7 +255,7 @@ #E94235 #D93025 - #3ddc84 + @android:color/system_accent1_200 #3dbaf4 From 543ac9c4d8da9b043644d43e8179701e3f494c8c Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 17 Feb 2026 15:58:53 +0000 Subject: [PATCH 085/190] SystemUI: Use bit darker accent color for privacy indicators Signed-off-by: Ghosuto --- packages/SystemUI/res/values-night/colors.xml | 2 ++ packages/SystemUI/res/values/colors.xml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/SystemUI/res/values-night/colors.xml b/packages/SystemUI/res/values-night/colors.xml index 28a8a0ad1cc6..1ae95e19e7d8 100644 --- a/packages/SystemUI/res/values-night/colors.xml +++ b/packages/SystemUI/res/values-night/colors.xml @@ -146,4 +146,6 @@ @*android:color/system_on_surface_variant_dark + + @android:color/system_accent1_400 diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 47266cc2b332..7a479af9c69a 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -255,7 +255,7 @@ #E94235 #D93025 - @android:color/system_accent1_200 + @android:color/system_accent1_500 #3dbaf4 From ba68e4dd25549c8d4952502ad8ed27c4370ddb95 Mon Sep 17 00:00:00 2001 From: Pawit Pornkitprasan Date: Mon, 17 Nov 2014 18:59:57 +0100 Subject: [PATCH 086/190] AbsListView: Improve scrolling cache Scrolling cache helps make short scrolls/flings smooth but will cause stutter when long flings are made. This patch disables scrolling cache when long flings are made. This patch also fixes a related bug where scrolling cache will not be enabled properly when transitioning from flinging to scrolling. Patch Set 2: Calculate threshold based on maximum velocity (Sang Tae Park) Change-Id: Iad52a35120212c871ffd35df6184aeb678ee44aa Signed-off-by: Alex Naidis Signed-off-by: Ghosuto --- core/java/android/widget/AbsListView.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java index 406961490d2e..49c1c2f3535d 100644 --- a/core/java/android/widget/AbsListView.java +++ b/core/java/android/widget/AbsListView.java @@ -713,6 +713,7 @@ public abstract class AbsListView extends AdapterView implements Te private int mMinimumVelocity; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 124051740) private int mMaximumVelocity; + private int mDecacheThreshold; private float mVelocityScale = 1.0f; final boolean[] mIsScrap = new boolean[1]; @@ -1029,6 +1030,7 @@ private void initAbsListView() { mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor(); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mDecacheThreshold = mMaximumVelocity / 2; mOverscrollDistance = configuration.getScaledOverscrollDistance(); mOverflingDistance = configuration.getScaledOverflingDistance(); @@ -4997,7 +4999,7 @@ public void run() { // Keep the fling alive a little longer postDelayed(this, FLYWHEEL_TIMEOUT); } else { - endFling(); + endFling(false); // Don't disable the scrolling cache right after it was enabled mTouchMode = TOUCH_MODE_SCROLL; reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL); } @@ -5017,6 +5019,11 @@ float getSplineFlingDistance(int velocity) { // Use AbsListView#fling(int) instead @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void start(int initialVelocity) { + if (Math.abs(initialVelocity) > mDecacheThreshold) { + // For long flings, scrolling cache causes stutter, so don't use it + clearScrollingCache(); + } + int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0; mLastFlingY = initialY; mScroller.setInterpolator(null); @@ -5097,6 +5104,10 @@ void startScroll(int distance, int duration, boolean linear, // To interrupt a fling early you should use smoothScrollBy(0,0) instead @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) void endFling() { + endFling(true); + } + + void endFling(boolean clearCache) { mTouchMode = TOUCH_MODE_REST; removeCallbacks(this); @@ -5105,7 +5116,8 @@ void endFling() { if (!mSuppressIdleStateChangeCall) { reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE); } - clearScrollingCache(); + if (clearCache) + clearScrollingCache(); mScroller.abortAnimation(); if (mFlingStrictSpan != null) { From 767849d368987dac36c306e3fc1aa838d83b9e2d Mon Sep 17 00:00:00 2001 From: Ido Ben-Hur Date: Fri, 22 Aug 2025 14:30:12 +0300 Subject: [PATCH 087/190] base: Add support for daily and weekly data usage cycles [1/2] Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/net/NetworkPolicy.java | 16 ++++++++ core/java/android/util/RecurrenceRule.java | 39 +++++++++++++++++++ .../settingslib/NetworkPolicyEditor.java | 34 ++++++++++++++++ 3 files changed, 89 insertions(+) diff --git a/core/java/android/net/NetworkPolicy.java b/core/java/android/net/NetworkPolicy.java index 570211ecc88d..e5995d3005a5 100644 --- a/core/java/android/net/NetworkPolicy.java +++ b/core/java/android/net/NetworkPolicy.java @@ -103,6 +103,22 @@ public static RecurrenceRule buildRule(int cycleDay, ZoneId cycleTimezone) { } } + public static RecurrenceRule buildWeeklyRule(int cycleDay, ZoneId cycleTimezone) { + if (cycleDay != NetworkPolicy.CYCLE_NONE) { + return RecurrenceRule.buildRecurringWeekly(cycleDay, cycleTimezone); + } else { + return RecurrenceRule.buildNever(); + } + } + + public static RecurrenceRule buildDailyRule(int cycleDay, ZoneId cycleTimezone) { + if (cycleDay != NetworkPolicy.CYCLE_NONE) { + return RecurrenceRule.buildRecurringDaily(cycleDay, cycleTimezone); + } else { + return RecurrenceRule.buildNever(); + } + } + @Deprecated public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone, long warningBytes, long limitBytes, boolean metered) { diff --git a/core/java/android/util/RecurrenceRule.java b/core/java/android/util/RecurrenceRule.java index 9ef9c723ca27..fc373ee9c9ce 100644 --- a/core/java/android/util/RecurrenceRule.java +++ b/core/java/android/util/RecurrenceRule.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.ProtocolException; import java.time.Clock; +import java.time.DayOfWeek; import java.time.LocalTime; import java.time.Period; import java.time.ZoneId; @@ -80,6 +81,26 @@ public static RecurrenceRule buildRecurringMonthly(int dayOfMonth, ZoneId zone) return new RecurrenceRule(start, null, Period.ofMonths(1)); } + @UnsupportedAppUsage + public static RecurrenceRule buildRecurringWeekly(int dayOfWeek, ZoneId zone) { + final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone); + final DayOfWeek dayNow = now.getDayOfWeek(); + final DayOfWeek dayStart = DayOfWeek.of(dayOfWeek); + final int minusDays = dayNow.getValue() - dayStart.getValue(); + final ZonedDateTime start = ZonedDateTime.of( + now.toLocalDate().minusWeeks(1).minusDays(minusDays), + LocalTime.MIDNIGHT, zone); + return new RecurrenceRule(start, null, Period.ofWeeks(1)); + } + + @UnsupportedAppUsage + public static RecurrenceRule buildRecurringDaily(int hourOfDay, ZoneId zone) { + final ZonedDateTime now = ZonedDateTime.now(sClock).withZoneSameInstant(zone); + final ZonedDateTime start = ZonedDateTime.of(now.toLocalDate(), + LocalTime.MIDNIGHT.plusHours(hourOfDay), zone); + return new RecurrenceRule(start, null, Period.ofDays(1)); + } + private RecurrenceRule(Parcel source) { start = convertZonedDateTime(source.readString()); end = convertZonedDateTime(source.readString()); @@ -168,6 +189,24 @@ public boolean isMonthly() { && period.getDays() == 0; } + @UnsupportedAppUsage + public boolean isWeekly() { + return start != null + && period != null + && period.getYears() == 0 + && period.getMonths() == 0 + && period.getDays() == 7; + } + + @UnsupportedAppUsage + public boolean isDaily() { + return start != null + && period != null + && period.getYears() == 0 + && period.getMonths() == 0 + && period.getDays() == 1; + } + public Iterator> cycleIterator() { if (period != null) { return new RecurringIterator(); diff --git a/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java b/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java index b4e84dd54654..abff66f8e180 100644 --- a/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java +++ b/packages/SettingsLib/src/com/android/settingslib/NetworkPolicyEditor.java @@ -155,6 +155,24 @@ public int getPolicyCycleDay(NetworkTemplate template) { } } + public int getPolicyCycleDayOfWeek(NetworkTemplate template) { + final NetworkPolicy policy = getPolicy(template); + if (policy != null && policy.cycleRule.isWeekly()) { + return policy.cycleRule.start.getDayOfWeek().getValue(); + } else { + return CYCLE_NONE; + } + } + + public int getPolicyCycleHour(NetworkTemplate template) { + final NetworkPolicy policy = getPolicy(template); + if (policy != null && policy.cycleRule.isDaily()) { + return policy.cycleRule.start.getHour(); + } else { + return CYCLE_NONE; + } + } + @Deprecated public void setPolicyCycleDay(NetworkTemplate template, int cycleDay, String cycleTimezone) { final NetworkPolicy policy = getOrCreatePolicy(template); @@ -164,6 +182,22 @@ public void setPolicyCycleDay(NetworkTemplate template, int cycleDay, String cyc writeAsync(); } + public void setPolicyCycleDayOfWeek(NetworkTemplate template, int cycleDay, String cycleTimezone) { + final NetworkPolicy policy = getOrCreatePolicy(template); + policy.cycleRule = NetworkPolicy.buildWeeklyRule(cycleDay, ZoneId.of(cycleTimezone)); + policy.inferred = false; + policy.clearSnooze(); + writeAsync(); + } + + public void setPolicyCycleHour(NetworkTemplate template, int cycleHour, String cycleTimezone) { + final NetworkPolicy policy = getOrCreatePolicy(template); + policy.cycleRule = NetworkPolicy.buildDailyRule(cycleHour, ZoneId.of(cycleTimezone)); + policy.inferred = false; + policy.clearSnooze(); + writeAsync(); + } + public long getPolicyWarningBytes(NetworkTemplate template) { final NetworkPolicy policy = getPolicy(template); return (policy != null) ? policy.warningBytes : WARNING_DISABLED; From 99d99a670e787e9ce892bd81d0fdf2555d2680ea Mon Sep 17 00:00:00 2001 From: Ali B Date: Fri, 6 Apr 2018 16:28:25 +0300 Subject: [PATCH 088/190] QS footer icon visibilities [1/2] Ref: https://github.com/crdroidandroid/android_frameworks_base/commit/f0256e94528b26f52b0444b16cb36e843497b04e Co-authored-by: Pranav Vashi Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 18 ++++++ .../qs/footer/ui/compose/FooterActions.kt | 62 ++++++++++++++++--- .../panels/ui/compose/PaginatedGridLayout.kt | 9 ++- .../qs/panels/ui/compose/toolbar/Toolbar.kt | 32 +++++++--- 4 files changed, 101 insertions(+), 20 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ebdd60a11ae8..1a200e3ccd0c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7810,6 +7810,24 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String AMBIENT_SHOW_SETTINGS_ICONS = "ambient_show_settings_icons"; + /** + * Show Settings icon in QS Footer + * @hide + */ + public static final String QS_FOOTER_SHOW_SETTINGS = "qs_footer_show_settings"; + + /** + * Show Edit icon in QS Footer. + * @hide + */ + public static final String QS_FOOTER_SHOW_EDIT = "qs_footer_show_edit"; + + /** + * Show power menu icon in QS Footer. + * @hide + */ + public static final String QS_FOOTER_SHOW_POWER_MENU = "qs_footer_show_power_menu"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt index 30b696b103b3..a37c43335636 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/footer/ui/compose/FooterActions.kt @@ -16,6 +16,11 @@ package com.android.systemui.qs.footer.ui.compose +import android.database.ContentObserver +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.provider.Settings import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable @@ -53,9 +58,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -96,6 +103,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.qs.footer.ui.compose.FooterActionsDefaults.FOOTER_TEXT_FADE_DURATION_MILLIS import com.android.systemui.qs.footer.ui.compose.FooterActionsDefaults.FOOTER_TEXT_MINIMUM_SCALE_Y import com.android.systemui.qs.footer.ui.compose.FooterActionsDefaults.FooterButtonHeight +import com.android.systemui.qs.footer.ui.compose.rememberSystemSettingEnabled import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsForegroundServicesButtonViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsSecurityButtonViewModel @@ -141,6 +149,31 @@ fun ContentScope.FooterActionsWithAnimatedVisibility( } } +@Composable +fun rememberSystemSettingEnabled( + key: String, + defaultValue: Int = 1, + userId: Int = UserHandle.USER_CURRENT, +): State { + val context = LocalContext.current + val resolver = context.contentResolver + val uri = remember(key) { Settings.System.getUriFor(key) } + + return produceState( + initialValue = Settings.System.getIntForUser(resolver, key, defaultValue, userId) == 1, + key1 = uri, + key2 = userId, + ) { + val observer = object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean) { + value = Settings.System.getIntForUser(resolver, key, defaultValue, userId) == 1 + } + } + resolver.registerContentObserver(uri, false, observer, userId) + awaitDispose { resolver.unregisterContentObserver(observer) } + } +} + /** The Quick Settings footer actions row. */ @Composable fun FooterActions(viewModel: FooterActionsViewModel, modifier: Modifier = Modifier) { @@ -254,16 +287,25 @@ fun FooterActions(viewModel: FooterActionsViewModel, modifier: Modifier = Modifi useModifierBasedExpandable, Modifier.sysuiResTag("multi_user_switch"), ) - IconButton( - { settings }, - useModifierBasedExpandable, - Modifier.sysuiResTag("settings_button_container"), - ) - IconButton( - { viewModel.power }, - useModifierBasedExpandable, - Modifier.sysuiResTag("pm_lite"), - ) + + val showSettings by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_SETTINGS) + val showPowerMenu by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_POWER_MENU) + + if (showSettings) { + IconButton( + { settings }, + useModifierBasedExpandable, + Modifier.sysuiResTag("settings_button_container"), + ) + } + + if (showPowerMenu) { + IconButton( + { viewModel.power }, + useModifierBasedExpandable, + Modifier.sysuiResTag("pm_lite"), + ) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 633fecee67bb..a43de64a220a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.compose +import android.provider.Settings import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -30,6 +31,7 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -46,6 +48,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.development.ui.compose.BuildNumber import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.qs.footer.ui.compose.rememberSystemSettingEnabled import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType import com.android.systemui.qs.panels.ui.compose.Dimensions.FooterHeight import com.android.systemui.qs.panels.ui.compose.Dimensions.InterPageSpacing @@ -172,6 +175,8 @@ private fun FooterBar( editButtonViewModelFactory: EditModeButtonViewModel.Factory, isVisible: () -> Boolean = { true }, ) { + val showEdit by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_EDIT) + val editButtonViewModel = rememberViewModel(traceName = "PaginatedGridLayout-editButtonViewModel") { editButtonViewModelFactory.create() @@ -203,7 +208,9 @@ private fun FooterBar( ) Row(Modifier.weight(1f)) { Spacer(modifier = Modifier.weight(1f)) - EditModeButton(viewModel = editButtonViewModel, isVisible = isVisible()) + if (showEdit) { + EditModeButton(viewModel = editButtonViewModel, isVisible = isVisible()) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt index 5111ff7d7b46..c5eaa6a5898f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/toolbar/Toolbar.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.compose.toolbar +import android.provider.Settings import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibility @@ -35,6 +36,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -50,6 +52,7 @@ import com.android.systemui.common.ui.compose.load import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsButtonViewModel +import com.android.systemui.qs.footer.ui.compose.rememberSystemSettingEnabled import com.android.systemui.qs.panels.ui.compose.toolbar.Toolbar.TransitionKeys.SecurityInfoKey import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackContentViewModel import com.android.systemui.qs.panels.ui.viewmodel.TextFeedbackViewModel @@ -92,10 +95,14 @@ fun Toolbar( } } - IconButton( - viewModel.powerButtonViewModel, - Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), - ) + val showPowerMenu by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_POWER_MENU) + + if (showPowerMenu) { + IconButton( + viewModel.powerButtonViewModel, + Modifier.sysuiResTag("pm_lite").minimumInteractiveComponentSize(), + ) + } } } @@ -107,6 +114,9 @@ private fun SharedTransitionScope.StandardToolbarLayout( isFullyVisible: () -> Boolean, modifier: Modifier = Modifier, ) { + val showEdit by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_EDIT) + val showSettings by rememberSystemSettingEnabled(Settings.System.QS_FOOTER_SHOW_SETTINGS) + Row(modifier) { // User switcher button IconButton( @@ -119,13 +129,17 @@ private fun SharedTransitionScope.StandardToolbarLayout( // Edit mode button val editModeButtonViewModel = rememberViewModel("Toolbar") { viewModel.editModeButtonViewModelFactory.create() } - EditModeButton(editModeButtonViewModel, isVisible = isFullyVisible()) + if (showEdit) { + EditModeButton(editModeButtonViewModel, isVisible = isFullyVisible()) + } // Settings button - IconButton( - model = viewModel.settingsButtonViewModel, - Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), - ) + if (showSettings) { + IconButton( + model = viewModel.settingsButtonViewModel, + Modifier.sysuiResTag("settings_button_container").minimumInteractiveComponentSize(), + ) + } // Security info button SecurityInfo( From 6a11f170e461bf4f126a4cdd29f4920a2875fc58 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 19 Feb 2026 23:35:49 +0530 Subject: [PATCH 089/190] SystemUI: Fix static color in clock background chip styles Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../shared/ui/binder/HomeStatusBarViewBinder.kt | 4 ++++ .../systemui/statusbar/policy/Clock.java | 17 +++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 8dc5b2230b6d..4c67aed7917e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -723,6 +723,7 @@ constructor( ) if (style < 1 || style > clockBackgrounds.size) { + clock.setStaticColor(false) return } @@ -742,7 +743,10 @@ constructor( // Set text color to white for visibility on filled chip backgrounds // Styles 2 and 8 are outline-only (transparent background), so use normal color if (style != 2 && style != 8) { + clock.setStaticColor(true) clock.setTextColor(Color.WHITE) + } else { + clock.setStaticColor(false) } // For outline styles (2, 8), let Clock's DarkIconDispatcher handle the color } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 848038f8553a..1be9c20d7016 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -140,6 +140,8 @@ public class Clock extends TextView implements private String mClockDateFormat = null; private boolean mIsStatusBar; + private boolean useStaticColor = false; + private int lastDynamicColor = -1; // Tracks config changes that will make the clock change dimensions private final InterestingConfigChanges mInterestingConfigChanges; @@ -432,14 +434,25 @@ public void disable(int displayId, int state1, int state2, boolean animate) { @Override public void onDarkChanged(ArrayList areas, float darkIntensity, int tint) { mNonAdaptedColor = DarkIconDispatcher.getTint(areas, this, tint); - setTextColor(mNonAdaptedColor); + lastDynamicColor = mNonAdaptedColor; + if (useStaticColor) return; + setTextColor(lastDynamicColor); } // Update text color based when shade scrim changes color. public void onColorsChanged(boolean lightTheme) { final Context context = new ContextThemeWrapper(mContext, lightTheme ? R.style.Theme_SystemUI_LightWallpaper : R.style.Theme_SystemUI); - setTextColor(Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor)); + lastDynamicColor = Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColor); + if (useStaticColor) return; + setTextColor(lastDynamicColor); + } + + public void setStaticColor(boolean enable) { + useStaticColor = enable; + if (!useStaticColor && lastDynamicColor != -1) { + setTextColor(lastDynamicColor); + } } public void onDensityOrFontScaleChanged() { From da0013dd907202df3bcb0b7b3be83c2c1748fce6 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 20 Feb 2026 00:30:35 +0530 Subject: [PATCH 090/190] SystemUI: Fix neumorph color for clock background chip Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- packages/SystemUI/res/values/colors.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 7a479af9c69a..02e9a29a42fe 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -296,7 +296,7 @@ @android:color/system_accent2_200 - #1F000000 - #FFFFFFFF - #00FFFFFF + #14000000 + #FFBDBDBD + #FFF0F0F0 From 39a8f530e2b505be7c2fcb0821ddc0bb206e66ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Pra=C5=BE=C3=A1k?= Date: Thu, 9 Jan 2020 11:37:29 +0100 Subject: [PATCH 091/190] SystemUI: add FloatingRotationButton for hw-key devices Add floating rotation button for hardware key devices. Screenshot: https://imgur.com/a/KPNGD43 Co-authored-by: Timi Change-Id: I26953cb83edc28483b88cad61affade526647cbe Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../rotation/FloatingRotationButton.java | 11 ++- .../shared/rotation/RotationButton.java | 1 + .../src/com/android/systemui/Dependency.java | 3 + .../statusbar/phone/PhoneStatusBarView.java | 81 ++++++++++++++++++- 4 files changed, 94 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java index dfb62c0b2eb0..08b32160f063 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java @@ -73,6 +73,7 @@ public class FloatingRotationButton implements RotationButton { private AnimatedVectorDrawable mAnimatedDrawable; private boolean mIsShowing; + private boolean mCanShow = true; private int mDisplayRotation; private boolean mIsTaskbarVisible = false; @@ -154,7 +155,7 @@ public View getCurrentView() { @Override public boolean show() { - if (mIsShowing) { + if (!mCanShow || mIsShowing) { return false; } @@ -251,6 +252,14 @@ public void setDarkIntensity(float darkIntensity) { mKeyButtonView.setDarkIntensity(darkIntensity); } + @Override + public void setCanShowRotationButton(boolean canShow) { + mCanShow = canShow; + if (!mCanShow) { + hide(); + } + } + @Override public void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) { mIsTaskbarVisible = taskbarVisible; diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButton.java index a44472ae96c0..43d6e384f153 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButton.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/RotationButton.java @@ -40,6 +40,7 @@ default void onDestroy() {} default boolean isVisible() { return false; } + default void setCanShowRotationButton(boolean canShow) {} default void onTaskbarStateChanged(boolean taskbarVisible, boolean taskbarStashed) {} default void updateIcon(int lightIconColor, int darkIconColor) { } default void setOnClickListener(View.OnClickListener onClickListener) { } diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index db2956a6b23c..699a360b4263 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -41,6 +41,7 @@ import com.android.systemui.plugins.PluginManager; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.rotation.RotationPolicyWrapper; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManager; @@ -152,6 +153,7 @@ public class Dependency { @Inject Lazy mUserTrackerLazy; @Inject Lazy mStatusBarWindowControllerStoreLazy; @Inject Lazy mSysUIStateDisplaysInteractor; + @Inject Lazy mRotationPolicyWrapperLazy; @Inject public Dependency() { @@ -199,6 +201,7 @@ protected void start() { mProviders.put(SysUIStateDisplaysInteractor.class, mSysUIStateDisplaysInteractor::get); mProviders.put( StatusBarWindowControllerStore.class, mStatusBarWindowControllerStoreLazy::get); + mProviders.put(RotationPolicyWrapper.class, mRotationPolicyWrapperLazy::get); Dependency.setInstance(this); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index 01227c43ea07..caae7fe62d38 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -23,16 +23,21 @@ import android.graphics.Insets; import android.graphics.Rect; import android.graphics.Region; +import android.inputmethodservice.InputMethodService; +import android.os.RemoteException; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; +import android.view.ContextThemeWrapper; import android.view.Display; import android.view.DisplayCutout; +import android.view.IWindowManager; import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.WindowInsets; +import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; import android.widget.LinearLayout; @@ -41,10 +46,15 @@ import androidx.annotation.NonNull; import com.android.internal.policy.SystemBarUtils; +import com.android.settingslib.Utils; import com.android.systemui.Gefingerpoken; import com.android.systemui.res.R; import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress; import com.android.systemui.shade.StatusBarLongPressGestureDetector; +import com.android.systemui.shared.rotation.FloatingRotationButton; +import com.android.systemui.shared.rotation.RotationButtonController; +import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.CommandQueue.Callbacks; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.statusbar.policy.Offset; @@ -52,16 +62,20 @@ import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; import com.android.systemui.user.ui.viewmodel.StatusBarUserChipViewModel; import com.android.systemui.util.leak.RotationUtils; +import com.android.systemui.Dependency; +import com.android.systemui.rotation.RotationPolicyWrapper; import java.util.Objects; import java.util.function.BooleanSupplier; -public class PhoneStatusBarView extends FrameLayout { +public class PhoneStatusBarView extends FrameLayout implements Callbacks { private static final String TAG = "PhoneStatusBarView"; + private final CommandQueue mCommandQueue; private StatusBarWindowControllerStore mStatusBarWindowControllerStore; private boolean mShouldUpdateStatusBarHeightWhenControllerSet = false; private int mRotationOrientation = -1; + private RotationButtonController mRotationButtonController; @Nullable private View mCutoutSpace; @Nullable @@ -96,6 +110,54 @@ public class PhoneStatusBarView extends FrameLayout { public PhoneStatusBarView(Context context, AttributeSet attrs) { super(context, attrs); + mCommandQueue = Dependency.get(CommandQueue.class); + + // Only create FRB here if there is no navbar + if (!hasNavigationBar()) { + final Context lightContext = new ContextThemeWrapper(context, + Utils.getThemeAttr(context, R.attr.lightIconTheme)); + final Context darkContext = new ContextThemeWrapper(context, + Utils.getThemeAttr(context, R.attr.darkIconTheme)); + final int lightIconColor = + Utils.getColorAttrDefaultColor(lightContext, R.attr.singleToneColor); + final int darkIconColor = + Utils.getColorAttrDefaultColor(darkContext, R.attr.singleToneColor); + final FloatingRotationButton floatingRotationButton = new FloatingRotationButton( + context, + R.string.accessibility_rotate_button, R.layout.rotate_suggestion, + R.id.rotate_suggestion, R.dimen.floating_rotation_button_min_margin, + R.dimen.rounded_corner_content_padding, + R.dimen.floating_rotation_button_taskbar_left_margin, + R.dimen.floating_rotation_button_taskbar_bottom_margin, + R.dimen.floating_rotation_button_diameter, R.dimen.key_button_ripple_max_width, + R.bool.floating_rotation_button_position_left); + + final RotationPolicyWrapper rotationPolicyWrapper = + Dependency.get(RotationPolicyWrapper.class); + mRotationButtonController = new RotationButtonController(rotationPolicyWrapper, + lightContext, lightIconColor, darkIconColor, + R.drawable.ic_sysbar_rotate_button_ccw_start_0, + R.drawable.ic_sysbar_rotate_button_ccw_start_90, + R.drawable.ic_sysbar_rotate_button_cw_start_0, + R.drawable.ic_sysbar_rotate_button_cw_start_90, + () -> getDisplay().getRotation()); + mRotationButtonController.setRotationButton(floatingRotationButton, null); + } + } + + @Override + public void onRotationProposal(final int rotation, boolean isValid) { + if (mRotationButtonController != null && !hasNavigationBar()) { + mRotationButtonController.onRotationProposal(rotation, isValid); + } + } + + private boolean hasNavigationBar() { + try { + IWindowManager windowManager = WindowManagerGlobal.getWindowManagerService(); + return windowManager.hasNavigationBar(Display.DEFAULT_DISPLAY); + } catch (RemoteException ex) { } + return false; } void setLongPressGestureDetector( @@ -159,12 +221,20 @@ protected void onAttachedToWindow() { updateLayoutForCutout(); updateWindowHeight(); } + + if (mRotationButtonController != null && !hasNavigationBar()) { + mCommandQueue.addCallback(this); + } } @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); mDisplayCutout = null; + + if (mRotationButtonController != null) { + mCommandQueue.removeCallback(this); + } } // Per b/300629388, we let the PhoneStatusBarView detect onConfigurationChanged to @@ -330,6 +400,15 @@ public boolean onInterceptTouchEvent(MotionEvent event) { return mTouchEventHandler.onInterceptTouchEvent(event); } + @Override + public void setImeWindowStatus(int displayId, int vis, int backDisposition, + boolean showImeSwitcher) { + if (mRotationButtonController != null) { + final boolean imeShown = (vis & InputMethodService.IME_VISIBLE) != 0; + mRotationButtonController.getRotationButton().setCanShowRotationButton(!imeShown); + } + } + public void updateResources() { mCutoutSideNudge = getResources().getDimensionPixelSize( R.dimen.display_cutout_margin_consumption); From 716098de873a13f73d4d725ceec88cd0105c17f8 Mon Sep 17 00:00:00 2001 From: John Galt Date: Thu, 13 Apr 2023 15:17:28 -0400 Subject: [PATCH 092/190] SystemUI/AuthRippleView: Less Boring Dwell Ripple Change-Id: I9258a3424d40cd5df0c3f6ec3cb967dded2230f6 Change-Id: I46ea360022a701a402889ba5d9af96053d8fa1c1 Signed-off-by: Ghosuto --- .../src/com/android/systemui/biometrics/AuthRippleView.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt index 5b665ed550e1..895779840b80 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleView.kt @@ -32,7 +32,7 @@ import com.android.app.animation.Interpolators import com.android.settingslib.Utils import com.android.systemui.surfaceeffects.ripple.RippleShader -private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.3f +private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.7f /** * Handles two ripple effects: dwell ripple and unlocked ripple @@ -76,7 +76,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at private var radius: Float = 0f set(value) { field = value * .9f - rippleShader.rippleSize.setMaxSize(field * 2f, field * 2f) + rippleShader.rippleSize.setMaxSize(field * 2.2f, field * 2.2f) } private var origin: Point = Point() set(value) { @@ -95,7 +95,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at dwellShader.color = 0xffffffff.toInt() // default color dwellShader.progress = 0f - dwellShader.distortionStrength = .4f + dwellShader.distortionStrength = .8f dwellPaint.shader = dwellShader visibility = GONE } @@ -109,7 +109,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at origin = location radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat() dwellOrigin = location - dwellRadius = sensorRadius * 1.5f + dwellRadius = sensorRadius * 1.7f } /** From e863d60c5062df024ffc33f47d598046f67f2f08 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 20 Feb 2026 13:54:23 +0000 Subject: [PATCH 093/190] SystemUI: Show SSID as label when tile data usage on Signed-off-by: Ghosuto --- .../com/android/systemui/qs/tiles/WifiTile.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt index 227091bb3cd6..7e944fe239c5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.kt @@ -169,23 +169,21 @@ constructor( (tileState.icon as? Icon.Loaded)?.resId?.let { resId -> maybeLoadResourceIcon(resId) } ?: SignalIcon(SignalDrawable.getState(0, 4, false)) - label = tileState.label - - secondaryLabel = if (this.state == Tile.STATE_ACTIVE) { - if (showDataUsage) { - val dataUsage = getFormattedWifiDataUsage() - if (!TextUtils.isEmpty(dataUsage)) { - dataUsage - } else { - tileState.secondaryLabel - } + + if (this.state == Tile.STATE_ACTIVE && showDataUsage) { + val dataUsage = getFormattedWifiDataUsage() + if (!TextUtils.isEmpty(dataUsage)) { + label = tileState.secondaryLabel + secondaryLabel = dataUsage } else { - tileState.secondaryLabel + label = tileState.label + secondaryLabel = tileState.secondaryLabel } } else { - null + label = tileState.label + secondaryLabel = if (this.state == Tile.STATE_ACTIVE) tileState.secondaryLabel else null } - + contentDescription = tileState.contentDescription expandedAccessibilityClassName = tileState.expandedAccessibilityClassName handlesSecondaryClick = false From 474df26412f2a85ffa56eecda9f2a559a9808a59 Mon Sep 17 00:00:00 2001 From: Travis Allen Date: Thu, 12 Feb 2026 12:35:12 -0500 Subject: [PATCH 094/190] Allow complex resource types to follow references Address a bug where complex resources (Styles, arrays, etc) can use resource references ( --- libs/androidfw/Android.bp | 8 ++ libs/androidfw/AssetManager2.cpp | 27 +++++ libs/androidfw/tests/AssetManager2_test.cpp | 96 ++++++++++++++++++ libs/androidfw/tests/LoadedArsc_test.cpp | 6 ++ libs/androidfw/tests/data/basic/R.h | 4 + libs/androidfw/tests/data/basic/basic.apk | Bin 5448 -> 6856 bytes .../tests/data/basic/basic_de_fr.apk | Bin 1327 -> 2363 bytes .../tests/data/basic/basic_hdpi-v4.apk | Bin 1151 -> 1135 bytes .../tests/data/basic/basic_xhdpi-v4.apk | Bin 1155 -> 1139 bytes .../tests/data/basic/basic_xxhdpi-v4.apk | Bin 1155 -> 1139 bytes .../tests/data/basic/res/values/values.xml | 23 ++++- 11 files changed, 161 insertions(+), 3 deletions(-) diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 5b3012875ac5..a529618ecf62 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -272,6 +272,14 @@ cc_test { ":FrameworkResourcesNotSparseTestApp", ], test_suites: ["device-tests"], + test_options: { + test_runner_options: [ + { + name: "native-test-timeout", + value: "1m", + } + ] + }, } cc_benchmark { diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 1186772cac86..8d922486ebd3 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -1256,6 +1256,33 @@ base::expected AssetManager2::GetBag( return base::unexpected(entry.error()); } + // Resolve reference resources recursively + auto final_resid = resid; + std::vector referenced_ids; + while (!(entry->entry_flags & ResTable_entry::FLAG_COMPLEX)) { + if (final_resid == 0U || + std::find(referenced_ids.begin(), referenced_ids.end(), + final_resid) != referenced_ids.end()) { + return base::unexpected(std::nullopt); + } + referenced_ids.push_back(final_resid); + auto value = GetResource(final_resid, false, 0); + if (value.has_value() && value->type == Res_value::TYPE_REFERENCE) { + // Follow the reference + final_resid = value->data; + // Re-fetch the entry for the target of the reference + entry = FindEntry(final_resid, 0u, false, false); + + if (!entry.has_value()) { + // Target is still not a bag, can't resolve as a bag + return base::unexpected(std::nullopt); + } + } else { + // Not a bag and not a reference to one + return base::unexpected(std::nullopt); + } + } + auto entry_map = std::get_if>(&entry->entry); if (entry_map == nullptr) { // Not a bag, nothing to do. diff --git a/libs/androidfw/tests/AssetManager2_test.cpp b/libs/androidfw/tests/AssetManager2_test.cpp index 7e1a0add672f..26186a461836 100644 --- a/libs/androidfw/tests/AssetManager2_test.cpp +++ b/libs/androidfw/tests/AssetManager2_test.cpp @@ -889,4 +889,100 @@ TEST_F(AssetManager2Test, GetFlaggedAssets) { EXPECT_TRUE(value->entry_flags & ResTable_entry::FLAG_USES_FEATURE_FLAGS); } +TEST_F(AssetManager2Test, FindsBagThroughConcreteStringArray) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::concrete_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, FindsBagThroughSingleReference) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::aliased_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, FindsBagThroughDoubleReference) { + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::double_aliased_string_array); + ASSERT_TRUE(bag.has_value()); + + ASSERT_EQ(3u, (*bag)->entry_count); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[0].value.dataType); + EXPECT_EQ(std::string("a"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[0].cookie), (*bag)->entries[0].value.data)); + EXPECT_EQ(0, (*bag)->entries[0].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[1].value.dataType); + EXPECT_EQ(std::string("b"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[1].cookie), (*bag)->entries[1].value.data)); + EXPECT_EQ(0, (*bag)->entries[1].cookie); + + EXPECT_EQ(static_cast(Res_value::TYPE_STRING), (*bag)->entries[2].value.dataType); + EXPECT_EQ(std::string("c"), + GetStringFromPool(assetmanager.GetStringPoolForCookie((*bag)->entries[2].cookie), (*bag)->entries[2].value.data)); + EXPECT_EQ(0, (*bag)->entries[2].cookie); +} + +TEST_F(AssetManager2Test, DetectsCircularBag) { + // If the test fails because it is hung you will see the following: + // Result: died but not with expected exit code: + // Terminated by signal 14 + // caused by the alarm(1) timeout. + EXPECT_EXIT({ + alarm(1); + AssetManager2 assetmanager; + assetmanager.SetApkAssets({basic_assets_}); + + auto bag = assetmanager.GetBag(basic::R::array::circular_aliased_string_array1); + if (!bag.has_value()) { + // Success, a circular reference should not return a bag + exit(0); + } + // Fail, we returned a bag + fprintf(stderr, "GetBag returned a bag from a circular reference!"); + exit(1); + }, ::testing::ExitedWithCode(0), ""); +} + } // namespace android diff --git a/libs/androidfw/tests/LoadedArsc_test.cpp b/libs/androidfw/tests/LoadedArsc_test.cpp index eb90765cd17a..3bf4ade50084 100644 --- a/libs/androidfw/tests/LoadedArsc_test.cpp +++ b/libs/androidfw/tests/LoadedArsc_test.cpp @@ -288,6 +288,12 @@ TEST(LoadedArscTest, ResourceIdentifierIterator) { ASSERT_EQ(0x7f050000u, *iter++); ASSERT_EQ(0x7f050001u, *iter++); ASSERT_EQ(0x7f060000u, *iter++); + ASSERT_EQ(0x7f060001u, *iter++); + ASSERT_EQ(0x7f060002u, *iter++); + ASSERT_EQ(0x7f060003u, *iter++); + ASSERT_EQ(0x7f060004u, *iter++); + ASSERT_EQ(0x7f060005u, *iter++); + ASSERT_EQ(0x7f060006u, *iter++); ASSERT_EQ(0x7f070000u, *iter++); ASSERT_EQ(0x7f070001u, *iter++); ASSERT_EQ(0x7f070002u, *iter++); diff --git a/libs/androidfw/tests/data/basic/R.h b/libs/androidfw/tests/data/basic/R.h index b7e814fea079..d6eb0e41e461 100644 --- a/libs/androidfw/tests/data/basic/R.h +++ b/libs/androidfw/tests/data/basic/R.h @@ -73,6 +73,10 @@ struct R { struct array { enum : uint32_t { integerArray1 = 0x7f060000, + concrete_string_array = 0x7f060001, + aliased_string_array = 0x7f060002, + double_aliased_string_array = 0x7f060003, + circular_aliased_string_array1 = 0x7f060004, }; }; }; diff --git a/libs/androidfw/tests/data/basic/basic.apk b/libs/androidfw/tests/data/basic/basic.apk index b721ebfde4459a75a626a452c8fd573614469bfb..42a50eaf70d30538f32f079c7a4b3eaa0e2ae3a5 100644 GIT binary patch delta 1686 zcmZ`(4NOy46h80uwMAMC&{w2DTYh8?#*l)z5K(?M#RPDYXa>dwN@;7Q0&D4p8?ns9 zIh};*WsI2s%7i)j*<42=Bu zUHve!W3{O^=w!aipl5WtpoVYr#~bs#S*KRvkn$j(E{;InmRsSOYu7ATfnRXv{y4Kp!b>y>?druwj>)WjR zN}bmpR@Tao{;mC}dv?12%--YUn|F@9?=C1!h%u{FBKwZR?<8hTUH{Z|CEK1>d!qfc zwDHDoZHk$ZFD^`G=9*SszdL;X_FrEgm>B=~xx<~R-ri~FgQL^0)jxkV(RAm4uXAk6 z*q85)_L$ul?`wwS!)?u-NlnU)b9Hx2BbQdKdO|;Z&V4&Qeo`|Lb&m{~PdO&cvt#Vf zS+!Mf7x+zFemVbPL0oj!tM)(A=7y9z8}7FMzB{=hG`g$qDcSD8>RDIQ!MdLB4VzlG zKb^8Bc69v*16^n5WQ%$($EI@uH;pye>|10X(;T#T9RD=gXVo|kU|l+PBeTS|VY2>> zEoduZUG*X}qg)(!uxU-wCJd`2;o@EYV`&jvJo(TOt7v(zpf@fMATE%^OG0*0mhQ|z z7)&AcrCxNQ@|k%~_|nTm?~WXyL*yX#AZic|h-L)C=n=&T14KrWIzEzgN~+m)0j^kS z9}`f`0z{&lurU1@GUHPk5ro{6h-`!vQH$tC^dc@JuDL8L7{;ko8p_KIHe;o+&|Yb? zl$i_lHk*Dw@Vwq?(c6tBOVy#FCFPD{tFdrdt43q6*bEM<-nOh=2ah-;EX|+zsK-)= zBx3PRW*E}q+re-oTjmIm;<0^z1^nY^BSjK2A}dAPh-@vf%2OFJa#}Jd&k3U*EbEU0 zK{uQOm2st0kOaR9Eu8~RB^D@%Rv@JTTaj`ga$WMf&*I)4{^wKTP$j_}ba!0o%Yf>E zVRyje*lh{&)VG4<$wNtzG)}n=B|7LT#OsJg#91;Bpm3r)gQfT*#bV}pq_kc^PwVza zDHthTRQALONAkpoXuKyzMC0kvR074D1QI8eLp(C;kwXF2(>5|+j=w}P=)n#a zFhC)c!;4@98(2}dA5n~)5qF!wHeOqVHy;yYJ+vwG;Pk*aVkfqK;f+QW{-|UqFc*u# z5>W6y*d#`=7IpJ#QltoBY~&S1c+`I??^^;3ou>%_qRA(Us0Baoy8=SD=Y8n6ig5gD z?-mJs2s^`(9A53!y}*YsCFB^NsKwg6FABg)ZyMx&5R;p{T0{@?5VNEP6O}SXQYT!9 Fe*q2YfRX?J delta 1316 zcmaJ>dq|T}6ukcJHmtGdTGk|GZLqA(&M%lym}A>_(3pHZR$2D4XyYoMGF3omRFsmN z_b4T7BOZ0y9H1Lr8CAae={A(yYPl5R$mqDC3!B(*F{Zb>XX%9m^ZcrZuG)i}o12d$ zJ7gnTH6?kSik6+uqPDBs?Kw@Ub!DkFZ!%KGPYJ6xU(Yd{oSzQVFMl7s@MOhZ7oW4` z+1wYLYfm3Byt`UmtL>?M;_R`sR}>jXepy$XlH7i%`^&V*bnCsktk<^m-kO(JGGlHR zzg1^0Uf%m{(TeiprVmA5wQUWCF8hy3SH7RC%1Am@5|DZG*5la7_Qo`Wb7kOcr0QF} zE7un9xY5}4OTHu5US54n+&1}V2DLTaAM2MM)ScPsKjZV3YkeKI2OZ}cjrY$?{e1hP zgP0N}(meI9=~_6$Jc7{B=cHcF=B}Q zbu|BbFuSS$ptvhV>u`ibI-{$4?|g_|Idm+QbITxd$I;O-v!H0f&>hfioCWazjf51> z^vLgp7RnJSl+zy^KZmKTPmF@lV~`RS#t4klJesgZz#0pj4$R>909OHrhf{w(oK}h* zmh(6BE!vXn|s(P?->=0KuXe1H}Tj(r1di5Dqj}$|I3T411Ok4-#P9 zR=>9dP}5Y6VF6%bZs?0qD<)=C0ZPAZ3H_*8Fk6I%3GM+b_8}_&P7HZZf=wJsCm>@}s6k5@H_G%E!j(Cs6_>X_MXD|3Y0|y4-5@~`iN!|Y* zcqf|H=?v0-GwsyzBo)*Bc-S~oB7g$*`v#9E>&i=dLgNTO;qQdOGs@cbk^}xPJuBAJ z6MCr^`@EhfGLEV^t@7_u^dtCobF`EjHPDw(jy-Jy!-*&6Z;T5V8JRXuXJ%#8U}555kpWtf#E{5P z%#g{D%n;9z&rrsY%233R0~D(S;v^tz@3L;z)=Z5E(F;gH@4j1xPsq06_;W@c;k- delta 180 zcmdljw4O^Mz?+$cfq?@E85-s@GQhz`2`feib`G(G)u-fv!aYn33=pMY6+oryYV5xX zFf%ZKFdtB`D784hv?w{XSTC`tIGKro2PiYSi7|<34b$dlj0+eU88){wvoZ<z?+$c0SGvNSdk%v;rSdbMh1o{OdBO!83iCxAZbO0V}|TgH`g-0Wn^TW ioXy+`R1eWI`46)lJ5+wMBa0Yg^yCN@MYcQ4AZ-Aw{1XlU delta 115 zcmaFQ@t;E?z?+$cfq?@E85-s@GQhz`30Fo1c8&~&=X11x!c&+S7$8c)Du7C7CGK9c pIhXM*BO}A)Xy#51m=TlTFx#<1ri~IFi~_-gCM@;TcQ}3=pMY6+orEPi}^8 tE?|7e$jC4`p1G3)X2j$V%yx26`4FHk5axp$3u5F=_F++E`@jrh0|1$d8H4}; diff --git a/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk b/libs/androidfw/tests/data/basic/basic_xxhdpi-v4.apk index 61369d5067862d7dd1b7ab2cb8c97d371b60d9a2..e10788980613a664c42d2a4421653e7d77db6e6d 100644 GIT binary patch delta 115 zcmZqX{LCQ{;LXg!00bOBtjMrz&u4XQMh1pCOdBQK7zH3wAZbMghplhsZLVYdz{tor mIiI-`s2-wa5{sQ2R62r*fdPd1Ksw+cW^xRRBHIIIkN^Pj-4qD` delta 132 zcmey&(ab3k;LXg!z`y~73=Q)c8Q@@}gd3v*JIAs;pVhU2!gH7y7$8c)Du7C5o(3%6 toX7Zqk&$6?JaZ=p%!tV!nC;}C@)1B?Aj}6h7R1Pz?8BnS_JJA11^{`r7@z3 - - - + + + + + @id/middle_ref @id/low_ref + + + + a + b + c + + + @array/concrete_string_array + + @array/aliased_string_array + + @array/circular_aliased_string_array2 + @array/circular_aliased_string_array3 + @array/circular_aliased_string_array1 From cc1f77348df6a67bcf382e93f69768531ba5e93a Mon Sep 17 00:00:00 2001 From: ruyue Date: Wed, 11 Feb 2026 15:58:11 +0800 Subject: [PATCH 095/190] base: Fix crash caused by back gesture on popup view Change-Id: Ib9f9cd63713946004fb0f0e7622de572f1b2431c Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/view/SurfaceControlViewHost.java | 6 ++++-- core/java/android/view/ViewRootImpl.java | 12 +++++++++--- .../WindowlessSnapshotWindowCreator.java | 3 +++ 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index c26506eafe99..3242716bd8b9 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -511,8 +511,10 @@ public void setView(@NonNull View view, @NonNull WindowManager.LayoutParams attr attrs.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; addWindowToken(attrs); view.setLayoutParams(attrs); - mViewRoot.setView(view, attrs, null); - mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); + if (mViewRoot.mDisplay != null) { + mViewRoot.setView(view, attrs, null); + mViewRoot.setBackKeyCallbackForWindowlessWindow(mWm::forwardBackKeyToParent); + } } /** diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index e7251622facd..66d15d3d466a 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1559,8 +1559,12 @@ public void setView(View view, WindowManager.LayoutParams attrs, View panelParen attrs.setSurfaceInsets(view, false /*manual*/, true /*preservePrevious*/); } - CompatibilityInfo compatibilityInfo = - mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + CompatibilityInfo compatibilityInfo; + if (mDisplay != null) { + compatibilityInfo = mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + } else { + compatibilityInfo = CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; + } mTranslator = compatibilityInfo.getTranslator(); // If the application owns the surface, don't enable hardware acceleration @@ -6771,7 +6775,9 @@ private void performConfigurationChange(@NonNull MergedConfiguration mergedConfi + ", globalConfig: " + globalConfig + ", overrideConfig: " + overrideConfig); - final CompatibilityInfo ci = mDisplay.getDisplayAdjustments().getCompatibilityInfo(); + final CompatibilityInfo ci = mDisplay != null + ? mDisplay.getDisplayAdjustments().getCompatibilityInfo() + : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO; if (!ci.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) { globalConfig = new Configuration(globalConfig); overrideConfig = new Configuration(overrideConfig); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java index 3d211516f6bb..3c193f2baaf3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/WindowlessSnapshotWindowCreator.java @@ -72,6 +72,9 @@ void makeTaskSnapshotWindow(StartingWindowInfo info, SurfaceControl rootSurface, return; } final Display display = mDisplayManager.getDisplay(runningTaskInfo.displayId); + if (display == null) { + return; + } final StartingSurfaceDrawer.WindowlessStartingWindow wlw = new StartingSurfaceDrawer.WindowlessStartingWindow( mContext.getResources().getConfiguration(), rootSurface); From 93ca2228cc881215a64ed643713f17fde85dd500 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:43:55 +0800 Subject: [PATCH 096/190] SystemUI: switch to mirror blur method Change-Id: I024055b5ce6e0dc080e74b4d8b5598c05a2a56ba Signed-off-by: Ghosuto --- packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java | 2 +- .../android/systemui/shade/NotificationPanelViewController.java | 2 +- .../notification/stack/NotificationStackScrollLayout.java | 2 +- .../stack/NotificationStackScrollLayoutController.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java index 97ef9fc90b41..0e350359d491 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimView.java @@ -407,7 +407,7 @@ public void setBlurRadius(float blurRadius) { setRenderEffect(RenderEffect.createBlurEffect( blurRadius, blurRadius, - Shader.TileMode.CLAMP)); + Shader.TileMode.MIRROR)); } else { debugLog("Resetting blur RenderEffect to ScrimView " + mScrimName); setRenderEffect(null); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 809e3d4682d2..bcbc790bb364 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1072,7 +1072,7 @@ private void handleBouncerShowingChanged(Boolean isBouncerShowing) { mBlurRenderEffect = RenderEffect.createBlurEffect( mBlurConfig.getMaxBlurRadiusPx(), mBlurConfig.getMaxBlurRadiusPx(), - Shader.TileMode.CLAMP); + Shader.TileMode.MIRROR); } debugLog("Applying blur RenderEffect to shade."); Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_APP, "ShadeBlurRenderEffect", "active", 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 9a3a7daead9d..c8149990863a 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 @@ -6539,7 +6539,7 @@ public void setBlurRadius(float blurRadius) { private void updateBlurEffect() { if (mBlurRadius > 0) { mBlurEffect = - RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.CLAMP); + RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.MIRROR); spewLog("Setting up blur RenderEffect for NotificationStackScrollLayout"); } else { spewLog("Clearing the blur RenderEffect setup for NotificationStackScrollLayout"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 072a3cab5c13..4fdf8eabd759 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1491,7 +1491,7 @@ public void setBlurRadius(float blurRadius) { mView.setRenderEffect(RenderEffect.createBlurEffect( blurRadius, blurRadius, - Shader.TileMode.CLAMP)); + Shader.TileMode.MIRROR)); } else { debugLog("Resetting blur RenderEffect for NotificationStackScrollLayoutController"); mView.setRenderEffect(null); From 061071692cf0fd3200d0947f16f9e92130e55c66 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Mon, 23 Feb 2026 07:42:46 +0000 Subject: [PATCH 097/190] SystemUI: Reduce default system blur Signed-off-by: Ghosuto --- packages/SystemUI/res/values/dimens.xml | 2 +- .../AlternateBouncerToPrimaryBouncerTransitionViewModel.kt | 2 +- .../SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 44e637247433..2c91e7d0fb71 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1341,7 +1341,7 @@ @dimen/max_window_blur_radius - 40dp + 34dp 0px diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt index 937f79d15e05..82c2980dd66c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToPrimaryBouncerTransitionViewModel.kt @@ -93,7 +93,7 @@ constructor( } val maxBlurRadius: Float - get() = secureSettings.getFloat("system_blur_radius", 70f) + get() = secureSettings.getFloat("system_blur_radius", 34f) override val notificationBlurRadius: Flow = if (Flags.bouncerUiRevamp()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt index 2bde29cb21d5..fe16970fe9e1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BlurUtils.kt @@ -60,7 +60,7 @@ constructor( ) : Dumpable { val minBlurRadius = resources.getDimensionPixelSize(R.dimen.min_window_blur_radius).toFloat() val maxBlurRadius: Float - get() = secureSettings.getFloatForUser("system_blur_radius", 70f, UserHandle.USER_CURRENT) + get() = secureSettings.getFloatForUser("system_blur_radius", 34f, UserHandle.USER_CURRENT) val maxBlurRadiusFlow: Flow = secureSettings .observerFlow("system_blur_radius") From f9cb446292e2733d03a765b0e816750f8f91c5f5 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 20 Oct 2025 08:31:07 +0800 Subject: [PATCH 098/190] SystemUI: Adding system color hooks Change-Id: I9b653ac17549b245c08090453aafc8dbb0fc2a0b Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../theme/ThemeOverlayController.java | 100 +++++++++++++----- 1 file changed, 75 insertions(+), 25 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index b80bf940d329..efe962992791 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.cam.Cam; +import com.android.internal.graphics.ColorUtils; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -92,6 +94,7 @@ import com.google.ux.material.libmonet.dynamiccolor.DynamicColor; import com.google.ux.material.libmonet.dynamiccolor.MaterialDynamicColors; +import com.google.ux.material.libmonet.hct.Cam16; import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.flow.StateFlow; @@ -156,6 +159,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 @@ -490,6 +495,7 @@ public void start() { mThemeController.observeSettings(() -> reevaluateSystemTheme(true)); mBroadcastDispatcher.registerReceiver(mBroadcastReceiver, filter, mMainExecutor, UserHandle.ALL); + int userId = mUserTracker.getUserId(); mSecureSettings.registerContentObserverForUserSync( Settings.Secure.THEME_CUSTOMIZATION_OVERLAY_PACKAGES, false, @@ -515,19 +521,6 @@ public void onChange(boolean selfChange, Collection collection, int flags, } }, UserHandle.USER_ALL); - int userId = mUserTracker.getUserId(); - if (fixContrastAndForceInvertStateForMultiUser()) { - UiModeManager uiModeManager = mUiModeManagerProvider.forUser(UserHandle.of(userId)); - uiModeManager.addContrastChangeListener(mMainExecutor, mContrastChangeListener); - mContrast = uiModeManager.getContrast(); - } else { - mContrast = mUiModeManager.getContrast(); - mUiModeManager.addContrastChangeListener(mMainExecutor, contrast -> { - mContrast = contrast; - // Force reload so that we update even when the main color has not changed - reevaluateSystemTheme(true /* forceReload */); - }); - } mSecureSettings.registerContentObserverForUserSync( LineageSettings.Secure.getUriFor(LineageSettings.Secure.BERRY_BLACK_THEME), @@ -672,6 +665,7 @@ private void reevaluateSystemTheme(boolean forceReload) { mMainWallpaperColor = mainColor; if (mIsMonetEnabled) { + fetchCustomThemeSettings(); mThemeStyle = fetchThemeStyleFromSetting(); createOverlays(mMainWallpaperColor); mNeedsOverlayCreation = true; @@ -684,6 +678,28 @@ 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.optBoolean("_fidelity_enabled", true); + 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. */ @@ -713,8 +729,12 @@ protected boolean isPrivateProfile(UserHandle userHandle) { } private void createOverlays(int color) { - mDarkColorScheme = new ColorScheme(color, true /* isDark */, mThemeStyle, mContrast); - mLightColorScheme = new ColorScheme(color, false /* isDark */, mThemeStyle, mContrast); + int style = mThemeStyle; + if (mIsFidelityEnabled) { + style = ThemeStyle.CONTENT; + } + mDarkColorScheme = new ColorScheme(color, true /* isDark */, style, mContrast); + mLightColorScheme = new ColorScheme(color, false /* isDark */, style, mContrast); mColorScheme = isNightMode() ? mDarkColorScheme : mLightColorScheme; mAccentOverlay = newFabricatedOverlay("accent"); @@ -734,20 +754,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)); } /** @@ -803,6 +849,10 @@ private void updateThemeOverlays() { if (!TextUtils.isEmpty(overlayPackageJson)) { try { JSONObject object = new JSONObject(overlayPackageJson); + + mContrast = object.optDouble("_contrast_level", 0.0); + mChromaBoost = object.optDouble("_chroma_boost", 0.0); + for (String category : ThemeOverlayApplier.THEME_CATEGORIES) { if (object.has(category)) { OverlayIdentifier identifier = @@ -921,7 +971,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; } From 0f13e4028850edb268d218238ca8c9e00a9176ff Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Tue, 24 Feb 2026 00:34:49 +0530 Subject: [PATCH 099/190] SystemUI: Hide mobile data tile when unsupported Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../domain/interactor/MobileDataTileDataInteractor.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 509162339011..bc2bfd7c5de6 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 @@ -18,6 +18,7 @@ package com.android.systemui.qs.tiles.impl.cell.domain.interactor import android.content.Context import android.os.UserHandle +import android.telephony.TelephonyManager import com.android.settingslib.graph.SignalDrawable import com.android.systemui.Flags as AconfigFlags import com.android.systemui.common.shared.model.ContentDescription @@ -126,7 +127,12 @@ constructor( override fun availability(user: UserHandle): Flow = flowOf(isAvailable()) + fun isVoiceCapable(): Boolean { + val telephony = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + return telephony?.isVoiceCapable == true + } + fun isAvailable(): Boolean { - return AconfigFlags.qsSplitInternetTile() + return isVoiceCapable() && AconfigFlags.qsSplitInternetTile() } } From 144b66c5f25d789227fa99d266bae726c5073bc8 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 18:51:09 +0000 Subject: [PATCH 100/190] SystemUI: Show lockscreen emergency button by default Signed-off-by: Ghosuto --- .../src/com/android/keyguard/KeyguardAbsKeyInputView.java | 2 +- .../SystemUI/src/com/android/keyguard/KeyguardPatternView.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java index 2cccb62d6299..b286accef388 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardAbsKeyInputView.java @@ -87,7 +87,7 @@ private void updateEmergencyButtonVisibility() { boolean showButton = Settings.Secure.getInt( mContext.getContentResolver(), Settings.Secure.LOCKSCREEN_SHOW_EMERGENCY_BUTTON, - 0) == 1; + 1) == 1; mEcaView.setVisibility(showButton ? View.VISIBLE : View.GONE); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java index a46b4443dcac..1d0daa58bd5f 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardPatternView.java @@ -276,7 +276,7 @@ private void updateEmergencyButtonVisibility() { boolean showButton = Settings.Secure.getInt( mContext.getContentResolver(), Settings.Secure.LOCKSCREEN_SHOW_EMERGENCY_BUTTON, - 0) == 1; + 1) == 1; mEcaView.setVisibility(showButton ? View.VISIBLE : View.GONE); } } From a0e5b525b4c49044b459d48c2b803106fcf19072 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 19:31:35 +0000 Subject: [PATCH 101/190] SystemUI: Introduce cutout progress ring [1/2] - Initial code based on https://github.com/hxreborn/punch-hole-download-progress Co-authored-by: hxreborn Signed-off-by: Ghosuto --- .../CutoutProgressController.java | 167 +++++ .../CutoutProgressSettings.java | 401 ++++++++++++ .../cutoutprogress/DownloadStateTracker.java | 197 ++++++ .../dagger/CutoutProgressModule.java | 34 ++ .../ring/CapsuleRingRenderer.java | 106 ++++ .../ring/CircleRingRenderer.java | 61 ++ .../ring/CountBadgePainter.java | 108 ++++ .../cutoutprogress/ring/CutoutRingView.java | 572 ++++++++++++++++++ .../ring/OverlayAnimationHelper.java | 325 ++++++++++ .../cutoutprogress/ring/RingViewRenderer.java | 35 ++ .../systemui/dagger/SystemUIModule.java | 2 + 11 files changed, 2008 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java create mode 100644 packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java new file mode 100644 index 000000000000..566bafb6eaaf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java @@ -0,0 +1,167 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress; + +import android.content.Context; +import android.graphics.PixelFormat; +import android.os.Handler; +import android.view.WindowManager; + +import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.cutoutprogress.ring.CutoutRingView; +import com.android.systemui.statusbar.notification.collection.NotifPipeline; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; + +import javax.inject.Inject; + +@SysUISingleton +public class CutoutProgressController implements CoreStartable { + + private static final int WINDOW_TYPE = 2024; + + private final Context mContext; + private final NotifPipeline mPipeline; + private final Handler mMainHandler; + + private final CutoutProgressSettings mSettings; + private final DownloadStateTracker mTracker; + private CutoutRingView mRingView; + + private boolean mOverlayAttached = false; + private boolean mListenerRegistered = false; + + @Inject + public CutoutProgressController( + Context context, + NotifPipeline notifPipeline, + @Main Handler mainHandler) { + mContext = context; + mPipeline = notifPipeline; + mMainHandler = mainHandler; + mSettings = new CutoutProgressSettings( + context.getContentResolver(), mainHandler); + mTracker = new DownloadStateTracker(); + } + + @Override + public void start() { + mRingView = new CutoutRingView(mContext); + mRingView.applySettings(mSettings); + bindTrackerToView(); + mSettings.observe(this::onSettingsChanged); + onSettingsChanged(); + } + + private void onSettingsChanged() { + mRingView.applySettings(mSettings); + + if (mSettings.isEnabled()) { + enableFeature(); + } else { + disableFeature(); + } + } + + private void enableFeature() { + attachOverlay(); + registerPipelineListener(); + } + + private void disableFeature() { + mTracker.reset(); + detachOverlay(); + } + + private void attachOverlay() { + if (mOverlayAttached) return; + + WindowManager.LayoutParams params = new WindowManager.LayoutParams( + WindowManager.LayoutParams.MATCH_PARENT, + WindowManager.LayoutParams.MATCH_PARENT, + WINDOW_TYPE, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS + | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, + PixelFormat.TRANSLUCENT); + + params.layoutInDisplayCutoutMode = + WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; + params.setTitle("CutoutProgressOverlay"); + + WindowManager wm = mContext.getSystemService(WindowManager.class); + if (wm != null) { + wm.addView(mRingView, params); + mOverlayAttached = true; + } + } + + private void detachOverlay() { + if (!mOverlayAttached) return; + WindowManager wm = mContext.getSystemService(WindowManager.class); + if (wm != null) { + wm.removeView(mRingView); + mOverlayAttached = false; + } + } + + private void registerPipelineListener() { + if (mListenerRegistered) return; + mListenerRegistered = true; + + mPipeline.addCollectionListener(new NotifCollectionListener() { + + @Override + public void onEntryAdded(NotificationEntry entry) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationChanged(entry); + } + + @Override + public void onEntryUpdated(NotificationEntry entry) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationChanged(entry); + } + + @Override + public void onEntryRemoved(NotificationEntry entry, int reason) { + if (!mSettings.isEnabled()) return; + mTracker.onNotificationRemoved(entry, reason); + } + }); + } + + private void bindTrackerToView() { + mTracker.setOnProgress(progress -> + mMainHandler.post(() -> mRingView.setProgress(progress))); + + mTracker.setOnComplete(() -> + mMainHandler.post(() -> mRingView.setProgress(100))); + + mTracker.setOnError(() -> + mMainHandler.post(() -> mRingView.showError())); + + mTracker.setOnCountChanged(count -> + mMainHandler.post(() -> mRingView.setDownloadCount(count))); + + mTracker.setOnLabelChanged(label -> + mMainHandler.post(() -> mRingView.setFilenameHint(label))); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java new file mode 100644 index 000000000000..3a7fdc14d84e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.graphics.Color; +import android.net.Uri; +import android.os.Handler; +import android.provider.Settings; + +public final class CutoutProgressSettings { + + public static final String KEY_ENABLED = "cutout_progress_enabled"; + + public static final String KEY_RING_COLOR = "cutout_progress_ring_color"; + + public static final String KEY_ERROR_COLOR = "cutout_progress_error_color"; + + public static final String KEY_FINISH_FLASH_COLOR = "cutout_progress_finish_flash_color"; + + public static final String KEY_STROKE_WIDTH_DP10 = "cutout_progress_stroke_width_dp10"; + + public static final String KEY_RING_GAP_X1000 = "cutout_progress_ring_gap_x1000"; + + public static final String KEY_OPACITY = "cutout_progress_opacity"; + + public static final String KEY_CLOCKWISE = "cutout_progress_clockwise"; + + public static final String KEY_FINISH_STYLE = "cutout_progress_finish_style"; + + public static final String KEY_FINISH_HOLD_MS = "cutout_progress_finish_hold_ms"; + + public static final String KEY_FINISH_EXIT_MS = "cutout_progress_finish_exit_ms"; + + public static final String KEY_FINISH_USE_FLASH = "cutout_progress_finish_use_flash"; + + public static final String KEY_COMPLETION_PULSE = "cutout_progress_completion_pulse"; + + public static final String KEY_PATH_MODE = "cutout_progress_path_mode"; + + public static final String KEY_RING_SCALE_X_X1000 = "cutout_progress_ring_scale_x_x1000"; + + public static final String KEY_RING_SCALE_Y_X1000 = "cutout_progress_ring_scale_y_x1000"; + + public static final String KEY_RING_OFFSET_X_DP10 = "cutout_progress_ring_offset_x_dp10"; + + public static final String KEY_RING_OFFSET_Y_DP10 = "cutout_progress_ring_offset_y_dp10"; + + public static final String KEY_BG_RING_ENABLED = "cutout_progress_bg_ring_enabled"; + + public static final String KEY_BG_RING_COLOR = "cutout_progress_bg_ring_color"; + + public static final String KEY_BG_RING_OPACITY = "cutout_progress_bg_ring_opacity"; + + public static final String KEY_MIN_VIS_ENABLED = "cutout_progress_min_vis_enabled"; + + public static final String KEY_MIN_VIS_MS = "cutout_progress_min_vis_ms"; + + public static final String KEY_SHOW_COUNT_BADGE = "cutout_progress_show_count_badge"; + + public static final String KEY_BADGE_OFFSET_X_DP10 = "cutout_progress_badge_offset_x_dp10"; + + public static final String KEY_BADGE_OFFSET_Y_DP10 = "cutout_progress_badge_offset_y_dp10"; + + public static final String KEY_BADGE_TEXT_SIZE_SP10 = "cutout_progress_badge_text_size_sp10"; + + public static final String KEY_PERCENT_ENABLED = "cutout_progress_percent_enabled"; + + public static final String KEY_PERCENT_SIZE_SP10 = "cutout_progress_percent_size_sp10"; + + public static final String KEY_PERCENT_BOLD = "cutout_progress_percent_bold"; + + public static final String KEY_PERCENT_POSITION = "cutout_progress_percent_position"; + + public static final String KEY_PERCENT_OFFSET_X = "cutout_progress_percent_offset_x"; + + public static final String KEY_PERCENT_OFFSET_Y = "cutout_progress_percent_offset_y"; + + public static final String KEY_FILENAME_ENABLED = "cutout_progress_filename_enabled"; + + public static final String KEY_FILENAME_SIZE_SP10 = "cutout_progress_filename_size_sp10"; + + public static final String KEY_FILENAME_BOLD = "cutout_progress_filename_bold"; + + public static final String KEY_FILENAME_POSITION = "cutout_progress_filename_position"; + + public static final String KEY_FILENAME_OFFSET_X = "cutout_progress_filename_offset_x"; + + public static final String KEY_FILENAME_OFFSET_Y = "cutout_progress_filename_offset_y"; + + public static final String KEY_FILENAME_MAX_CHARS = "cutout_progress_filename_max_chars"; + + public static final String KEY_FILENAME_TRUNCATE = "cutout_progress_filename_truncate"; + + public static final String KEY_PROGRESS_EASING = "cutout_progress_easing"; + + private static final boolean DEF_ENABLED = false; + private static final int DEF_RING_COLOR = 0xFF2196F3; + private static final int DEF_ERROR_COLOR = 0xFFF44336; + private static final int DEF_FINISH_FLASH_COLOR = Color.WHITE; + private static final float DEF_STROKE_DP = 2.0f; + private static final float DEF_RING_GAP = 1.155f; + private static final int DEF_OPACITY = 90; + private static final boolean DEF_CLOCKWISE = true; + private static final int DEF_FINISH_STYLE = 0; + private static final int DEF_FINISH_HOLD_MS = 500; + private static final int DEF_FINISH_EXIT_MS = 500; + private static final boolean DEF_FINISH_USE_FLASH = true; + private static final boolean DEF_COMPLETION_PULSE = true; + private static final boolean DEF_PATH_MODE = false; + private static final float DEF_RING_SCALE = 1.0f; + private static final float DEF_RING_OFFSET = 0.0f; + private static final boolean DEF_BG_RING_ENABLED = true; + private static final int DEF_BG_RING_COLOR = 0xFF808080; + private static final int DEF_BG_RING_OPACITY = 30; + private static final boolean DEF_MIN_VIS_ENABLED = true; + private static final int DEF_MIN_VIS_MS = 500; + private static final boolean DEF_SHOW_COUNT_BADGE = false; + private static final float DEF_BADGE_OFFSET = 0.0f; + private static final float DEF_BADGE_TEXT_SP = 10.0f; + private static final boolean DEF_PERCENT_ENABLED = false; + private static final float DEF_PERCENT_SP = 8.0f; + private static final boolean DEF_PERCENT_BOLD = true; + private static final int DEF_PERCENT_POSITION = 0; + private static final boolean DEF_FILENAME_ENABLED = false; + private static final float DEF_FILENAME_SP = 7.0f; + private static final boolean DEF_FILENAME_BOLD = false; + private static final int DEF_FILENAME_POSITION = 4; + private static final int DEF_FILENAME_MAX_CHARS = 20; + private static final int DEF_FILENAME_TRUNCATE = 0; + private static final int DEF_EASING = 0; + + static final String[] POSITION_NAMES = { + "right", "left", "top", "bottom", + "top_right", "top_left", "bottom_right", "bottom_left" + }; + + static final String[] FINISH_STYLE_NAMES = { "pop", "segmented", "snap" }; + + static final String[] EASING_NAMES = { + "linear", "accelerate", "decelerate", "ease_in_out" + }; + + static final String[] TRUNCATE_MODE_NAMES = { "middle", "start", "end" }; + + private final ContentResolver mCr; + private final Handler mHandler; + private ContentObserver mObserver; + private Runnable mCallback; + + public CutoutProgressSettings(ContentResolver cr, Handler handler) { + mCr = cr; + mHandler = handler; + } + + public void observe(Runnable onChange) { + mCallback = onChange; + mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + if (mCallback != null) mCallback.run(); + } + }; + Uri base = Settings.Secure.getUriFor("cutout_progress_enabled").buildUpon() + .path("").build(); + mCr.registerContentObserver( + Settings.Secure.CONTENT_URI, true, mObserver); + } + + public void stopObserving() { + if (mObserver != null) { + mCr.unregisterContentObserver(mObserver); + mObserver = null; + } + } + + public boolean isEnabled() { + return getInt(KEY_ENABLED, DEF_ENABLED ? 1 : 0) != 0; + } + + public int getRingColor() { + return getInt(KEY_RING_COLOR, DEF_RING_COLOR); + } + + public int getErrorColor() { + return getInt(KEY_ERROR_COLOR, DEF_ERROR_COLOR); + } + + public int getFinishFlashColor() { + return getInt(KEY_FINISH_FLASH_COLOR, DEF_FINISH_FLASH_COLOR); + } + + public float getStrokeWidthDp() { + return getInt(KEY_STROKE_WIDTH_DP10, (int)(DEF_STROKE_DP * 10)) / 10f; + } + + public float getRingGap() { + return getInt(KEY_RING_GAP_X1000, (int)(DEF_RING_GAP * 1000)) / 1000f; + } + + public int getOpacity() { + return clamp(getInt(KEY_OPACITY, DEF_OPACITY), 0, 100); + } + + public boolean isClockwise() { + return getInt(KEY_CLOCKWISE, DEF_CLOCKWISE ? 1 : 0) != 0; + } + + public String getFinishStyle() { + int idx = clamp(getInt(KEY_FINISH_STYLE, DEF_FINISH_STYLE), 0, + FINISH_STYLE_NAMES.length - 1); + return FINISH_STYLE_NAMES[idx]; + } + + public int getFinishHoldMs() { + return getInt(KEY_FINISH_HOLD_MS, DEF_FINISH_HOLD_MS); + } + + public int getFinishExitMs() { + return getInt(KEY_FINISH_EXIT_MS, DEF_FINISH_EXIT_MS); + } + + public boolean isFinishUseFlash() { + return getInt(KEY_FINISH_USE_FLASH, DEF_FINISH_USE_FLASH ? 1 : 0) != 0; + } + + public boolean isCompletionPulse() { + return getInt(KEY_COMPLETION_PULSE, DEF_COMPLETION_PULSE ? 1 : 0) != 0; + } + + public boolean isPathMode() { + return getInt(KEY_PATH_MODE, DEF_PATH_MODE ? 1 : 0) != 0; + } + + public float getRingScaleX() { + return getInt(KEY_RING_SCALE_X_X1000, (int)(DEF_RING_SCALE * 1000)) / 1000f; + } + + public float getRingScaleY() { + return getInt(KEY_RING_SCALE_Y_X1000, (int)(DEF_RING_SCALE * 1000)) / 1000f; + } + + public float getRingOffsetXDp() { + return getInt(KEY_RING_OFFSET_X_DP10, (int)(DEF_RING_OFFSET * 10)) / 10f; + } + + public float getRingOffsetYDp() { + return getInt(KEY_RING_OFFSET_Y_DP10, (int)(DEF_RING_OFFSET * 10)) / 10f; + } + + public boolean isBgRingEnabled() { + return getInt(KEY_BG_RING_ENABLED, DEF_BG_RING_ENABLED ? 1 : 0) != 0; + } + + public int getBgRingColor() { + return getInt(KEY_BG_RING_COLOR, DEF_BG_RING_COLOR); + } + + public int getBgRingOpacity() { + return clamp(getInt(KEY_BG_RING_OPACITY, DEF_BG_RING_OPACITY), 0, 100); + } + + public boolean isMinVisEnabled() { + return getInt(KEY_MIN_VIS_ENABLED, DEF_MIN_VIS_ENABLED ? 1 : 0) != 0; + } + + public int getMinVisMs() { + return getInt(KEY_MIN_VIS_MS, DEF_MIN_VIS_MS); + } + + public boolean isShowCountBadge() { + return getInt(KEY_SHOW_COUNT_BADGE, DEF_SHOW_COUNT_BADGE ? 1 : 0) != 0; + } + + public float getBadgeOffsetXDp() { + return getInt(KEY_BADGE_OFFSET_X_DP10, (int)(DEF_BADGE_OFFSET * 10)) / 10f; + } + + public float getBadgeOffsetYDp() { + return getInt(KEY_BADGE_OFFSET_Y_DP10, (int)(DEF_BADGE_OFFSET * 10)) / 10f; + } + + public float getBadgeTextSizeSp() { + return getInt(KEY_BADGE_TEXT_SIZE_SP10, (int)(DEF_BADGE_TEXT_SP * 10)) / 10f; + } + + public boolean isPercentEnabled() { + return getInt(KEY_PERCENT_ENABLED, DEF_PERCENT_ENABLED ? 1 : 0) != 0; + } + + public float getPercentTextSizeSp() { + return getInt(KEY_PERCENT_SIZE_SP10, (int)(DEF_PERCENT_SP * 10)) / 10f; + } + + public boolean isPercentBold() { + return getInt(KEY_PERCENT_BOLD, DEF_PERCENT_BOLD ? 1 : 0) != 0; + } + + public String getPercentPosition() { + int idx = clamp(getInt(KEY_PERCENT_POSITION, DEF_PERCENT_POSITION), 0, + POSITION_NAMES.length - 1); + return POSITION_NAMES[idx]; + } + + public float getPercentOffsetXDp() { + return getInt(KEY_PERCENT_OFFSET_X, 0) / 10f; + } + + public float getPercentOffsetYDp() { + return getInt(KEY_PERCENT_OFFSET_Y, 0) / 10f; + } + + public boolean isFilenameEnabled() { + return getInt(KEY_FILENAME_ENABLED, DEF_FILENAME_ENABLED ? 1 : 0) != 0; + } + + public float getFilenameTextSizeSp() { + return getInt(KEY_FILENAME_SIZE_SP10, (int)(DEF_FILENAME_SP * 10)) / 10f; + } + + public boolean isFilenameBold() { + return getInt(KEY_FILENAME_BOLD, DEF_FILENAME_BOLD ? 1 : 0) != 0; + } + + public String getFilenamePosition() { + int idx = clamp(getInt(KEY_FILENAME_POSITION, DEF_FILENAME_POSITION), 0, + POSITION_NAMES.length - 1); + return POSITION_NAMES[idx]; + } + + public float getFilenameOffsetXDp() { + return getInt(KEY_FILENAME_OFFSET_X, 0) / 10f; + } + + public float getFilenameOffsetYDp() { + return getInt(KEY_FILENAME_OFFSET_Y, 0) / 10f; + } + + public int getFilenameMaxChars() { + return getInt(KEY_FILENAME_MAX_CHARS, DEF_FILENAME_MAX_CHARS); + } + + public String getFilenameTruncateMode() { + int idx = clamp(getInt(KEY_FILENAME_TRUNCATE, DEF_FILENAME_TRUNCATE), 0, + TRUNCATE_MODE_NAMES.length - 1); + return TRUNCATE_MODE_NAMES[idx]; + } + + public String getProgressEasing() { + int idx = clamp(getInt(KEY_PROGRESS_EASING, DEF_EASING), 0, + EASING_NAMES.length - 1); + return EASING_NAMES[idx]; + } + + public void setEnabled(boolean value) { + putInt(KEY_ENABLED, value ? 1 : 0); + } + + public void setRingColor(int argb) { + putInt(KEY_RING_COLOR, argb); + } + + public void setOpacity(int opacity) { + putInt(KEY_OPACITY, clamp(opacity, 0, 100)); + } + + public void setClockwise(boolean cw) { + putInt(KEY_CLOCKWISE, cw ? 1 : 0); + } + + public void setFinishStyle(int styleIndex) { + putInt(KEY_FINISH_STYLE, clamp(styleIndex, 0, FINISH_STYLE_NAMES.length - 1)); + } + + private int getInt(String key, int def) { + return Settings.Secure.getInt(mCr, key, def); + } + + private void putInt(String key, int value) { + Settings.Secure.putInt(mCr, key, value); + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java new file mode 100644 index 000000000000..be2e4af90932 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/DownloadStateTracker.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress; + +import android.os.Bundle; + +import com.android.systemui.statusbar.notification.collection.NotificationEntry; + +import java.util.concurrent.ConcurrentHashMap; + +public final class DownloadStateTracker { + + private static final String EXTRA_PROGRESS = "android.progress"; + private static final String EXTRA_PROGRESS_MAX = "android.progressMax"; + private static final String EXTRA_TITLE = "android.title"; + + private static final long STALE_TIMEOUT_MS = 5 * 60 * 1000L; + + private static final int ERROR_THRESHOLD_PCT = 5; + + private static final int REASON_APP_CANCEL = 8; + private static final int REASON_APP_CANCEL_ALL = 9; + + private static final int RESET_DROP_PCT = 25; + + private static final class DownloadSnapshot { + final String pkg; + String label; + int progress; + long updatedAt; + + DownloadSnapshot(String pkg, String label, int progress) { + this.pkg = pkg; + this.label = label; + this.progress = progress; + this.updatedAt = System.currentTimeMillis(); + } + } + + private final ConcurrentHashMap mActive = + new ConcurrentHashMap<>(); + + public interface IntCallback { void onValue(int value); } + public interface StringCallback { void onValue(String value); } + + private IntCallback mOnProgress; + private Runnable mOnComplete; + private Runnable mOnError; + private IntCallback mOnCountChanged; + private StringCallback mOnLabelChanged; + + public void setOnProgress(IntCallback cb) { mOnProgress = cb; } + public void setOnComplete(Runnable cb) { mOnComplete = cb; } + public void setOnError(Runnable cb) { mOnError = cb; } + public void setOnCountChanged(IntCallback cb) { mOnCountChanged = cb; } + public void setOnLabelChanged(StringCallback cb) { mOnLabelChanged = cb; } + + public void onNotificationChanged(NotificationEntry entry) { + Bundle extras = entry.getSbn().getNotification().extras; + if (extras == null) return; + + int rawProgress = extras.getInt(EXTRA_PROGRESS, -1); + int rawMax = extras.getInt(EXTRA_PROGRESS_MAX, -1); + + String id = entryKey(entry); + String pkg = entry.getSbn().getPackageName(); + + if (rawProgress < 0 || rawMax <= 0) { + if (mActive.remove(id) != null) { + notifyCountChanged(); + publishAggregated(); + fireComplete(); + } + return; + } + + int pct = clamp(rawProgress * 100 / rawMax, 0, 100); + String label = charSeqStr(extras.getCharSequence(EXTRA_TITLE)); + + DownloadSnapshot existing = mActive.get(id); + boolean isNew = existing == null; + boolean isReset = existing != null && (existing.progress - pct) >= RESET_DROP_PCT; + + if (isReset) mActive.remove(id); + pruneStale(pkg, id); + + if (isNew || isReset || existing.progress != pct) { + String usedLabel = (label != null) ? label + : (existing != null ? existing.label : null); + mActive.put(id, new DownloadSnapshot(pkg, usedLabel, pct)); + publishAggregated(); + if (isNew || isReset) notifyCountChanged(); + } + + if (pct >= 100) { + mActive.remove(id); + notifyCountChanged(); + publishAggregated(); + fireComplete(); + } + } + + public void onNotificationRemoved(NotificationEntry entry, int reason) { + DownloadSnapshot snap = mActive.remove(entryKey(entry)); + if (snap == null) return; + + notifyCountChanged(); + publishAggregated(); + + if (snap.progress >= 100) { + fireComplete(); + } else if (reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL) { + fireComplete(); + } else if (snap.progress >= ERROR_THRESHOLD_PCT) { + fireError(); + } + } + + public void reset() { + mActive.clear(); + notifyCountChanged(); + fire(mOnProgress, 0); + fire(mOnLabelChanged, null); + } + + public int getActiveCount() { + return mActive.size(); + } + + private String entryKey(NotificationEntry e) { + return e.getSbn().getPackageName() + ":" + e.getSbn().getId(); + } + + private void pruneStale(String pkg, String currentId) { + long now = System.currentTimeMillis(); + for (java.util.Map.Entry e : mActive.entrySet()) { + if (!e.getKey().equals(currentId) + && e.getValue().pkg.equals(pkg) + && now - e.getValue().updatedAt > STALE_TIMEOUT_MS) { + mActive.remove(e.getKey()); + } + } + } + + private void publishAggregated() { + int avg = 0; + if (!mActive.isEmpty()) { + int sum = 0; + for (DownloadSnapshot s : mActive.values()) sum += s.progress; + avg = sum / mActive.size(); + } + fire(mOnProgress, avg); + publishBestLabel(); + } + + private void publishBestLabel() { + DownloadSnapshot best = null; + for (DownloadSnapshot s : mActive.values()) { + if (best == null || s.progress > best.progress) best = s; + } + String lbl = null; + if (best != null && best.label != null + && !best.label.toLowerCase().contains("untitled")) { + lbl = best.label; + } + fire(mOnLabelChanged, lbl); + } + + private void notifyCountChanged() { fire(mOnCountChanged, mActive.size()); } + private void fireComplete() { if (mOnComplete != null) mOnComplete.run(); } + private void fireError() { if (mOnError != null) mOnError.run(); } + + private void fire(IntCallback cb, int v) { if (cb != null) cb.onValue(v); } + private void fire(StringCallback cb, String v) { if (cb != null) cb.onValue(v); } + + private static String charSeqStr(CharSequence cs) { + return cs != null ? cs.toString() : null; + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java new file mode 100644 index 000000000000..3d5d4c8b37fc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/dagger/CutoutProgressModule.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.dagger; + +import com.android.systemui.CoreStartable; +import com.android.systemui.cutoutprogress.CutoutProgressController; + +import dagger.Binds; +import dagger.Module; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; + +@Module +public abstract class CutoutProgressModule { + + @Binds + @IntoMap + @ClassKey(CutoutProgressController.class) + abstract CoreStartable bindCutoutProgressController(CutoutProgressController controller); +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java new file mode 100644 index 000000000000..e100dde1ba48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CapsuleRingRenderer.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; + +public final class CapsuleRingRenderer implements RingViewRenderer { + + private final Path mOutline = new Path(); + private final PathMeasure mMeasure = new PathMeasure(); + private final Path mWorkPath = new Path(); + private float mTotalLength = 0f; + + @Override + public void updateBounds(RectF bounds) { + mOutline.reset(); + buildCapsule(bounds); + mMeasure.setPath(mOutline, false); + mTotalLength = mMeasure.getLength(); + } + + private void buildCapsule(RectF b) { + float r = Math.min(b.width(), b.height()) / 2f; + float cx = b.centerX(); + + mOutline.moveTo(cx, b.top); + + if (b.width() >= b.height()) { + mOutline.lineTo(b.right - r, b.top); + mOutline.arcTo(b.right - 2*r, b.top, b.right, b.bottom, -90f, 180f, false); + mOutline.lineTo(b.left + r, b.bottom); + mOutline.arcTo(b.left, b.top, b.left + 2*r, b.bottom, 90f, 180f, false); + } else { + mOutline.arcTo(b.left, b.top, b.right, b.top + 2*r, -90f, 90f, false); + mOutline.lineTo(b.right, b.bottom - r); + mOutline.arcTo(b.left, b.bottom - 2*r, b.right, b.bottom, 0f, 180f, false); + mOutline.lineTo(b.left, b.top + r); + mOutline.arcTo(b.left, b.top, b.right, b.top + 2*r, 180f, 90f, false); + } + mOutline.close(); + } + + @Override + public void drawFullRing(Canvas canvas, Paint paint) { + if (mTotalLength == 0f) return; + canvas.drawPath(mOutline, paint); + } + + @Override + public void drawProgress(Canvas canvas, float sweepFraction, + boolean clockwise, Paint paint) { + if (mTotalLength == 0f) return; + if (sweepFraction >= 1f) { drawFullRing(canvas, paint); return; } + float len = Math.max(0f, Math.min(1f, sweepFraction)) * mTotalLength; + mWorkPath.reset(); + if (clockwise) { + mMeasure.getSegment(0f, len, mWorkPath, true); + } else { + mMeasure.getSegment(mTotalLength - len, mTotalLength, mWorkPath, true); + } + canvas.drawPath(mWorkPath, paint); + } + + @Override + public void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha) { + if (mTotalLength == 0f) return; + float totalDeg = segments * (arcDeg + gapDeg); + float segLen = mTotalLength * (arcDeg / totalDeg); + float gapLen = mTotalLength * (gapDeg / totalDeg); + + for (int i = 0; i < segments; i++) { + float start = i * (segLen + gapLen); + mWorkPath.reset(); + mMeasure.getSegment(start, start + segLen, mWorkPath, true); + + if (i == highlight || i == highlight - 1) { + Paint tmp = new Paint(shinePaint); + tmp.setAlpha((int)(255 * alpha)); + canvas.drawPath(mWorkPath, tmp); + } else { + canvas.drawPath(mWorkPath, basePaint); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java new file mode 100644 index 000000000000..1cda0ec70db6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CircleRingRenderer.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +public final class CircleRingRenderer implements RingViewRenderer { + + private final RectF mBounds = new RectF(); + + @Override + public void updateBounds(RectF bounds) { + mBounds.set(bounds); + } + + @Override + public void drawFullRing(Canvas canvas, Paint paint) { + canvas.drawArc(mBounds, 0f, 360f, false, paint); + } + + @Override + public void drawProgress(Canvas canvas, float sweepFraction, + boolean clockwise, Paint paint) { + float sweep = 360f * Math.max(0f, Math.min(1f, sweepFraction)); + float actual = clockwise ? sweep : -sweep; + canvas.drawArc(mBounds, -90f, actual, false, paint); + } + + @Override + public void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha) { + for (int i = 0; i < segments; i++) { + float startAngle = -90f + i * (arcDeg + gapDeg); + if (i == highlight || i == highlight - 1) { + Paint tmp = new Paint(shinePaint); + tmp.setAlpha((int)(255 * alpha)); + canvas.drawArc(mBounds, startAngle, arcDeg, false, tmp); + } else { + canvas.drawArc(mBounds, startAngle, arcDeg, false, basePaint); + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java new file mode 100644 index 000000000000..b1726b0fea4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CountBadgePainter.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; + +import androidx.core.graphics.ColorUtils; + +public final class CountBadgePainter { + + private static final float H_PAD_DP = 6f; + private static final float V_PAD_DP = 3f; + private static final float MIN_WIDTH_DP = 20f; + private static final float DARKEN_AMOUNT = 0.75f; + private static final float BRIGHTEN_AMOUNT = 0.80f; + private static final float LIGHT_THRESHOLD = 0.40f; + private static final int BG_ALPHA = 230; + + private final float mDp; + private final Paint mBgPaint; + private final Paint mTextPaint; + private final RectF mRect = new RectF(); + + private int mLastCount = Integer.MIN_VALUE; + private String mLastText = ""; + + public CountBadgePainter(float density) { + mDp = density; + + mBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mBgPaint.setStyle(Paint.Style.FILL); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setSubpixelText(true); + mTextPaint.setTypeface(Typeface.DEFAULT_BOLD); + } + + public void applyConfig(int baseColor, float textSizeSp, float density) { + double lum = ColorUtils.calculateLuminance(baseColor); + mBgPaint.setColor(darken(baseColor, DARKEN_AMOUNT)); + int textColor = lum > LIGHT_THRESHOLD + ? Color.WHITE : brighten(baseColor, BRIGHTEN_AMOUNT); + mTextPaint.setColor(textColor); + mTextPaint.setTextSize(textSizeSp * density); + } + + public void draw(Canvas canvas, float cx, float topY, int count, int opacityPercent) { + if (count != mLastCount) { + mLastCount = count; + mLastText = String.valueOf(count); + } + + float textW = mTextPaint.measureText(mLastText); + float hPad = H_PAD_DP * mDp; + float vPad = V_PAD_DP * mDp; + float height = mTextPaint.getTextSize() + vPad * 2f; + float width = Math.max(textW + hPad * 2f, MIN_WIDTH_DP * mDp); + + mRect.set(cx - width / 2f, topY, cx + width / 2f, topY + height); + + int baseAlpha = clamp(opacityPercent, 0, 100) * 255 / 100; + mBgPaint.setAlpha(baseAlpha * BG_ALPHA / 255); + mTextPaint.setAlpha(baseAlpha); + + canvas.drawRoundRect(mRect, height / 2f, height / 2f, mBgPaint); + + Paint.FontMetrics fm = mTextPaint.getFontMetrics(); + float textY = mRect.centerY() - (fm.ascent + fm.descent) / 2f; + canvas.drawText(mLastText, cx, textY, mTextPaint); + } + + private static int darken(int color, float fraction) { + float[] hsl = new float[3]; + ColorUtils.colorToHSL(color, hsl); + hsl[2] *= (1f - fraction); + return ColorUtils.HSLToColor(hsl); + } + + private static int brighten(int color, float fraction) { + float[] hsl = new float[3]; + ColorUtils.colorToHSL(color, hsl); + hsl[2] += (1f - hsl[2]) * fraction; + return ColorUtils.HSLToColor(hsl); + } + + private static int clamp(int v, int lo, int hi) { + return Math.max(lo, Math.min(hi, v)); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java new file mode 100644 index 000000000000..23c79ecfa15b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java @@ -0,0 +1,572 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.text.TextPaint; +import android.util.TypedValue; +import android.view.DisplayCutout; +import android.view.Surface; +import android.view.View; +import android.view.WindowInsets; + +import com.android.systemui.cutoutprogress.CutoutProgressSettings; + +import java.util.Objects; + +public final class CutoutRingView extends View { + + private static final long BURN_IN_HIDE_MS = 10_000L; + + private final float mDp; + + private final Path mCutoutPath = new Path(); + private final Path mScaledPath = new Path(); + private final Matrix mScaleMatrix = new Matrix(); + private final RectF mPathBounds = new RectF(); + private final RectF mArcBounds = new RectF(); + private boolean mHasCutout = false; + + private final OverlayAnimationHelper mAnim; + private RingViewRenderer mRenderer; + private final CountBadgePainter mBadge; + + private final Paint mRingPaint = makePaint(); + private final Paint mShinePaint = makePaint(); + private final Paint mErrorPaint = makePaint(); + private final Paint mAnimPaint = makePaint(); + private final Paint mBgPaint = makePaint(); + private final TextPaint mPercentPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + private final TextPaint mFilenamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + + private int mProgress = 0; + private int mDownloadCount = 0; + private String mFilenameHint = null; + + private long mDownloadStartMs = 0L; + private long mLastProgressMs = 0L; + private Runnable mPendingFinish = null; + + private int sCfgRingColor; + private int sCfgErrorColor; + private int sCfgFlashColor; + private float sCfgStrokeDp; + private float sCfgRingGap; + private int sCfgOpacity; + private boolean sCfgClockwise; + private String sCfgFinishStyle; + private int sCfgFinishHoldMs; + private int sCfgFinishExitMs; + private boolean sCfgFinishFlash; + private boolean sCfgPulse; + private boolean sCfgPathMode; + private float sCfgScaleX; + private float sCfgScaleY; + private float sCfgOffsetXDp; + private float sCfgOffsetYDp; + private boolean sCfgBgRing; + private int sCfgBgColor; + private int sCfgBgOpacity; + private boolean sCfgMinVis; + private int sCfgMinVisMs; + private boolean sCfgShowBadge; + private float sCfgBadgeOffXDp; + private float sCfgBadgeOffYDp; + private float sCfgBadgeSp; + private boolean sCfgPct; + private float sCfgPctSp; + private boolean sCfgPctBold; + private String sCfgPctPos; + private float sCfgPctOffXDp; + private float sCfgPctOffYDp; + private boolean sCfgFname; + private float sCfgFnameSp; + private boolean sCfgFnameBold; + private String sCfgFnamePos; + private float sCfgFnameOffXDp; + private float sCfgFnameOffYDp; + private int sCfgFnameMaxChars; + private String sCfgFnameTruncate; + private String sCfgEasing; + + public CutoutRingView(Context ctx) { + super(ctx); + mDp = ctx.getResources().getDisplayMetrics().density; + mAnim = new OverlayAnimationHelper(this); + mRenderer = new CircleRingRenderer(); + mBadge = new CountBadgePainter(mDp); + initPaints(); + } + + public void applySettings(CutoutProgressSettings s) { + sCfgRingColor = s.getRingColor(); + sCfgErrorColor = s.getErrorColor(); + sCfgFlashColor = s.getFinishFlashColor(); + sCfgStrokeDp = s.getStrokeWidthDp(); + sCfgRingGap = s.getRingGap(); + sCfgOpacity = s.getOpacity(); + sCfgClockwise = s.isClockwise(); + sCfgFinishStyle = s.getFinishStyle(); + sCfgFinishHoldMs = s.getFinishHoldMs(); + sCfgFinishExitMs = s.getFinishExitMs(); + sCfgFinishFlash = s.isFinishUseFlash(); + sCfgPulse = s.isCompletionPulse(); + sCfgPathMode = s.isPathMode(); + sCfgScaleX = s.getRingScaleX(); + sCfgScaleY = s.getRingScaleY(); + sCfgOffsetXDp = s.getRingOffsetXDp(); + sCfgOffsetYDp = s.getRingOffsetYDp(); + sCfgBgRing = s.isBgRingEnabled(); + sCfgBgColor = s.getBgRingColor(); + sCfgBgOpacity = s.getBgRingOpacity(); + sCfgMinVis = s.isMinVisEnabled(); + sCfgMinVisMs = s.getMinVisMs(); + sCfgShowBadge = s.isShowCountBadge(); + sCfgBadgeOffXDp = s.getBadgeOffsetXDp(); + sCfgBadgeOffYDp = s.getBadgeOffsetYDp(); + sCfgBadgeSp = s.getBadgeTextSizeSp(); + sCfgPct = s.isPercentEnabled(); + sCfgPctSp = s.getPercentTextSizeSp(); + sCfgPctBold = s.isPercentBold(); + sCfgPctPos = s.getPercentPosition(); + sCfgPctOffXDp = s.getPercentOffsetXDp(); + sCfgPctOffYDp = s.getPercentOffsetYDp(); + sCfgFname = s.isFilenameEnabled(); + sCfgFnameSp = s.getFilenameTextSizeSp(); + sCfgFnameBold = s.isFilenameBold(); + sCfgFnamePos = s.getFilenamePosition(); + sCfgFnameOffXDp = s.getFilenameOffsetXDp(); + sCfgFnameOffYDp = s.getFilenameOffsetYDp(); + sCfgFnameMaxChars= s.getFilenameMaxChars(); + sCfgFnameTruncate= s.getFilenameTruncateMode(); + sCfgEasing = s.getProgressEasing(); + + boolean needPath = sCfgPathMode; + if (needPath && !(mRenderer instanceof CapsuleRingRenderer)) { + mRenderer = new CapsuleRingRenderer(); + } else if (!needPath && !(mRenderer instanceof CircleRingRenderer)) { + mRenderer = new CircleRingRenderer(); + } + + refreshPaints(); + recalcScaledPath(); + invalidate(); + } + + public void setProgress(int value) { + int pct = Math.max(0, Math.min(100, value)); + if (mProgress == pct) return; + + int prev = mProgress; + mProgress = pct; + mLastProgressMs = System.currentTimeMillis(); + + removeCallbacks(mBurnInHide); + if (pct > 0 && pct < 100) { + postDelayed(mBurnInHide, BURN_IN_HIDE_MS); + } + + if (prev == 0 && pct > 0) { + mDownloadStartMs = System.currentTimeMillis(); + cancelPendingFinish(); + } + + if (pct == 100 && !mAnim.isFinishAnimating) { + long elapsed = System.currentTimeMillis() - mDownloadStartMs; + long remaining = (sCfgMinVis ? sCfgMinVisMs : 0) - elapsed; + if (remaining > 0 && mDownloadStartMs > 0) { + mPendingFinish = () -> { mPendingFinish = null; beginFinishAnim(); }; + postDelayed(mPendingFinish, remaining); + } else { + beginFinishAnim(); + } + } else if (pct > 0 && pct < 100 && mAnim.isFinishAnimating) { + mAnim.cancelFinish(); + } else if (pct == 0) { + mDownloadStartMs = 0L; + cancelPendingFinish(); + } + + invalidate(); + } + + public void setDownloadCount(int count) { + if (mDownloadCount != count) { mDownloadCount = count; invalidate(); } + } + + public void setFilenameHint(String hint) { + if (!Objects.equals(mFilenameHint, hint)) { mFilenameHint = hint; invalidate(); } + } + + public void showError() { + mAnim.startError(() -> setProgress(0)); + } + + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + mCutoutPath.reset(); + mHasCutout = false; + + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + Path native31 = null; + try { native31 = cutout.getCutoutPath(); } catch (NoSuchMethodError ignored) {} + + if (native31 != null && !native31.isEmpty()) { + mCutoutPath.set(native31); + mHasCutout = true; + } else if (!cutout.getBoundingRects().isEmpty()) { + android.graphics.Rect r = cutout.getBoundingRects().get(0); + float cx = r.exactCenterX(); + float cy = r.exactCenterY(); + float radius = Math.min(r.width(), r.height()) / 2f; + mCutoutPath.addCircle(cx, cy, radius, Path.Direction.CW); + mHasCutout = true; + } + } + + if (!mHasCutout) { + float cx = getResources().getDisplayMetrics().widthPixels / 2f; + float radius = 15f * mDp; + mCutoutPath.addCircle(cx, radius * 2f, radius, Path.Direction.CW); + mHasCutout = true; + } + + recalcScaledPath(); + invalidate(); + return super.onApplyWindowInsets(insets); + } + + private void recalcScaledPath() { + mCutoutPath.computeBounds(mPathBounds, true); + mScaleMatrix.setScale(sCfgRingGap, sCfgRingGap, + mPathBounds.centerX(), mPathBounds.centerY()); + mScaledPath.reset(); + mCutoutPath.transform(mScaleMatrix, mScaledPath); + } + + private final Runnable mBurnInHide = this::invalidate; + + @Override + protected void onDraw(Canvas canvas) { + if (!mHasCutout) return; + + if (mAnim.isErrorAnimating) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + mErrorPaint.setAlpha((int)(mAnim.errorAlpha * 255)); + mRenderer.drawFullRing(canvas, mErrorPaint); + return; + } + + int effectivePct = mAnim.isGeometryPreviewActive() ? 100 + : mAnim.isDynamicPreviewActive() ? mAnim.previewProgress + : mProgress; + + boolean burnedOut = effectivePct > 0 && effectivePct < 100 + && mLastProgressMs > 0 + && System.currentTimeMillis() - mLastProgressMs >= BURN_IN_HIDE_MS; + + boolean shouldDraw = mAnim.isFinishAnimating + || mAnim.isGeometryPreviewActive() + || mAnim.isDynamicPreviewActive() + || (effectivePct > 0 && effectivePct < 100 && !burnedOut) + || mPendingFinish != null; + + if (!shouldDraw) return; + + if (mAnim.displayScale != 1f) { + mScaledPath.computeBounds(mArcBounds, true); + canvas.save(); + canvas.scale(mAnim.displayScale, mAnim.displayScale, + mArcBounds.centerX(), mArcBounds.centerY()); + } + + mAnimPaint.set(mRingPaint); + int alpha = (int)(sCfgOpacity * 255f / 100f + * mAnim.displayAlpha * mAnim.completionPulseAlpha); + mAnimPaint.setAlpha(alpha); + + if (mAnim.successColorBlend > 0f) { + int successColor = sCfgFinishFlash ? sCfgFlashColor + : brighten(sCfgRingColor, mAnim.successColorBlend); + mAnimPaint.setColor(blendColors(sCfgRingColor, successColor, + mAnim.successColorBlend)); + } + + boolean isActive = effectivePct > 0 && effectivePct < 100 + || mAnim.isGeometryPreviewActive() + || mAnim.isDynamicPreviewActive(); + + if (sCfgBgRing && !mAnim.isFinishAnimating && isActive) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + mBgPaint.setAlpha((int)(sCfgBgOpacity * 255 / 100 * mAnim.displayAlpha)); + mRenderer.drawFullRing(canvas, mBgPaint); + } + + if (mAnim.isFinishAnimating) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + drawFinish(canvas, mAnimPaint); + } else { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + float sweep = eased(effectivePct, sCfgEasing); + mRenderer.drawProgress(canvas, sweep, sCfgClockwise, mAnimPaint); + + if (isActive) drawLabels(canvas, effectivePct); + } + + boolean showBadge = !mAnim.isDynamicPreviewActive() && sCfgShowBadge + && (mDownloadCount > 1 || mAnim.isGeometryPreviewActive()); + if (showBadge) { + mScaledPath.computeBounds(mArcBounds, true); + float badgeCx = mArcBounds.centerX() + sCfgBadgeOffXDp * mDp; + float badgeTop = mArcBounds.bottom + 4f * mDp + sCfgBadgeOffYDp * mDp; + int badgeN = mAnim.isGeometryPreviewActive() ? 3 : mDownloadCount; + mBadge.draw(canvas, badgeCx, badgeTop, badgeN, sCfgOpacity); + } + + if (mAnim.displayScale != 1f) canvas.restore(); + } + + private void drawFinish(Canvas canvas, Paint paint) { + if ("segmented".equals(sCfgFinishStyle)) { + mRenderer.drawSegmented(canvas, + OverlayAnimationHelper.SEGMENT_COUNT, + OverlayAnimationHelper.SEGMENT_GAP_DEG, + OverlayAnimationHelper.SEGMENT_ARC_DEG, + mAnim.segmentHighlight, + paint, mShinePaint, mAnim.displayAlpha); + } else { + mRenderer.drawFullRing(canvas, paint); + } + } + + private void drawLabels(Canvas canvas, int pct) { + float pad = 4f * mDp; + int alpha = sCfgOpacity * 255 / 100; + + if (sCfgPct) { + String text = pct + "%"; + float tw = mPercentPaint.measureText(text); + float[] pos = labelXY(sCfgPctPos, pad, mPercentPaint.getTextSize(), tw); + mPercentPaint.setColor(sCfgRingColor); + mPercentPaint.setAlpha(alpha); + canvas.drawText(text, pos[0] + sCfgPctOffXDp * mDp, + pos[1] + sCfgPctOffYDp * mDp, mPercentPaint); + } + + boolean geoPreview = mAnim.isGeometryPreviewActive(); + String fname = mFilenameHint != null ? mFilenameHint + : geoPreview ? "EvolutionX-16.0-arm64.zip" : null; + + if (sCfgFname && fname != null && (mDownloadCount <= 1 || geoPreview)) { + String display = truncate(fname, sCfgFnameMaxChars, sCfgFnameTruncate); + float[] pos = labelXY(sCfgFnamePos, pad, mFilenamePaint.getTextSize(), null); + mFilenamePaint.setColor(sCfgRingColor); + mFilenamePaint.setAlpha(alpha); + canvas.drawText(display, pos[0] + sCfgFnameOffXDp * mDp, + pos[1] + sCfgFnameOffYDp * mDp, mFilenamePaint); + } + } + + private float[] labelXY(String position, float pad, float textHeight, Float textW) { + switch (position) { + case "left": + return new float[]{ + textW != null ? mArcBounds.left - textW / 2f - pad + : mArcBounds.left - pad, + mArcBounds.centerY() + textHeight / 3f}; + case "top": + return new float[]{mArcBounds.centerX(), mArcBounds.top - pad}; + case "bottom": + return new float[]{mArcBounds.centerX(), + mArcBounds.bottom + textHeight + pad}; + case "top_left": + return new float[]{mArcBounds.left - pad, mArcBounds.top - pad}; + case "top_right": + return new float[]{mArcBounds.right + pad, mArcBounds.top - pad}; + case "bottom_left": + return new float[]{mArcBounds.left - pad, + mArcBounds.bottom + textHeight + pad}; + case "bottom_right": + return new float[]{mArcBounds.right + pad, + mArcBounds.bottom + textHeight + pad}; + default: + return new float[]{ + textW != null ? mArcBounds.right + textW / 2f + pad + : mArcBounds.right + pad, + mArcBounds.centerY() + textHeight / 3f}; + } + } + + private void computeArcBounds() { + mScaledPath.computeBounds(mArcBounds, true); + float[] offRotated = rotateOffset(sCfgOffsetXDp, sCfgOffsetYDp); + float cx = mArcBounds.centerX() + offRotated[0]; + float cy = mArcBounds.centerY() + offRotated[1]; + + float halfW, halfH; + if (sCfgPathMode) { + halfW = mArcBounds.width() / 2f * sCfgScaleX; + halfH = mArcBounds.height() / 2f * sCfgScaleY; + } else { + float halfBase = Math.max(mArcBounds.width(), mArcBounds.height()) / 2f; + halfW = halfBase * sCfgScaleX; + halfH = halfBase * sCfgScaleY; + } + mArcBounds.set(cx - halfW, cy - halfH, cx + halfW, cy + halfH); + } + + private float[] rotateOffset(float dx, float dy) { + int rot = getDisplay() != null ? getDisplay().getRotation() : Surface.ROTATION_0; + switch (rot) { + case Surface.ROTATION_90: return new float[]{ dy * mDp, -dx * mDp}; + case Surface.ROTATION_180: return new float[]{-dx * mDp, -dy * mDp}; + case Surface.ROTATION_270: return new float[]{-dy * mDp, dx * mDp}; + default: return new float[]{ dx * mDp, dy * mDp}; + } + } + + private void initPaints() { + sCfgRingColor = 0xFF2196F3; + sCfgErrorColor = 0xFFF44336; + sCfgFlashColor = Color.WHITE; + sCfgStrokeDp = 2f; + sCfgRingGap = 1.155f; + sCfgOpacity = 90; + sCfgBgColor = 0xFF808080; + sCfgBgOpacity = 30; + sCfgPctSp = 8f; + sCfgPctBold = true; + sCfgFnameSp = 7f; + sCfgBadgeSp = 10f; + sCfgPctPos = "right"; + sCfgFnamePos = "top_right"; + sCfgFnameTruncate = "middle"; + sCfgFnameMaxChars = 20; + sCfgEasing = "linear"; + sCfgClockwise = true; + sCfgFinishStyle= "pop"; + sCfgScaleX = sCfgScaleY = 1f; + sCfgBgRing = sCfgMinVis = true; + sCfgMinVisMs = 500; + refreshPaints(); + } + + private void refreshPaints() { + float stroke = sCfgStrokeDp * mDp; + applyStroke(mRingPaint, sCfgRingColor, stroke, sCfgOpacity * 255 / 100); + applyStroke(mShinePaint, sCfgFlashColor, stroke * 1.2f, 255); + applyStroke(mErrorPaint, sCfgErrorColor, stroke * 1.5f, 255); + applyStroke(mBgPaint, sCfgBgColor, stroke, sCfgBgOpacity * 255 / 100); + + mPercentPaint.setTypeface(sCfgPctBold + ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + mPercentPaint.setTextSize(spToPx(sCfgPctSp)); + mPercentPaint.setTextAlign(Paint.Align.CENTER); + + mFilenamePaint.setTypeface(sCfgFnameBold + ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + mFilenamePaint.setTextSize(spToPx(sCfgFnameSp)); + mFilenamePaint.setTextAlign(Paint.Align.LEFT); + + mBadge.applyConfig(sCfgRingColor, sCfgBadgeSp, mDp); + } + + private static void applyStroke(Paint p, int color, float width, int alpha) { + p.setStyle(Paint.Style.STROKE); + p.setAntiAlias(true); + p.setColor(color); + p.setAlpha(alpha); + p.setStrokeWidth(width); + p.setStrokeCap(Paint.Cap.BUTT); + } + + private void beginFinishAnim() { + mAnim.startFinish(sCfgFinishStyle, sCfgFinishHoldMs, sCfgFinishExitMs, + sCfgPulse, () -> setProgress(0)); + } + + private void cancelPendingFinish() { + if (mPendingFinish != null) { + removeCallbacks(mPendingFinish); + mPendingFinish = null; + } + } + + private static float eased(int pct, String mode) { + float v = pct / 100f; + switch (mode) { + case "accelerate": return v * v; + case "decelerate": return 1f - (1f - v) * (1f - v); + case "ease_in_out": return v < .5f ? 2*v*v : 1f - (float)Math.pow(-2*v+2,2)/2f; + default: return v; + } + } + + private static int brighten(int c, float f) { + return Color.argb(Color.alpha(c), + Math.min(255, (int)(Color.red(c) + (255 - Color.red(c)) * f)), + Math.min(255, (int)(Color.green(c) + (255 - Color.green(c)) * f)), + Math.min(255, (int)(Color.blue(c) + (255 - Color.blue(c)) * f))); + } + + private static int blendColors(int c1, int c2, float ratio) { + float inv = 1f - ratio; + return Color.argb(Color.alpha(c1), + (int)(Color.red(c1)*inv + Color.red(c2)*ratio), + (int)(Color.green(c1)*inv + Color.green(c2)*ratio), + (int)(Color.blue(c1)*inv + Color.blue(c2)*ratio)); + } + + private static String truncate(String s, int max, String mode) { + if (s.length() <= max) return s; + String e = "\u2026"; + int avail = max - 1; + if (avail <= 0) return e; + switch (mode) { + case "start": return e + s.substring(s.length() - avail); + case "end": return s.substring(0, avail) + e; + default: { + int head = (avail + 1) / 2; + int tail = avail - head; + return s.substring(0, head) + e + s.substring(s.length() - tail); + } + } + } + + private float spToPx(float sp) { + return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, + getResources().getDisplayMetrics()); + } + + private static Paint makePaint() { + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); + p.setStyle(Paint.Style.STROKE); + return p; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java new file mode 100644 index 000000000000..f20b562ba6f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/OverlayAnimationHelper.java @@ -0,0 +1,325 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; +import android.animation.ValueAnimator; +import android.view.View; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.OvershootInterpolator; + +public final class OverlayAnimationHelper { + + public static final int SEGMENT_COUNT = 12; + public static final float SEGMENT_GAP_DEG = 6f; + public static final float SEGMENT_ARC_DEG = + (360f - SEGMENT_COUNT * SEGMENT_GAP_DEG) / SEGMENT_COUNT; + + public boolean isFinishAnimating = false; + public boolean isErrorAnimating = false; + + public float displayAlpha = 1f; + public float displayScale = 1f; + public int segmentHighlight = -1; + public float successColorBlend = 0f; + public float completionPulseAlpha = 1f; + public float errorAlpha = 0f; + + public enum PreviewMode { NONE, DYNAMIC, GEOMETRY } + public PreviewMode previewMode = PreviewMode.NONE; + public int previewProgress = 0; + + public boolean isDynamicPreviewActive() { return previewMode == PreviewMode.DYNAMIC; } + public boolean isGeometryPreviewActive() { return previewMode == PreviewMode.GEOMETRY; } + + private static final int MAX_ANIM_MS = 800; + private static final float INTENSITY_POP = 1.5f; + private static final float POP_SCALE_FACTOR = 0.08f; + private static final float SEGMENT_SHINE_BLEND = 0.4f; + private static final float PULSE_MIN_ALPHA = 0.7f; + private static final long PULSE_DURATION_MS = 400L; + private static final long PREVIEW_DEBOUNCE_MS = 300L; + private static final long GEOMETRY_HOLD_MS = 3000L; + + private final View mHost; + + private ValueAnimator mFinishAnim; + private ValueAnimator mPulseAnim; + private ValueAnimator mErrorAnim; + private ValueAnimator mPreviewAnim; + private Runnable mPreviewDebounce; + private Runnable mGeometryHideTask; + + public OverlayAnimationHelper(View host) { + mHost = host; + } + + public void startFinish(String style, int holdMs, int exitMs, + boolean pulse, Runnable onComplete) { + cancelFinish(); + isFinishAnimating = true; + displayAlpha = 1f; + displayScale = 1f; + segmentHighlight = -1; + successColorBlend = 1f; + completionPulseAlpha = 1f; + + Runnable runStyle = () -> { + switch (style) { + case "snap": + isFinishAnimating = false; + resetFinishState(); + onComplete.run(); + break; + case "segmented": + animateSegmented(holdMs, exitMs, INTENSITY_POP, onComplete); + break; + default: + animatePop(holdMs, exitMs, INTENSITY_POP, onComplete); + } + }; + + if (pulse && !"snap".equals(style)) { + animatePulse(runStyle); + } else { + runStyle.run(); + } + } + + private void animatePop(int holdMs, int exitMs, float intensity, Runnable onComplete) { + int total = Math.min(holdMs + exitMs, MAX_ANIM_MS); + long scaleDuration = (long)(total * 0.4f); + long fadeDuration = total - scaleDuration; + + mFinishAnim = animate(0f, 1f, scaleDuration, new OvershootInterpolator(2f * intensity), + f -> displayScale = 1f + POP_SCALE_FACTOR * intensity * f, + () -> mFinishAnim = animate(0f, 1f, fadeDuration, + new AccelerateDecelerateInterpolator(), + f -> { + displayScale = 1f + POP_SCALE_FACTOR * intensity * (1f - f * 0.5f); + displayAlpha = 1f - f; + }, + () -> endFinish(onComplete))); + } + + private void animateSegmented(int holdMs, int exitMs, float intensity, Runnable onComplete) { + int total = Math.min(holdMs + exitMs, MAX_ANIM_MS); + long cascadeDuration = (long)(total * 0.6f); + long fadeDuration = total - cascadeDuration; + + mFinishAnim = animateInt(0, SEGMENT_COUNT + 2, cascadeDuration, + new LinearInterpolator(), + seg -> { + segmentHighlight = seg; + successColorBlend = intensity * SEGMENT_SHINE_BLEND; + }, + () -> { + segmentHighlight = -1; + mFinishAnim = animate(1f, 0f, fadeDuration, + new AccelerateDecelerateInterpolator(), + f -> displayAlpha = f, + () -> endFinish(onComplete)); + }); + } + + private void animatePulse(Runnable then) { + if (mPulseAnim != null) mPulseAnim.cancel(); + mPulseAnim = animate(1f, PULSE_MIN_ALPHA, PULSE_DURATION_MS, + new AccelerateDecelerateInterpolator(), + f -> completionPulseAlpha = f, null); + ValueAnimator second = animate(PULSE_MIN_ALPHA, 1f, PULSE_DURATION_MS, + new AccelerateDecelerateInterpolator(), + f -> completionPulseAlpha = f, then); + mPulseAnim.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { second.start(); } + }); + mPulseAnim.start(); + } + + private void endFinish(Runnable onComplete) { + isFinishAnimating = false; + resetFinishState(); + onComplete.run(); + mHost.invalidate(); + } + + public void cancelFinish() { + cancel(mFinishAnim); mFinishAnim = null; + cancel(mPulseAnim); mPulseAnim = null; + isFinishAnimating = false; + resetFinishState(); + } + + private void resetFinishState() { + displayAlpha = 1f; + displayScale = 1f; + segmentHighlight = -1; + successColorBlend = 0f; + completionPulseAlpha = 1f; + } + + public void startError(Runnable onComplete) { + if (isFinishAnimating || isErrorAnimating) return; + isErrorAnimating = true; + cancel(mErrorAnim); + + mErrorAnim = ValueAnimator.ofFloat(0f, 1f, 0f, 1f, 0f); + mErrorAnim.setDuration(600); + mErrorAnim.setInterpolator(new LinearInterpolator()); + mErrorAnim.addUpdateListener(a -> { + errorAlpha = (float) a.getAnimatedValue(); + mHost.invalidate(); + }); + mErrorAnim.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { + isErrorAnimating = false; + errorAlpha = 0f; + if (onComplete != null) onComplete.run(); + mHost.invalidate(); + } + }); + mErrorAnim.start(); + } + + public void cancelError() { + cancel(mErrorAnim); mErrorAnim = null; + isErrorAnimating = false; + errorAlpha = 0f; + } + + public void startDynamicPreview(String finishStyle, int holdMs, + int exitMs, boolean pulse) { + mHost.removeCallbacks(mPreviewDebounce); + mPreviewDebounce = () -> runDynamicPreview(finishStyle, holdMs, exitMs, pulse); + mHost.postDelayed(mPreviewDebounce, PREVIEW_DEBOUNCE_MS); + } + + private void runDynamicPreview(String finishStyle, int holdMs, + int exitMs, boolean pulse) { + cancelDynamicPreview(); + previewMode = PreviewMode.DYNAMIC; + previewProgress = 0; + + mPreviewAnim = animateInt(0, 100, 800, + new AccelerateDecelerateInterpolator(), + p -> previewProgress = p, + () -> { + previewProgress = 100; + startFinish(finishStyle, holdMs, exitMs, pulse, () -> + mHost.postDelayed(() -> { + previewMode = PreviewMode.NONE; + previewProgress = 0; + mHost.invalidate(); + }, 200)); + }); + } + + public void cancelDynamicPreview() { + mHost.removeCallbacks(mPreviewDebounce); + mPreviewDebounce = null; + cancel(mPreviewAnim); mPreviewAnim = null; + previewMode = PreviewMode.NONE; + previewProgress = 0; + } + + public void showGeometryPreview(boolean autoHide) { + mHost.removeCallbacks(mGeometryHideTask); + mGeometryHideTask = null; + previewMode = PreviewMode.GEOMETRY; + mHost.invalidate(); + if (autoHide) { + mGeometryHideTask = () -> { + previewMode = PreviewMode.NONE; + mGeometryHideTask = null; + mHost.invalidate(); + }; + mHost.postDelayed(mGeometryHideTask, GEOMETRY_HOLD_MS); + } + } + + public void cancelGeometryPreview() { + mHost.removeCallbacks(mGeometryHideTask); + mGeometryHideTask = null; + if (previewMode == PreviewMode.GEOMETRY) { + previewMode = PreviewMode.NONE; + mHost.invalidate(); + } + } + + public void cancelAll() { + cancelFinish(); + cancelError(); + cancelDynamicPreview(); + cancelGeometryPreview(); + } + + private interface FloatConsumer { void accept(float v); } + private interface IntConsumer { void accept(int v); } + + private ValueAnimator animate(float from, float to, long durationMs, + TimeInterpolator interp, + FloatConsumer onUpdate, Runnable onEnd) { + if (durationMs <= 0) { + onUpdate.accept(to); + mHost.invalidate(); + if (onEnd != null) onEnd.run(); + return new ValueAnimator(); + } + ValueAnimator va = ValueAnimator.ofFloat(from, to); + va.setDuration(durationMs); + va.setInterpolator(interp); + va.addUpdateListener(a -> { + onUpdate.accept((float) a.getAnimatedValue()); + mHost.invalidate(); + }); + if (onEnd != null) va.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { onEnd.run(); } + }); + va.start(); + return va; + } + + private ValueAnimator animateInt(int from, int to, long durationMs, + TimeInterpolator interp, + IntConsumer onUpdate, Runnable onEnd) { + if (durationMs <= 0) { + onUpdate.accept(to); + mHost.invalidate(); + if (onEnd != null) onEnd.run(); + return new ValueAnimator(); + } + ValueAnimator va = ValueAnimator.ofInt(from, to); + va.setDuration(durationMs); + va.setInterpolator(interp); + va.addUpdateListener(a -> { + onUpdate.accept((int) a.getAnimatedValue()); + mHost.invalidate(); + }); + if (onEnd != null) va.addListener(new AnimatorListenerAdapter() { + @Override public void onAnimationEnd(Animator a) { onEnd.run(); } + }); + va.start(); + return va; + } + + private static void cancel(ValueAnimator va) { + if (va != null) va.cancel(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java new file mode 100644 index 000000000000..26ef233598b0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/RingViewRenderer.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024-2026 Lunaris AOSP + * + * 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.cutoutprogress.ring; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; + +public interface RingViewRenderer { + + void updateBounds(RectF bounds); + + void drawFullRing(Canvas canvas, Paint paint); + + void drawProgress(Canvas canvas, float sweepFraction, boolean clockwise, Paint paint); + + void drawSegmented(Canvas canvas, + int segments, float gapDeg, float arcDeg, + int highlight, + Paint basePaint, Paint shinePaint, float alpha); +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 46d706dc7c2d..db21ba592045 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -107,6 +107,7 @@ import com.android.systemui.navigationbar.gestural.dagger.GestureModule; import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; +import com.android.systemui.cutoutprogress.dagger.CutoutProgressModule; import com.android.systemui.plugins.BcSmartspaceConfigPlugin; import com.android.systemui.plugins.BcSmartspaceDataPlugin; import com.android.systemui.privacy.PrivacyModule; @@ -343,6 +344,7 @@ LowLightClockModule.class, PerDisplayRepositoriesModule.class, InputDeviceModule.class, + CutoutProgressModule.class, }, subcomponents = { ComplicationComponent.class, From 51e24c4fd83dfad54322dd39fb73ab5f40ebb675 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 26 Feb 2026 09:00:59 +0000 Subject: [PATCH 102/190] SystemUI: Progress ring charging indicator [1/2] Signed-off-by: Ghosuto --- .../CutoutProgressController.java | 43 +++++ .../CutoutProgressSettings.java | 14 ++ .../cutoutprogress/ring/CutoutRingView.java | 163 +++++++++++++++++- 3 files changed, 217 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java index 566bafb6eaaf..16ca95a150d5 100644 --- a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressController.java @@ -16,8 +16,12 @@ package com.android.systemui.cutoutprogress; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.graphics.PixelFormat; +import android.os.BatteryManager; import android.os.Handler; import android.view.WindowManager; @@ -46,6 +50,29 @@ public class CutoutProgressController implements CoreStartable { private boolean mOverlayAttached = false; private boolean mListenerRegistered = false; + private boolean mBatteryReceiverRegistered = false; + + private final BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (!mSettings.isEnabled() || !mSettings.isChargingRingEnabled()) { + mMainHandler.post(() -> mRingView.setChargingState(false, 0)); + return; + } + int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, + BatteryManager.BATTERY_STATUS_UNKNOWN); + boolean charging = status == BatteryManager.BATTERY_STATUS_CHARGING + || status == BatteryManager.BATTERY_STATUS_FULL; + int level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0); + int scale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 100); + int pct = scale > 0 ? level * 100 / scale : 0; + boolean pulseEnabled = mSettings.isChargingPulseEnabled(); + mMainHandler.post(() -> { + mRingView.setChargingPulseEnabled(pulseEnabled); + mRingView.setChargingState(charging, pct); + }); + } + }; @Inject public CutoutProgressController( @@ -82,11 +109,13 @@ private void onSettingsChanged() { private void enableFeature() { attachOverlay(); registerPipelineListener(); + registerBatteryReceiver(); } private void disableFeature() { mTracker.reset(); detachOverlay(); + unregisterBatteryReceiver(); } private void attachOverlay() { @@ -148,6 +177,20 @@ public void onEntryRemoved(NotificationEntry entry, int reason) { }); } + private void registerBatteryReceiver() { + if (mBatteryReceiverRegistered) return; + IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED); + mContext.registerReceiver(mBatteryReceiver, filter); + mBatteryReceiverRegistered = true; + } + + private void unregisterBatteryReceiver() { + if (!mBatteryReceiverRegistered) return; + mContext.unregisterReceiver(mBatteryReceiver); + mBatteryReceiverRegistered = false; + mMainHandler.post(() -> mRingView.setChargingState(false, 0)); + } + private void bindTrackerToView() { mTracker.setOnProgress(progress -> mMainHandler.post(() -> mRingView.setProgress(progress))); diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java index 3a7fdc14d84e..41d0ab8df51d 100644 --- a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java @@ -109,6 +109,10 @@ public final class CutoutProgressSettings { public static final String KEY_PROGRESS_EASING = "cutout_progress_easing"; + public static final String KEY_CHARGING_RING_ENABLED = "cutout_progress_charging_ring_enabled"; + + public static final String KEY_CHARGING_PULSE_ENABLED = "cutout_progress_charging_pulse_enabled"; + private static final boolean DEF_ENABLED = false; private static final int DEF_RING_COLOR = 0xFF2196F3; private static final int DEF_ERROR_COLOR = 0xFFF44336; @@ -144,6 +148,8 @@ public final class CutoutProgressSettings { private static final int DEF_FILENAME_MAX_CHARS = 20; private static final int DEF_FILENAME_TRUNCATE = 0; private static final int DEF_EASING = 0; + private static final boolean DEF_CHARGING_RING_ENABLED = true; + private static final boolean DEF_CHARGING_PULSE_ENABLED = true; static final String[] POSITION_NAMES = { "right", "left", "top", "bottom", @@ -367,6 +373,14 @@ public String getProgressEasing() { return EASING_NAMES[idx]; } + public boolean isChargingRingEnabled() { + return getInt(KEY_CHARGING_RING_ENABLED, DEF_CHARGING_RING_ENABLED ? 1 : 0) != 0; + } + + public boolean isChargingPulseEnabled() { + return getInt(KEY_CHARGING_PULSE_ENABLED, DEF_CHARGING_PULSE_ENABLED ? 1 : 0) != 0; + } + public void setEnabled(boolean value) { putInt(KEY_ENABLED, value ? 1 : 0); } diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java index 23c79ecfa15b..43973e31564e 100644 --- a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java @@ -16,6 +16,7 @@ package com.android.systemui.cutoutprogress.ring; +import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; @@ -30,6 +31,7 @@ import android.view.Surface; import android.view.View; import android.view.WindowInsets; +import android.view.animation.LinearInterpolator; import com.android.systemui.cutoutprogress.CutoutProgressSettings; @@ -39,6 +41,9 @@ public final class CutoutRingView extends View { private static final long BURN_IN_HIDE_MS = 10_000L; + private static final long CHARGING_PULSE_INTERVAL_MS = 1500L; + private static final long CHARGING_PULSE_DURATION_MS = 900L; + private final float mDp; private final Path mCutoutPath = new Path(); @@ -57,6 +62,7 @@ public final class CutoutRingView extends View { private final Paint mErrorPaint = makePaint(); private final Paint mAnimPaint = makePaint(); private final Paint mBgPaint = makePaint(); + private final Paint mChargingPaint = makePaint(); private final TextPaint mPercentPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private final TextPaint mFilenamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); @@ -68,6 +74,14 @@ public final class CutoutRingView extends View { private long mLastProgressMs = 0L; private Runnable mPendingFinish = null; + private boolean mIsCharging = false; + private int mBatteryPct = 0; + private boolean mChargingPulseEnabled = true; + private float mChargingPulsePhase = 0f; + private ValueAnimator mChargingPulseAnim = null; + private float mChargingDisplayPct = 0f; + private ValueAnimator mChargingLevelAnim = null; + private int sCfgRingColor; private int sCfgErrorColor; private int sCfgFlashColor; @@ -107,8 +121,10 @@ public final class CutoutRingView extends View { private float sCfgFnameOffXDp; private float sCfgFnameOffYDp; private int sCfgFnameMaxChars; - private String sCfgFnameTruncate; - private String sCfgEasing; + private String sCfgFnameTruncate; + private String sCfgEasing; + private boolean sCfgChargingRing; + private boolean sCfgChargingPulse; public CutoutRingView(Context ctx) { super(ctx); @@ -161,6 +177,8 @@ public void applySettings(CutoutProgressSettings s) { sCfgFnameMaxChars= s.getFilenameMaxChars(); sCfgFnameTruncate= s.getFilenameTruncateMode(); sCfgEasing = s.getProgressEasing(); + sCfgChargingRing = s.isChargingRingEnabled(); + sCfgChargingPulse = s.isChargingPulseEnabled(); boolean needPath = sCfgPathMode; if (needPath && !(mRenderer instanceof CapsuleRingRenderer)) { @@ -169,11 +187,102 @@ public void applySettings(CutoutProgressSettings s) { mRenderer = new CircleRingRenderer(); } + if (!sCfgChargingRing && mIsCharging) { + stopChargingAnimations(); + } + mChargingPulseEnabled = sCfgChargingPulse; + refreshPaints(); recalcScaledPath(); invalidate(); } + public void setChargingState(boolean charging, int batteryPct) { + boolean wasCharging = mIsCharging; + mIsCharging = charging; + mBatteryPct = batteryPct; + + if (!charging) { + stopChargingAnimations(); + invalidate(); + return; + } + + if (!wasCharging) { + mChargingDisplayPct = 0f; + } + + animateChargingLevelTo(batteryPct); + + if (mChargingPulseEnabled && sCfgChargingPulse && mChargingPulseAnim == null) { + startChargingPulse(); + } + invalidate(); + } + + public void setChargingPulseEnabled(boolean enabled) { + mChargingPulseEnabled = enabled; + sCfgChargingPulse = enabled; + if (!enabled) { + stopChargingPulse(); + } else if (mIsCharging && mChargingPulseAnim == null) { + startChargingPulse(); + } + } + + private void animateChargingLevelTo(int targetPct) { + if (mChargingLevelAnim != null) mChargingLevelAnim.cancel(); + float start = mChargingDisplayPct; + float end = Math.max(0f, Math.min(100f, targetPct)); + if (Math.abs(end - start) < 0.5f) { + mChargingDisplayPct = end; + invalidate(); + return; + } + long dur = (long)(Math.abs(end - start) * 12f); + dur = Math.max(200L, Math.min(dur, 1200L)); + mChargingLevelAnim = ValueAnimator.ofFloat(start, end); + mChargingLevelAnim.setDuration(dur); + mChargingLevelAnim.setInterpolator(new LinearInterpolator()); + mChargingLevelAnim.addUpdateListener(a -> { + mChargingDisplayPct = (float) a.getAnimatedValue(); + invalidate(); + }); + mChargingLevelAnim.start(); + } + + private void startChargingPulse() { + stopChargingPulse(); + mChargingPulseAnim = ValueAnimator.ofFloat(0f, 1f); + mChargingPulseAnim.setDuration(CHARGING_PULSE_DURATION_MS); + mChargingPulseAnim.setRepeatCount(ValueAnimator.INFINITE); + mChargingPulseAnim.setRepeatMode(ValueAnimator.REVERSE); + mChargingPulseAnim.setInterpolator(new LinearInterpolator()); + mChargingPulseAnim.setStartDelay(0); + mChargingPulseAnim.addUpdateListener(a -> { + mChargingPulsePhase = (float) a.getAnimatedValue(); + invalidate(); + }); + mChargingPulseAnim.start(); + } + + private void stopChargingPulse() { + if (mChargingPulseAnim != null) { + mChargingPulseAnim.cancel(); + mChargingPulseAnim = null; + } + mChargingPulsePhase = 0f; + } + + private void stopChargingAnimations() { + stopChargingPulse(); + if (mChargingLevelAnim != null) { + mChargingLevelAnim.cancel(); + mChargingLevelAnim = null; + } + mChargingDisplayPct = 0f; + } + public void setProgress(int value) { int pct = Math.max(0, Math.min(100, value)); if (mProgress == pct) return; @@ -294,7 +403,15 @@ protected void onDraw(Canvas canvas) { || (effectivePct > 0 && effectivePct < 100 && !burnedOut) || mPendingFinish != null; - if (!shouldDraw) return; + boolean showCharging = mIsCharging && sCfgChargingRing && !mAnim.isFinishAnimating + && !mAnim.isErrorAnimating && effectivePct == 0 && mProgress == 0; + + if (!shouldDraw && !showCharging) return; + + if (showCharging) { + drawChargingRing(canvas); + return; + } if (mAnim.displayScale != 1f) { mScaledPath.computeBounds(mArcBounds, true); @@ -352,6 +469,43 @@ protected void onDraw(Canvas canvas) { if (mAnim.displayScale != 1f) canvas.restore(); } + private void drawChargingRing(Canvas canvas) { + computeArcBounds(); + mRenderer.updateBounds(mArcBounds); + + int baseAlpha = sCfgOpacity * 255 / 100; + + if (sCfgBgRing) { + mBgPaint.setAlpha(sCfgBgOpacity * 255 / 100); + mRenderer.drawFullRing(canvas, mBgPaint); + } + + int levelColor = chargingColor(mChargingDisplayPct); + applyStroke(mChargingPaint, levelColor, sCfgStrokeDp * mDp, baseAlpha); + + if (mChargingPulseEnabled && sCfgChargingPulse && mChargingPulseAnim != null) { + float drawFraction = mChargingPulsePhase * (mChargingDisplayPct / 100f); + drawSymmetricArc(canvas, drawFraction, mChargingPaint); + } else { + drawSymmetricArc(canvas, mChargingDisplayPct / 100f, mChargingPaint); + } + } + + private void drawSymmetricArc(Canvas canvas, float fraction, Paint paint) { + if (fraction <= 0f) return; + fraction = Math.min(fraction, 1f); + float sweep = fraction * 180f; + + canvas.drawArc(mArcBounds, 90f - sweep, sweep, false, paint); + canvas.drawArc(mArcBounds, 90f, sweep, false, paint); + } + + private static int chargingColor(float pct) { + if (pct < 30f) return 0xFFF44336; + if (pct < 60f) return 0xFFFF9800; + return 0xFF4CAF50; + } + private void drawFinish(Canvas canvas, Paint paint) { if ("segmented".equals(sCfgFinishStyle)) { mRenderer.drawSegmented(canvas, @@ -474,6 +628,8 @@ private void initPaints() { sCfgScaleX = sCfgScaleY = 1f; sCfgBgRing = sCfgMinVis = true; sCfgMinVisMs = 500; + sCfgChargingRing = true; + sCfgChargingPulse = true; refreshPaints(); } @@ -483,6 +639,7 @@ private void refreshPaints() { applyStroke(mShinePaint, sCfgFlashColor, stroke * 1.2f, 255); applyStroke(mErrorPaint, sCfgErrorColor, stroke * 1.5f, 255); applyStroke(mBgPaint, sCfgBgColor, stroke, sCfgBgOpacity * 255 / 100); + applyStroke(mChargingPaint, sCfgRingColor, stroke, sCfgOpacity * 255 / 100); mPercentPaint.setTypeface(sCfgPctBold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); From f010159ba7565712c562da73fe7fbefb1edcf9e6 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 26 Feb 2026 14:43:16 +0000 Subject: [PATCH 103/190] SystemUI: Add progress ring color mode [1/2] Signed-off-by: Ghosuto --- .../CutoutProgressSettings.java | 15 ++ .../cutoutprogress/ring/CutoutRingView.java | 138 ++++++++++++++++-- 2 files changed, 138 insertions(+), 15 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java index 41d0ab8df51d..5c0f9e0d6ed9 100644 --- a/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/CutoutProgressSettings.java @@ -27,6 +27,8 @@ public final class CutoutProgressSettings { public static final String KEY_ENABLED = "cutout_progress_enabled"; + public static final String KEY_RING_COLOR_MODE = "cutout_progress_ring_color_mode"; + public static final String KEY_RING_COLOR = "cutout_progress_ring_color"; public static final String KEY_ERROR_COLOR = "cutout_progress_error_color"; @@ -113,7 +115,11 @@ public final class CutoutProgressSettings { public static final String KEY_CHARGING_PULSE_ENABLED = "cutout_progress_charging_pulse_enabled"; + public static final int RING_COLOR_MODE_ACCENT = 0; + public static final int RING_COLOR_MODE_RAINBOW = 1; + public static final int RING_COLOR_MODE_CUSTOM = 2; private static final boolean DEF_ENABLED = false; + private static final int DEF_RING_COLOR_MODE = RING_COLOR_MODE_ACCENT; private static final int DEF_RING_COLOR = 0xFF2196F3; private static final int DEF_ERROR_COLOR = 0xFFF44336; private static final int DEF_FINISH_FLASH_COLOR = Color.WHITE; @@ -199,6 +205,11 @@ public boolean isEnabled() { return getInt(KEY_ENABLED, DEF_ENABLED ? 1 : 0) != 0; } + public int getRingColorMode() { + return clamp(getInt(KEY_RING_COLOR_MODE, DEF_RING_COLOR_MODE), + RING_COLOR_MODE_ACCENT, RING_COLOR_MODE_CUSTOM); + } + public int getRingColor() { return getInt(KEY_RING_COLOR, DEF_RING_COLOR); } @@ -385,6 +396,10 @@ public void setEnabled(boolean value) { putInt(KEY_ENABLED, value ? 1 : 0); } + public void setRingColorMode(int mode) { + putInt(KEY_RING_COLOR_MODE, clamp(mode, RING_COLOR_MODE_ACCENT, RING_COLOR_MODE_CUSTOM)); + } + public void setRingColor(int argb) { putInt(KEY_RING_COLOR, argb); } diff --git a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java index 43973e31564e..7910bc286775 100644 --- a/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java +++ b/packages/SystemUI/src/com/android/systemui/cutoutprogress/ring/CutoutRingView.java @@ -18,12 +18,16 @@ import android.animation.ValueAnimator; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.LinearGradient; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.SweepGradient; import android.graphics.Typeface; import android.text.TextPaint; import android.util.TypedValue; @@ -44,6 +48,17 @@ public final class CutoutRingView extends View { private static final long CHARGING_PULSE_INTERVAL_MS = 1500L; private static final long CHARGING_PULSE_DURATION_MS = 900L; + private static final int[] RAINBOW_COLORS = { + 0xFFFF0000, + 0xFFFF7F00, + 0xFFFFFF00, + 0xFF00FF00, + 0xFF00FFFF, + 0xFF0000FF, + 0xFF8B00FF, + 0xFFFF0000, + }; + private final float mDp; private final Path mCutoutPath = new Path(); @@ -66,6 +81,11 @@ public final class CutoutRingView extends View { private final TextPaint mPercentPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); private final TextPaint mFilenamePaint = new TextPaint(Paint.ANTI_ALIAS_FLAG); + private final Paint mRainbowPaint = makePaint(); + private SweepGradient mRainbowShader = null; + private float mRainbowCx = Float.NaN; + private float mRainbowCy = Float.NaN; + private int mProgress = 0; private int mDownloadCount = 0; private String mFilenameHint = null; @@ -82,6 +102,7 @@ public final class CutoutRingView extends View { private float mChargingDisplayPct = 0f; private ValueAnimator mChargingLevelAnim = null; + private int sCfgRingColorMode; private int sCfgRingColor; private int sCfgErrorColor; private int sCfgFlashColor; @@ -136,6 +157,7 @@ public CutoutRingView(Context ctx) { } public void applySettings(CutoutProgressSettings s) { + sCfgRingColorMode = s.getRingColorMode(); sCfgRingColor = s.getRingColor(); sCfgErrorColor = s.getErrorColor(); sCfgFlashColor = s.getFinishFlashColor(); @@ -192,11 +214,77 @@ public void applySettings(CutoutProgressSettings s) { } mChargingPulseEnabled = sCfgChargingPulse; + if (sCfgRingColorMode != CutoutProgressSettings.RING_COLOR_MODE_RAINBOW) { + mRainbowShader = null; + mRainbowCx = Float.NaN; + } + refreshPaints(); recalcScaledPath(); invalidate(); } + private int resolveRingColor() { + switch (sCfgRingColorMode) { + case CutoutProgressSettings.RING_COLOR_MODE_ACCENT: + return resolveAccentColor(); + case CutoutProgressSettings.RING_COLOR_MODE_RAINBOW: + return RAINBOW_COLORS[0]; + default: + return sCfgRingColor; + } + } + + private int resolveAccentColor() { + TypedValue tv = new TypedValue(); + boolean resolved = getContext().getTheme() + .resolveAttribute(android.R.attr.colorAccent, tv, true); + int base = resolved ? tv.data : 0xFF2196F3; + + boolean isDark = (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES; + + if (isDark) { + return lightenColor(base, 0.50f); + } else { + return base; + } + } + + private static int lightenColor(int color, float fraction) { + int r = Color.red(color); + int g = Color.green(color); + int b = Color.blue(color); + r = (int)(r + (255 - r) * fraction); + g = (int)(g + (255 - g) * fraction); + b = (int)(b + (255 - b) * fraction); + return Color.argb(Color.alpha(color), + Math.min(255, r), Math.min(255, g), Math.min(255, b)); + } + + private SweepGradient requireRainbowShader(float cx, float cy) { + if (mRainbowShader == null + || Math.abs(cx - mRainbowCx) > 0.5f + || Math.abs(cy - mRainbowCy) > 0.5f) { + mRainbowShader = new SweepGradient(cx, cy, RAINBOW_COLORS, null); + mRainbowCx = cx; + mRainbowCy = cy; + } + return mRainbowShader; + } + + private void applyRainbowShader(Paint paint, float cx, float cy) { + SweepGradient shader = requireRainbowShader(cx, cy); + Matrix m = new Matrix(); + m.setRotate(-90f, cx, cy); + shader.setLocalMatrix(m); + paint.setShader(shader); + } + + private static void clearShader(Paint paint) { + paint.setShader(null); + } + public void setChargingState(boolean charging, int batteryPct) { boolean wasCharging = mIsCharging; mIsCharging = charging; @@ -362,6 +450,9 @@ public WindowInsets onApplyWindowInsets(WindowInsets insets) { mHasCutout = true; } + mRainbowShader = null; + mRainbowCx = Float.NaN; + recalcScaledPath(); invalidate(); return super.onApplyWindowInsets(insets); @@ -420,16 +511,28 @@ protected void onDraw(Canvas canvas) { mArcBounds.centerX(), mArcBounds.centerY()); } + int activeRingColor = resolveRingColor(); + mAnimPaint.set(mRingPaint); + mAnimPaint.setColor(activeRingColor); + mAnimPaint.setStrokeWidth(sCfgStrokeDp * mDp); int alpha = (int)(sCfgOpacity * 255f / 100f * mAnim.displayAlpha * mAnim.completionPulseAlpha); mAnimPaint.setAlpha(alpha); + computeArcBounds(); + if (sCfgRingColorMode == CutoutProgressSettings.RING_COLOR_MODE_RAINBOW) { + applyRainbowShader(mAnimPaint, mArcBounds.centerX(), mArcBounds.centerY()); + } else { + clearShader(mAnimPaint); + } + if (mAnim.successColorBlend > 0f) { - int successColor = sCfgFinishFlash ? sCfgFlashColor - : brighten(sCfgRingColor, mAnim.successColorBlend); - mAnimPaint.setColor(blendColors(sCfgRingColor, successColor, + int flashColor = sCfgFinishFlash ? sCfgFlashColor + : brighten(activeRingColor, mAnim.successColorBlend); + mAnimPaint.setColor(blendColors(activeRingColor, flashColor, mAnim.successColorBlend)); + clearShader(mAnimPaint); } boolean isActive = effectivePct > 0 && effectivePct < 100 @@ -437,23 +540,19 @@ protected void onDraw(Canvas canvas) { || mAnim.isDynamicPreviewActive(); if (sCfgBgRing && !mAnim.isFinishAnimating && isActive) { - computeArcBounds(); mRenderer.updateBounds(mArcBounds); mBgPaint.setAlpha((int)(sCfgBgOpacity * 255 / 100 * mAnim.displayAlpha)); mRenderer.drawFullRing(canvas, mBgPaint); } + mRenderer.updateBounds(mArcBounds); if (mAnim.isFinishAnimating) { - computeArcBounds(); - mRenderer.updateBounds(mArcBounds); drawFinish(canvas, mAnimPaint); } else { - computeArcBounds(); - mRenderer.updateBounds(mArcBounds); float sweep = eased(effectivePct, sCfgEasing); mRenderer.drawProgress(canvas, sweep, sCfgClockwise, mAnimPaint); - if (isActive) drawLabels(canvas, effectivePct); + if (isActive) drawLabels(canvas, effectivePct, activeRingColor); } boolean showBadge = !mAnim.isDynamicPreviewActive() && sCfgShowBadge @@ -519,7 +618,7 @@ private void drawFinish(Canvas canvas, Paint paint) { } } - private void drawLabels(Canvas canvas, int pct) { + private void drawLabels(Canvas canvas, int pct, int ringColor) { float pad = 4f * mDp; int alpha = sCfgOpacity * 255 / 100; @@ -527,7 +626,7 @@ private void drawLabels(Canvas canvas, int pct) { String text = pct + "%"; float tw = mPercentPaint.measureText(text); float[] pos = labelXY(sCfgPctPos, pad, mPercentPaint.getTextSize(), tw); - mPercentPaint.setColor(sCfgRingColor); + mPercentPaint.setColor(ringColor); mPercentPaint.setAlpha(alpha); canvas.drawText(text, pos[0] + sCfgPctOffXDp * mDp, pos[1] + sCfgPctOffYDp * mDp, mPercentPaint); @@ -540,7 +639,7 @@ private void drawLabels(Canvas canvas, int pct) { if (sCfgFname && fname != null && (mDownloadCount <= 1 || geoPreview)) { String display = truncate(fname, sCfgFnameMaxChars, sCfgFnameTruncate); float[] pos = labelXY(sCfgFnamePos, pad, mFilenamePaint.getTextSize(), null); - mFilenamePaint.setColor(sCfgRingColor); + mFilenamePaint.setColor(ringColor); mFilenamePaint.setAlpha(alpha); canvas.drawText(display, pos[0] + sCfgFnameOffXDp * mDp, pos[1] + sCfgFnameOffYDp * mDp, mFilenamePaint); @@ -606,6 +705,7 @@ private float[] rotateOffset(float dx, float dy) { } private void initPaints() { + sCfgRingColorMode = CutoutProgressSettings.RING_COLOR_MODE_ACCENT; sCfgRingColor = 0xFF2196F3; sCfgErrorColor = 0xFFF44336; sCfgFlashColor = Color.WHITE; @@ -635,11 +735,19 @@ private void initPaints() { private void refreshPaints() { float stroke = sCfgStrokeDp * mDp; - applyStroke(mRingPaint, sCfgRingColor, stroke, sCfgOpacity * 255 / 100); + int baseColor = (sCfgRingColorMode == CutoutProgressSettings.RING_COLOR_MODE_CUSTOM) + ? sCfgRingColor : resolveRingColor(); + applyStroke(mRingPaint, baseColor, stroke, sCfgOpacity * 255 / 100); applyStroke(mShinePaint, sCfgFlashColor, stroke * 1.2f, 255); applyStroke(mErrorPaint, sCfgErrorColor, stroke * 1.5f, 255); applyStroke(mBgPaint, sCfgBgColor, stroke, sCfgBgOpacity * 255 / 100); - applyStroke(mChargingPaint, sCfgRingColor, stroke, sCfgOpacity * 255 / 100); + applyStroke(mChargingPaint, baseColor, stroke, sCfgOpacity * 255 / 100); + + mRainbowPaint.setStyle(Paint.Style.STROKE); + mRainbowPaint.setAntiAlias(true); + mRainbowPaint.setStrokeWidth(stroke); + mRainbowPaint.setStrokeCap(Paint.Cap.BUTT); + mRainbowPaint.setAlpha(sCfgOpacity * 255 / 100); mPercentPaint.setTypeface(sCfgPctBold ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); @@ -651,7 +759,7 @@ private void refreshPaints() { mFilenamePaint.setTextSize(spToPx(sCfgFnameSp)); mFilenamePaint.setTextAlign(Paint.Align.LEFT); - mBadge.applyConfig(sCfgRingColor, sCfgBadgeSp, mDp); + mBadge.applyConfig(baseColor, sCfgBadgeSp, mDp); } private static void applyStroke(Paint p, int color, float width, int alpha) { From 1b1da52e90042ac78c145ec354a1b5ff4979be87 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Tue, 24 Feb 2026 14:21:25 +0000 Subject: [PATCH 104/190] SystemUI: Implement Weather views Signed-off-by: Ghosuto --- .../res-keyguard/layout/keyguard_weather.xml | 29 +++++++++ .../SystemUI/res/values/lineage_dimens.xml | 3 + .../SystemUI/res/values/lunaris_attrs.xml | 6 +- .../SystemUI/res/values/lunaris_dimens.xml | 3 + .../SystemUI/res/values/lunaris_strings.xml | 9 +++ .../systemui/weather/WeatherImageView.kt | 59 +++++++++++++++++++ .../systemui/weather/WeatherTextView.kt | 58 ++++++++++++++++++ .../systemui/weather/WeatherViewController.kt | 14 +++++ 8 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_weather.xml create mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt create mode 100644 packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_weather.xml b/packages/SystemUI/res-keyguard/layout/keyguard_weather.xml new file mode 100644 index 000000000000..cdf4a2ada9e8 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_weather.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/packages/SystemUI/res/values/lineage_dimens.xml b/packages/SystemUI/res/values/lineage_dimens.xml index 4a24b9d35714..2b695422554e 100644 --- a/packages/SystemUI/res/values/lineage_dimens.xml +++ b/packages/SystemUI/res/values/lineage_dimens.xml @@ -18,4 +18,7 @@ 24dp + + + 24dp diff --git a/packages/SystemUI/res/values/lunaris_attrs.xml b/packages/SystemUI/res/values/lunaris_attrs.xml index 30f90e391676..db0e424e35c7 100644 --- a/packages/SystemUI/res/values/lunaris_attrs.xml +++ b/packages/SystemUI/res/values/lunaris_attrs.xml @@ -3,7 +3,7 @@ Copyright (C) 2024-2026 Lunaris AOSP SPDX-License-Identifier: Apache-2.0 --> - + @@ -16,4 +16,8 @@ + + + + diff --git a/packages/SystemUI/res/values/lunaris_dimens.xml b/packages/SystemUI/res/values/lunaris_dimens.xml index b3a04ce2e768..908ea92da598 100644 --- a/packages/SystemUI/res/values/lunaris_dimens.xml +++ b/packages/SystemUI/res/values/lunaris_dimens.xml @@ -122,4 +122,7 @@ 28dp 0dp 8dp + + + 24dp diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index f26f7312c7fa..1e856bdc1bd1 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -219,4 +219,13 @@ VPN tethering VPN tethering turned off. VPN tethering turned on. + + + Cloudy + Rainy + Sunny + Stormy + Snowy + Windy + Misty diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt new file mode 100644 index 000000000000..c6c3fba59ede --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt @@ -0,0 +1,59 @@ +/* + * 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.util.DisplayMetrics +import android.view.View +import android.view.View.MeasureSpec +import android.widget.ImageView +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 = WeatherViewController(context, this, null, null) + + init { + visibility = View.GONE + } + + fun setWeatherEnabled(enabled: Boolean) { + visibility = if (enabled) View.VISIBLE else View.GONE + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + weatherViewController.updateWeatherSettings() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + weatherViewController.disableUpdates() + 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/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt new file mode 100644 index 000000000000..e48d23424cc6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt @@ -0,0 +1,58 @@ +/* + * 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 + private val mWeatherText: String? + + init { + val a = context.obtainStyledAttributes(attrs, R.styleable.WeatherTextView, defStyle, 0) + mWeatherText = a.getString(R.styleable.WeatherTextView_weatherText) + a.recycle() + + mWeatherViewController = WeatherViewController(context, null, this, mWeatherText) + + text = if (!mWeatherText.isNullOrEmpty()) mWeatherText else "" + visibility = View.GONE + } + + fun setWeatherEnabled(enabled: Boolean) { + visibility = if (enabled) View.VISIBLE else View.GONE + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mWeatherViewController.updateWeatherSettings() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mWeatherViewController.disableUpdates() + mWeatherViewController.removeObserver() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index de191bb92388..334829ee0d25 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -91,6 +91,7 @@ class WeatherViewController( private fun getWeatherSettings() = WeatherSettings( weatherEnabled = getSystemSetting(LOCKSCREEN_WEATHER_ENABLED), + clockFaceEnabled = getSecureSetting(CLOCK_STYLE), showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), @@ -100,6 +101,9 @@ class WeatherViewController( private fun getSystemSetting(setting: String, defaultValue: Int = 0) = Settings.System.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 + private fun getSecureSetting(setting: String, defaultValue: Int = 0) = + Settings.Secure.getIntForUser(context.contentResolver, setting, defaultValue, UserHandle.USER_CURRENT) != 0 + private fun applyWeatherSettings(settings: WeatherSettings) { if (!settings.weatherEnabled) { hideAllViews() @@ -108,6 +112,14 @@ class WeatherViewController( OmniJawsClient.get().addObserver(context, this@WeatherViewController) updateWeather() showAllViews() + // When a clock face style is active, hide the default weather views + // so they don't overlap the clock face layout (mirrors risingOS behaviour). + if (weatherIcon.id == R.id.default_weather_image) { + scope.launch { updateViewVisibility(weatherIcon, !settings.clockFaceEnabled) } + } + if (weatherTemp.id == R.id.default_weather_text) { + scope.launch { updateViewVisibility(weatherTemp, !settings.clockFaceEnabled) } + } } } @@ -194,6 +206,7 @@ class WeatherViewController( data class WeatherSettings( val weatherEnabled: Boolean, + val clockFaceEnabled: Boolean, val showWeatherLocation: Boolean, val showWeatherText: Boolean, val showWindInfo: Boolean, @@ -206,6 +219,7 @@ class WeatherViewController( 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 const val CLOCK_STYLE = "clock_style" private val WEATHER_CONDITIONS = mapOf( "clouds" to R.string.weather_condition_clouds, From 32137143b8bd42f145cebe210967a3d8676d183f Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Mon, 14 Oct 2024 12:28:40 +0800 Subject: [PATCH 105/190] SystemUI: Introduce Clock face feature Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- core/res/res/values/dimens.xml | 5 + core/res/res/values/symbols.xml | 5 + .../clocks/common/res/values/dimens.xml | 8 +- .../layout/keyguard_clock_center.xml | 67 ++++ .../layout/keyguard_clock_ide.xml | 375 ++++++++++++++++++ .../layout/keyguard_clock_miui.xml | 74 ++++ .../layout/keyguard_clock_moto.xml | 74 ++++ .../layout/keyguard_clock_oos.xml | 107 +++++ .../layout/keyguard_clock_simple.xml | 60 +++ .../layout/keyguard_clock_style.xml | 17 + .../res/values-sw600dp-land/config.xml | 2 +- .../SystemUI/res/values/lunaris_dimens.xml | 5 + .../SystemUI/res/values/lunaris_strings.xml | 3 + .../android/systemui/clocks/ClockStyle.java | 234 +++++++++++ .../repository/KeyguardClockRepository.kt | 11 + .../NotificationPanelViewController.java | 9 + 16 files changed, 1051 insertions(+), 5 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ide.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_miui.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_moto.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_oos.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_simple.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_style.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 60a0cff5c41f..6aa31ad0050a 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -1306,4 +1306,9 @@ 24dp + + + 50dp + 150dp + 86dp diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ec82d658425b..122bcca8c789 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -6347,4 +6347,9 @@ + + + + + diff --git a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml index 4e5ed62e10c7..8b1edd8506ea 100644 --- a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml +++ b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml @@ -17,9 +17,9 @@ --> - 50dp - 150dp - 86dp + @*android:dimen/presentation_clock_text_size + @*android:dimen/large_clock_text_size + @*android:dimen/small_clock_text_size @*android:dimen/keyguard_clock_line_spacing_scale @@ -45,4 +45,4 @@ 104dp 0dp 74dp - \ No newline at end of file + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml new file mode 100644 index 000000000000..3ad509abe38a --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_center.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ide.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ide.xml new file mode 100644 index 000000000000..9952a619d15a --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ide.xml @@ -0,0 +1,375 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_miui.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_miui.xml new file mode 100644 index 000000000000..b8c2a95a8ed4 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_miui.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_moto.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_moto.xml new file mode 100644 index 000000000000..73a38f19b1a3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_moto.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_oos.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_oos.xml new file mode 100644 index 000000000000..80921f9631f9 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_oos.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_simple.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_simple.xml new file mode 100644 index 000000000000..a8869ec6766b --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_simple.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_style.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_style.xml new file mode 100644 index 000000000000..c983fc3a05a7 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_style.xml @@ -0,0 +1,17 @@ + + + + + + diff --git a/packages/SystemUI/res/values-sw600dp-land/config.xml b/packages/SystemUI/res/values-sw600dp-land/config.xml index 30fdd3dfc161..e5ba0acade69 100644 --- a/packages/SystemUI/res/values-sw600dp-land/config.xml +++ b/packages/SystemUI/res/values-sw600dp-land/config.xml @@ -31,7 +31,7 @@ - false + true 3 diff --git a/packages/SystemUI/res/values/lunaris_dimens.xml b/packages/SystemUI/res/values/lunaris_dimens.xml index 908ea92da598..a656d27c23ab 100644 --- a/packages/SystemUI/res/values/lunaris_dimens.xml +++ b/packages/SystemUI/res/values/lunaris_dimens.xml @@ -125,4 +125,9 @@ 24dp + + + 180dp + 0dp + 0dp diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index 1e856bdc1bd1..96ca704a6f9c 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -228,4 +228,7 @@ Snowy Windy Misty + + + null diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java new file mode 100644 index 000000000000..47b6c97f3c97 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2023-2024 the 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.clocks; + +import android.app.KeyguardManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.UserHandle; +import android.provider.Settings; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextClock; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.tuner.TunerService; + +public class ClockStyle extends RelativeLayout implements TunerService.Tunable { + + private static final int[] CLOCK_LAYOUTS = { + 0, + R.layout.keyguard_clock_oos, + R.layout.keyguard_clock_center, + R.layout.keyguard_clock_simple, + R.layout.keyguard_clock_miui, + R.layout.keyguard_clock_ide, + R.layout.keyguard_clock_moto + }; + + private final static int[] mCenterClocks = {2, 3, 5, 6}; + + private static final int DEFAULT_STYLE = 0; // Disabled + public static final String CLOCK_STYLE_KEY = "clock_style"; + + private final Context mContext; + private final KeyguardManager mKeyguardManager; + private final TunerService mTunerService; + + private View currentClockView; + private int mClockStyle; + + private static final long UPDATE_INTERVAL_MILLIS = 15 * 1000; + private long lastUpdateTimeMillis = 0; + + private final StatusBarStateController mStatusBarStateController; + + private boolean mDozing; + + // Burn-in protection + private static final int BURN_IN_PROTECTION_INTERVAL = 10000; // 10 seconds + private static final int BURN_IN_PROTECTION_MAX_SHIFT = 4; // 4 pixels + private final Handler mBurnInProtectionHandler = new Handler(); + private int mCurrentShiftX = 0; + private int mCurrentShiftY = 0; + + private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (mKeyguardManager != null + && mKeyguardManager.isKeyguardLocked()) { + onTimeChanged(); + } + } + }; + + private final Runnable mBurnInProtectionRunnable = new Runnable() { + @Override + public void run() { + if (mDozing) { + mCurrentShiftX = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + mCurrentShiftY = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + if (currentClockView != null) { + currentClockView.setTranslationX(mCurrentShiftX); + currentClockView.setTranslationY(mCurrentShiftY); + } + invalidate(); + mBurnInProtectionHandler.postDelayed(this, BURN_IN_PROTECTION_INTERVAL); + } + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + if (mDozing) { + startBurnInProtection(); + } else { + stopBurnInProtection(); + } + } + }; + + public ClockStyle(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); + mTunerService = Dependency.get(TunerService.class); + mTunerService.addTunable(this, CLOCK_STYLE_KEY); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction("com.android.systemui.doze.pulse"); + mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + updateClockView(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + mContext.unregisterReceiver(mScreenReceiver); + } + + private void startBurnInProtection() { + if (mClockStyle == 0) return; + mBurnInProtectionHandler.post(mBurnInProtectionRunnable); + } + + private void stopBurnInProtection() { + if (mClockStyle == 0) return; + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (currentClockView != null) { + currentClockView.setTranslationX(0); + currentClockView.setTranslationY(0); + } + } + + private void updateTextClockViews(View view) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View childView = viewGroup.getChildAt(i); + updateTextClockViews(childView); + if (childView instanceof TextClock) { + ((TextClock) childView).refreshTime(); + } + } + } + } + + public void onTimeChanged() { + long currentTimeMillis = System.currentTimeMillis(); + if (currentTimeMillis - lastUpdateTimeMillis >= UPDATE_INTERVAL_MILLIS) { + if (currentClockView != null) { + updateTextClockViews(currentClockView); + lastUpdateTimeMillis = currentTimeMillis; + } + } + } + + private void updateClockView() { + if (currentClockView != null) { + ((ViewGroup) currentClockView.getParent()).removeView(currentClockView); + currentClockView = null; + } + if (mClockStyle > 0 && mClockStyle < CLOCK_LAYOUTS.length) { + ViewStub stub = findViewById(R.id.clock_view_stub); + if (stub != null) { + stub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); + currentClockView = stub.inflate(); + int gravity = isCenterClock(mClockStyle) ? Gravity.CENTER : Gravity.START; + if (currentClockView instanceof LinearLayout) { + ((LinearLayout) currentClockView).setGravity(gravity); + } + } + } + onTimeChanged(); + setVisibility(mClockStyle != 0 ? View.VISIBLE : View.GONE); + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case CLOCK_STYLE_KEY: + mClockStyle = TunerService.parseInteger(newValue, DEFAULT_STYLE); + if (mClockStyle != 0) { + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, UserHandle.USER_CURRENT); + } + updateClockView(); + break; + } + } + + private boolean isCenterClock(int clockStyle) { + for (int centerClock : mCenterClocks) { + if (centerClock == clockStyle) { + return true; + } + } + return false; + } +} 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 afeb7f45e555..d6210280d12c 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 @@ -170,5 +170,16 @@ constructor( UserHandle.USER_CURRENT, ) ) + val clockStyleEnabled = secureSettings.getIntForUser( + "clock_style", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + val clockSettingValue = if (clockStyleEnabled) { + 0 + } else { + isDoubleLineClock + } + return ClockSizeSetting.fromSettingValue(clockSettingValue) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index bcbc790bb364..9dd18a831348 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; @@ -1271,6 +1272,14 @@ private ClockSize computeDesiredClockSizeForSplitShade() { return ClockSize.LARGE; } + private boolean shouldForceSmallClock() { + return !isOnAod() + // True on small landscape screens + && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) + || Settings.Secure.getIntForUser( + mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0; + } + private void updateKeyguardStatusViewAlignment() { boolean shouldBeCentered = shouldKeyguardStatusViewBeCentered(); mKeyguardUnfoldTransition.ifPresent(t -> t.setStatusViewCentered(shouldBeCentered)); From 2e670e33e3848857afb142e0ff275803335b6f5c Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 7 Nov 2024 18:12:59 +0800 Subject: [PATCH 106/190] SystemUI: Introduce Lockscreen Widgets feature Arman-ATI: Adapted to A16 Signed-off-by: minaripenguin Signed-off-by: Arman Altafi Signed-off-by: Ghosuto --- packages/SystemUI/animation/Android.bp | 1 + .../systemui/animation/view/LaunchableFAB.kt | 52 + .../layout/keyguard_clock_widgets.xml | 111 ++ .../qs_footer_power_button_overlay_color.xml | 22 + .../SystemUI/res/drawable/ic_calculator.xml | 25 + .../SystemUI/res/drawable/ic_media_pause.xml | 92 ++ .../SystemUI/res/drawable/ic_media_play.xml | 92 ++ .../res/drawable/ic_mobiledata_off_24.xml | 13 + .../res/drawable/ic_ring_volume_24.xml | 15 + .../drawable/ic_signal_cellular_alt_24.xml | 10 + .../SystemUI/res/drawable/ic_vibration_24.xml | 15 + packages/SystemUI/res/drawable/ic_weather.xml | 26 + packages/SystemUI/res/drawable/ic_wifi_24.xml | 10 + .../SystemUI/res/drawable/ic_wifi_off_24.xml | 10 + .../lockscreen_widget_background_circle.xml | 24 + .../lockscreen_widget_background_square.xml | 24 + .../qs_footer_action_circle_color.xml | 43 + .../res/values-night/kg_widgets_colors.xml | 3 + .../SystemUI/res/values/kg_widgets_colors.xml | 7 + .../SystemUI/res/values/kg_widgets_dimens.xml | 28 + .../res/values/kg_widgets_strings.xml | 12 + .../SystemUI/res/values/kg_widgets_styles.xml | 26 + .../src/com/android/systemui/Dependency.java | 28 + .../repository/KeyguardClockRepository.kt | 9 +- .../lockscreen/ActivityLauncherUtils.java | 130 +++ .../systemui/lockscreen/LockScreenWidgets.kt | 67 ++ .../LockScreenWidgetsController.java | 1040 +++++++++++++++++ .../NotificationPanelViewController.java | 8 +- 28 files changed, 1939 insertions(+), 4 deletions(-) create mode 100644 packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFAB.kt create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml create mode 100644 packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml create mode 100644 packages/SystemUI/res/drawable/ic_calculator.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_pause.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_play.xml create mode 100644 packages/SystemUI/res/drawable/ic_mobiledata_off_24.xml create mode 100644 packages/SystemUI/res/drawable/ic_ring_volume_24.xml create mode 100644 packages/SystemUI/res/drawable/ic_signal_cellular_alt_24.xml create mode 100644 packages/SystemUI/res/drawable/ic_vibration_24.xml create mode 100644 packages/SystemUI/res/drawable/ic_weather.xml create mode 100644 packages/SystemUI/res/drawable/ic_wifi_24.xml create mode 100644 packages/SystemUI/res/drawable/ic_wifi_off_24.xml create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml create mode 100644 packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml create mode 100644 packages/SystemUI/res/values-night/kg_widgets_colors.xml create mode 100644 packages/SystemUI/res/values/kg_widgets_colors.xml create mode 100644 packages/SystemUI/res/values/kg_widgets_dimens.xml create mode 100644 packages/SystemUI/res/values/kg_widgets_strings.xml create mode 100644 packages/SystemUI/res/values/kg_widgets_styles.xml create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java diff --git a/packages/SystemUI/animation/Android.bp b/packages/SystemUI/animation/Android.bp index dec664fa7a14..57f6fee6d5c7 100644 --- a/packages/SystemUI/animation/Android.bp +++ b/packages/SystemUI/animation/Android.bp @@ -47,6 +47,7 @@ android_library { "com_android_systemui_flags_lib", "SystemUIShaderLib", "WindowManager-Shell-shared", + "com.google.android.material_material", "//frameworks/libs/systemui:animationlib", "//frameworks/libs/systemui:com_android_systemui_shared_flags_lib", ], diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFAB.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFAB.kt new file mode 100644 index 000000000000..3fa237643b38 --- /dev/null +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/view/LaunchableFAB.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The 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.animation.view + +import android.content.Context +import android.util.AttributeSet +import com.android.systemui.animation.LaunchableView +import com.android.systemui.animation.LaunchableViewDelegate +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton + +/** A custom [ExtendedFloatingActionButton] that implements [LaunchableView]. */ +open class LaunchableFAB : ExtendedFloatingActionButton, LaunchableView { + + private val delegate: LaunchableViewDelegate + private val MAX_SIZE_PX = 64 + + constructor(context: Context) : this(context, null) + + constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { + delegate = LaunchableViewDelegate( + this, + superSetVisibility = { visibility -> super.setVisibility(visibility) } + ) + setIconSize(MAX_SIZE_PX) + } + + override fun setShouldBlockVisibilityChanges(block: Boolean) { + delegate.setShouldBlockVisibilityChanges(block) + } + + override fun setVisibility(visibility: Int) { + super.setVisibility(visibility) + delegate.setVisibility(visibility) + } +} diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml new file mode 100644 index 000000000000..1542bb40aed8 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml new file mode 100644 index 000000000000..a8abd793bd00 --- /dev/null +++ b/packages/SystemUI/res/color/qs_footer_power_button_overlay_color.xml @@ -0,0 +1,22 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_calculator.xml b/packages/SystemUI/res/drawable/ic_calculator.xml new file mode 100644 index 000000000000..4b454aeffc9d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_calculator.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_pause.xml b/packages/SystemUI/res/drawable/ic_media_pause.xml new file mode 100644 index 000000000000..1acd49d55220 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_pause.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_play.xml b/packages/SystemUI/res/drawable/ic_media_play.xml new file mode 100644 index 000000000000..0794c084efbe --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_play.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_mobiledata_off_24.xml b/packages/SystemUI/res/drawable/ic_mobiledata_off_24.xml new file mode 100644 index 000000000000..a945c9e910a5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_mobiledata_off_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_ring_volume_24.xml b/packages/SystemUI/res/drawable/ic_ring_volume_24.xml new file mode 100644 index 000000000000..19043b682f23 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_ring_volume_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_signal_cellular_alt_24.xml b/packages/SystemUI/res/drawable/ic_signal_cellular_alt_24.xml new file mode 100644 index 000000000000..9368f4983bd5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_signal_cellular_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_vibration_24.xml b/packages/SystemUI/res/drawable/ic_vibration_24.xml new file mode 100644 index 000000000000..9367158dfe39 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_vibration_24.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_weather.xml b/packages/SystemUI/res/drawable/ic_weather.xml new file mode 100644 index 000000000000..1e4e1ae192e5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_weather.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/ic_wifi_24.xml b/packages/SystemUI/res/drawable/ic_wifi_24.xml new file mode 100644 index 000000000000..b390c0d9ccab --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wifi_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/ic_wifi_off_24.xml b/packages/SystemUI/res/drawable/ic_wifi_off_24.xml new file mode 100644 index 000000000000..4ad5e601dbfa --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_wifi_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml new file mode 100644 index 000000000000..1d2a38200621 --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml new file mode 100644 index 000000000000..1af71a4b567c --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_square.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml new file mode 100644 index 000000000000..b43ba9357107 --- /dev/null +++ b/packages/SystemUI/res/drawable/qs_footer_action_circle_color.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/values-night/kg_widgets_colors.xml b/packages/SystemUI/res/values-night/kg_widgets_colors.xml new file mode 100644 index 000000000000..01075f56c771 --- /dev/null +++ b/packages/SystemUI/res/values-night/kg_widgets_colors.xml @@ -0,0 +1,3 @@ + + @*android:color/system_neutral1_900 + diff --git a/packages/SystemUI/res/values/kg_widgets_colors.xml b/packages/SystemUI/res/values/kg_widgets_colors.xml new file mode 100644 index 000000000000..6849e789f514 --- /dev/null +++ b/packages/SystemUI/res/values/kg_widgets_colors.xml @@ -0,0 +1,7 @@ + + @android:color/system_neutral1_900 + @android:color/system_neutral1_0 + @android:color/system_accent1_100 + @android:color/system_accent1_600 + @*android:color/system_neutral1_0 + diff --git a/packages/SystemUI/res/values/kg_widgets_dimens.xml b/packages/SystemUI/res/values/kg_widgets_dimens.xml new file mode 100644 index 000000000000..18bc92edd963 --- /dev/null +++ b/packages/SystemUI/res/values/kg_widgets_dimens.xml @@ -0,0 +1,28 @@ + + + + 10dp + 6dp + 12dp + 12dp + 12dp + 68dp + 20dp + 20dp + 88dp + 175dp + diff --git a/packages/SystemUI/res/values/kg_widgets_strings.xml b/packages/SystemUI/res/values/kg_widgets_strings.xml new file mode 100644 index 000000000000..a9eb69090e30 --- /dev/null +++ b/packages/SystemUI/res/values/kg_widgets_strings.xml @@ -0,0 +1,12 @@ + + Camera + Clock/Timer + Calculator + Gallery + No default %1$s app found + Data Unavailable + OmniJaws + Mobile data + Ringer mode + Torch Active + diff --git a/packages/SystemUI/res/values/kg_widgets_styles.xml b/packages/SystemUI/res/values/kg_widgets_styles.xml new file mode 100644 index 000000000000..e688869a6b5e --- /dev/null +++ b/packages/SystemUI/res/values/kg_widgets_styles.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 699a360b4263..5cd37e29f8f5 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -56,6 +56,16 @@ import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.tuner.TunerService; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.media.dialog.MediaOutputDialogManager; +import com.android.systemui.statusbar.phone.ScrimController; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.FlashlightController; import dagger.Lazy; @@ -154,6 +164,15 @@ public class Dependency { @Inject Lazy mStatusBarWindowControllerStoreLazy; @Inject Lazy mSysUIStateDisplaysInteractor; @Inject Lazy mRotationPolicyWrapperLazy; + @Inject Lazy mActivityStarter; + @Inject Lazy mAccessPointController; + @Inject Lazy mNetworkController; + @Inject Lazy mInternetDialogManager; + @Inject Lazy mMediaOutputDialogManager; + @Inject Lazy mConfigurationController; + @Inject Lazy mFlashlightController; + @Inject Lazy mBluetoothDetailsContentViewModel; + @Inject Lazy mHotspotController; @Inject public Dependency() { @@ -202,6 +221,15 @@ protected void start() { mProviders.put( StatusBarWindowControllerStore.class, mStatusBarWindowControllerStoreLazy::get); mProviders.put(RotationPolicyWrapper.class, mRotationPolicyWrapperLazy::get); + mProviders.put(MediaOutputDialogManager.class, mMediaOutputDialogManager::get); + mProviders.put(AccessPointController.class, mAccessPointController::get); + mProviders.put(NetworkController.class, mNetworkController::get); + mProviders.put(InternetDialogManager.class, mInternetDialogManager::get); + mProviders.put(ConfigurationController.class, mConfigurationController::get); + mProviders.put(FlashlightController.class, mFlashlightController::get); + mProviders.put(BluetoothDetailsContentViewModel.class, mBluetoothDetailsContentViewModel::get); + mProviders.put(ActivityStarter.class, mActivityStarter::get); + mProviders.put(HotspotController.class, mHotspotController::get); Dependency.setInstance(this); } 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 d6210280d12c..6d4d4f2e1e1b 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 @@ -36,6 +36,7 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.shared.clocks.ClockRegistry import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SystemSettings import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -84,6 +85,7 @@ class KeyguardClockRepositoryImpl @Inject constructor( private val secureSettings: SecureSettings, + private val systemSettings: SystemSettings, private val clockRegistry: ClockRegistry, override val clockEventController: ClockEventController, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -175,7 +177,12 @@ constructor( 0, // Default value UserHandle.USER_CURRENT ) != 0 - val clockSettingValue = if (clockStyleEnabled) { + val lockscreenWidgetsEnabled = systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + val clockSettingValue = if (clockStyleEnabled || lockscreenWidgetsEnabled) { 0 } else { isDoubleLineClock diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java new file mode 100644 index 000000000000..29a82ab10b12 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java @@ -0,0 +1,130 @@ +/* + Copyright (C) 2024 the 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.lockscreen; + +import android.content.Context; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.AudioManager; +import android.os.DeviceIdleManager; +import android.provider.AlarmClock; +import android.provider.MediaStore; +import android.speech.RecognizerIntent; +import android.widget.Toast; + +import androidx.annotation.StringRes; + +import com.android.systemui.Dependency; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.res.R; + +import java.util.List; + +public class ActivityLauncherUtils { + + private final static String PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings$personalizationSettingsLayoutActivity"; + private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; + + private final Context mContext; + private final ActivityStarter mActivityStarter; + private PackageManager mPackageManager; + + public ActivityLauncherUtils(Context context) { + this.mContext = context; + this.mActivityStarter = Dependency.get(ActivityStarter.class); + mPackageManager = mContext.getPackageManager(); + } + + public String getInstalledMusicApp() { + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_APP_MUSIC); + final List musicApps = mPackageManager.queryIntentActivities(intent, 0); + ResolveInfo musicApp = musicApps.isEmpty() ? null : musicApps.get(0); + return musicApp != null ? musicApp.activityInfo.packageName : ""; + } + + public void launchAppIfAvailable(Intent launchIntent, @StringRes int appTypeResId) { + final List apps = mPackageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY); + if (!apps.isEmpty()) { + mActivityStarter.startActivity(launchIntent, true); + } else { + showNoDefaultAppFoundToast(appTypeResId); + } + } + + public void launchVoiceAssistant() { + DeviceIdleManager dim = mContext.getSystemService(DeviceIdleManager.class); + if (dim != null) { + dim.endIdle("voice-search"); + } + Intent voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE); + voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE, true); + mActivityStarter.startActivity(voiceIntent, true); + } + + public void launchCamera() { + final Intent launchIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); + launchAppIfAvailable(launchIntent, R.string.camera); + } + + public void launchTimer() { + final Intent launchIntent = new Intent(AlarmClock.ACTION_SET_TIMER); + launchAppIfAvailable(launchIntent, R.string.clock_timer); + } + + public void launchCalculator() { + final Intent launchIntent = new Intent(); + launchIntent.setAction(Intent.ACTION_MAIN); + launchIntent.addCategory(Intent.CATEGORY_APP_CALCULATOR); + launchAppIfAvailable(launchIntent, R.string.calculator); + } + + public void launchSettingsComponent(String className) { + if (mActivityStarter == null) return; + Intent intent = className.equals(PERSONALIZATIONS_ACTIVITY) ? new Intent(Intent.ACTION_MAIN) : new Intent(); + intent.setComponent(new ComponentName("com.android.settings", className)); + mActivityStarter.startActivity(intent, true); + } + + public void launchWeatherApp() { + final Intent launchIntent = new Intent(); + launchIntent.setAction(Intent.ACTION_MAIN); + launchIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); + launchAppIfAvailable(launchIntent, R.string.omnijaws_weather); + } + + public void launchMediaPlayerApp(String packageName) { + if (!packageName.isEmpty()) { + Intent launchIntent = mPackageManager.getLaunchIntentForPackage(packageName); + if (launchIntent != null) { + mActivityStarter.startActivity(launchIntent, true); + } + } + } + + public void startSettingsActivity() { + if (mActivityStarter == null) return; + mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), true /* dismissShade */); + } + + public void startIntent(Intent intent) { + if (mActivityStarter == null) return; + mActivityStarter.startActivity(intent, true /* dismissShade */); + } + + private void showNoDefaultAppFoundToast(@StringRes int appTypeResId) { + Toast.makeText(mContext, mContext.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt new file mode 100644 index 000000000000..45f6b02fc47a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgets.kt @@ -0,0 +1,67 @@ +/* + Copyright (C) 2024 the 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.lockscreen + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.LinearLayout + +import com.android.internal.jank.InteractionJankMonitor + +import com.android.systemui.Dependency +import com.android.systemui.animation.Expandable +import com.android.systemui.animation.DialogCuj +import com.android.systemui.animation.DialogTransitionAnimator +import com.android.systemui.media.dialog.MediaOutputDialogManager + +class LockScreenWidgets(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) { + + private val mViewController: LockScreenWidgetsController? = LockScreenWidgetsController(this) + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + mViewController?.registerCallbacks() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + mViewController?.unregisterCallbacks() + } + + override fun onFinishInflate() { + super.onFinishInflate() + mViewController?.initViews() + } + + fun showMediaDialog(view: View, lastMediaPackage: String) { + val packageName = lastMediaPackage.takeIf { it.isNotEmpty() } ?: return + Dependency.get(MediaOutputDialogManager::class.java) + .createAndShowWithController( + packageName, + true, + Expandable.fromView(view).dialogController() + ) + } + + private fun Expandable.dialogController(): DialogTransitionAnimator.Controller? { + return dialogTransitionController( + cuj = + DialogCuj( + InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN, + MediaOutputDialogManager.INTERACTION_JANK_TAG + ) + ) + } + +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java new file mode 100644 index 000000000000..dfc615b727e2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -0,0 +1,1040 @@ +/* + Copyright (C) 2024 the 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.lockscreen; + +import android.annotation.NonNull; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.PackageManager; +import android.content.res.Configuration; +import android.content.res.ColorStateList; +import android.database.ContentObserver; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.hardware.camera2.CameraManager; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.session.MediaSessionLegacyHelper; +import android.os.Bundle; +import android.os.Handler; +import android.os.SystemClock; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.KeyEvent; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.AttributeSet; +import android.os.UserHandle; +import android.text.TextUtils; +import android.widget.LinearLayout; +import android.widget.Toast; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.settingslib.net.DataUsageController; +import com.android.settingslib.Utils; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.animation.Expandable; +import com.android.systemui.animation.view.LaunchableImageView; +import com.android.systemui.animation.view.LaunchableFAB; +import com.android.systemui.plugins.ActivityStarter; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.qs.tiles.dialog.InternetDialogManager; +import com.android.systemui.statusbar.policy.BluetoothController; +import com.android.systemui.statusbar.policy.BluetoothController.Callback; +import com.android.systemui.statusbar.policy.ConfigurationController; +import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; +import com.android.systemui.statusbar.policy.FlashlightController; +import com.android.systemui.statusbar.policy.HotspotController; +import com.android.systemui.statusbar.connectivity.AccessPointController; +import com.android.systemui.statusbar.connectivity.IconState; +import com.android.systemui.statusbar.connectivity.NetworkController; +import com.android.systemui.statusbar.connectivity.SignalCallback; +import com.android.systemui.statusbar.connectivity.MobileDataIndicators; +import com.android.systemui.statusbar.connectivity.WifiIndicators; +import com.android.systemui.util.MediaSessionManagerHelper; +import com.android.internal.util.android.VibrationUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.android.internal.util.android.OmniJawsClient; + +public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObserver, MediaSessionManagerHelper.MediaMetadataListener { + + private static final String LOCKSCREEN_WIDGETS_ENABLED = + "lockscreen_widgets_enabled"; + + private static final String LOCKSCREEN_WIDGETS = + "lockscreen_widgets"; + + private static final String LOCKSCREEN_WIDGETS_EXTRAS = + "lockscreen_widgets_extras"; + + private static final String LOCKSCREEN_WIDGETS_STYLE = + "lockscreen_widgets_style"; + + private static final int[] MAIN_WIDGETS_VIEW_IDS = { + R.id.main_kg_item_placeholder1, + R.id.main_kg_item_placeholder2 + }; + + private static final int[] WIDGETS_VIEW_IDS = { + R.id.kg_item_placeholder1, + R.id.kg_item_placeholder2, + R.id.kg_item_placeholder3, + R.id.kg_item_placeholder4 + }; + + public static final int BT_ACTIVE = R.drawable.qs_bluetooth_icon_on; + public static final int BT_INACTIVE = R.drawable.qs_bluetooth_icon_off; + public static final int DATA_ACTIVE = R.drawable.ic_signal_cellular_alt_24; + public static final int DATA_INACTIVE = R.drawable.ic_mobiledata_off_24; + public static final int RINGER_ACTIVE = R.drawable.ic_vibration_24; + public static final int RINGER_INACTIVE = R.drawable.ic_ring_volume_24; + public static final int TORCH_RES_ACTIVE = R.drawable.ic_flashlight_on; + public static final int TORCH_RES_INACTIVE = R.drawable.ic_flashlight_off; + public static final int WIFI_ACTIVE = R.drawable.ic_wifi_24; + public static final int WIFI_INACTIVE = R.drawable.ic_wifi_off_24; + public static final int HOTSPOT_ACTIVE = R.drawable.qs_hotspot_icon_on; + public static final int HOTSPOT_INACTIVE = R.drawable.qs_hotspot_icon_off; + + public static final int BT_LABEL_INACTIVE = R.string.quick_settings_bluetooth_label; + public static final int DATA_LABEL_INACTIVE = R.string.quick_settings_data_label; + public static final int RINGER_LABEL_INACTIVE = R.string.quick_settings_ringer_label; + public static final int TORCH_LABEL_ACTIVE = R.string.torch_active; + public static final int TORCH_LABEL_INACTIVE = R.string.quick_settings_flashlight_label; + public static final int WIFI_LABEL_INACTIVE = R.string.quick_settings_wifi_label; + public static final int HOTSPOT_LABEL = R.string.accessibility_status_bar_hotspot; + + private OmniJawsClient mWeatherClient; + private OmniJawsClient.WeatherInfo mWeatherInfo; + + private final AccessPointController mAccessPointController; + private final BluetoothController mBluetoothController; + private final BluetoothDetailsContentViewModel mBluetoothDetailsContentViewModel; + private final ConfigurationController mConfigurationController; + private final DataUsageController mDataController; + private final FlashlightController mFlashlightController; + private final InternetDialogManager mInternetDialogManager; + private final NetworkController mNetworkController; + private final StatusBarStateController mStatusBarStateController; + private final MediaSessionManagerHelper mMediaSessionManagerHelper; + private final LockscreenWidgetsObserver mLockscreenWidgetsObserver; + private final ActivityLauncherUtils mActivityLauncherUtils; + private final HotspotController mHotspotController; + + protected final CellSignalCallback mCellSignalCallback = new CellSignalCallback(); + protected final WifiSignalCallback mWifiSignalCallback = new WifiSignalCallback(); + private final HotspotCallback mHotspotCallback = new HotspotCallback(); + + private boolean mIsHotspotEnabled = false; + + private Context mContext; + private LaunchableImageView mWidget1, mWidget2, mWidget3, mWidget4, mediaButton, torchButton, weatherButton; + private LaunchableFAB mediaButtonFab, torchButtonFab, weatherButtonFab, hotspotButtonFab; + private LaunchableFAB wifiButtonFab, dataButtonFab, ringerButtonFab, btButtonFab; + private LaunchableImageView wifiButton, dataButton, ringerButton, btButton, hotspotButton; + private int mDarkColor, mDarkColorActive, mLightColor, mLightColorActive; + + private CameraManager mCameraManager; + private String mCameraId; + private boolean isFlashOn = false; + + private String mMainLockscreenWidgetsList; + private String mSecondaryLockscreenWidgetsList; + private LaunchableFAB[] mMainWidgetViews; + private LaunchableImageView[] mSecondaryWidgetViews; + private List mMainWidgetsList = new ArrayList<>(); + private List mSecondaryWidgetsList = new ArrayList<>(); + private String mWidgetImagePath; + + private AudioManager mAudioManager; + private String mLastTrackTitle = null; + + private boolean mDozing; + + private boolean mIsInflated = false; + private GestureDetector mGestureDetector; + private boolean mIsLongPress = false; + + private boolean mLockscreenWidgetsEnabled; + private int mThemeStyle = 0; + + final ConfigurationListener mConfigurationListener = new ConfigurationListener() { + @Override + public void onUiModeChanged() { + updateWidgetViews(); + } + @Override + public void onThemeChanged() { + updateWidgetViews(); + } + }; + + private final View mView; + private final Handler mHandler = new Handler(); + + public LockScreenWidgetsController(View view) { + mView = view; + mContext = mView.getContext(); + mAccessPointController = Dependency.get(AccessPointController.class); + mBluetoothDetailsContentViewModel = Dependency.get(BluetoothDetailsContentViewModel.class); + mConfigurationController = Dependency.get(ConfigurationController.class); + mFlashlightController = Dependency.get(FlashlightController.class); + mInternetDialogManager = Dependency.get(InternetDialogManager.class); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mBluetoothController = Dependency.get(BluetoothController.class); + mNetworkController = Dependency.get(NetworkController.class); + mDataController = mNetworkController.getMobileDataController(); + mHotspotController = Dependency.get(HotspotController.class); + mMediaSessionManagerHelper = MediaSessionManagerHelper.Companion.getInstance(mContext); + + mActivityLauncherUtils = new ActivityLauncherUtils(mContext); + + mLockscreenWidgetsObserver = new LockscreenWidgetsObserver(); + mLockscreenWidgetsObserver.observe(); + + mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); + mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); + + initResources(); + + if (mWeatherClient == null) { + mWeatherClient = new OmniJawsClient(mContext); + } + + try { + mCameraId = mCameraManager.getCameraIdList()[0]; + } catch (Exception e) {} + + IntentFilter ringerFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); + mContext.registerReceiver(mRingerModeReceiver, ringerFilter); + } + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + updateContainerVisibility(); + } + }; + + private final FlashlightController.FlashlightListener mFlashlightCallback = + new FlashlightController.FlashlightListener() { + @Override + public void onFlashlightChanged(boolean enabled) { + isFlashOn = enabled; + updateTorchButtonState(); + } + @Override + public void onFlashlightError() { + } + @Override + public void onFlashlightAvailabilityChanged(boolean available) { + isFlashOn = mFlashlightController.isEnabled() && available; + updateTorchButtonState(); + } + }; + + private void initResources() { + mDarkColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_dark); + mLightColor = mContext.getResources().getColor(R.color.lockscreen_widget_background_color_light); + mDarkColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_dark); + mLightColorActive = mContext.getResources().getColor(R.color.lockscreen_widget_active_color_light); + } + + public void registerCallbacks() { + if (isWidgetEnabled("hotspot")) { + mHotspotController.addCallback(mHotspotCallback); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.addCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.addCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.addCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.addCallback(mFlashlightCallback); + } + mConfigurationController.addCallback(mConfigurationListener); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + mMediaSessionManagerHelper.addMediaMetadataListener(this); + updateWidgetViews(); + updateMediaPlaybackState(); + } + + public void unregisterCallbacks() { + if (isWidgetEnabled("weather")) { + disableWeatherUpdates(); + } + if (isWidgetEnabled("wifi")) { + mNetworkController.removeCallback(mWifiSignalCallback); + } + if (isWidgetEnabled("data")) { + mNetworkController.removeCallback(mCellSignalCallback); + } + if (isWidgetEnabled("bt")) { + mBluetoothController.removeCallback(mBtCallback); + } + if (isWidgetEnabled("torch")) { + mFlashlightController.removeCallback(mFlashlightCallback); + } + if (isWidgetEnabled("hotspot")) { + mHotspotController.removeCallback(mHotspotCallback); + } + mConfigurationController.removeCallback(mConfigurationListener); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mContext.unregisterReceiver(mRingerModeReceiver); + mLockscreenWidgetsObserver.unobserve(); + mHandler.removeCallbacksAndMessages(null); + mMediaSessionManagerHelper.removeMediaMetadataListener(this); + } + + public void initViews() { + mMainWidgetViews = new LaunchableFAB[MAIN_WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mMainWidgetViews.length; i++) { + mMainWidgetViews[i] = mView.findViewById(MAIN_WIDGETS_VIEW_IDS[i]); + } + mSecondaryWidgetViews = new LaunchableImageView[WIDGETS_VIEW_IDS.length]; + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + mSecondaryWidgetViews[i] = mView.findViewById(WIDGETS_VIEW_IDS[i]); + } + mIsInflated = true; + updateWidgetViews(); + } + + public void updateWidgetViews() { + if (!mIsInflated) return; + if (mMainWidgetViews != null && mMainWidgetsList != null) { + for (int i = 0; i < mMainWidgetViews.length; i++) { + if (mMainWidgetViews[i] != null) { + mMainWidgetViews[i].setVisibility(i < mMainWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mMainWidgetsList.size(), mMainWidgetViews.length); i++) { + String widgetType = mMainWidgetsList.get(i); + if (widgetType != null && i < mMainWidgetViews.length && mMainWidgetViews[i] != null) { + setUpWidgetWiews(null, mMainWidgetViews[i], widgetType); + updateMainWidgetResources(mMainWidgetViews[i], false); + } + } + } + if (mSecondaryWidgetViews != null && mSecondaryWidgetsList != null) { + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + if (mSecondaryWidgetViews[i] != null) { + mSecondaryWidgetViews[i].setVisibility(i < mSecondaryWidgetsList.size() ? View.VISIBLE : View.GONE); + } + } + for (int i = 0; i < Math.min(mSecondaryWidgetsList.size(), mSecondaryWidgetViews.length); i++) { + String widgetType = mSecondaryWidgetsList.get(i); + if (widgetType != null && i < mSecondaryWidgetViews.length && mSecondaryWidgetViews[i] != null) { + setUpWidgetWiews(mSecondaryWidgetViews[i], null, widgetType); + updateWidgetsResources(mSecondaryWidgetViews[i]); + } + } + } + updateContainerVisibility(); + } + + private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { + if (efab == null) return; + efab.setElevation(0); + setButtonActiveState(null, efab, false); + long visibleWidgetCount = mMainWidgetsList.stream().filter(widget -> !"none".equals(widget)).count(); + ViewGroup.LayoutParams params = efab.getLayoutParams(); + if (params instanceof LinearLayout.LayoutParams) { + LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) params; + if (efab.getVisibility() == View.VISIBLE && visibleWidgetCount == 1) { + layoutParams.width = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_width); + layoutParams.height = mContext.getResources().getDimensionPixelSize(R.dimen.kg_widget_main_height); + } else { + layoutParams.width = 0; + layoutParams.weight = 1; + } + efab.setLayoutParams(layoutParams); + } + } + + private void updateContainerVisibility() { + final boolean isMainWidgetsEmpty = mMainLockscreenWidgetsList == null + || TextUtils.isEmpty(mMainLockscreenWidgetsList); + final boolean isSecondaryWidgetsEmpty = mSecondaryLockscreenWidgetsList == null + || TextUtils.isEmpty(mSecondaryLockscreenWidgetsList); + final boolean isEmpty = isMainWidgetsEmpty && isSecondaryWidgetsEmpty; + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(isMainWidgetsEmpty ? View.GONE : View.VISIBLE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(isSecondaryWidgetsEmpty ? View.GONE : View.VISIBLE); + } + final boolean shouldHideContainer = isEmpty || mDozing || !mLockscreenWidgetsEnabled; + mView.setVisibility(shouldHideContainer ? View.GONE : View.VISIBLE); + } + + private void updateWidgetsResources(LaunchableImageView iv) { + if (iv == null) return; + final int themeStyle = mThemeStyle; + int bgRes; + switch (themeStyle) { + case 0: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + } + iv.setBackgroundResource(bgRes); + setButtonActiveState(iv, null, false); + } + + private boolean isNightMode() { + final Configuration config = mContext.getResources().getConfiguration(); + return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES; + } + + private void setUpWidgetWiews(LaunchableImageView iv, LaunchableFAB efab, String type) { + switch (type) { + case "none": + if (iv != null) iv.setVisibility(View.GONE); + if (efab != null) efab.setVisibility(View.GONE); + break; + case "wifi": + if (iv != null) { + wifiButton = iv; + wifiButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + if (efab != null) { + wifiButtonFab = efab; + wifiButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleWiFi(), WIFI_INACTIVE, R.string.quick_settings_wifi_label); + break; + case "data": + if (iv != null) { + dataButton = iv; + dataButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + if (efab != null) { + dataButtonFab = efab; + dataButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleMobileData(), DATA_INACTIVE, DATA_LABEL_INACTIVE); + break; + case "ringer": + if (iv != null) ringerButton = iv; + if (efab != null) ringerButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleRingerMode(), RINGER_INACTIVE, RINGER_LABEL_INACTIVE); + break; + case "bt": + if (iv != null) { + btButton = iv; + btButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + if (efab != null) { + btButtonFab = efab; + btButtonFab.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleBluetoothState(), BT_INACTIVE, BT_LABEL_INACTIVE); + break; + case "torch": + if (iv != null) torchButton = iv; + if (efab != null) torchButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleFlashlight(), TORCH_RES_INACTIVE, TORCH_LABEL_INACTIVE); + break; + case "timer": + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchTimer(), R.drawable.ic_alarm, R.string.clock_timer); + break; + case "calculator": + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchCalculator(), R.drawable.ic_calculator, R.string.calculator); + break; + case "media": + if (iv != null) { + mediaButton = iv; + mediaButton.setOnLongClickListener(v -> { showMediaDialog(v); return true; }); + } + if (efab != null) mediaButtonFab = efab; + setUpWidgetResources(iv, efab, v -> toggleMediaPlaybackState(), R.drawable.ic_media_play, R.string.controls_media_button_play); + break; + case "weather": + if (iv != null) weatherButton = iv; + if (efab != null) weatherButtonFab = efab; + setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchWeatherApp(), R.drawable.ic_weather, R.string.weather_data_unavailable); + enableWeatherUpdates(); + break; + case "hotspot": + if (iv != null) { + hotspotButton = iv; + hotspotButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); + } + if (efab != null) { + hotspotButtonFab = efab; + hotspotButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); + } + setUpWidgetResources(iv, efab, v -> toggleHotspot(), HOTSPOT_INACTIVE, HOTSPOT_LABEL); + break; + default: + break; + } + } + + private void setUpWidgetResources(LaunchableImageView iv, LaunchableFAB efab, + View.OnClickListener cl, int drawableRes, int stringRes){ + if (efab != null) { + efab.setOnClickListener(cl); + efab.setIcon(mContext.getDrawable(drawableRes)); + efab.setText(mContext.getResources().getString(stringRes)); + if (mediaButtonFab == efab) { + attachSwipeGesture(efab); + } + } + if (iv != null) { + iv.setOnClickListener(cl); + iv.setImageResource(drawableRes); + } + } + + private void attachSwipeGesture(LaunchableFAB efab) { + final GestureDetector gestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() { + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS); + VibrationUtils.triggerVibration(mContext, 2); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT); + VibrationUtils.triggerVibration(mContext, 2); + } + return true; + } + return false; + } + @Override + public void onLongPress(MotionEvent e) { + super.onLongPress(e); + mIsLongPress = true; + showMediaDialog(efab); + mHandler.postDelayed(() -> { + mIsLongPress = false; + }, 2500); + } + }); + efab.setOnTouchListener((v, event) -> { + boolean isClick = gestureDetector.onTouchEvent(event); + if (event.getAction() == MotionEvent.ACTION_UP && !isClick && !mIsLongPress) { + v.performClick(); + } + return true; + }); + } + + private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { + int bgTint; + int tintColor; + if (mThemeStyle == 2) { + if (active) { + bgTint = Utils.applyAlpha(0.3f, mDarkColorActive); + tintColor = mDarkColorActive; + } else { + bgTint = Utils.applyAlpha(0.3f, Color.WHITE); + tintColor = Color.WHITE; + } + } else { + if (active) { + bgTint = isNightMode() ? mDarkColorActive : mLightColorActive; + tintColor = isNightMode() ? mDarkColor : mLightColor; + } else { + bgTint = isNightMode() ? mDarkColor : mLightColor; + tintColor = isNightMode() ? mLightColor : mDarkColor; + } + } + if (iv != null) { + iv.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + if (iv != weatherButton) { + iv.setImageTintList(ColorStateList.valueOf(tintColor)); + } else { + iv.setImageTintList(null); + } + } + if (efab != null) { + efab.setBackgroundTintList(ColorStateList.valueOf(bgTint)); + if (efab != weatherButtonFab) { + efab.setIconTint(ColorStateList.valueOf(tintColor)); + } else { + efab.setIconTint(null); + } + efab.setTextColor(tintColor); + } + } + + private void toggleMediaPlaybackState() { + if (mMediaSessionManagerHelper.isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE); + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY); + } + } + + private void showMediaDialog(View view) { + String lastMediaPkg = getLastUsedMedia(); + if (TextUtils.isEmpty(lastMediaPkg)) return; // Return if null or empty + mHandler.post(() -> { + ((LockScreenWidgets) mView).showMediaDialog(view, lastMediaPkg); + VibrationUtils.triggerVibration(mContext, 2); // Trigger vibration + }); + } + + private String getLastUsedMedia() { + return Settings.System.getString(mContext.getContentResolver(), + "media_session_last_package_name"); + } + + private void dispatchMediaKeyWithWakeLockToMediaSession(final int keycode) { + final MediaSessionLegacyHelper helper = MediaSessionLegacyHelper.getHelper(mContext); + if (helper == null) return; + KeyEvent event = new KeyEvent(SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), KeyEvent.ACTION_DOWN, keycode, 0); + helper.sendMediaButtonEvent(event, true); + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP); + helper.sendMediaButtonEvent(event, true); + mHandler.postDelayed(() -> { + updateMediaPlaybackState(); + }, 250); + } + + private void updateMediaPlaybackState() { + boolean isPlaying = mMediaSessionManagerHelper.isMediaPlaying(); + int stateIcon = isPlaying ? R.drawable.ic_media_pause : R.drawable.ic_media_play; + if (mediaButton != null) { + mediaButton.setImageResource(stateIcon); + setButtonActiveState(mediaButton, null, isPlaying); + } + if (mediaButtonFab != null) { + MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getMediaMetadata(); + String trackTitle = mMediaMetadata != null ? mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE) : ""; + if (!TextUtils.isEmpty(trackTitle) && mLastTrackTitle != trackTitle) { + mLastTrackTitle = trackTitle; + } + final boolean canShowTrackTitle = isPlaying || !TextUtils.isEmpty(mLastTrackTitle); + mediaButtonFab.setIcon(mContext.getDrawable(isPlaying ? R.drawable.ic_media_pause : R.drawable.ic_media_play)); + mediaButtonFab.setText(canShowTrackTitle ? mLastTrackTitle : mContext.getResources().getString(R.string.controls_media_button_play)); + setButtonActiveState(null, mediaButtonFab, isPlaying); + } + } + + private void toggleFlashlight() { + if (torchButton == null && torchButtonFab == null) return; + try { + mCameraManager.setTorchMode(mCameraId, !isFlashOn); + isFlashOn = !isFlashOn; + updateTorchButtonState(); + } catch (Exception e) {} + } + + private void toggleWiFi() { + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + mNetworkController.setWifiEnabled(!cbi.enabled); + updateWiFiButtonState(!cbi.enabled); + mHandler.postDelayed(() -> { + updateWiFiButtonState(cbi.enabled); + }, 250); + } + + private boolean isMobileDataEnabled() { + return mDataController.isMobileDataEnabled(); + } + + private void toggleMobileData() { + mDataController.setMobileDataEnabled(!isMobileDataEnabled()); + updateMobileDataState(!isMobileDataEnabled()); + mHandler.postDelayed(() -> { + updateMobileDataState(isMobileDataEnabled()); + }, 250); + } + + private void showInternetDialog(View view) { + mHandler.post(() -> mInternetDialogManager.create(true, + mAccessPointController.canConfigMobileData(), + mAccessPointController.canConfigWifi(), Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void toggleRingerMode() { + if (mAudioManager != null) { + int mode = mAudioManager.getRingerMode(); + if (mode == mAudioManager.RINGER_MODE_NORMAL) { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_VIBRATE); + } else { + mAudioManager.setRingerMode(AudioManager.RINGER_MODE_NORMAL); + } + updateRingerButtonState(); + } + } + + private void updateTileButtonState( + LaunchableImageView iv, LaunchableFAB efab, + boolean active, int activeResource, int inactiveResource, + String activeString, String inactiveString) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (iv != null) { + iv.setImageResource(active ? activeResource : inactiveResource); + setButtonActiveState(iv, null, active); + } + if (efab != null) { + efab.setIcon(mContext.getDrawable(active ? activeResource : inactiveResource)); + efab.setText(active ? activeString : inactiveString); + setButtonActiveState(null, efab, active); + } + } + }); + } + + public void updateTorchButtonState() { + if (!isWidgetEnabled("torch")) return; + String activeString = mContext.getResources().getString(TORCH_LABEL_ACTIVE); + String inactiveString = mContext.getResources().getString(TORCH_LABEL_INACTIVE); + updateTileButtonState(torchButton, torchButtonFab, isFlashOn, + TORCH_RES_ACTIVE, TORCH_RES_INACTIVE, activeString, inactiveString); + } + + private BroadcastReceiver mRingerModeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + updateRingerButtonState(); + } + }; + + private final BluetoothController.Callback mBtCallback = new BluetoothController.Callback() { + @Override + public void onBluetoothStateChange(boolean enabled) { + updateBtState(); + } + @Override + public void onBluetoothDevicesChanged() { + updateBtState(); + } + }; + + private void updateWiFiButtonState(boolean enabled) { + if (!isWidgetEnabled("wifi")) return; + if (wifiButton == null && wifiButtonFab == null) return; + final WifiCallbackInfo cbi = mWifiSignalCallback.mInfo; + String inactiveString = mContext.getResources().getString(WIFI_LABEL_INACTIVE); + updateTileButtonState(wifiButton, wifiButtonFab, enabled, + WIFI_ACTIVE, WIFI_INACTIVE, cbi.ssid != null ? removeDoubleQuotes(cbi.ssid) : inactiveString, inactiveString); + } + + private void updateRingerButtonState() { + if (!isWidgetEnabled("ringer")) return; + if (ringerButton == null && ringerButtonFab == null) return; + if (mAudioManager != null) { + boolean isVibrateActive = mAudioManager.getRingerMode() == AudioManager.RINGER_MODE_VIBRATE; + String inactiveString = mContext.getResources().getString(RINGER_LABEL_INACTIVE); + updateTileButtonState(ringerButton, ringerButtonFab, isVibrateActive, + RINGER_ACTIVE, RINGER_INACTIVE, inactiveString, inactiveString); + } + } + + private void updateMobileDataState(boolean enabled) { + if (!isWidgetEnabled("data")) return; + if (dataButton == null && dataButtonFab == null) return; + String networkName = mNetworkController == null ? "" : mNetworkController.getMobileDataNetworkName(); + boolean hasNetwork = !TextUtils.isEmpty(networkName) && mNetworkController != null + && mNetworkController.hasMobileDataFeature(); + String inactiveString = mContext.getResources().getString(DATA_LABEL_INACTIVE); + updateTileButtonState(dataButton, dataButtonFab, enabled, + DATA_ACTIVE, DATA_INACTIVE, hasNetwork && enabled ? networkName : inactiveString, inactiveString); + } + + private void toggleBluetoothState() { + mBluetoothController.setBluetoothEnabled(!isBluetoothEnabled()); + updateBtState(); + mHandler.postDelayed(() -> { + updateBtState(); + }, 250); + } + + private void showBluetoothDialog(View view) { + mHandler.post(() -> + mBluetoothDetailsContentViewModel.showDialog(Expandable.fromView(view))); + VibrationUtils.triggerVibration(mContext, 2); + } + + private void updateBtState() { + if (!isWidgetEnabled("bt")) return; + if (btButton == null && btButtonFab == null) return; + String deviceName = isBluetoothEnabled() ? mBluetoothController.getConnectedDeviceName() : ""; + boolean isConnected = !TextUtils.isEmpty(deviceName); + String inactiveString = mContext.getResources().getString(BT_LABEL_INACTIVE); + updateTileButtonState(btButton, btButtonFab, isBluetoothEnabled(), + BT_ACTIVE, BT_INACTIVE, isConnected ? deviceName : inactiveString, inactiveString); + } + + private boolean isBluetoothEnabled() { + final BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + return mBluetoothAdapter != null && mBluetoothAdapter.isEnabled(); + } + + @Nullable + private static String removeDoubleQuotes(String string) { + if (string == null) return null; + final int length = string.length(); + if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) { + return string.substring(1, length - 1); + } + return string; + } + + protected static final class WifiCallbackInfo { + boolean enabled; + @Nullable + String ssid; + } + + protected final class WifiSignalCallback implements SignalCallback { + final WifiCallbackInfo mInfo = new WifiCallbackInfo(); + @Override + public void setWifiIndicators(@NonNull WifiIndicators indicators) { + if (indicators.qsIcon == null) { + updateWiFiButtonState(false); + return; + } + mInfo.enabled = indicators.enabled; + mInfo.ssid = indicators.description; + updateWiFiButtonState(mInfo.enabled); + } + } + + private final class CellSignalCallback implements SignalCallback { + @Override + public void setMobileDataIndicators(@NonNull MobileDataIndicators indicators) { + if (indicators.qsIcon == null) { + updateMobileDataState(false); + return; + } + updateMobileDataState(isMobileDataEnabled()); + } + @Override + public void setNoSims(boolean show, boolean simDetected) { + updateMobileDataState(simDetected && isMobileDataEnabled()); + } + @Override + public void setIsAirplaneMode(@NonNull IconState icon) { + updateMobileDataState(!icon.visible && isMobileDataEnabled()); + } + } + + public void enableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.addObserver(this); + queryAndUpdateWeather(); + } + } + + public void disableWeatherUpdates() { + if (mWeatherClient != null) { + mWeatherClient.removeObserver(this); + } + } + + @Override + public void weatherError(int errorReason) { + if (errorReason == OmniJawsClient.EXTRA_ERROR_DISABLED) { + mWeatherInfo = null; + } + } + + @Override + public void weatherUpdated() { + queryAndUpdateWeather(); + } + + @Override + public void updateSettings() { + queryAndUpdateWeather(); + } + + private void queryAndUpdateWeather() { + try { + if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled()) return; + mWeatherClient.queryWeather(); + mWeatherInfo = mWeatherClient.getWeatherInfo(); + if (mWeatherInfo != null) { + // OpenWeatherMap + String formattedCondition = mWeatherInfo.condition; + if (formattedCondition.toLowerCase().contains("clouds")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_clouds); + } else if (formattedCondition.toLowerCase().contains("rain")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_rain); + } else if (formattedCondition.toLowerCase().contains("clear")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_clear); + } else if (formattedCondition.toLowerCase().contains("storm")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_storm); + } else if (formattedCondition.toLowerCase().contains("snow")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_snow); + } else if (formattedCondition.toLowerCase().contains("wind")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_wind); + } else if (formattedCondition.toLowerCase().contains("mist")) { + formattedCondition = mContext.getResources().getString(R.string.weather_condition_mist); + } + // MET Norway + if (formattedCondition.toLowerCase().contains("_")) { + final String[] words = formattedCondition.split("_"); + final StringBuilder formattedConditionBuilder = new StringBuilder(); + for (String word : words) { + final String capitalizedWord = word.substring(0, 1).toUpperCase() + word.substring(1); + formattedConditionBuilder.append(capitalizedWord).append(" "); + } + formattedCondition = formattedConditionBuilder.toString().trim(); + } + final Drawable d = mWeatherClient.getWeatherConditionImage(mWeatherInfo.conditionCode); + if (weatherButtonFab != null) { + weatherButtonFab.setIcon(d); + weatherButtonFab.setText(mWeatherInfo.temp + mWeatherInfo.tempUnits + " \u2022 " + formattedCondition); + weatherButtonFab.setIconTint(null); + } + if (weatherButton != null) { + weatherButton.setImageDrawable(d); + weatherButton.setImageTintList(null); + } + } + } catch(Exception e) {} + } + + private boolean isWidgetEnabled(String widget) { + return (mMainLockscreenWidgetsList != null + && !mMainLockscreenWidgetsList.contains(widget)) + || (mSecondaryLockscreenWidgetsList != null + && !mSecondaryLockscreenWidgetsList.contains(widget)); + } + + @Override + public void onMediaMetadataChanged() { + updateMediaPlaybackState(); + } + + @Override + public void onPlaybackStateChanged() { + updateMediaPlaybackState(); + } + + private class LockscreenWidgetsObserver extends ContentObserver { + public LockscreenWidgetsObserver() { + super(null); + } + @Override + public void onChange(boolean selfChange) { + super.onChange(selfChange); + updateSettings(); + } + void observe() { + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_ENABLED), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_EXTRAS), + false, + this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_STYLE), + false, + this); + updateSettings(); + } + void unobserve() { + mContext.getContentResolver().unregisterContentObserver(this); + } + void updateSettings() { + mLockscreenWidgetsEnabled = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_ENABLED, 0) == 1; + mMainLockscreenWidgetsList = Settings.System.getString(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS); + mSecondaryLockscreenWidgetsList = Settings.System.getString(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_EXTRAS); + mThemeStyle = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_STYLE, 0); + if (mMainLockscreenWidgetsList != null) { + mMainWidgetsList = Arrays.asList(mMainLockscreenWidgetsList.split(",")); + } + if (mSecondaryLockscreenWidgetsList != null) { + mSecondaryWidgetsList = Arrays.asList(mSecondaryLockscreenWidgetsList.split(",")); + } + updateWidgetViews(); + } + }; + + private void updateHotspotState() { + if (!isWidgetEnabled("hotspot")) return; + if (hotspotButton == null && hotspotButtonFab == null) return; + String hotspotString = mContext.getResources().getString(HOTSPOT_LABEL); + updateTileButtonState(hotspotButton, hotspotButtonFab, mIsHotspotEnabled, + HOTSPOT_ACTIVE, HOTSPOT_INACTIVE, hotspotString, hotspotString); + } + + private void toggleHotspot() { + mHotspotController.setHotspotEnabled(!mIsHotspotEnabled); + updateHotspotState(); + mHandler.postDelayed(() -> { + updateHotspotState(); + }, 250); + } + + private final class HotspotCallback implements HotspotController.Callback { + @Override + public void onHotspotChanged(boolean enabled, int numDevices) { + mIsHotspotEnabled = enabled; + updateHotspotState(); + } + @Override + public void onHotspotAvailabilityChanged(boolean available) {} + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 9dd18a831348..e7d4befbbc91 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1275,9 +1275,11 @@ private ClockSize computeDesiredClockSizeForSplitShade() { private boolean shouldForceSmallClock() { return !isOnAod() // True on small landscape screens - && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) - || Settings.Secure.getIntForUser( - mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0; + && mResources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + (Settings.Secure.getIntForUser( + mContentResolver, "clock_style", 0, UserHandle.USER_CURRENT) != 0 || + Settings.System.getIntForUser( + mContentResolver, "lockscreen_widgets_enabled", 0, UserHandle.USER_CURRENT) != 0); } private void updateKeyguardStatusViewAlignment() { From 0699a0a44089c3cc415f019e4bd1986717f8a138 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 5 Dec 2024 22:01:42 +0800 Subject: [PATCH 107/190] SystemUI: Introduce Lockscreen info widgets * inspired from motorola lockscreen widgets Co-authored-by: DrDisagree Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- .../res-keyguard/drawable/arc_progress.xml | 17 ++ .../res-keyguard/drawable/ic_battery.xml | 10 + .../res-keyguard/drawable/ic_memory.xml | 28 +++ .../res-keyguard/drawable/ic_temperature.xml | 10 + .../res-keyguard/drawable/ic_volume_eq.xml | 10 + .../layout/keyguard_info_widgets.xml | 59 ++++++ .../SystemUI/res/values/lunaris_attrs.xml | 4 + .../systemui/util/ArcProgressWidget.java | 83 ++++++++ .../systemui/util/ProgressImageView.kt | 188 ++++++++++++++++++ 9 files changed, 409 insertions(+) create mode 100644 packages/SystemUI/res-keyguard/drawable/arc_progress.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_battery.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_memory.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_temperature.xml create mode 100644 packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml create mode 100644 packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java create mode 100644 packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt diff --git a/packages/SystemUI/res-keyguard/drawable/arc_progress.xml b/packages/SystemUI/res-keyguard/drawable/arc_progress.xml new file mode 100644 index 000000000000..194c813e7217 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/arc_progress.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/drawable/ic_battery.xml b/packages/SystemUI/res-keyguard/drawable/ic_battery.xml new file mode 100644 index 000000000000..9138349dd22b --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_battery.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_memory.xml b/packages/SystemUI/res-keyguard/drawable/ic_memory.xml new file mode 100644 index 000000000000..ada36c58ff1d --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_memory.xml @@ -0,0 +1,28 @@ + + + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml b/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml new file mode 100644 index 000000000000..25dc1f40fc6b --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_temperature.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml b/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml new file mode 100644 index 000000000000..3e256c0ba5b3 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/ic_volume_eq.xml @@ -0,0 +1,10 @@ + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml b/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml new file mode 100644 index 000000000000..e54599eb0220 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_info_widgets.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + diff --git a/packages/SystemUI/res/values/lunaris_attrs.xml b/packages/SystemUI/res/values/lunaris_attrs.xml index db0e424e35c7..46f9d23364b0 100644 --- a/packages/SystemUI/res/values/lunaris_attrs.xml +++ b/packages/SystemUI/res/values/lunaris_attrs.xml @@ -20,4 +20,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java new file mode 100644 index 000000000000..ed5485b6d5e0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ArcProgressWidget.java @@ -0,0 +1,83 @@ +package com.android.systemui.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BlendMode; +import android.graphics.BlendModeColorFilter; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import androidx.annotation.Nullable; + +public class ArcProgressWidget { + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable String textBottom, int textBottomSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, null, 28, textBottom, textBottomSizePx, tf); + } + + public static Bitmap generateBitmap(Context context, int percentage, String textInside, int textInsideSizePx, @Nullable Drawable iconDrawable, int iconSizePx, @Nullable String tf) { + return generateBitmap(context, percentage, textInside, textInsideSizePx, iconDrawable, iconSizePx, "Usage", 28, tf); + } + + public static Bitmap generateBitmap(Context context, + int percentage, + String textInside, + int textInsideSizePx, + @Nullable Drawable iconDrawable, + int iconSizePx, + @Nullable String textBottom, + int textBottomSizePx, + @Nullable String tf) { + int width = 400; + int height = 400; + int stroke = 40; + int padding = 5; + int minAngle = 135; + int maxAngle = 275; + Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG); + paint.setStrokeWidth(stroke); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTextSize(dp2px(context, textInsideSizePx)); + mTextPaint.setColor(Color.WHITE); + mTextPaint.setTextAlign(Paint.Align.CENTER); + final RectF arc = new RectF(); + arc.set(((float) stroke / 2) + padding, ((float) stroke / 2) + padding, width - padding - ((float) stroke / 2), height - padding - ((float) stroke / 2)); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + paint.setColor(Color.argb(75, 255, 255, 255)); + canvas.drawArc(arc, minAngle, maxAngle, false, paint); + paint.setColor(Color.WHITE); + canvas.drawArc(arc, minAngle, ((float) maxAngle / 100) * percentage, false, paint); + if (tf != null) { + mTextPaint.setTypeface(Typeface.create(tf, Typeface.BOLD)); + } else { + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + } + canvas.drawText(textInside, (float) bitmap.getWidth() / 2, (bitmap.getHeight() - mTextPaint.ascent() * 0.7f) / 2, mTextPaint); + if (iconDrawable != null) { + int size = dp2px(context, iconSizePx); + int left = (bitmap.getWidth() - size) / 2; + int top = bitmap.getHeight() - (int) (size / 1.3) - (stroke + padding) - dp2px(context, 4); + int right = left + size; + int bottom = top + size; + iconDrawable.setBounds(left, top, right, bottom); + iconDrawable.setColorFilter(new BlendModeColorFilter(Color.WHITE, BlendMode.SRC_IN)); + iconDrawable.draw(canvas); + } else if (textBottom != null) { + mTextPaint.setTextSize(dp2px(context, textBottomSizePx)); + canvas.drawText(textBottom, (float) bitmap.getWidth() / 2, bitmap.getHeight() - (stroke + padding), mTextPaint); + } + return bitmap; + } + + private static int dp2px(Context context, float dp) { + DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt new file mode 100644 index 000000000000..1f9edfd32083 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/ProgressImageView.kt @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2023-2024 the 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.util + +import android.app.ActivityManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.ContentResolver +import android.content.Intent +import android.content.IntentFilter +import android.database.ContentObserver +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.AudioManager +import android.os.BatteryManager +import android.provider.Settings +import android.util.AttributeSet +import android.widget.ImageView +import androidx.core.content.ContextCompat +import kotlinx.coroutines.* + +import com.android.systemui.res.R + +class ProgressImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ImageView(context, attrs, defStyleAttr) { + + private var progressType: ProgressType = ProgressType.UNKNOWN + private var progressPercent = -1 + private var batteryLevel = -1 + private var batteryTemperature = -1 + private var updateJob: Job? = null + private var receiverRegistered = false + private var typeface: String? = null + + private val batteryReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BATTERY_CHANGED) { + batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) + batteryLevel = batteryLevel.coerceIn(0, 100) + batteryTemperature = intent.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) / 10 + updateProgress() + } + } + } + + private val settingsObserver = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + updateVisibility() + } + } + + enum class ProgressType(val iconRes: Int) { + BATTERY(R.drawable.ic_battery), + MEMORY(R.drawable.ic_memory), + TEMPERATURE(R.drawable.ic_temperature), + VOLUME(R.drawable.ic_volume_eq), + UNKNOWN(-1) + } + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressImageView, defStyleAttr, 0) + typeface = typedArray.getString(R.styleable.ProgressImageView_typeface) + typedArray.recycle() + when (id) { + R.id.battery_progress -> progressType = ProgressType.BATTERY + R.id.memory_progress -> progressType = ProgressType.MEMORY + R.id.temperature_progress -> progressType = ProgressType.TEMPERATURE + R.id.volume_progress -> progressType = ProgressType.VOLUME + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + context.contentResolver.registerContentObserver( + Settings.System.getUriFor("lockscreen_info_widgets_enabled"), + false, + settingsObserver + ) + if (!receiverRegistered) { + if (progressType == ProgressType.BATTERY || progressType == ProgressType.TEMPERATURE) { + val filter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + context.registerReceiver(batteryReceiver, filter, Context.RECEIVER_EXPORTED) + receiverRegistered = true + } + } + startProgressUpdates() + updateVisibility() + updateProgress() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + context.contentResolver.unregisterContentObserver(settingsObserver) + if (receiverRegistered) { + context.unregisterReceiver(batteryReceiver) + } + stopProgressUpdates() + } + + private fun startProgressUpdates() { + if (progressType == ProgressType.MEMORY || progressType == ProgressType.VOLUME) { + updateJob = CoroutineScope(Dispatchers.Main).launch { + while (isActive) { + updateProgress() + delay(1000L) + } + } + } + } + + private fun stopProgressUpdates() { + updateJob?.cancel() + } + + private fun updateProgress() { + val newProgressPercent = when (progressType) { + ProgressType.BATTERY -> batteryLevel + ProgressType.MEMORY -> getMemoryLevel() + ProgressType.TEMPERATURE -> batteryTemperature + ProgressType.VOLUME -> getVolumeLevel() + ProgressType.UNKNOWN -> -1 + } + if (newProgressPercent != progressPercent) { + progressPercent = newProgressPercent + updateImageView() + } + } + + private fun updateImageView() { + val degree = "\u2103" + val progressText = if (progressType == ProgressType.TEMPERATURE) { + if (progressPercent != -1) "$progressPercent$degree" else "N/A" + } else { + if (progressPercent == -1) "..." else "$progressPercent%" + } + val icon: Drawable? = ContextCompat.getDrawable(context, progressType.iconRes) + val widgetBitmap: Bitmap = ArcProgressWidget.generateBitmap( + context, + if (progressPercent == -1) 0 else progressPercent, + progressText, + 40, + icon, + 36, + typeface + ) + setImageBitmap(widgetBitmap) + } + + private fun getMemoryLevel(): Int { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val memoryInfo = ActivityManager.MemoryInfo() + activityManager.getMemoryInfo(memoryInfo) + val usedMemory = memoryInfo.totalMem - memoryInfo.availMem + val usedMemoryPercentage = ((usedMemory * 100) / memoryInfo.totalMem).toInt() + return usedMemoryPercentage.coerceIn(0, 100) + } + + private fun getVolumeLevel(): Int { + val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + val maxVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_MUSIC) ?: 1 + val currentVolume = audioManager?.getStreamVolume(AudioManager.STREAM_MUSIC) ?: 0 + return ((currentVolume * 100) / maxVolume).coerceIn(0, 100) + } + + private fun updateVisibility() { + val enabled = Settings.System.getInt( + context.contentResolver, + "lockscreen_info_widgets_enabled", 0 + ) == 1 + visibility = if (enabled) VISIBLE else GONE + } +} From 822bd030e90fbcf9351e22f26e417090fd1d50fd Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Sat, 18 Jan 2025 10:10:26 +0800 Subject: [PATCH 108/190] Lockscreen widget styles update Signed-off-by: Ghosuto --- .../lockscreen/LockScreenWidgetsController.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index dfc615b727e2..49de94370bb0 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -96,6 +96,9 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser private static final String LOCKSCREEN_WIDGETS_STYLE = "lockscreen_widgets_style"; + + private static final String LOCKSCREEN_WIDGETS_TRANSPARENCY = + "lockscreen_widgets_transparency"; private static final int[] MAIN_WIDGETS_VIEW_IDS = { R.id.main_kg_item_placeholder1, @@ -183,6 +186,7 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser private boolean mLockscreenWidgetsEnabled; private int mThemeStyle = 0; + private float mTransparency = 0.3f; final ConfigurationListener mConfigurationListener = new ConfigurationListener() { @Override @@ -413,6 +417,7 @@ private void updateWidgetsResources(LaunchableImageView iv) { int bgRes; switch (themeStyle) { case 0: + case 3: default: bgRes = R.drawable.lockscreen_widget_background_circle; break; @@ -573,12 +578,12 @@ public void onLongPress(MotionEvent e) { private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { int bgTint; int tintColor; - if (mThemeStyle == 2) { + if (mThemeStyle == 2 || mThemeStyle == 3) { if (active) { - bgTint = Utils.applyAlpha(0.3f, mDarkColorActive); + bgTint = Utils.applyAlpha(mTransparency, mDarkColorActive); tintColor = mDarkColorActive; } else { - bgTint = Utils.applyAlpha(0.3f, Color.WHITE); + bgTint = Utils.applyAlpha(mTransparency, Color.WHITE); tintColor = Color.WHITE; } } else { @@ -988,6 +993,10 @@ void observe() { Settings.System.getUriFor(LOCKSCREEN_WIDGETS_STYLE), false, this); + mContext.getContentResolver().registerContentObserver( + Settings.System.getUriFor(LOCKSCREEN_WIDGETS_TRANSPARENCY), + false, + this); updateSettings(); } void unobserve() { @@ -1002,6 +1011,8 @@ void updateSettings() { LOCKSCREEN_WIDGETS_EXTRAS); mThemeStyle = Settings.System.getInt(mContext.getContentResolver(), LOCKSCREEN_WIDGETS_STYLE, 0); + mTransparency = Settings.System.getInt(mContext.getContentResolver(), + LOCKSCREEN_WIDGETS_TRANSPARENCY, 30) / 100f; if (mMainLockscreenWidgetsList != null) { mMainWidgetsList = Arrays.asList(mMainLockscreenWidgetsList.split(",")); } From 32b11214f24e3c3d2c903786efc09e2318a97cfd Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Mon, 27 Jan 2025 08:05:44 +0800 Subject: [PATCH 109/190] SystemUI: Fixes, improvements to lockscreen widget and google wallet integration - Ghost: Adapt new omnijaw changes Signed-off-by: Ghosuto --- .../res/values/kg_widgets_strings.xml | 1 + .../SystemUI/res/values/lunaris_dimens.xml | 3 - .../SystemUI/res/values/lunaris_strings.xml | 9 - .../lockscreen/ActivityLauncherUtils.java | 130 ---------- .../lockscreen/ActivityLauncherUtils.kt | 155 ++++++++++++ .../LockScreenWidgetsController.java | 236 ++++++++++++------ 6 files changed, 314 insertions(+), 220 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java create mode 100644 packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt diff --git a/packages/SystemUI/res/values/kg_widgets_strings.xml b/packages/SystemUI/res/values/kg_widgets_strings.xml index a9eb69090e30..93a0839c90d5 100644 --- a/packages/SystemUI/res/values/kg_widgets_strings.xml +++ b/packages/SystemUI/res/values/kg_widgets_strings.xml @@ -9,4 +9,5 @@ Mobile data Ringer mode Torch Active + Google Wallet diff --git a/packages/SystemUI/res/values/lunaris_dimens.xml b/packages/SystemUI/res/values/lunaris_dimens.xml index a656d27c23ab..e87f7eec8c4b 100644 --- a/packages/SystemUI/res/values/lunaris_dimens.xml +++ b/packages/SystemUI/res/values/lunaris_dimens.xml @@ -123,9 +123,6 @@ 0dp 8dp - - 24dp - 180dp 0dp diff --git a/packages/SystemUI/res/values/lunaris_strings.xml b/packages/SystemUI/res/values/lunaris_strings.xml index 96ca704a6f9c..f30dd0da74c7 100644 --- a/packages/SystemUI/res/values/lunaris_strings.xml +++ b/packages/SystemUI/res/values/lunaris_strings.xml @@ -220,15 +220,6 @@ VPN tethering turned off. VPN tethering turned on. - - Cloudy - Rainy - Sunny - Stormy - Snowy - Windy - Misty - null diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java deleted file mode 100644 index 29a82ab10b12..000000000000 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - Copyright (C) 2024 the 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.lockscreen; - -import android.content.Context; -import android.content.ComponentName; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.media.AudioManager; -import android.os.DeviceIdleManager; -import android.provider.AlarmClock; -import android.provider.MediaStore; -import android.speech.RecognizerIntent; -import android.widget.Toast; - -import androidx.annotation.StringRes; - -import com.android.systemui.Dependency; -import com.android.systemui.plugins.ActivityStarter; -import com.android.systemui.res.R; - -import java.util.List; - -public class ActivityLauncherUtils { - - private final static String PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings$personalizationSettingsLayoutActivity"; - private static final String SERVICE_PACKAGE = "org.omnirom.omnijaws"; - - private final Context mContext; - private final ActivityStarter mActivityStarter; - private PackageManager mPackageManager; - - public ActivityLauncherUtils(Context context) { - this.mContext = context; - this.mActivityStarter = Dependency.get(ActivityStarter.class); - mPackageManager = mContext.getPackageManager(); - } - - public String getInstalledMusicApp() { - final Intent intent = new Intent(Intent.ACTION_MAIN); - intent.addCategory(Intent.CATEGORY_APP_MUSIC); - final List musicApps = mPackageManager.queryIntentActivities(intent, 0); - ResolveInfo musicApp = musicApps.isEmpty() ? null : musicApps.get(0); - return musicApp != null ? musicApp.activityInfo.packageName : ""; - } - - public void launchAppIfAvailable(Intent launchIntent, @StringRes int appTypeResId) { - final List apps = mPackageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY); - if (!apps.isEmpty()) { - mActivityStarter.startActivity(launchIntent, true); - } else { - showNoDefaultAppFoundToast(appTypeResId); - } - } - - public void launchVoiceAssistant() { - DeviceIdleManager dim = mContext.getSystemService(DeviceIdleManager.class); - if (dim != null) { - dim.endIdle("voice-search"); - } - Intent voiceIntent = new Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE); - voiceIntent.putExtra(RecognizerIntent.EXTRA_SECURE, true); - mActivityStarter.startActivity(voiceIntent, true); - } - - public void launchCamera() { - final Intent launchIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA); - launchAppIfAvailable(launchIntent, R.string.camera); - } - - public void launchTimer() { - final Intent launchIntent = new Intent(AlarmClock.ACTION_SET_TIMER); - launchAppIfAvailable(launchIntent, R.string.clock_timer); - } - - public void launchCalculator() { - final Intent launchIntent = new Intent(); - launchIntent.setAction(Intent.ACTION_MAIN); - launchIntent.addCategory(Intent.CATEGORY_APP_CALCULATOR); - launchAppIfAvailable(launchIntent, R.string.calculator); - } - - public void launchSettingsComponent(String className) { - if (mActivityStarter == null) return; - Intent intent = className.equals(PERSONALIZATIONS_ACTIVITY) ? new Intent(Intent.ACTION_MAIN) : new Intent(); - intent.setComponent(new ComponentName("com.android.settings", className)); - mActivityStarter.startActivity(intent, true); - } - - public void launchWeatherApp() { - final Intent launchIntent = new Intent(); - launchIntent.setAction(Intent.ACTION_MAIN); - launchIntent.setClassName(SERVICE_PACKAGE, SERVICE_PACKAGE + ".WeatherActivity"); - launchAppIfAvailable(launchIntent, R.string.omnijaws_weather); - } - - public void launchMediaPlayerApp(String packageName) { - if (!packageName.isEmpty()) { - Intent launchIntent = mPackageManager.getLaunchIntentForPackage(packageName); - if (launchIntent != null) { - mActivityStarter.startActivity(launchIntent, true); - } - } - } - - public void startSettingsActivity() { - if (mActivityStarter == null) return; - mActivityStarter.startActivity(new Intent(android.provider.Settings.ACTION_SETTINGS), true /* dismissShade */); - } - - public void startIntent(Intent intent) { - if (mActivityStarter == null) return; - mActivityStarter.startActivity(intent, true /* dismissShade */); - } - - private void showNoDefaultAppFoundToast(@StringRes int appTypeResId) { - Toast.makeText(mContext, mContext.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show(); - } -} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt new file mode 100644 index 000000000000..1866196925ff --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/ActivityLauncherUtils.kt @@ -0,0 +1,155 @@ +/* + Copyright (C) 2023-2025 the 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.lockscreen + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.media.AudioManager +import android.os.DeviceIdleManager +import android.provider.AlarmClock +import android.provider.MediaStore +import android.speech.RecognizerIntent +import android.widget.Toast +import androidx.annotation.StringRes +import com.android.systemui.Dependency +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.res.R + +class ActivityLauncherUtils(private val context: Context) { + + companion object { + private const val PERSONALIZATIONS_ACTIVITY = "com.android.settings.Settings\$mistifySettingsLayoutActivity" + private const val SERVICE_PACKAGE = "org.omnirom.omnijaws" + } + + private val activityStarter: ActivityStarter? = Dependency.get(ActivityStarter::class.java) + private val packageManager: PackageManager = context.packageManager + + fun getInstalledMusicApp(): String { + val intent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_MUSIC) + } + val musicApps = packageManager.queryIntentActivities(intent, 0) + return musicApps.firstOrNull()?.activityInfo?.packageName.orEmpty() + } + + fun launchAppIfAvailable(launchIntent: Intent, @StringRes appTypeResId: Int) { + val apps = packageManager.queryIntentActivities(launchIntent, PackageManager.MATCH_DEFAULT_ONLY) + if (apps.isNotEmpty()) { + activityStarter?.startActivity(launchIntent, true) + } else { + showNoDefaultAppFoundToast(appTypeResId) + } + } + + fun launchVoiceAssistant() { + val dim = context.getSystemService(DeviceIdleManager::class.java) + dim?.endIdle("voice-search") + val voiceIntent = Intent(RecognizerIntent.ACTION_VOICE_SEARCH_HANDS_FREE).apply { + putExtra(RecognizerIntent.EXTRA_SECURE, true) + } + activityStarter?.startActivity(voiceIntent, true) + } + + fun launchCamera() { + val launchIntent = Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA) + launchAppIfAvailable(launchIntent, R.string.camera) + } + + fun launchTimer() { + val launchIntent = Intent(AlarmClock.ACTION_SET_TIMER) + launchAppIfAvailable(launchIntent, R.string.clock_timer) + } + + fun launchCalculator() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + addCategory(Intent.CATEGORY_APP_CALCULATOR) + } + launchAppIfAvailable(launchIntent, R.string.calculator) + } + + fun launchSettingsComponent(className: String) { + val intent = if (className == PERSONALIZATIONS_ACTIVITY) { + Intent(Intent.ACTION_MAIN) + } else { + Intent().setComponent(ComponentName("com.android.settings", className)) + } + activityStarter?.startActivity(intent, true) + } + + fun launchWeatherApp() { + val launchIntent = Intent(Intent.ACTION_MAIN).apply { + setClassName(SERVICE_PACKAGE, "$SERVICE_PACKAGE.WeatherActivity") + } + launchAppIfAvailable(launchIntent, R.string.omnijaws_weather) + } + + fun launchWalletApp() { + val launchIntent = context.packageManager.getLaunchIntentForPackage("com.google.android.apps.walletnfcrel")?.apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + launchIntent?.let { + launchAppIfAvailable(it, R.string.google_wallet) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { + activityStarter?.startActivity(it, true) + } + } + } + + fun launchQrScanner() { + try { + val qrScannerComponent = context.resources.getString( + com.android.internal.R.string.config_defaultQrCodeComponent + ) + val intent = if (qrScannerComponent.isNotEmpty()) { + Intent().apply { + component = ComponentName.unflattenFromString(qrScannerComponent) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } else { + Intent().apply { + component = ComponentName( + "com.google.android.googlequicksearchbox", + "com.google.android.apps.search.lens.LensActivity" + ) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + } + startIntent(intent) + } catch (e: Exception) { + Toast.makeText(context, "Unable to launch QR Scanner", Toast.LENGTH_SHORT).show() + } + } + + fun startSettingsActivity() { + val settingsIntent = Intent(android.provider.Settings.ACTION_SETTINGS) + activityStarter?.startActivity(settingsIntent, true) + } + + fun startIntent(intent: Intent) { + activityStarter?.startActivity(intent, true) + } + + private fun showNoDefaultAppFoundToast(@StringRes appTypeResId: Int) { + Toast.makeText(context, context.getString(appTypeResId) + " not found", Toast.LENGTH_SHORT).show() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index 49de94370bb0..e35581bf313e 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -60,7 +60,7 @@ import com.android.systemui.animation.view.LaunchableFAB; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; +import com.android.systemui.bluetooth.ui.viewModel.BluetoothDetailsContentViewModel; import com.android.systemui.qs.tiles.dialog.InternetDialogManager; import com.android.systemui.statusbar.policy.BluetoothController; import com.android.systemui.statusbar.policy.BluetoothController.Callback; @@ -75,13 +75,13 @@ import com.android.systemui.statusbar.connectivity.MobileDataIndicators; import com.android.systemui.statusbar.connectivity.WifiIndicators; import com.android.systemui.util.MediaSessionManagerHelper; -import com.android.internal.util.android.VibrationUtils; +import com.android.internal.util.lunaris.VibrationUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import com.android.internal.util.android.OmniJawsClient; +import com.android.internal.util.lunaris.OmniJawsClient; public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObserver, MediaSessionManagerHelper.MediaMetadataListener { @@ -153,8 +153,6 @@ public class LockScreenWidgetsController implements OmniJawsClient.OmniJawsObser protected final CellSignalCallback mCellSignalCallback = new CellSignalCallback(); protected final WifiSignalCallback mWifiSignalCallback = new WifiSignalCallback(); private final HotspotCallback mHotspotCallback = new HotspotCallback(); - - private boolean mIsHotspotEnabled = false; private Context mContext; private LaunchableImageView mWidget1, mWidget2, mWidget3, mWidget4, mediaButton, torchButton, weatherButton; @@ -227,8 +225,9 @@ public LockScreenWidgetsController(View view) { initResources(); + // FIX 1: OmniJawsClient no longer takes Context in constructor; use get() if (mWeatherClient == null) { - mWeatherClient = new OmniJawsClient(mContext); + mWeatherClient = OmniJawsClient.get(); } try { @@ -268,6 +267,12 @@ public void onFlashlightAvailabilityChanged(boolean available) { isFlashOn = mFlashlightController.isEnabled() && available; updateTorchButtonState(); } + // Add the missing method + @Override + public void onFlashlightStrengthChanged(int level) { + // Handle flashlight strength changes if needed + updateTorchButtonState(); + } }; private void initResources() { @@ -343,6 +348,39 @@ public void initViews() { public void updateWidgetViews() { if (!mIsInflated) return; + + // Forcefully hide all widgets if lockscreen widgets are disabled + if (!mLockscreenWidgetsEnabled) { + // Hide all main widget views + if (mMainWidgetViews != null) { + for (int i = 0; i < mMainWidgetViews.length; i++) { + if (mMainWidgetViews[i] != null) { + mMainWidgetViews[i].setVisibility(View.GONE); + } + } + } + // Hide all secondary widget views + if (mSecondaryWidgetViews != null) { + for (int i = 0; i < mSecondaryWidgetViews.length; i++) { + if (mSecondaryWidgetViews[i] != null) { + mSecondaryWidgetViews[i].setVisibility(View.GONE); + } + } + } + // Hide all containers + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(View.GONE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(View.GONE); + } + // Hide the main view itself + mView.setVisibility(View.GONE); + return; + } + if (mMainWidgetViews != null && mMainWidgetsList != null) { for (int i = 0; i < mMainWidgetViews.length; i++) { if (mMainWidgetViews[i] != null) { @@ -394,6 +432,20 @@ private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { } private void updateContainerVisibility() { + // If widgets are disabled, forcefully hide everything + if (!mLockscreenWidgetsEnabled) { + final View mainWidgetsContainer = mView.findViewById(R.id.main_widgets_container); + if (mainWidgetsContainer != null) { + mainWidgetsContainer.setVisibility(View.GONE); + } + final View secondaryWidgetsContainer = mView.findViewById(R.id.secondary_widgets_container); + if (secondaryWidgetsContainer != null) { + secondaryWidgetsContainer.setVisibility(View.GONE); + } + mView.setVisibility(View.GONE); + return; + } + final boolean isMainWidgetsEmpty = mMainLockscreenWidgetsList == null || TextUtils.isEmpty(mMainLockscreenWidgetsList); final boolean isSecondaryWidgetsEmpty = mSecondaryLockscreenWidgetsList == null @@ -437,102 +489,128 @@ private boolean isNightMode() { } private void setUpWidgetWiews(LaunchableImageView iv, LaunchableFAB efab, String type) { + View.OnClickListener clickListener = null; + View.OnLongClickListener longClickListener = null; + int drawableRes = 0; + int stringRes = 0; + switch (type) { case "none": if (iv != null) iv.setVisibility(View.GONE); if (efab != null) efab.setVisibility(View.GONE); - break; + return; case "wifi": - if (iv != null) { - wifiButton = iv; - wifiButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - if (efab != null) { - wifiButtonFab = efab; - wifiButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleWiFi(), WIFI_INACTIVE, R.string.quick_settings_wifi_label); + clickListener = v -> toggleWiFi(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = WIFI_INACTIVE; + stringRes = R.string.quick_settings_wifi_label; + if (iv != null) wifiButton = iv; + if (efab != null) wifiButtonFab = efab; break; case "data": - if (iv != null) { - dataButton = iv; - dataButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - if (efab != null) { - dataButtonFab = efab; - dataButtonFab.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleMobileData(), DATA_INACTIVE, DATA_LABEL_INACTIVE); + clickListener = v -> toggleMobileData(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = DATA_INACTIVE; + stringRes = DATA_LABEL_INACTIVE; + if (iv != null) dataButton = iv; + if (efab != null) dataButtonFab = efab; break; case "ringer": + clickListener = v -> toggleRingerMode(); + drawableRes = RINGER_INACTIVE; + stringRes = RINGER_LABEL_INACTIVE; if (iv != null) ringerButton = iv; if (efab != null) ringerButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleRingerMode(), RINGER_INACTIVE, RINGER_LABEL_INACTIVE); break; case "bt": - if (iv != null) { - btButton = iv; - btButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - if (efab != null) { - btButtonFab = efab; - btButtonFab.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleBluetoothState(), BT_INACTIVE, BT_LABEL_INACTIVE); + clickListener = v -> toggleBluetoothState(); + longClickListener = v -> { + showBluetoothDialog(v); + return true; + }; + drawableRes = BT_INACTIVE; + stringRes = BT_LABEL_INACTIVE; + if (iv != null) btButton = iv; + if (efab != null) btButtonFab = efab; break; case "torch": + clickListener = v -> toggleFlashlight(); + drawableRes = TORCH_RES_INACTIVE; + stringRes = TORCH_LABEL_INACTIVE; if (iv != null) torchButton = iv; if (efab != null) torchButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleFlashlight(), TORCH_RES_INACTIVE, TORCH_LABEL_INACTIVE); break; case "timer": - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchTimer(), R.drawable.ic_alarm, R.string.clock_timer); + clickListener = v -> mActivityLauncherUtils.launchTimer(); + drawableRes = R.drawable.ic_alarm; + stringRes = R.string.clock_timer; break; case "calculator": - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchCalculator(), R.drawable.ic_calculator, R.string.calculator); + clickListener = v -> mActivityLauncherUtils.launchCalculator(); + drawableRes = R.drawable.ic_calculator; + stringRes = R.string.calculator; break; case "media": - if (iv != null) { - mediaButton = iv; - mediaButton.setOnLongClickListener(v -> { showMediaDialog(v); return true; }); - } + clickListener = v -> toggleMediaPlaybackState(); + longClickListener = v -> { + showMediaDialog(v); + return true; + }; + drawableRes = R.drawable.ic_media_play; + stringRes = R.string.controls_media_button_play; + if (iv != null) mediaButton = iv; if (efab != null) mediaButtonFab = efab; - setUpWidgetResources(iv, efab, v -> toggleMediaPlaybackState(), R.drawable.ic_media_play, R.string.controls_media_button_play); break; - case "weather": + case "weather": + clickListener = v -> mActivityLauncherUtils.launchWeatherApp(); + drawableRes = R.drawable.ic_weather; + stringRes = R.string.weather_data_unavailable; if (iv != null) weatherButton = iv; if (efab != null) weatherButtonFab = efab; - setUpWidgetResources(iv, efab, v -> mActivityLauncherUtils.launchWeatherApp(), R.drawable.ic_weather, R.string.weather_data_unavailable); enableWeatherUpdates(); break; case "hotspot": - if (iv != null) { - hotspotButton = iv; - hotspotButton.setOnLongClickListener(v -> { showBluetoothDialog(v); return true; }); - } - if (efab != null) { - hotspotButtonFab = efab; - hotspotButton.setOnLongClickListener(v -> { showInternetDialog(v); return true; }); - } - setUpWidgetResources(iv, efab, v -> toggleHotspot(), HOTSPOT_INACTIVE, HOTSPOT_LABEL); + clickListener = v -> toggleHotspot(); + longClickListener = v -> { + showInternetDialog(v); + return true; + }; + drawableRes = HOTSPOT_INACTIVE; + stringRes = HOTSPOT_LABEL; + if (iv != null) hotspotButton = iv; + if (efab != null) hotspotButtonFab = efab; break; - default: + case "wallet": + clickListener = v -> mActivityLauncherUtils.launchWalletApp(); + drawableRes = R.drawable.ic_wallet_lockscreen; + stringRes = R.string.google_wallet; break; + case "qrscanner": + clickListener = v -> mActivityLauncherUtils.launchQrScanner(); + drawableRes = R.drawable.ic_qr_code_scanner; + stringRes = R.string.qr_code_scanner_title; + break; + default: + return; } - } - private void setUpWidgetResources(LaunchableImageView iv, LaunchableFAB efab, - View.OnClickListener cl, int drawableRes, int stringRes){ if (efab != null) { - efab.setOnClickListener(cl); + efab.setOnClickListener(clickListener); efab.setIcon(mContext.getDrawable(drawableRes)); efab.setText(mContext.getResources().getString(stringRes)); - if (mediaButtonFab == efab) { - attachSwipeGesture(efab); - } + if (longClickListener != null) efab.setOnLongClickListener(longClickListener); + if (mediaButtonFab == efab) attachSwipeGesture(efab); } + if (iv != null) { - iv.setOnClickListener(cl); + iv.setOnClickListener(clickListener); + if (longClickListener != null) iv.setOnLongClickListener(longClickListener); iv.setImageResource(drawableRes); } } @@ -657,7 +735,7 @@ private void updateMediaPlaybackState() { setButtonActiveState(mediaButton, null, isPlaying); } if (mediaButtonFab != null) { - MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getMediaMetadata(); + MediaMetadata mMediaMetadata = mMediaSessionManagerHelper.getCurrentMediaMetadata(); String trackTitle = mMediaMetadata != null ? mMediaMetadata.getString(MediaMetadata.METADATA_KEY_TITLE) : ""; if (!TextUtils.isEmpty(trackTitle) && mLastTrackTitle != trackTitle) { mLastTrackTitle = trackTitle; @@ -672,9 +750,8 @@ private void updateMediaPlaybackState() { private void toggleFlashlight() { if (torchButton == null && torchButtonFab == null) return; try { - mCameraManager.setTorchMode(mCameraId, !isFlashOn); - isFlashOn = !isFlashOn; - updateTorchButtonState(); + boolean newState = !isFlashOn; + mFlashlightController.setFlashlight(newState); } catch (Exception e) {} } @@ -872,17 +949,19 @@ public void setIsAirplaneMode(@NonNull IconState icon) { updateMobileDataState(!icon.visible && isMobileDataEnabled()); } } - + + // FIX 2: addObserver now requires Context as first argument public void enableWeatherUpdates() { if (mWeatherClient != null) { - mWeatherClient.addObserver(this); + mWeatherClient.addObserver(mContext, this); queryAndUpdateWeather(); } } + // FIX 3: removeObserver now requires Context as first argument public void disableWeatherUpdates() { if (mWeatherClient != null) { - mWeatherClient.removeObserver(this); + mWeatherClient.removeObserver(mContext, this); } } @@ -905,8 +984,10 @@ public void updateSettings() { private void queryAndUpdateWeather() { try { - if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled()) return; - mWeatherClient.queryWeather(); + // FIX 4: isOmniJawsEnabled and queryWeather now require Context argument + // FIX 5: getWeatherConditionImage now requires Context as first argument + if (mWeatherClient == null || !mWeatherClient.isOmniJawsEnabled(mContext)) return; + mWeatherClient.queryWeather(mContext); mWeatherInfo = mWeatherClient.getWeatherInfo(); if (mWeatherInfo != null) { // OpenWeatherMap @@ -936,7 +1017,7 @@ private void queryAndUpdateWeather() { } formattedCondition = formattedConditionBuilder.toString().trim(); } - final Drawable d = mWeatherClient.getWeatherConditionImage(mWeatherInfo.conditionCode); + final Drawable d = mWeatherClient.getWeatherConditionImage(mContext, mWeatherInfo.conditionCode); if (weatherButtonFab != null) { weatherButtonFab.setIcon(d); weatherButtonFab.setText(mWeatherInfo.temp + mWeatherInfo.tempUnits + " \u2022 " + formattedCondition); @@ -952,9 +1033,9 @@ private void queryAndUpdateWeather() { private boolean isWidgetEnabled(String widget) { return (mMainLockscreenWidgetsList != null - && !mMainLockscreenWidgetsList.contains(widget)) + && mMainLockscreenWidgetsList.contains(widget)) || (mSecondaryLockscreenWidgetsList != null - && !mSecondaryLockscreenWidgetsList.contains(widget)); + && mSecondaryLockscreenWidgetsList.contains(widget)); } @Override @@ -1027,12 +1108,12 @@ private void updateHotspotState() { if (!isWidgetEnabled("hotspot")) return; if (hotspotButton == null && hotspotButtonFab == null) return; String hotspotString = mContext.getResources().getString(HOTSPOT_LABEL); - updateTileButtonState(hotspotButton, hotspotButtonFab, mIsHotspotEnabled, + updateTileButtonState(hotspotButton, hotspotButtonFab, mHotspotController.isHotspotEnabled(), HOTSPOT_ACTIVE, HOTSPOT_INACTIVE, hotspotString, hotspotString); } private void toggleHotspot() { - mHotspotController.setHotspotEnabled(!mIsHotspotEnabled); + mHotspotController.setHotspotEnabled(!mHotspotController.isHotspotEnabled()); updateHotspotState(); mHandler.postDelayed(() -> { updateHotspotState(); @@ -1042,7 +1123,6 @@ private void toggleHotspot() { private final class HotspotCallback implements HotspotController.Callback { @Override public void onHotspotChanged(boolean enabled, int numDevices) { - mIsHotspotEnabled = enabled; updateHotspotState(); } @Override From 7e7a23f6a3780501edb67771e49dbc7688cc37e0 Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Fri, 3 Jan 2025 13:44:50 +0800 Subject: [PATCH 110/190] SystemUI: Introduce MediaSessionManagerHelper Signed-off-by: Dmitrii Signed-off-by: Ghosuto --- .../util/MediaSessionManagerHelper.kt | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt diff --git a/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt new file mode 100644 index 000000000000..1e416e40a017 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/MediaSessionManagerHelper.kt @@ -0,0 +1,263 @@ +/* +* Copyright (C) 2023-2024 The risingOS Android Project +* 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.util + +import android.content.Context +import android.content.res.Configuration +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionLegacyHelper +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.SystemClock +import android.provider.Settings +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +class MediaSessionManagerHelper private constructor(private val context: Context) { + + interface MediaMetadataListener { + fun onMediaMetadataChanged() {} + fun onPlaybackStateChanged() {} + } + + private val _mediaMetadata = MutableStateFlow(null) + val mediaMetadata: StateFlow = _mediaMetadata + + private val _playbackState = MutableStateFlow(null) + val playbackState: StateFlow = _playbackState + + private val scope = CoroutineScope(Dispatchers.Main) + private var collectJob: Job? = null + + private var lastSavedPackageName: String? = null + private val mediaSessionManager: MediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! + private var activeController: MediaController? = null + private val listeners = mutableSetOf() + + private val mediaControllerCallback = object : MediaController.Callback() { + override fun onMetadataChanged(metadata: MediaMetadata?) { + _mediaMetadata.value = metadata + } + + override fun onPlaybackStateChanged(state: PlaybackState?) { + _playbackState.value = state + } + } + + private val tickerFlow = flow { + while (true) { + emit(Unit) + delay(1000) + } + }.flowOn(Dispatchers.Default) + + init { + lastSavedPackageName = Settings.System.getString( + context.contentResolver, + "media_session_last_package_name" + ) + + scope.launch { + tickerFlow + .map { fetchActiveController() } + .distinctUntilChanged { old, new -> sameSessions(old, new) } + .collect { controller -> + activeController?.unregisterCallback(mediaControllerCallback) + activeController = controller + controller?.registerCallback(mediaControllerCallback) + _mediaMetadata.value = controller?.metadata + _playbackState.value = controller?.playbackState + saveLastNonNullPackageName() + } + } + } + + private suspend fun fetchActiveController(): MediaController? = withContext(Dispatchers.IO) { + var localController: MediaController? = null + val remoteSessions = mutableSetOf() + + mediaSessionManager.getActiveSessions(null) + .filter { controller -> + controller.playbackState?.state == PlaybackState.STATE_PLAYING && + controller.playbackInfo != null + } + .forEach { controller -> + when (controller.playbackInfo?.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + remoteSessions.add(controller.packageName) + if (localController?.packageName == controller.packageName) { + localController = null + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (!remoteSessions.contains(controller.packageName)) { + localController = localController ?: controller + } + } + } + } + localController + } + + fun addMediaMetadataListener(listener: MediaMetadataListener) { + listeners.add(listener) + if (listeners.size == 1) { + startCollecting() + } + listener.onMediaMetadataChanged() + listener.onPlaybackStateChanged() + } + + fun removeMediaMetadataListener(listener: MediaMetadataListener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + stopCollecting() + } + } + + private fun startCollecting() { + collectJob = scope.launch { + launch { mediaMetadata.collect { notifyListeners { onMediaMetadataChanged() } } } + launch { playbackState.collect { notifyListeners { onPlaybackStateChanged() } } } + } + } + + private fun stopCollecting() { + collectJob?.cancel() + collectJob = null + } + + private fun notifyListeners(action: MediaMetadataListener.() -> Unit) { + listeners.forEach { it.action() } + } + + fun seekTo(time: Long) { + activeController?.transportControls?.seekTo(time) + } + + fun getTotalDuration() = mediaMetadata.value?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + private fun saveLastNonNullPackageName() { + activeController?.packageName?.takeIf { it.isNotEmpty() }?.let { pkg -> + if (pkg != lastSavedPackageName) { + Settings.System.putString( + context.contentResolver, + "media_session_last_package_name", + pkg + ) + lastSavedPackageName = pkg + } + } + } + + fun getMediaBitmap(): Bitmap? = mediaMetadata.value?.let { + it.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + } + + fun getCurrentMediaMetadata(): MediaMetadata? { + return mediaMetadata.value + } + + fun getMediaAppIcon(): Drawable? { + val packageName = activeController?.packageName ?: return null + return try { + val pm = context.packageManager + pm.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun isMediaControllerAvailable() = activeController?.packageName?.isNotEmpty() ?: false + + fun isMediaPlaying() = playbackState.value?.state == PlaybackState.STATE_PLAYING + + fun getMediaControllerPlaybackState(): PlaybackState? { + return activeController?.playbackState ?: null + } + + private fun sameSessions(a: MediaController?, b: MediaController?): Boolean { + if (a == b) return true + if (a == null) return false + return a.controlsSameSession(b) + } + + private fun dispatchMediaKeyWithWakeLockToMediaSession(keycode: Int) { + val helper = MediaSessionLegacyHelper.getHelper(context) ?: return + var event = KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keycode, + 0 + ) + helper.sendMediaButtonEvent(event, true) + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP) + helper.sendMediaButtonEvent(event, true) + } + + fun prevSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS) + } + + fun nextSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT) + } + + fun toggleMediaPlaybackState() { + if (isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE) + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY) + } + } + + fun launchMediaApp() { + lastSavedPackageName?.takeIf { it.isNotEmpty() }?.let { + launchMediaPlayerApp(it) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { intent -> + context.startActivity(intent) + } + } + } + + companion object { + @Volatile + private var instance: MediaSessionManagerHelper? = null + + fun getInstance(context: Context): MediaSessionManagerHelper = + instance ?: synchronized(this) { + instance ?: MediaSessionManagerHelper(context).also { instance = it } + } + } +} From 80f683c764ba0c4bf2ce578d058da3248c4ffa8e Mon Sep 17 00:00:00 2001 From: minaripenguin Date: Thu, 17 Oct 2024 16:53:46 +0800 Subject: [PATCH 111/190] SystemUI: Introduce AOD styles [1/2] Signed-off-by: minaripenguin Signed-off-by: Ghosuto --- .../layout/keyguard_aod_style.xml | 21 ++ .../com/android/systemui/clocks/AODStyle.java | 221 ++++++++++++++++++ 2 files changed, 242 insertions(+) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml b/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml new file mode 100644 index 000000000000..659f966e7147 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_aod_style.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java new file mode 100644 index 000000000000..275073c604d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AODStyle.java @@ -0,0 +1,221 @@ +/* + * Copyright (C) 2023-2024 the 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.clocks; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import com.android.settingslib.drawable.CircleFramedDrawable; + +import com.android.systemui.res.R; +import com.android.systemui.Dependency; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.tuner.TunerService; + +public class AODStyle extends RelativeLayout implements TunerService.Tunable { + + private static final String CUSTOM_AOD_IMAGE_URI_KEY = "system:custom_aod_image_uri"; + private static final String CUSTOM_AOD_IMAGE_ENABLED_KEY = "system:custom_aod_image_enabled"; + + private final Context mContext; + private final TunerService mTunerService; + + private final StatusBarStateController mStatusBarStateController; + + private boolean mDozing; + + private ImageView mAodImageView; + private String mImagePath; + private String mCurrImagePath; + private boolean mAodImageEnabled; + private boolean mImageLoaded = false; + private boolean mCustomClockEnabled; + + // Burn-in protection + private static final int BURN_IN_PROTECTION_INTERVAL = 10000; // 10 seconds + private static final int BURN_IN_PROTECTION_MAX_SHIFT = 4; // 4 pixels + private final Handler mBurnInProtectionHandler = new Handler(); + private int mCurrentShiftX = 0; + private int mCurrentShiftY = 0; + + private final Runnable mBurnInProtectionRunnable = new Runnable() { + @Override + public void run() { + if (mDozing) { + mCurrentShiftX = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + mCurrentShiftY = (int) (Math.random() * BURN_IN_PROTECTION_MAX_SHIFT * 2) - BURN_IN_PROTECTION_MAX_SHIFT; + if (mAodImageView != null) { + mAodImageView.setTranslationX(mCurrentShiftX); + mAodImageView.setTranslationY(mCurrentShiftY); + } + invalidate(); + mBurnInProtectionHandler.postDelayed(this, BURN_IN_PROTECTION_INTERVAL); + } + } + }; + + private final StatusBarStateController.StateListener mStatusBarStateListener = + new StatusBarStateController.StateListener() { + @Override + public void onStateChanged(int newState) {} + + @Override + public void onDozingChanged(boolean dozing) { + if (mDozing == dozing) { + return; + } + mDozing = dozing; + updateAodImageView(); + if (mDozing) { + startBurnInProtection(); + } else { + stopBurnInProtection(); + } + } + }; + + public AODStyle(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mTunerService = Dependency.get(TunerService.class); + mTunerService.addTunable(this, ClockStyle.CLOCK_STYLE_KEY, CUSTOM_AOD_IMAGE_URI_KEY, CUSTOM_AOD_IMAGE_ENABLED_KEY); + mStatusBarStateController = Dependency.get(StatusBarStateController.class); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mAodImageView = findViewById(R.id.custom_aod_image_view); + loadAodImage(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.animate().cancel(); + mAodImageView.setImageDrawable(null); + } + } + + private void startBurnInProtection() { + mBurnInProtectionHandler.post(mBurnInProtectionRunnable); + } + + private void stopBurnInProtection() { + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + if (mAodImageView != null) { + mAodImageView.setTranslationX(0); + mAodImageView.setTranslationY(0); + } + } + + @Override + public void onTuningChanged(String key, String newValue) { + switch (key) { + case ClockStyle.CLOCK_STYLE_KEY: + int clockStyle = TunerService.parseInteger(newValue, 0); + mCustomClockEnabled = clockStyle != 0; + break; + case CUSTOM_AOD_IMAGE_URI_KEY: + mImagePath = newValue; + if (mImagePath != null && !mImagePath.isEmpty() + && !mImagePath.equals(mCurrImagePath)) { + mCurrImagePath = mImagePath; + mImageLoaded = false; + loadAodImage(); + } + break; + case CUSTOM_AOD_IMAGE_ENABLED_KEY: + mAodImageEnabled = TunerService.parseIntegerSwitch( + newValue, false) && mCustomClockEnabled; + break; + } + } + + private void updateAodImageView() { + if (mAodImageView == null || !mAodImageEnabled) { + if (mAodImageView != null) mAodImageView.setVisibility(View.GONE); + return; + } + loadAodImage(); + if (mDozing) { + mAodImageView.setVisibility(View.VISIBLE); + mAodImageView.setScaleX(0f); + mAodImageView.setScaleY(0f); + mAodImageView.animate() + .scaleX(1f) + .scaleY(1f) + .setDuration(500) + .withEndAction(this::startBurnInProtection) + .start(); + } else { + mAodImageView.animate() + .scaleX(0f) + .scaleY(0f) + .setDuration(250) + .withEndAction(() -> { + mAodImageView.setVisibility(View.GONE); + stopBurnInProtection(); + }) + .start(); + } + } + + private void loadAodImage() { + if (mAodImageView == null || mCurrImagePath == null || mCurrImagePath.isEmpty() || mImageLoaded) return; + Bitmap bitmap = null; + try { + bitmap = BitmapFactory.decodeFile(mCurrImagePath); + if (bitmap != null) { + int targetSize = (int) mContext.getResources().getDimension(R.dimen.custom_aod_image_size); + Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, targetSize, targetSize, true); + try (java.io.ByteArrayOutputStream stream = new java.io.ByteArrayOutputStream()) { + scaledBitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, 90, stream); + byte[] byteArray = stream.toByteArray(); + Bitmap compressedBitmap = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length); + Drawable roundedImg = new CircleFramedDrawable(compressedBitmap, targetSize); + mAodImageView.setImageDrawable(roundedImg); + scaledBitmap.recycle(); + compressedBitmap.recycle(); + mImageLoaded = true; + } + } else { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } + } catch (Exception e) { + mImageLoaded = false; + mAodImageView.setVisibility(View.GONE); + } finally { + if (bitmap != null) { + bitmap.recycle(); + } + } + } +} From 1fd7540ed233404496138ccafbf49fe988943e8c Mon Sep 17 00:00:00 2001 From: Arman-ATI Date: Tue, 24 Feb 2026 16:11:16 +0000 Subject: [PATCH 112/190] SystemUI: Adapt lockscreen features to the latest A16 keyguard changes Arman-ATI: * Added back dummy keyguard_clock_switch that does nothing (only a pleace holder) * Removed the flags that we were using to exclude these changes (now you are forced to use the blueprint workaround) This is a complete rewrite of the following widgets to work with the latest changes android made * AODStyles * ClockStyles * Omni Weather * Info Widgets * Lockscreen Widgets TODO: add translucent widget options, add peek display height, fix the notification approach, AOD Signed-off-by: Ghosuto --- packages/SystemUI/AndroidManifest.xml | 2 + .../clocks/common/res/values/dimens.xml | 1 + .../layout/keyguard_clock_switch.xml | 92 ++++++ .../layout/keyguard_clock_widgets.xml | 2 +- .../src/com/android/systemui/Dependency.java | 2 +- .../blueprints/DefaultKeyguardBlueprint.kt | 13 + .../blueprints/SplitShadeKeyguardBlueprint.kt | 9 + .../view/layout/sections/AODStyleSection.kt | 178 ++++++++++++ .../layout/sections/InfoWidgetsSection.kt | 139 +++++++++ .../sections/KeyguardClockStyleSection.kt | 135 +++++++++ .../sections/KeyguardWeatherViewSection.kt | 192 ++++++++---- .../sections/KeyguardWidgetViewSection.kt | 273 ++++++++++++++++++ 12 files changed, 987 insertions(+), 51 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt create mode 100644 packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 42bfc7b2b604..772bcc519ea7 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -247,6 +247,8 @@ + + diff --git a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml index 8b1edd8506ea..4c81e51b3db6 100644 --- a/packages/SystemUI/customization/clocks/common/res/values/dimens.xml +++ b/packages/SystemUI/customization/clocks/common/res/values/dimens.xml @@ -33,6 +33,7 @@ 114dp 28dp 28dp + 100dp 28dp 8dp diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml new file mode 100644 index 000000000000..797f28c81783 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml index 1542bb40aed8..baab1b46d4a3 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_widgets.xml @@ -2,7 +2,7 @@ (R.id.aod_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + // Inflate the AOD style layout + aodStyleView = LayoutInflater.from(context).inflate( + R.layout.keyguard_aod_style, + constraintLayout, + false + ).apply { + id = R.id.aod_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(aodStyleView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // The AODStyle component handles its own data binding + // through its TunerService integration and StatusBarStateController callbacks + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + constraintSet.apply { + // Position AOD style within the keyguard_status_area + connect( + R.id.aod_ls, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.aod_ls, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Position at the top of status area with proper margin to avoid status bar overlap + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect( + R.id.aod_ls, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + topMargin + ) + + // Set dimensions + constrainHeight(R.id.aod_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.aod_ls, ConstraintSet.MATCH_CONSTRAINT) + + // Set appropriate margins matching the original XML structure + setMargin(R.id.aod_ls, ConstraintSet.START, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + setMargin(R.id.aod_ls, ConstraintSet.END, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start)) + + // Ensure proper layering within the status area + setElevation(R.id.aod_ls, 2f) // Higher elevation than info widgets + + // Update other elements to position below AOD style when it's visible + if (constraintSet.getConstraint(R.id.lockscreen_clock_view) != null) { + connect( + R.id.lockscreen_clock_view, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) + } + + // Apply consistent top margin to clock_ls whether AOD is visible or not + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + // If AOD is present, position clock below it + // If AOD is hidden/gone, ensure clock still has proper top margin + val clockTopMargin = if (aodStyleView?.visibility == View.VISIBLE) { + 8 // Small gap below AOD + } else { + topMargin // Same margin as AOD would have had + } + + if (aodStyleView?.visibility == View.VISIBLE) { + connect( + R.id.clock_ls, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + clockTopMargin + ) + } else { + // When AOD is hidden, position clock at top with proper margin + connect( + R.id.clock_ls, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + clockTopMargin + ) + } + } + + // Update the barrier to include AOD style for proper notification positioning + // This ensures notifications appear below all status area content including AOD + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf( + R.id.aod_ls, + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets + ) + ) + + // Ensure notification icons are positioned below the barrier + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + aodStyleView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + aodStyleView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt new file mode 100644 index 000000000000..ef20ce8897a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/InfoWidgetsSection.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2025 the RisingOS Revived 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.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject + +class InfoWidgetsSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var infoWidgetsView: View? = null + + override fun addViews(constraintLayout: ConstraintLayout) { + + constraintLayout.findViewById(R.id.keyguard_info_widgets)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + infoWidgetsView = LayoutInflater.from(context).inflate( + R.layout.keyguard_info_widgets, + constraintLayout, + false + ).apply { + id = R.id.keyguard_info_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + } + + constraintLayout.addView(infoWidgetsView) + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // ProgressImageView components handle their own data binding + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + + constraintSet.apply { + // Info widgets positioning - below WEATHER (3rd in hierarchy) + connect(R.id.keyguard_info_widgets, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.keyguard_info_widgets, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to weather (primary) or fallback hierarchy + when { + constraintSet.getConstraint(R.id.keyguard_weather) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_weather, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.default_weather_image) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.clock_ls) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 12) + } + constraintSet.getConstraint(R.id.keyguard_slice_view) != null -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 12) + } + else -> { + connect(R.id.keyguard_info_widgets, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 12) + } + } + + constrainHeight(R.id.keyguard_info_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_info_widgets, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_info_widgets, ConstraintSet.END, 0) + // Add small bottom margin for AOD to prevent notification overlap + setMargin(R.id.keyguard_info_widgets, ConstraintSet.BOTTOM, 6) // 6dp bottom margin for AOD + setElevation(R.id.keyguard_info_widgets, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + 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, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + infoWidgetsView?.let { view -> + (view.parent as? ViewGroup)?.removeView(view) + } + infoWidgetsView = null + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt new file mode 100644 index 000000000000..8402a9066dc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2025 the RisingOS Revived 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.os.UserHandle +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.clocks.ClockStyle +import com.android.systemui.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import com.android.systemui.util.settings.SecureSettings +import javax.inject.Inject + +class KeyguardClockStyleSection +@Inject +constructor( + private val context: Context, + private val secureSettings: SecureSettings, +) : KeyguardSection() { + + private var clockStyleView: ClockStyle? = null + private var isCustomClockEnabled: Boolean = false + + override fun addViews(constraintLayout: ConstraintLayout) { + + val clockStyle = secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) + isCustomClockEnabled = clockStyle != 0 + + if (!isCustomClockEnabled) return + + constraintLayout.findViewById(R.id.clock_ls)?.let { existingView -> + (existingView.parent as? ViewGroup)?.removeView(existingView) + } + + val inflater = android.view.LayoutInflater.from(context) + clockStyleView = inflater.inflate(R.layout.keyguard_clock_style, null) as ClockStyle + clockStyleView?.apply { + id = R.id.clock_ls + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + clockStyleView?.let { constraintLayout.addView(it) } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + clockView.onTimeChanged() + clockView.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + if (!isCustomClockEnabled) return + + constraintSet.apply { + // Clock positioning - TOP of hierarchy + connect(R.id.clock_ls, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START) + connect(R.id.clock_ls, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + val topMargin = (context.resources.getDimensionPixelSize(R.dimen.status_bar_height) * 1.25f).toInt() + connect(R.id.clock_ls, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP, topMargin) + + constrainHeight(R.id.clock_ls, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.clock_ls, ConstraintSet.MATCH_CONSTRAINT) + setMargin(R.id.clock_ls, ConstraintSet.START, 0) + setMargin(R.id.clock_ls, ConstraintSet.END, 0) + setElevation(R.id.clock_ls, 1f) + + // UNIFIED BARRIER - Create barrier in every section that could be last + 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, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) + ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + clockStyleView?.let { clockView -> + (clockView.parent as? ViewGroup)?.removeView(clockView) + } + clockStyleView = null + } +} 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 index f0817a139680..07ee85aadd9e 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024-2026 crDroid Android Project + * Copyright (C) 2025 the RisingOS Revived Android Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -12,91 +12,185 @@ * 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.customization.R as custR import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R -import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController +import com.android.systemui.weather.WeatherImageView +import com.android.systemui.weather.WeatherTextView import javax.inject.Inject -import com.android.systemui.weather.WeatherInfoView - -class KeyguardWeatherViewSection -@Inject -constructor( +class KeyguardWeatherViewSection @Inject constructor( private val context: Context, - val layoutInflater: LayoutInflater, - val smartspaceController: LockscreenSmartspaceController, ) : KeyguardSection() { - private lateinit var weatherView: WeatherInfoView + + private var weatherImageView: WeatherImageView? = null + private var weatherTextView: WeatherTextView? = null 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) + val weatherContainer = constraintLayout.findViewById(R.id.keyguard_weather) + + if (weatherContainer != null) { + weatherImageView = weatherContainer.findViewById(R.id.default_weather_image) + weatherTextView = weatherContainer.findViewById(R.id.default_weather_text) + + (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) + constraintLayout.addView(weatherContainer) + } else { + createWeatherViews(constraintLayout) + } + + initializeWeatherViews() } - override fun bindData(constraintLayout: ConstraintLayout) { - if (!smartspaceController.isOmniWeatherEnabled || smartspaceController.isEnabled) return + private fun createWeatherViews(constraintLayout: ConstraintLayout) { + weatherImageView = WeatherImageView(context).apply { + id = R.id.default_weather_image + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.GONE + } + + weatherTextView = WeatherTextView(context).apply { + id = R.id.default_weather_text + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.WRAP_CONTENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + setTextColor(context.getColor(android.R.color.white)) + textSize = 20f + visibility = View.GONE + } - weatherView.init() + weatherImageView?.let { constraintLayout.addView(it) } + weatherTextView?.let { constraintLayout.addView(it) } + } + + private fun initializeWeatherViews() { + // Weather views initialize automatically when attached to window + } + + override fun bindData(constraintLayout: ConstraintLayout) { + // Weather data binding handled by individual weather views } 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) + val startMargin = context.resources.getDimensionPixelSize(custR.dimen.clock_padding_start) + + context.resources.getDimensionPixelSize(custR.dimen.status_view_margin_horizontal) - connect( - R.id.keyguard_weather_area, - ConstraintSet.TOP, - R.id.keyguard_slice_view, - ConstraintSet.BOTTOM - ) + // Weather positioning - below CLOCK (2nd in hierarchy) + if (constraintSet.getConstraint(R.id.keyguard_weather) != null) { + connect(R.id.keyguard_weather, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + connect(R.id.keyguard_weather, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + + // Chain to clock (primary) or fallback to slice_view + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.keyguard_weather, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.keyguard_weather, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_weather, ConstraintSet.MATCH_CONSTRAINT) + } else { + applyWeatherImageConstraints(constraintSet, startMargin) + applyWeatherTextConstraints(constraintSet) + } + + // UNIFIED BARRIER - Create barrier in every section that could be last + createUnifiedBarrierAndNotificationConstraints(constraintSet) + } + } + private fun applyWeatherImageConstraints(constraintSet: ConstraintSet, startMargin: Int) { + if (constraintSet.getConstraint(R.id.default_weather_image) != null) { + constraintSet.apply { + connect(R.id.default_weather_image, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, startMargin) + + if (constraintSet.getConstraint(R.id.clock_ls) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.clock_ls, ConstraintSet.BOTTOM, 8) + } else if (constraintSet.getConstraint(R.id.keyguard_slice_view) != null) { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.keyguard_slice_view, ConstraintSet.BOTTOM, 8) + } else { + connect(R.id.default_weather_image, ConstraintSet.TOP, R.id.lockscreen_clock_view, ConstraintSet.BOTTOM, 8) + } + + constrainHeight(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_image, ConstraintSet.WRAP_CONTENT) + } + } + } + + private fun applyWeatherTextConstraints(constraintSet: ConstraintSet) { + if (constraintSet.getConstraint(R.id.default_weather_text) != null) { + constraintSet.apply { + connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, + context.resources.getDimensionPixelSize(R.dimen.weather_text_margin_start)) + connect(R.id.default_weather_text, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.TOP) + connect(R.id.default_weather_text, ConstraintSet.BOTTOM, R.id.default_weather_image, ConstraintSet.BOTTOM) + connect(R.id.default_weather_text, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) + constrainHeight(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) + } + } + } + + 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_weather_area) + *intArrayOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.default_weather_image, + R.id.default_weather_text, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets, + R.id.lockscreen_clock_view // Include fallback clock + ) ) + + // Position notifications below ALL status area content + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + ) + } } } 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) + constraintLayout.findViewById(R.id.keyguard_weather)?.let { weatherContainer -> + constraintLayout.removeView(weatherContainer) } + + weatherImageView?.let { constraintLayout.removeView(it) } + weatherTextView?.let { constraintLayout.removeView(it) } + + weatherImageView = null + weatherTextView = null } } 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 000000000000..0e26d298d258 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardWidgetViewSection.kt @@ -0,0 +1,273 @@ +/* + * Copyright (C) 2025 the RisingOS Revived 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.util.Log +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.keyguard.shared.model.KeyguardSection +import com.android.systemui.res.R +import javax.inject.Inject +import com.android.systemui.lockscreen.LockScreenWidgets + +class KeyguardWidgetViewSection +@Inject +constructor( + private val context: Context, +) : KeyguardSection() { + + private var widgetView: LockScreenWidgets? = null + private val TAG = "KeyguardWidgetViewSection" + + private fun createWidgetView(): LockScreenWidgets? { + Log.d(TAG, "Creating LockScreenWidgets view") + return try { + val layoutInflater = android.view.LayoutInflater.from(context) + // Try to inflate from your actual layout file + val view = layoutInflater.inflate(R.layout.keyguard_clock_widgets, null) as LockScreenWidgets + + view.apply { + // Override the ID to match what the keyguard system expects + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + + Log.d(TAG, "Successfully inflated LockScreenWidgets from keyguard_clock_widgets layout") + view + } catch (e: Exception) { + Log.e(TAG, "Failed to inflate from keyguard_clock_widgets, trying direct instantiation", e) + try { + // Create a dummy AttributeSet for direct instantiation + val parser = context.resources.getLayout(android.R.layout.simple_list_item_1) + val attrs = android.util.Xml.asAttributeSet(parser) + + LockScreenWidgets(context, attrs).apply { + id = R.id.keyguard_widgets + layoutParams = ConstraintLayout.LayoutParams( + ConstraintLayout.LayoutParams.MATCH_PARENT, + ConstraintLayout.LayoutParams.WRAP_CONTENT + ) + visibility = View.VISIBLE + } + } catch (e2: Exception) { + Log.e(TAG, "Failed to create LockScreenWidgets directly", e2) + null + } + } + } + + override fun addViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "addViews called") + + // Check if the widget view already exists in the layout + val existingView = constraintLayout.findViewById(R.id.keyguard_widgets) + + if (existingView != null) { + Log.d(TAG, "Found existing widget view") + widgetView = existingView as? LockScreenWidgets + return + } + + // Check if we already have a widget view instance + if (widgetView != null) { + Log.d(TAG, "Reusing existing widget view instance") + // Remove from any previous parent + (widgetView?.parent as? ViewGroup)?.removeView(widgetView) + } else { + Log.d(TAG, "Creating new widget view") + widgetView = createWidgetView() + } + + widgetView?.let { view -> + try { + constraintLayout.addView(view) + Log.d(TAG, "Successfully added widget view to constraint layout") + } catch (e: Exception) { + Log.e(TAG, "Failed to add widget view", e) + } + } + } + + override fun bindData(constraintLayout: ConstraintLayout) { + Log.d(TAG, "bindData called") + // Ensure the widget view is properly initialized and visible + widgetView?.let { view -> + if (view.visibility != View.VISIBLE) { + view.visibility = View.VISIBLE + Log.d(TAG, "Set widget view visibility to VISIBLE") + } + + // Force a layout pass to ensure the view is measured and laid out + view.requestLayout() + } + } + + override fun applyConstraints(constraintSet: ConstraintSet) { + Log.d(TAG, "applyConstraints called") + + // Only apply constraints if the widget view exists + widgetView ?: run { + Log.w(TAG, "Widget view is null, skipping constraints") + return + } + + try { + constraintSet.apply { + // Position widgets within the keyguard layout + connect( + R.id.keyguard_widgets, + ConstraintSet.START, + ConstraintSet.PARENT_ID, + ConstraintSet.START + ) + connect( + R.id.keyguard_widgets, + ConstraintSet.END, + ConstraintSet.PARENT_ID, + ConstraintSet.END + ) + + // Try to position below other status content with priority order + val anchorViews = listOf( + R.id.keyguard_info_widgets, + R.id.clock_ls, + R.id.keyguard_weather, + R.id.keyguard_slice_view, + R.id.lockscreen_clock_view + ) + + var positioned = false + for (anchorId in anchorViews) { + try { + // Check if the constraint exists by trying to get it + constraintSet.getConstraint(anchorId) + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + anchorId, + ConstraintSet.BOTTOM, + 8 // 8dp margin + ) + positioned = true + Log.d(TAG, "Positioned widgets below anchor: ${context.resources.getResourceEntryName(anchorId)}") + break + } catch (e: Exception) { + // Continue to next anchor if this one fails + continue + } + } + + // If no anchor found, position at top with margin + if (!positioned) { + connect( + R.id.keyguard_widgets, + ConstraintSet.TOP, + ConstraintSet.PARENT_ID, + ConstraintSet.TOP, + 16 // 16dp margin from top + ) + Log.d(TAG, "No anchor found, positioned at top") + } + + // Set dimensions + constrainHeight(R.id.keyguard_widgets, ConstraintSet.WRAP_CONTENT) + constrainWidth(R.id.keyguard_widgets, ConstraintSet.MATCH_CONSTRAINT) + + // Set margins + setMargin(R.id.keyguard_widgets, ConstraintSet.START, 0) + setMargin(R.id.keyguard_widgets, ConstraintSet.END, 0) + + // Set elevation for proper layering + setElevation(R.id.keyguard_widgets, 2f) + + // Update barrier to include widgets + try { + val barrierViews = mutableListOf() + val potentialBarrierViews = listOf( + R.id.keyguard_slice_view, + R.id.keyguard_weather, + R.id.clock_ls, + R.id.keyguard_info_widgets, + R.id.keyguard_widgets + ) + + potentialBarrierViews.forEach { viewId -> + try { + constraintSet.getConstraint(viewId) + barrierViews.add(viewId) + } catch (e: Exception) { + // Skip this view if it doesn't exist + } + } + + if (barrierViews.isNotEmpty()) { + createBarrier( + R.id.smart_space_barrier_bottom, + Barrier.BOTTOM, + 0, + *barrierViews.toIntArray() + ) + Log.d(TAG, "Created barrier with ${barrierViews.size} views") + } + + // Position notification icons below barrier + try { + constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) + connect( + R.id.left_aligned_notification_icon_container, + ConstraintSet.TOP, + R.id.smart_space_barrier_bottom, + ConstraintSet.BOTTOM, + try { + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start_icons) + } catch (e: Exception) { + 8 // fallback margin + } + ) + } catch (e: Exception) { + // Notification container doesn't exist, skip + } + } catch (e: Exception) { + Log.w(TAG, "Failed to create barrier", e) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to apply constraints", e) + } + } + + override fun removeViews(constraintLayout: ConstraintLayout) { + Log.d(TAG, "removeViews called") + widgetView?.let { view -> + try { + constraintLayout.removeView(view) + Log.d(TAG, "Successfully removed widget view") + } catch (e: Exception) { + Log.w(TAG, "Failed to remove widget view", e) + } + } + widgetView = null + } +} From 92336f21b6b0144d7a70de9a12072169d284fab6 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 9 Nov 2024 13:09:45 +0000 Subject: [PATCH 113/190] SystemUI: Added Label clock style Signed-off-by: Ghosuto --- .../layout/keyguard_clock_label.xml | 99 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 3 +- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml new file mode 100644 index 000000000000..958d1930b3fc --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_label.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 47b6c97f3c97..6fb208fa99ab 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -46,7 +46,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_simple, R.layout.keyguard_clock_miui, R.layout.keyguard_clock_ide, - R.layout.keyguard_clock_moto + R.layout.keyguard_clock_moto, + R.layout.keyguard_clock_label }; private final static int[] mCenterClocks = {2, 3, 5, 6}; From aa6e72a8ec851fb38e96b9045845bc39a3d3671b Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 06:42:50 +0000 Subject: [PATCH 114/190] SystemUI: Add ios like clock Signed-off-by: Ghosuto --- .../layout/keyguard_clock_ios.xml | 45 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 ++- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml new file mode 100644 index 000000000000..e9b816442596 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 6fb208fa99ab..e492c51092c4 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -47,10 +47,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_miui, R.layout.keyguard_clock_ide, R.layout.keyguard_clock_moto, - R.layout.keyguard_clock_label + R.layout.keyguard_clock_label, + R.layout.keyguard_clock_ios }; - private final static int[] mCenterClocks = {2, 3, 5, 6}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From e336edcc0e778a8b02ab6b6498fcc1f45da5c0ac Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 10 Nov 2024 05:11:11 +0000 Subject: [PATCH 115/190] SystemUI: Add number clockface Signed-off-by: Ghosuto --- .../layout/keyguard_clock_num.xml | 87 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 2 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml new file mode 100644 index 000000000000..550e5643d170 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_num.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index e492c51092c4..20e47b6e79d3 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -48,10 +48,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_ide, R.layout.keyguard_clock_moto, R.layout.keyguard_clock_label, - R.layout.keyguard_clock_ios + R.layout.keyguard_clock_ios, + R.layout.keyguard_clock_num }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 61c1a51de9ea98a9dbc5e82e47e9c3457696cc56 Mon Sep 17 00:00:00 2001 From: DrDisagree Date: Sun, 10 Nov 2024 07:19:51 +0000 Subject: [PATCH 116/190] SystemUI: Add 3 new clock face from iconify Signed-off-by: Ghosuto --- .../res-keyguard/font/astthillabeltaden.otf | Bin 0 -> 214632 bytes .../res-keyguard/font/montserrat_bold.ttf | Bin 0 -> 29560 bytes .../layout/keyguard_clock_accent.xml | 86 ++++++++++++++++++ .../layout/keyguard_clock_mont.xml | 66 ++++++++++++++ .../layout/keyguard_clock_taden.xml | 52 +++++++++++ .../android/systemui/clocks/ClockStyle.java | 7 +- 6 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/astthillabeltaden.otf create mode 100644 packages/SystemUI/res-keyguard/font/montserrat_bold.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml diff --git a/packages/SystemUI/res-keyguard/font/astthillabeltaden.otf b/packages/SystemUI/res-keyguard/font/astthillabeltaden.otf new file mode 100644 index 0000000000000000000000000000000000000000..63a9380b7d5efe1f1df3889073fb197bfae1b7c9 GIT binary patch literal 214632 zcmeFab#xWWx9?lsEAB!B2_#5xcXxLQZUF)TLIO!}hmC9;Htud4cXxMp3lJj2hf_anE>XXRPMCdRAA>n)0n#y?U+PzDt+(!bVIJ2GO8#vu2*H3wA0X zgw1AQsJgFthxVO2PHoXq7#g(^LNC|6vrnT_@>H%cG?^rXsL;N1`AS2tnED8zRp5R9 zfg^lJ9>_krO$d`RLYS`i_wyZ;uVGpMvU|L$`E$Zzfx(jYCcH1|KO%IT)x*ppLRd!9 z|EUqa<3@@D9S~_HIGBy_3-!ITEE{ zDKw6}X1H~{+|RyFiZB@HSBSTvJs$l2H#c0?HSJ?K!h3_zjaL}2(KBTY^nRBcp4-f9 z@2~UfqM!dA5CXk83KMM(qnk4RJpC;17ySHJ`zR`E`mff;f2Ijx4zT)tt~>vU210lh z{y7#pTt#NoU%S9Rp8TmE9K?-(>E*Y3NAPNe_|T=7*#W|Q0up^%7EV>9E(N%O4-9-=4Q}hzOMIX^u^g|C{ zF+dD7`fxD*Qyw0M-2%i2Y#by;icvRlMfHYWAQ{h70<+T@j|>3uf-ejR=gAM#Ygc; zgo)4Mi})(S#WxWlB1M#l7T-mTh!t@nUi=_$l0>pd5vd|wWQa_WC9*}1kXk3LGZyvL z#%WVL-97Vq7Vz}&+~-wTO6>dJ7XCkLVNIsXRQdJ4^xsu^g}=-4f;`LL-v8upkNoFv zxBTaCm;BHEcFKSLcF2GJw#!cZHRG>>Ed4R*)6$R8pB8-*ALko6!x+T>efU3Q0b?EU zzwTSN7nS~vGy(Q5%(*a8NxRM~T2#_?UUfOQnfJqaHRNUS^JP3|iEJD1cknvTYdP&T zUio=-;MJ0It9TvYmB{Nb?a%SL3~tZMkym$KTSO&;H|^6YYaYm%dRKYUFj|Ib4`e^B zn*7sXE8O&|!cuF&zaDa|-bi?9dxg272<=BpsYl6=dRw_upD#b@Q>gOXnTx5spqt7^ zT0yx~KPc~sEAp(qT9|7Aa+tP@G4Ju3jxLe>yO)38@VY9)#BmuTR`R;fYb&p_GDCcm z%fuo1N(_+S#md}p5e1$rziZWbAJ6MBuM0Ae*9}$&cX(bCL*z`_#EA2(gjxK%K&6)JO$_E9$aA78QZfwIrPd94;^y7GyW{PaJ4osgf5wo!Ibuf4zi4PgGm zcm?qa;oMJ~#9)`7uMODiCsuZg5%YMde|Z^gf?W*Hxx2BtJ(4l_%1oPr9f&<+ zOm4sy#Fa+DEi_Hb`>%TL-_?mqSSmfgNH3tL7ciuIm}DKy>Q=zCPi8Yy4>lnS?Kzr7 zI?ne{I~pf(NY(sBs6XmZYqX-4=uYJ@n95)bHNb4KRIC--#XfOJ92aMa+Z(L;_r)Vt z`PZ!NpYU`vYj~2#Mk!OxM$4nQYHpglmRHNC<<|;m9$G=Ikmjj*X@#{ST2ZZ-=B*Xi zN@yjuQd()Pj8;}FrZZD# z?yS4$9(obIq+Ug@t2fqL>Rt3c`apfC9;8pur|Ey{i}e-yI(?^pP(Q9;)UWGz^%wd( zJzS60WAtP_(;y7ihCGIR22Vq2Llr|ULjyxILq|h5Lx00yLx3UJFxfEMu)wgwu*$H; zu*tB)aKLcPaKUib@X+w;KQG=k-h`{{KgFo z@EtK~bWo_@paB7aqel!-TZAEjgM!#-1dsF!9_SYs>N^4u;u|=~*e+yf;E({nk-oux zfdPJlLyaJK=n(%<-;hwh;GrSIM+S@z85lHTgs=bjk^X*xTst&qP)LApi2nq?;Gm#D zztFKkq5i>seuINX2M-=P#xG>(xDdZFet~|fm%yQceglI7f&xSQMhrEA0KbqBzfq%o z1BL|q`7*?)(S9MJY-4;w8#Zd(q-nF}En2o}-KK53_8mHQ>fFVrYq#z_dJYKo9XQ-C zR4F(BWE}n1xnH^t96EI1(BOfiM~n^}#OwwJ1^W#N_8sHrJ7C}-zrjQNhYlMaFd{H$ z_N4zAPwk0GbZRPDUFm6DrdL zaALe);J~1fNhyRZ(KuT#|;M!rA^4t zkcPo@)v#&ffqsM3hOnveqNc`+n*MW9)8EZo8s#mG@|OR|TmF`}Gxpxz*s8s;Rr`Nh zwf`yn-L}2als?9`KE}2_|Fre_-KvLCGQ=-<1W_{}AjH?$-q(26_n-E@#=d=jU*)T& zYV35VQU1%9%2q>v%L9$Q2N_!h8CwPY(@W4#;qSIV z#%YZw{ z4hjus|31PucsMJRHcy+cEr5L%X^XWb+EQ(q_LsI?TcNGgR>4SXw6)qgZN0WZ+o)~Q zHfvkpsBPMIZHKl~+okQ+_Go*xzhSNY+5zpLc1Sy{9np?z$F$?x3GJkIN;|Ec(ayqY z=d}ykMVRffc163YUDK{>H?*7Z+->a+Tz5~quRYKnYLB$X+7s<5-1l62p}o{zX|J_6 z+FR|N_8vC;sD0AHw9ncX?W-29ebXY~$0#jY`>w@ku`p%4mZ1I6615~aGet|)(zJ9f zL(9~%v}`R$%Y{c-z;#_W=q50#nQpFI=$5*bZmrwswlJ){?w~vBPB3jAxYkv7)7|yF zdOkhBUI6ATs29>bbuYaz99&c{rhDtf^%AggDZR8_MlY+E)644>^on{Vy)q14Rj(!v zP{6U7J|@HoAx;T#Mo>zaih_=!;A`PkMHHzdyg!SQ6Gf@UqO`Rr zYbnZ6(`*yfs)$+!QU8EwoFSS=h}MHdyBDG}TjhPC*ACIwU-Ta=1~`d94#MA31P&CV zdy2^i#dKZF-5?en63aV?wJXKu!eZ-Lu`5vQt11o!h|@9R(jVf6gSgvQJb5Ety%Zno zh_Ay%R7Vl(DH6AcQQ7ZSqjf(knI6 zYo+Q9F6qsN>unzCKKu0E1@ykh^?^He|8aVti5`+ipXjDf7y6t(^!bPM#WnP0TlAIJ z^bLLW&2RPXWAxo?^n)ezqv!OK&-AnR^-I(AYbW))Vfv$K`m^!+tIqnnTl$x$dX%mH zW1OB7YcQNNSPO&wCPUr^hJvYvLKh5PtqsL@7|I+^-zHU2DD`tfxES~UGN%dkjMV<- ztg04Gf1XthOl9sU&Qf8~Rtx!81KA9idsj7($*RG3)j+mUXP^GlLk}aotj=CEj&@qX zd&Yhj0e_8l9jLW5HaMmYcW!0 zE;W%d);n6tjH$0dleZ#HSWQRIyzWXF9k@u!7bn&-%dxb1;lB{^yT!gzo~m9`%9lIx zNO`z+tQ2l_GNjy}w826O!^bT#=$!u|rZcztE4Xo!X9*qDW6CDIBBDgRZZW!uIoa+Vl!VZ1ymoHeLLsFkM#- zbiJq)Cbhk!aCV!GSc|NKS=J8&&B*{A*o zzdv(L1FRwp@@qSLdbaLo;b1p_vnX-_OHTT^_uxxLt z<}qBOjqN-pw`&g5tV;rzyu@E}Vn>EbzlAvCH9k+fz8q`%C(@aGiw*Cp?8+#%hpQKF zM2XM%BBd}rn){{lwq!G)+t@_H{GGEDC6oF|QDD}6DdS&~6dqF!adnEFE5V-eUdm67 z_ZVUBN0Qi?P>L}t!quZnU|ox`UQ8mzE}520M1FFJ``t26QPr|29sOT0#~;iwak`r0 zga2WUcIiE&How3?shxQm#XJ(8k}U>30aOvoW&be?w0|#lpZc76B(7tMxrh8f%S$m? za}Ut6Ku~!dgP84XFNO8o2cWZsK$HIjeTQzW`*uKVuP>CseNQFmCfHGmiZfEAC|sr` z11<|kY`G2>XxbtUY`%qkkr~+a-UniA)&jNrynpmJdaw-D^1!mfYghykQ~kOWg~BQlA~*hKHudHrw)=~TS%1J_F$+L>cD1AP z>o-vUI6Nw!AGeY+Y$ytR_N~R+HLau!ukA18)5N@p-;O8cGs^@p#h#Oh-bQ4>2k$hz zlvD$8>XWO)^^RQxZc$sNEKg#pC3(q$sh{tepooF?aktC6+9;LJjB%|7xI9z0`PJQMD5~^ z5e-GJaCQ1o_&D*8MWE+cAThW*{fd5|9x#+lZ$sWCWMq*|-_L<0nrG}@2kLtXamJxx zz>p<097IO`$YpUdY-o$s3Y1`MtzUK8toA~oF-gp!&puYM#ZKr`W*}n6*#?%n66j|+ z4(4;6Ul|lKksNOM6tUTwwur~nQ-RDnWb231%|7f0bzX$nVk=X2xIm;@ygd!tO2k`a zBX(GPR(0+O;c5%Zf(kw+e3K4@%HjiyJ4wTO77+dlWC4PzP zW5RXNgAt&IU9dm`85uLAu`-Ln8@QasC}k577OPLe+ynblKHO9(sCmCeay^cixLKbB z_BldKh^eT5jWiH;c@mh!#=;omEK_vyX^Q504EoLMf!O?F3nr0j530$!<5-iTfDkw5bFE07mlEbo%AE>@*bs#8@Z-qZ=$ z>mo`2?HtBvbb};cSB}nKk3?*`6Qi2BClh8zkD>lPl03a?Fqb&03{9L<0{ChZMcB^D zIJ9jg&b`1aIZGxZPFh6_=NyX!jSM8KHxeyL!7K~8SJo+UmxI7dtQARZu&k`N0n}nX zXrLuyi`V3`wvx;ekKcjTRsg+$y*P*1Yy-utO`E$Q#cMY8?||<6fU2lU$GQh0FQ#=@ zr~v3s6HvGQpp&l=8_7d!NpGHANxYoYM_4vEo~wJ!Q=OZJ5tWVaqsu(pW>cac{ys8| zv3HC{^MKkO5Jk5OF4!fAIj8DdLGvHs<-`)8^^i$Qlrlkf<&X4n?**s|rHru_%&u}C zba@HlpFCZJR7zYu&1e_U)=1DuXhvxBM-fPOmVs0?r{6CFTF@SJ` zd(eD;&|=C->ji2dwXHmk=7nd18WAqy8Kh*}l(-j%tPmSESpv%+0c|FgMDefc90E~l z3aaJ2CdRYNI4b1x8-17yD*M2~E`5sB4pz62`VtFOFGNgohlaP8fuy@RKv8#C0wO%| zbn>8r^p##3vd!+^8q}M_%F5gZnoJ^PT_pL0)mrMF!o^)cX}Gem-F=dx`*M79(oZRx z$(m%D#|=X1hR-c)%*4bw!!di$nsgpr_7C_wllSp$SK=1eBcO12>uGltWr-oxXxW3@ zOiVn4Tej6B%0ePQMYkdjyY7X)ulh>)r8)|I=|!qWOiHC@=u=N!oq{XlONC4M@EVfq z!xu|Y<}`$t=H!TKZe0d4iEe8_N1{oW_t~HgbwFV%PU7_=W z>v<4o2foC*p?E(#=r|}ufCh~O&76QZH8%&kyo=RRpG{@T0%}g}&)dwq%BX3+6i-<` zAL(*{HE#WO8xkB7p+3v$z0lk&_bTRYsB>7& z>SXTCT4w$5y9$8_#1^rRMA`WzL}D2nVfXiQ#`etS?cf7+9&wJO$#(kAw&+z^=9y)N z0u?b;uQNbd?E*xQz7K`+r@V%0uCYnUQ}rm( zS*{6Jr*s~JLszr*rY&6nDhRn|4TCG51qUOR#j7Am_DA2v7eJc=80IOdYvt`p=Xp*O z@YQ0Nw_};ZQq=Gw>K$gmay5Fg`w})2E@OfNp`hv>Tw_KO*q9y!15?rRd#-90>wV|lE#7VO| zXIYvjKr*6p1H{=;gom)4r%*vA5=prJLH2IvUUK)76m1Z2W@gkEHXDTDVO4SRF&BV=V6@7nyk*LMcicEG2XAM z1sanE3WnHCf{6rCtQBLw_BSO!YaWD%jdh{ykN#PsAxznvoXnb70#r(sIY#!&b%p&R zGx1}3Cs^r6_*;CP;=nkj-AAI2#NC89Wd=9M`!7#ZRheq{L zE%8hzDc@wlD~aI;K+$(FZ50CN&HeVQH!G!f;r<}0U#r}bX~yi^j*VM3!0sRLP;3L{ z5uZy$#WeYgzG81x37E={(6#bQ*3JxVdjHK;P#J+32Tzi zr7RpEbEh#6ttkaf&Wkfx?hZcB9mJ@)nVEHZ@i<`E9%SackB)WHKuP`B-y7l_@Ka-qkQ2V_OXc~X=v zx*7FPw34ED&)SHc7ZSM1y++aS6mHPU-eStJxYMkML~K8q95b0z2INHY0FkHNrqOYk;;b05xYo{ZwxzmarIozpw9x`aSEy{>kYeHwOIPc`D*}uTxn= z8-8OOWrzL`{I-Bz6lE#=qHd`fca9Iu9B_#RdG_F3576s|psMkz^OIF^(K2kF@aY+7 z925It#_||3z!`C3LtGu@&t5O?IO3?vSFwK06sRTcJmTyP*xX3dRwL@r#cL{PtIzEq zl=Es!Pw8dI9l_Wm zF}zt{AMAO?9XIv0Myzc+P0&jCGVKDBPg_rkuVkupT`-CF`Ndt2x@4thI zT|pdI8>9Zb+mqb_&NsP)&9lc10R8=eZo3Ud>~VTGtHWF(%*75)b+JGT=c;6`TjMCi z_T#1yaOb-+u?rX|Uo-M2yWHQPIcCbJPnIIFC%U7?4&m)U=rZBxdDLCT#AHkwYNTFi ziF0NRC%QY@vIJ~$M9?c2aju;rGwu$`?C+;QldVXN8E~7S$XCQ!8JI zCZe+Tt;A~6DLRGTWh*V$lBRlFRlja54eE-LhSjNv3oRiuE376}JS~XS3N2a8JS|-n zvOw&)awIo83evn#l}%Dq{To&;x%RshrCinK-O@aXgI5}wzimcfRC2=5_gS6N%BuW! z??|lmi+%$iWDF|}&D7eXfIex#8v3Q@KjZ}M#6Sj&hlz`4bC^FfZdpQ0w;UF63?qi7nKjF=A6{B%YkAU zRU5hqvBm6jym!D8djA_7Ce7w8>@QO;ol#dGI|#wO&a zHcx6m9&d&#KX*GsE$M}klNV3mYR!TGwfp#my8BogK>iAsu+z`7)z6Y>7B*XC`va(VFRoz38U~cCl1nm{+65c*R@7*`aa3_3sp2@cDfv_V z5SlL{Zp&B9F1j$hsx(|M>pMs|ZRjIn1csKaE?qD!EDct2-sSH$1?z$j_Q zaM8E3e{o@LED)Z^b}XZUS}{H}UV*rr+BMXEL}-0xAM)V@v}~KCHc#&#)x-Rq`x86u ztFSxzL{ivhLeOrG8&S>fFxvv|BQl+%8BC|mRj6?Q8|^$hgAf-^x7>RS3B?-9c|Yt>|I^tYh;4ao&2A%$8;PAujSF4`MUMF1dr*Cf|=jzlxbs zKAKh%)v{Wxq2}zmgV;Mq=ts77WlpZ&ITuw~iNB^mr|m)sk4;$MOLlYgTgE;!d;NVn zw|e;vNk8<{Ym(!Riq^eXaJ9uoZ%F8! zP8auzVU|SlGrH3Q=G{`2;*WA7h(`&-9mvm`bbse7cG=->xZst#J9x(&ONZ5*iR9%_ z0xpv%e3j3e>x$uT`MET4{;tLzDbE(z&u0(nK3v_=i>dGm7G~^1dyX*WNzRzEAylG& zYJj2B6N!^KZiwAR>4awg;mkJbG)woyZ3I}x42-%{srJza7W_~M)x!JJ&r6qFG~dVJ z7s`ekxD|i#!U)VBKOkXWkcUXRbnAwJ(d1%f2q`G2;qipm0o=w-{knCBjr`zGWHf z&B?d8Q743&73oA-Uj=M&0ChOpYxA@MvBwUh_ zKx%w8E5oNgG#<9>s?@jTEOQ}F{;Pgq!dk6W>{3?hy*lPFk8iOWc`}a3ijdS5Un^Nd z_;Bf({Sae(zDCi_&<2{{Ee~u@nD!{j&92{_I^lnhFJ^s+f zY@tjlMaiT0cn)&gPB=wR1TDLTSa*aC(_Y>Im51w7&qE)E5USUoH%<&O=%PqXm<}li z5w^}GJM4#}fUPfAn}vqaa@9y8U_Ml)|2dZ>rCB|~^n=+qo!uAHXG2RifO*supB;@RbD(G^K z&}G5H`|C`3`Bd8K=#!-D5DuQ7#BzWvA(=P24#^im1tti6(=Ttszp zz=?j!OrYNa2S>n=2UU%2|AT{DNptwjcNG~^R1n#Jq>*&RcOtP!{=gh-KOl{&T7%5V z4$HbHIiy3V!t#4@P9B|rS&F;^)k;9MjUcs1MDEyp2$t=>)2-@9W0nrA7guqw21U7S@IfU6Vk?JV1^RxCnm$GH6P? z6lVS0lVwHZfoMPXDuIsvc>iC!;w#Z*A;G#}3gX1JF$A;k zQ}m5k&n@Y=!UVjH4V$tG>gwcT`$?BqCX}%?9FbHYf)mT+1x1xdoRS5p8gFl;cqp_A zO;ft>1?`MPoSHR@=A~JTld{T#8vlWTt^^a-C7AcmTffPP6bs4&GWCudUTFkznYB?2 zn6JwjJR7}^Wv2L37J`m-c{>#=<)`Zu*SUk^Yq7cc@P&Us!Gs&pIIZS zUcehJ1zE^yTvoM2sZmnYD$2b8VL$KyP?9MYPs&MACm{?=@>K&-CvhWU&D2a0aUt^c z;xon`&GMAJ7>5>ok%srzOL5S@nbfNd{p14A25_-iYpC0H(HxxU*qLxDzlX^zRZ(nK zVkswYmmv;%E=J;t71Ad>!AtF%Vui#RKow&$sS%4zT1Z9Exr?-3OOVU#p)gdgS9{Q~ z=^)RMzt4G&qI2WYXWyU@D1grGTQaQf;RPCFjGYtr(QjT~+`>oCuMsUs#(8aZA+;c3`%6BN$D{cRUF(v<1w+!in&U3ewh0kePp|s6$=k? z4sq&H`pqg#>HlLzHLh_ccvGe=!Xb`|ma@K^1Ggc`x%CE%d>VyqGBZGfMj|%edX$G< z{|$=n#M{G3N?f@c7na?OqdWiLnohN_TK>Uw;j|RZMbrMYxpV~7WX5k~Jf}XM5r$%$ zxw@bUHorBIHqJzv{+`&;UoJ;%*$10zcPt0|6^^6DdgfsAE|50H z+r7q51udUwDDd(!<}QWX^jVcau8Q+SDmlZa&4~HZfiPJ+n{zoeLF)z~wwaAlliM7H zOPAn{h(n&l(LzYqrYzAS%$DaRkc(3W%1ISaAOH_#+QoA&FdNNt^d<~;?g%I6c4vgY z=WuR*3(&Yeh%?W#79NyjZR?!%T6St$*GS&^Yu~1IH`>owHvnn zQ6-5q3{0o$Yu^*aa$B&ebRqt8XJ1!Cb;;(SZhDJ$wj9pGNb2z*o|6ngY%*CH{|)qE za{2=31lBZ3q=K{Dtc;``#~=ouPM~v?riPAOs9mlK>R|L=DkP$-gP26qQID6wf z+KhMNuoX8L@A~FErl!rFs%{QsZn^@$^o&MQHj4~XA7p1$y~R1RyGmSyC`&PG0h*jZ z9DG$|@EC$fKAjhF!sn$per^}g7Z^eIP6H)T;r#sAHepdP)9KiavDKC=d>$5XO!lUs zXA>r763I&W9^_vYv?$v?_r;`f6uZ{wpa5)eLVYk1Iq4Sg zAgLG8dM;+2&WA-OJauF>fwiwL7Ip7%ftG%4*-WKY z=IR_9&Mn(XkK3vtb`2)JjZd)UooJ6g&e6tYJI*y5I}x$Xy=H{Llge}v*??GRTo<#? zpm;JFJQ;sfmty2JLNmAWRpucZ^uW#IsoP}zNeacNb3KjcP&DThEltwP(bqu}P?3|2 zw`1q8RF^pmyit$G@BSx>?Y#e z9fg3+)D+M426+n#a(nUI}vp04~6ombC{TOi1lWd(a-#enrIH_omy0S zRp;u1CQ2N_f~+35{GTAirnQpk=q~G_X%G!%3F>as9h(?IJv$~$?4Vashx9X?go3p) zu&<&28ane(w*w4Yr*m>j5XcJl)w|XCIgBt=uvBC7*|adYU82pAv7kSD(b=wkp!8md zwVnpx*UBfW_}rrgE~rW{HNDaKeg&5=1fqxGHf>qs=W*r!dwBP#MyV^ic6I+ z(*36_gx^oOv3Ebty7>Ha5#G+agD2fqAWoTu_Ua*MnfziUCcQ$#)ccE}t7)eYM~|(= z2zOfkT6aD#r(t3%WL`%<5qvv zAKOxi$2^pBgUbudpS8OoAzd4G%oqX>MI3%djNXH2BI~Nn!>1iJrQG4YjXkFg8;pHE zsZ!po|4PcE1ziy*^~2tAUw46;R0XX#&p0~{V)JI~?u>UkWSS#9o#y<2!}iU1=PFr{ zURl{U^<)^ZR2iDjEX(1dPCv(-A(kC&5x;SdLwxQGdqefW-^59eFzwXGOvmRoldzbA zeJlFB$Dmz6<=Gtzmvb~M>%!K9r#~WTd6%oL-Kv17|`y}zx4}{QXLWnyyh;2tKLsH@kHuq=*GB7)*MwKYS zI;mB`qbTueeZZZa`aUuu)j5k1o_ygJttaUw^n^2HXWVJD@Y+LCb;qIE^Hx#oH62Cb zmyjaIb+XiFb%#`VqQ3y)ci}!La|~$LKul^)m}ISoL}YZU!epS`22yO(Q}TBcm43u& zaxrV~Tf_+~2GG1d`zkA+C-AtTI@+%5g?5%*(8PN3T-w-*zU(naoIWj_nbZh@vZk-0VeOT$ z-qR%3oj6;>%#XwRX6Ryz`BV|af7n89gnDIW41AcqD3*3SnkKUz8#p8=Z@(LZILY)n z>Mw(o^Nzs{aidu!ZN3s(cFoks6Lz&~W06Zt+4(F}O#S;jG_fWHOCETE*uMPEpA0Ol zvtfAkjRIjEI|Lhdu7TL>CCyF0zatz=)S@aL`<(fO|6|*MiKPP|XlU@oJOF?DW|VgmDZ@qG9m}&b1_+h3RGz(%HW( z=qF?6_o7l8P z>CI%ai4zp1uf(2)oW&sZ=*7=ls+LD?qh!A$OeeV%T?k0fqFM@K(`#ga(Hka(APn;p z>xd5RFJ`w!QE2#wQLOmoNA+_Zj;Le-h`;d_8nH%Fq zStfK;)p>Ax&NZKjS6Zs5j~y2XteOWj6Drd>j0YWzV|drkh~HH_&8Sw@_^f4Hal(dA zEaZF3-Gs@BMu@+ktckW;;r{R~Fp$}-p5)QrWOq(d59VR~T9ch4=Ce52hFJ?p%+jqA%A{n&+ zYH^<4nH7Hfam>z6@)8(O|#CGWigHF+U3|M=-zj z8O@ZP-_XXs6}Hh;fs*!yisW5B)`voiSd)$Sk3Bk(8CiqYqng#LYiQN_KG9H>lyYi_ z7Ab46zHT1M)k(obgC3kkqHk(W06l=0GItUtKkwUTG?-6%|4n>3{sp5uuE&1X0Tt=i zGltId%)CgK6|Y;iF{4!b)WEJw9lj&=Lyxx`c`& zEbcNkj!&e`Uq0|?<=TiNhhnf8hxrhtx@{fvioB03lnV;Qq4M!X4EV-tEp_<)X?zSg z5UzdmrUM`Cl!-+Aa{ndjHa8scDhT4d(Q+}R-tq2IUh`9Bc~-qp*r(HQBDZ-kEUf1c zYs-!z=zv&Q2i`?#e^x(>axJjHIbzK$m2|eqI}Wi`@JJ?4MI_9#vWcVVxWJ-_jv1zn zSM%N;Ew8HdovZz|&Q_8Isx z2}d+HC4?ehzNDO~*n&u1qN;Ywl|-spG_;$(i_L<=n|7)KzKZonCy>qurMB*4K6rl=Yahbr>1uP`WT;uVr_+F#ju_ zyZ8f%Qyov1+pA*nd%@=TC=@OjvHD%Oh!GvlPN@+H^VHQgOcrPA%4C09*JHr8&s$)M(q0xG;9;rF7yNO+s90d zC!APH-VG$qpZ~>xHPRSx=r_bE9nd$ikUL60%}YYeoQ=(IzlM6RP)`Z#`lK8}mxty-9rIPyD_nw-b9hV4Q;nP|X79n>K?v=9(_XSbPCW#yZyF zZTE+~wRp`4$?D#LY~%uLQi6{6tYk#>O>6DodkztOP{a70H0Ou?$m%hKon5c;Wd2o4 zRJ9+DIDZAg&}=8w*e)S2x8D`mEsfoe5XDph+5>KTqWJd>D- zUwe&JZsa74eyuu=S=5QVJW&j){wj4do-RPSxlA)UE0T-fcL6P}&%rN@E6xqYK~wh7 z*Zc3Rd>=O<{_2>*d@rtNU7QGSy|OKaIQ==zle>_N=~?U-lWR>z+oCLD$(}08el)0w zDVBZ!HERu8;sxrv927$pAft}-2ZoWS_aNEGM#- zyaWx&kHJQwiqRISiwhzCF_k>bcnjer7A3{f=d*Aoep`U%4He;~`jb=Y>y9$<{U=UN zV3`mzRK%Wm!@PM27cIlCQHJ&z&%Do4HaRp}K+P~&)lMFH%&^TMY-9Yu+o|SkBF_e^ zIX_XglQ>Ruu^4jT+gu!$x8iVghB~L}Rjc^~v+?e!@#{TWK62A)c_YM+Bw{bK_C)E`Rvae+Vmg>)HfT`YPx}nr>Kdkub6(HiJ#K0V3zNN z|%CAL8^wtAKThN8`5w!>cFY-|9~=-OL4yt)AAEsrhV~-N6ZjbRHHj^NLK& zs!|ig)+68&_0X*_^nFJH8XTbNeT$*|+Y=C*`V&`<5nN+duO3J}0dE$w5Y!Kk37hez zl*w(dh+PooF1X`Q7L^CxG3Vn1#7;dKQGKOYIQ8mgv=tX-^uQKU)ou1+0l=M5tb7kU zxqbUVP}WjLeb|_ZB_%7HuP6!n)D~2z2I$pPT1Qc73x~T)7{_lOUEU@Q3hcmv&LuP) zn6Mo&-@9VU@2MKBf`dQ~H<@fth{(oYnZ%` zkkEVrhnm!CjFK-`pg=FIX4nqX2(2C`ZT8y{T+h04vK8e|O7HR{$6TNM=X^3 z`l_plzq=I)1dC;1)J{QV(6-qz7Vkh*w(aB2G~X8-hqioGNWO@LF>`8s!|~*Re3$bF zV)d!5OkO{U3nzEO0EGvjxB9w1w>{Cs_`#<2Sf*fcZaQy=V`s~xr|4u?jjN+0SW{lT zawK~S{EaIfrDNh{gw~7j5s2d}FQeE@dkiYqhm%!i(eT0x&Y7=8oL#XgW6${;S5>MH z|NS70E$58DkSDG)cG28D(9Hxmxk7;l@KS|TZmhk@7u@ovj(2lhhFB_4a6?J`R4MMPSK13(>r#x<{QiGzW%tw;^P6)?wb%l_X%N{B(Zi zFT~BpV`Q%)n7j7QiEOsVY~!%D5>>!SRn}GZr+Mw26FBKumK{xLRCAxuSBlo7dLp*? zO0}DR@D*clshcjDF#ZGF@03vn)xtfPj>FfJ81=;=?6()K!n>O>ku9uC^7_q*u+v;W zd@u>qeRA513-$!!m#|x^bDq-@uBJOd!?HnDp$QAWTcD0E;hWZ8gmMB+`fwyT;kGiWFw@d7W4Rl9$lAQ;R0BW2776 z!W9UD#B-=$v_NO{{T>CftdBUWG-8X_EUu|5?O7H~p*oWZ5Qa&6a?G|xbHpYQM+wq= z&&dsbELRC!pDVfeB&+0MtzF8eD$n(7Yx{ z{S1Zk*5>qhI|py?ASs0Tf-L5`3R26dUWy9`=OgN$K7A$5uFk0dg??Mbg;eo)!`zC{av(S&d!}-=)wB+%y7(ZL{)%U_4ec?@ zN21>HqY2_7OP65`^$4(6wU1O<^V(qZ_qMD(Cc_Zt_lRcU8&1TydN|^UH>+vw(GWKc zQ>xi4zd_4Nc+kH54ubh1O!MZ{6sY9t3N`PF_qdDfF^<4VSPzAT9lZ_v_kIU4xnhFI z$rOQn&PmI}E1n$6!p%SSlpy{J-QtqU%2uV0Ax?iu1)AliqG_`SsMBE3&1Xk znnYZlXB#df}rXQV>!fH+fWudPuOof+T!gkl2V2pFD zou$IyLx=trh+V(VA_FhfVI}UM;nA$|9F{jDP|m9o!!Z5=tkQ&hO3Vw@M3li!)H;HN~sOubv_M-5L#9S^$MrM=N*~m^E35mmT6q*rircehySg+YK?i?ClU92v zu?td^WUNyRmA}&?ti6La`S)OFS?wWcGO?tWT}S5$UCEE9^GJN-7i%b|$(iXaSgiku zqnkLOh94;E+YqAFnMnNZwud;leV>zYz zrp@-c1P2d13iHjBa{BXEsK(Dt;V|_Sm(2rGga7Sk2sIM&-zX!MAi4r9{Z}>6sfx@5~2!GL!^~wPLcH}VKe)@ zru9Z>UuHgGoT$~k5JwP`U(ynBd=Ga5vF;dDGk(`Bw?2i0`ei1W`v(5gJ&8eTXvEh0 zYh!-f1DtfTN9pQNls*^3+qS-h*m!5b`m{ub{M@ zq<0v^Y5Cw!P;+0zPOoYZ>u0H$+)luh#*csVwXT4tj~qm7T1Z90*+-x*`9Yfq2h-88 zDMX1_6k)b}Py#B7!G5Bj=W-Cc?*GD+_dFvA{D|B50>v37w>Pc1F-S*`GJyp)GEtL~ z#Q%BNG%LCU>Ni$*F;eQlDi*5=nZ(^R)YIqU)hPs%NwRrDLafQ*|hwUwcTq<+?-1{tSzs4N;Wxch4fD5fp5jyje> zX!SgeqF;9m;N<0+pufrx4Ds`c?IEFPJ54PbFNPDhafx%_#F}{V!_qBC_K@LE?!p{0 z&71^yb({6grXv=9(Xj~g;2Rk7Z5uvr#$F{k)FkDzviosp(}l4teL>mD zpDxr_V;;H_r(f%H^{p!#5J#SKLws;QCZ4xh$_o!}bL-^F3bN248Y@&8XUd%BLQA#V zmY{HTBqqJbU4_?YsEv{L1US6dER3IL!!`DlhgUI?<4?KnbS#}4w+?38SqO9L^d*9u z-$J#QH$Ya^x!U!923;oK2X)5-9$g46*9!R5qt98yUQhB8m{)mUvIEgs*42+;zLL3B zj-5el-{>?7OnHvGTb8G&J59>p-(&^f_Up~q{_B%uZo4%U5iKryQ#8t7?*n%U)?bjKPlKwYi+>5$ycy3K$MF;am-kj(N`<||l6zOe z$T@;U;>I9iK3M0FM%);hyylQf!SWL&n<5tFp5e3O0id#}preP>x#uADu$r(PhAz>Y z3Nj2Ig2>p~XYleTSS7lBS5%8X8V$67nBt=s5KS+i#J;>Jga$+If~u?i)7KiTweQx=22CLF-wmVR zaP$5kUER4(J4|M&r`ToctQF+g861_CmyEP2m4P@TqY?Vog&i`xT7zbN1RYKXT~v9P zx5)?sVlR}I@8UrGbOjbHB3MS9&oZ{K8P=TXN-`$RttlrhZZnA+V~9B8&-T48P0FNi z&ZMUJL;`>Q21SI=BpW}Qwq@*`5;(@JKI-Rm9RTVa3tCC_%?=WA^12OFH9n+G#%%JU z@N^7lok~@}m+u(cYUyqi8i%T`eTn2emqJi?=2^V@ZW-eIHSST2-XS)P-_+>Y#Y*8Ki@%6HxAJ*$^^8e~3+-!zk;jp(;SpKHYoYVFeyI(_kys7> zY0Bfq{(SjpO(b6umoWoa;X8!Krtw5rBR^K2PE6kT z%YZWZb$Q@*8fFi%qKkx$==T&`WE?4mE|qKv+INlPsY4pIl%jD17|pHp7ILHfDd^-yh)Oy|L&4mUYz_H80o z8-h2~56B3U1v{8dRw^+P=5WD<^6kMzJh7WJNlC|w2{#Wgy(^n|dsI~eiJu6&=E4F)qgeV&PSSFSf^glT*il)Ma~q?=E|8`bv{=aEf# zK)Hhy^V)Qk+U4eU%)|IS4E3v^+`_S=2P0D7&Cn;SyGyy%3NZkmDD!QV&x~SxM%R1? z!*5eN<($*?Ns@KA*7zjzPjs3L1Sxwtk}CR^N09vmYCGdo8=1oiD4E+ti6v|)Jd=r& z1`xL_N0?_%CldK~AYx;MmHTcJ=s_Ju$ZW}>J$aUs_kbGPvI+K4Q3HnSKBI7^!1J_^Y8e7UHb8Aq1Jev-)8-9q>3RWerE;Iz$ zya%nO+)BNZN?N^Dn`XC#R9<$6BS4vy7_ zofq(-a(PRQ>y(s_Dzq94jniDrV)e&PRI|6(M71_&Ed_{vg2`W`_OMHIM!y7q#7?Dm zQ0VXz+``EMi)47br8?sq){+nXm6)G(lIhQig7&k{r|&NWI!fJ}enuizk95n-g$o#F zVk-80i5A8i_*!sl#Om8LeE1lH9S*O-q2WwEr{^hbzTXuBqwIL$9VoRGCoJ%OR1Gfv zf7pBT@T`t2-M92(uI-SkFi_iVt5*@+N$`9B3~Jtd;P)K9O7s=fV$wbq?`BiG%qJdlL>tEt< zM=nP{OsAeNWMKV&dmnk;VT#j4%$bHVU(Lq6u}jeK$aR2RL@)2V5>4B!FRviopE(pI zDt?P6gavIPvwrqDTKupF)4dZz<-dQKD;ppFHHXJP;OK$Fh@t6)#z$2A71BuC-6FP*MNhV2>kdyX=G0}N06oEs~Dh~v0t7gd2FaI#MTp{#{;y>+Svr$y!6D7tbbE}c!~ zw>Q(XUmlN5Z~u5Y#WfdNxTkP0IIQhpT(XKF*J}<#ov_y5&~`uHJ2>3bbOzHaNi|b; z&*bRQK@6imtfV;k!&NZzC;KV!UFX8rzrxV#kv33Et*1D)Cjv_c@;m;pa^ENa%(=&; z{GcyWEC`Ht`?g(`UjT;ZJ_Lr%RT%W8IA$(J+kmH1pJG|SClOe`E+-Z5xjr2S%%s9z znHdauFHHpTsXTMB`}W*Jkhq_4@$tA9DA~oit1B+T`zd->aXDilxSu_Ylm5PmM@T&h zWcjWAI9H-4fnfmZ;a^vG@l=aq+z-J^t_FmFRVR0+WzWMj_N-1;gY+ zx6!VU9w+h$Ga}{pZ^N>iN8-%a=>%kC9(u$P@~Yn%OB@P=bd_HuewOdrPVw2uZ=qv7 zQ-TL^YkT>z-!e1s)l4m(7)tTU?Q~~HUxv4rqSC-78Jz;>LH!T^bpve&WK)q|2Q&i* z;3@9A4*`vDVa^y0vIB>$W{mr9X8`$khtT`nHXQz6`l9ULJKyKAt;uS=0XEOmKc(mQ z@Gtnc{QQZgWeW>HWOVFd9-J4gU~s$)7WMgMxi!-UjHNjG^`F77-f%tOEr1{J%s;`~ z=TA}jgg!`ao_xjnZ{4>}j?aE)CMeI37knSr4SeNKc-(71>8t){AvE?1bqwhG>`42W z-#yn!G4GO6d>w?k&c1;K-~A3p8$g8R1}feJ|FjLI^#_p&_uNS3=qa3}ClZ9*{tC9| z7m832h3}8dro?^np9#d^2Oy&vQDPH?X?@VEvd`;(Tb`0xN) zxE%c1(IwpS?zs?(N#`l<`yHSgrXcwxTs3Ikd$jOkl8R^3Lz;&_1jTN{jad~Oy*Y*> zw8U~z|TYZ>q8fmb+352Wp8!eTmhS>L(l)z&v+*+*Ev;t)ON zo)Q1SWMIMOyUQj{?Rkuuv4}$IIz1N$IJGI71q83d^`fUA!4=kGrQ%;{5^CR549u@) ze~wp2W(>6U`UqIIa~Tf6-f|RQ-a?q|+KJUJ%qG}t#e^;XB790%@+QUCiV(`B9!+8M z;OHHO-vJkHay0cRsaZN0&lwR#eAC=g|IeT1>i7*%xa&5>ab*{YmS7^6cqbb<{ZTV~Rh27_^5q;50L5%%*!C^$I(p zm4HS2?`Be*c62*L*iT>3XCwJad-N}8yI(p!^zb2ydv7B4l?9_tLjz&%tfi!(-)K*zr|iTq#)qQ#y{B9_$s zicG*i6Y(RVP3CXoiRn+3Q0(?g3Ymx#w;au(PkFVBw!U3Xa=26WuKvaMXmQODB6%Ou zjJofQMzJyPAwUIAVQbfalVZL0s(#!y4H?Oib~EyMjlF_N!K-SP|xreE-`> z9(azfEp+qa6#wV|NB?k(qkn`B-m8a^ePB<*G@LDJ_B~MM7w@56_-1%A%y4&HH$g9P zHB}^L$8lluc@X&xDNoGPdO#Yo{>Rkz%)K00LTu0%UZ-9yOymKsihuMhhe=bx?_Ij% zps&$Sg+B93{OW~G6vzJi(*%QtZ{v`Ceux3jqyWQ-7L@(YKcQI1kAH<5@YN0;IW(g8 zj7PY;csW16qEH>TJ(w=wek>NA2UDYdKK^dfp4ewCceic*3q3~CSSZBuap&^(&Od@= zNBRR4pFjF4ChD8RbI3>DhE@xRy=85QcvsVLiW~0R3-IwZnC&nIb<@JlLkO$xlR4hn zmfEL^xUiULM{^$=t~xnvf_M_(X(KFuN> zyYCx&NPWI{g`inBh6fh+QpM#aLgR;zF>tXp2j0L~dnvAu>CLSCu(??E<4*~a9~B^) zz6@p0zEzEOydiGSzXt-jPfo%l{_jydGvxwt_&CGZ1rz?BYqtN%ni;{sn?3eFDE_-G zB!$_PKVcQ!@hLofPvhu;9Gqs{PcKmHHpR_2NR@s5K=9341D?I7>;%rfN=eec-~+40 zZ&L5)pzFZ*zRuATIK;pWD=211DCpXDl)#lU6GN>0H7xx4c~mOiN6+%I4a}dMWdl~P zzSE0UkQJ>MZ0VQOVf==BcN66fTqUFjUiuAnh4R~^QEWnY^3OZkyn>_Wd9-4}=OI*9 z2A7AQ1fQ&=AK-hdbTsGxx;tYAzk>@3u=Z(&0Br}d;K2dqs~u~&Jb6(P5LZ)Wuf>c! zDw+Q`;h8%ru0%=!$co8a&)zR$d^r)aWaGTuD5{_2Fk=#9WBIy8R za$Rw*nr&nzqP_LM+PrW~M53H8|4rCbdWq!>)K^=hmWd2NHp)TL|cuc~sVcl{C? z>yh4d$EczQ4%oU{vQXQhiYf(07XR!Wh+$$jOeIL~aed-eh?Aa%8N&odI`x?F=K(P@ z2cDa7@HKsWt2mkn7#Z!bBRm)o`$SBjI6*4khqfjj?=$z&mPrtq?whnqcgJ(G&oV#_ zhoq(t@|iGU%n`-o_91V#KpiLT+9X=x1h4@hio=AWUv~#)P!hcY7}_O|PkQ^uaJR|4 zI=B~EjyTiJohFqlZMOtU%>$wC^|-nTMm=k%Ay%-8EhN2rZob^DRCNYirKD6=4?<@}`X0+JclE$r4D&2dtn%hX9Vk4{RGTf`PyI zTOwZ?M#c8XMruz>;qDWKAQmeQ@!0_yH>hiSguii~=8l#+*P2h2W)-!Wzd1t32NBH# z%QBM$Xt3&iQ1V$vl1?F1BY`^AY)viTbV|~iasrYWBS_xf`W8C2=%Nx`-cp9B-jO@! z#ij!^VelnevYu;f_Ee6<+8#k|uWh2v9*KSp2%K^4HC@^0Mex@!5C@5G`j;EYeRLlf7|>izaNI;-a*V zSOhIYA%Ka5k+yrQ?v7f6^K{RJN#RKxHH)S}a*1XIe0J_e%F5lauT7HQ26@|l1XrtC zj-S}pDY$1DEVivwVap68X@$?Wv}Hft-qY6Cng5pc{eF{p(m{dgg)wZ!62ys&pqe{s zIaqqwqf~36QO+Kgcy={t9@&NAJKk3BuvQ-2befaDiQ?$SKxBMtA^%QK(a_+y%u-Ih z_7u9?5?S$_uD&8u+DqVLZ|KG&s{H&mF0|t4r{XA&HW9p`WGWU=i=9+vOgw1DIt-0K z%iF@g5fZXLzF_)dUX$)Tw%@LBi_H$@8K%`@=M&E32n?1 zY9YQrS?x{Xk`9)i5mdLGf1Hs~ zZ=;<(t&$2|czQdiRaBGoIDfX?0$HBTu1r+n9i3Lk)ubD{S5w@CVzwI{nDGieKh%XR%Wvx!o)%MEzf`o+re_>? z^R*`cY!@d;nm9rywyq^2hdH){RwH>-Hnqj7?L;VJJLHwIE{AGITa!p*wnBQh%UI3v zw9xFBdR<|;L@nHwt6WgqRdbQ&lJvIKLhbS~h+Mmt3ooIktsz#}OX|36_h#_y7|7k- zGD(Nv?M4dQAu8JIqR<_%vNz@-dDT@)Zp`Pxsx2z6Ry&PCTHTc`SplBOoHJn}6SLT0 z9;koe@Y4hdo5)b?o17vCo*^6bFF>?M-X@sl zqMdm}{|31j8K=g=#G^QwO?{||%R%xm*CVHhS3watXNv0etpS0QY5X)VD+uPih!~~_ z0rG^W>xeRRblW*_e;MMs6uTu4I4NW+p(o$bQDizh9b@f5Dtk!U9zMsvPvnQ^l&{-Q zXYu>;E`INq)0aL=y?fVT2eNXC55WYJIvXn=6wgz#r}F0bCA=bDu-NlLD2k|K&r1BT zeK6|m45a9B8FvwySgU4FayM@ka&%K>Dpv5fw@aw4^nm&_;(AI23&ci;!0G=K_;$m5ByP(I$8I z@TD9!h%Q5`AzRI8AjB{11iLm zSlu0Xj}X+KT7cv%37F4FGfiQ`=8q zr`_ulYI_mG?w>-v2N2Ev`4a!GhEnX#yTD*(sTVQfx;tbi4$>yG#fu3p zV{gX|VmiZZ;IASk*noK}x8PDI;Ha&ZnkS@t^-175T92AdjytD>LNnFalSpsdu_U|Y z%a2?R(UU@7Wz|#bfJtl!V0NWm4jsPv{k?gN2 z=7VUM`*qp;43*ipiqy3((G<~nU3p8fA>~yq7#OgcuC$W?YhJ@i z^t;te!&Um%9-+pkov=X<*x2eqko7|j+ajn!oxfJWbrZXrdi#~r?BIPcFH+~CmMgW& z(v^-?gTyKZEd*v2r@BZpQ%oEsb-t{lK{*KhRaMr@Nj^e{k$G5Sq&zo76df7MKjVlw zq5?V^urlMHLq6sICJT$%3kv3@?-6u7r{7EwG68%}+yWrBCDIt71~BXeF|53vlb6K0 zvLyh2O=+FIsguREfLziE;oPY1#w4)O_>%PeLUfb0{Z9>l!Me`wR%u{h$0XCB8RVjSgz!xeV39tGQ?R^=RWmWqD#`_ia%CWZd9xGV(cN<1b1B0>uDBYcV4tC^;6T^)>G$8`aeT%sbeWGqKo{swW=h&7<2nH&R)Q2}UOqDCK2&6IF=b?JuE2x3b-ye)>Ia zOA?`dhXDO_9}ct?dF{zV0Qd)Lv!!kXR}@7Z4YJ8u+ObWt+c`n$57k)Y>KaVuhdB01 z1`jxZ@H>xDy z2*&#C9Cs(lcYHXaNmBInAquZrp_p&J(xFEH)r4I|phx83p&)Cf>%wq{nj`w{DHx&@ z1KSFDmJ4>3j0a-cpSA(p6k7gerk))AMF#VahQbF1(i^qmf!?`pSJy$&wHxZ$gD7V$ zT(Sp6S>rmHi&ERopk9=&nlZJPR7I!bX#70%$k~Zm;&kLj)3JDy>F*+Wq7H{Yhv^dG zff){qOfMQV!(1<7;64~rY#dT)*v|+VL)K1LBhbQf6+^ztYGO?0Rr(Nc#`V}+;l9{MqVH_VS*)m zDJEbxippbTdiRtdZ35^!_Xw|t;5EDD_#sqeo1uft=uBuK&}od%`$F>9JF0E4L_LKs zvYZnHTvk)=;$hWdDymgU?E(;}kOAuu#a0a^Z1D0FFjZv|#OpD&t%Sz5K?pE=OYvc_ zvKujkEfTV?zs=7xx_iA$Vq2h)trV@B1#jg_9Zf@&O4sifNz_JFc|k3d2}I3-+7k-& zCg3HmY?IG~O2tlDK1>|22Q>3ADh<&T?cI!l$rlw9^^?a?6vv$dg23kiEVde?1B$Rq z1kPoQ^fvh`2uwl3CLgx2(h8F1Z^8sTlT+ow)6AvqQTceg4+k-$fX)OulZ*rw69^7w zR53RO0-_lq@tKF`?g*LMCk}T5JPRvPv1JW$)4s?E!+$~U_Ll!UJe~=FQYMgcGf2fMdaK=wyP>+3tQ-F#`{=yKx}fC9k(Xlo+iN;>vIA-IV>k4`34w!P#A&>ynE`xGbnjmKZ5nx=`7S6_d+#E?)@Q z78xb6fa1m}9L2)|TN{k&qn^iitY4r9h}nKv%V)%3woBgwmZ`g+FOsBhhYAbCk3UKRwxvk9S~WLHVCthFcsS2>dHi8CBY*`66HPCzGXx!m-U zPRdr;-h`uC7c+bUqYJBheN+(5y2l|VC}S|IlaE;6f=R2a@3m1>Zmt~O!gcG2$^G_h3*tZYfep>si>irZ$Z(~-S=g^B_H zxjVu$Om9LB@3YCgfg-ArWxXS;WCEcv6mi^o6jk(`__=yLKPcCmM$fIWbt z+Zvhtz#gSCtqO=`(xfdO(v^uTtq{nTZPX%)?oRG*6>Z!D756x;`>H%)sj}D#jK@YW z=-oAoYpzqTH$`=GrxXj3pVi?MCRwBlpfVE*h$cqayNLuAUiHA`L#WcEiFs}sE7f(w zy%a!v5XJN&LU`*&lD5)KbYyB?PxE}X=NMH4>R)iX!rVZ7FW{15evOZyqi5r`63k7Q z>{})K@)()B!$6HM0^&nqGDLb2CJEb)(K(_xULcRb{w7XBkD0-Z@$$Gaa%U4f2T|iT zVwd2FTo{L*caNOUL{n^gme6c8H(EcUnP};EEw5Dd{8 zXWmU9Q2B2X2z3c|U&++Z5(vrrpA!g;NWAqPT~eNW2-|UsF)lX^OGB|VDHfJTkB}xR zh7pal@Q1dUEr+h^Wi+{OQUy@>AwoxrQ_Kw{qjWM@MvoGR!3sQ)F7*kzMCB0?6bHc; zmbwFXi~#C!J!LS)vsS1D3UlRWwmTcq0v#1E$_4`#LEj7F<1fX+mIc`Nw#yLOWdE81 zd7q-^9dWo_M!$1Jqg3(SWMgXEE?D|a;HXY*WyP!OXpxj&e3tls6Q3oj7oX*qUVNU9 zXnzx*CGX$FXJPf9<1;XP7M~S2{Yl)f9EpGW-vo%r0b!3GWmu7G(h`-#tgm`n(` zHWX+!3B%%(fd2<yh+wXHUP~#dCz?CKjUf62cA{g^(R7WPJ zL6hx)Mtmh4v-F(C0?b5lbObfYkX5`?eVp#`w}7Tm)bLAy9%VZAC?04pZl<7FLKlD=Wvjso zGCpZn@DetT=590C(K@CW@5+NL-tvY)=g_k+qL}`Q?q(2TcvCLJ#ho09e~(z-TYZCh zG>3Mm$eO1GSZ`7PtJ1E{<;7XcQMr?UMh_}%lR!)nMXQvXns61jp_rSMvu)sMf=GjG zlgloH#PAleiq3$Oc!u@IbJV;|iLX^uaY5QC-z~yB%>4s3lc#xcZ>qgf&?ynKEzf~c zi}DJ4TgJOB$0-%HYNiNo@VmRf?i!|N2Q6aRDVecxE~dRo05+pJvZZI(CWX*U;Seg|`{2_i3fJ_;U~w)R%LQDeq7s( zm1%nmpdn3)&(7XMCPRlzIbMHSFauHE^wrgaork)4-}|tEH~5`gL`g~ozB*s@r&m8D{nmh;%7IsCKOmOzY@!{zFX zn2^m4zkOJlZO~CPZ~|@->INX;!Y+9<5X8$55_J@}m|NV#chgsZ_FKy}xl$q(CYZrO zEoFgpoVJtVA%gaiRn*&GG@F59CfhNit^m^)Q5)Z`K*)=-`t5aa`f_a&6F;9?-{~z$ z)^}q%D41}`90{DZ4ya7DG;Cgt>Yc79u8{jQ``}-lpr{m*4OqZdxkz4%;Jmj5G5QmWq!ft-Xys?J)=OhifSAe3lkJl(fwv{7x4ST|eHW7X6o~ zttkTP`PEEQ6ujk`I4JZ&FEd=;pNyL9LyPz+p&&K9Q0Yyk{BFF^%@()rCCM@w0&={I zNuet_jC~t*ZZCnEQQTInD9ysQ3#3d2%a4u5umRN7QQ@Y0-Y9 zlXb#xmx95aXEZ{rmIXQ`K=4UazO@-i6NQbpY&Sv7Q@?7GatQ(A4m+{YOuZtSw-B>g zwD$?#dZaJ`0#@< zW6N8LdhLK=Z=Qo+v7)-myiCwGC0+B8JcbL6p#|d;#^ExkNx}wZvQX@;e-DA0>8&ar z)I6X`UI9l7RpRF23W3F>NzbwKBzXYpcxzw+QEc<6n4ArQRAJ@&FJ z_HQ(6O9k)0x?ZHUo|DC|Le18asT{2YoAz@ZdcoJ=%f)lM?%&o0pgs|U)CI?6C+>&I{D>ou}E&)Ccz-)W6`@wI=Y3|O$g~d zoNX!rKkM#}l+X2+08t87X)<7x+Ddalu&)$*Kmxd@)t`Hw;=BzAP$&oXe}dckyI#^% zMD10g*LnD0>#4||lbzgSp7Rl!0`>`w7$Sq~x=l42jL*04;oG2?R82K+06`p&s^te2k!0_D9bbh)r9|f;d^J%$NS067>cG(8O z?75+|5TdXil0nM!bru*ZOHYt{j|WIopcWbD|Ejo>)Q#TBzJsxlG4Bk>9LqPC*m<#}ZIGBbsk6R}xk}gZO<4Hh6v8DK%RTqwOhIxqGO8 z0s`6V;>F2q?yg^l?LOV39IZnpsxWXVn~>moFd{1A$1T$O-AsXzzXfDd5Z{cJ-o1tS zC@pv}y+y~7!iKR4CxvY9wOli@UY_k{l}5@M8X=jHD(S6jMrxYZr0RDb@|xbU0G}t; zCqgDOQllsnBNO%4-5ClFpXWw<4CU3Wk|g}e`tC=qfO)}L_}^A!v<;ha_CFfb>+&j-HZij$Th#9(~Dq^!XyN2@9KY+-~v$Quw16+16b zxrwI=dqE%>N9)ro5ZoZWJ^3~UYTQZN=X6xBC}B^_7ls8?IXH+bfC%Cq&L>Bj?sx_XY{g}>-M_k*{q~|;7x>8TYOz$Q@Qs-z=1y__a zP;f1#3zX2N5G!dA>PCOI$K@}Tszq`98>A=wWiuz*E|9t5M!V_;(f#ak1PXY&~a44;$% zB0cl5Z0tA`Yblb?BnqY`NgjyVY=uyA{h7D=zD+`(5Dqsd1NbztwJSNLbU()6I}ZZc!#vUzYK1 zEz{V|JsV@D8|@tlB%^dV8`c-n6WSr=^J3PAF)6qJzlN5r$YF`)f$ z0%SRYE7*^88}AoU*A8U#UhUWg#`e?GT)kU$9h5O_$5wRjl=Pt@cT34swBCi9!3z84 zf!T!FLTGLtqDpg*Lip#UTfTGM9NIRi%PHxfr{J#Q{;DGRIgv0)Dn%h0-*MxfC{ZF= zEQ@dvZ8A67A99bK2JUuAI|i9a+xA-plDskld*0c}$x5`fcXV~-V#K|pj@m@56}d(G zh*uFZDFPiL2$v?wXlU+r8lqKi$>((%YWr@dAi0I#_Kf1BCpe^6>9&* z`~__8<}cfU#k~Xe|0;iJ*KqHDp1&wb_;1NyWHB#)S%g9VJMtH2F)x3SLB0G%O!e}Y zBN~Z%`3vN_o4?2j{w9A>m+Iv&Wkk=v$zMcvH#YyP{KZxN|1E#X5F*3R!Jg#p^hR!_ zUz#F9zDvvJ5L!$Q0+|JJl|o%JTZ()n7c>v6yF4JL%A8CLdR4yg}7&H$5Dbl$# zHyS)&q+AN|-AX0z+1ANfu(?&;qb;kYu}ee^ZExXrww^A=Hc66JWpUnv_wrRRMsp1I676|Ij$)Rv6c;(#s5|3(@fEKB_!HsIxb8;W1TybEbyOZDLiI)C}Fu-3`QB6Ry4~=(NVA z1BG@XAw&9IEKvesAGoc zU-&X?94RaURBRFta58WKtWSDgNvw*Ay&UCg)+&#f>7Ln{;pM7#DD@8a0}JmZQ9PJX zZF^#Ya&QUq+SxEKnXQgEc8_My{bAH#5VGBEYL}Faf}ALS$w4T9|v?-pvAa zIXYb0hJmY)%3hp^UEE{db3(tzbv$P#W1=1*as~o=lTQjI?7B)qYKx$U2?*wz3$mRp zZ1B7vKP3H5<&wDL9+4TVg4_EbPU#D(eWabK4$q`T}V1^Swi zsv-r^%t&3Gf>q5(vBivx!p|1MM{|$B_&NqO6J^z}x@6ekAjTk&VLclEfX;k&j=SAD zr%)jsCSME{Q^TC3O9g7SES4dGvWW;L6GoXn0zO-h2u#=>1PYhYL*vmd_9Wicyag2_ zUAB9=7&4u9Gs^CXP4^Lq9l6=fJ1B3Y4WE{3d{@Pffpwl^%dfii`PPt*bT(<*L+KzF`KRr}*Py4G>-lnVdf`bVX zNVbQ|O#nA~o)g;6kcL0bF)rm0olNW;avV2N=w9y#+=OjnTw)d^Z%Yt6+#2k7-R&;q zAuxAG3R5@#6NQtQoP3&b?>6_->I*)z#mR?WgmvjFXyze#ft&ICyke5+1x((1HPQ0G z88UQ-DDlX0rB;DHZqy22wvmDzL7?PKGx63l#UbLpTTD<>h*S|R1eumloR4*-=v_&z`8+S^;NG)1a(#W3ta?is zr#&a*eHwwh=R}*sc(UHOq5E~6<#jk|PhbPL=))eBC{>d0knVQRdA&yzZrP=KSYg(K zaiR?B&9sh!J#Gc4871oVAetHFD(|61GaM+beS;SBe|5cQJxIG{Cbn9j`en;H z*`;%hhYx0@Yd>v|*XLSs`5~U$wc^Q?JNm-E`i0N!8=0nR8bGh;{jRtzwItMYrus5y-v3 z7$8JUK6-{a9}B4GaLO_KGG-I8aPl|;RbT94CXU|E-T6fv&d2EH!L_9>-tfp|I!e1Em(WDZOJb3n?V@Ow2_%j-Oy$UBi7r12(@D1)EKE0Qn!|)` z)|jGOp=bx9+Irx&yRy-#LJZm^Q0?sq@cdvhs9nb|?GBNt4BOf5j!X@xV=osf8+eHe zFQJIXze_GNcdI9B*8?(7cIfpSyg4*j>tc&b4&0lg{#e?se;(0Tu#O$voOYy)Rrad- ziQOu}AxvBWC?%;EG^u!v$r2rUA(eY?*z{V5U9y9f*C%6olf4fz^`c6XT@I+J7}@k) zthuXH7Zb$741_jAo2YkCF_p!*=nyJ9`Q#w}x4AL;W!TXs1X$)x{uOS0?;)-4x|=X> zgit1C2~K-Qa5=YYIz(V3-2ub%!lwj_*vmrhigdk9t@esTVLLL~9YSUMF6j3mT(P@U z+52wrQ>MGGxAU(I8NELH76h`zQ#3i{(%S@nn^5sUPjXDCbW3pQ>@YAii^Z5iY4lHW zZ1+yg4`wO&dryDnBU+cZlL(}?7Bx-y1`U4xpE^CjzuW0yrN8dZ%c=07ySR*Iz0jJb$WB@-~P{bdTu=Gb$ZWWtp8%C=Q?|@(~~X#bEoHqZCr@;W`@z+ZQIFYDP`_#WbODh}+n;JbT-E1Ohj68E$;R-q+o87E^! z*C-Xoy0LJkqLX`=+82g-uS;dq`MWDdYIs09+qoQR-AfR?mqdFa=(O~^f~eB zRudG);uRG@wqiy#J;%S1vw?o@$5!q^VVXO4db@5e- zKRqa7t6tJHY7hQnZyh6=b*tVoGA0w$i4=D;2@Gm$rlE=3JAkba&~5>!B|NX5#kT}R z5)_B+LV~}J_#`~&^L~Vib>jq;xLJk?C+V5pbKd*5@2A+UgC8QRYS$|>L|uA>`lTVx z1*1U7=01tb3IrY|4h28J0-<;3k>2nyd z4R!2CtAJvJm{#I)_hB#53oA=86E*-}vLGF%pK-3ZU^bqT2uvnl#l2sow;V2m@b(}O zPg3tW6j9BD2n_AEN*Rl|rjV%@98`Hl|Dg~nTdW!=56cu`cteijY{=|2))%48h)rOe zgt+D&mp>=SY9sW|O_`0SZQF_o%qVfkURaN|8|$@ITn)Zz-=qm|{l_~=XuF55+3r+3 z_Png+9@U-}s%K?oH?Lc?M&r?4&jI?OT=0E=DfJxR1Ay;b1KH1hSx*jf?C(45N3g7sBz%tnRM_efU(|UfBBmmtsa4+a!`nfFMJy+G%1Dbv( zSkSgf5nGy56jkE3V!Sq)BZj+e6GD0q(;F2ROfuxQmBL$Z&f9XiPlC>suh$SFL_Fgi zaWZWTZXz;}TyOE3a3wUhN>~ogrry$OiU$oOzFeMtg-iEPqD>U9bP-J}vTP?#<=!af zOO^=WTSNeY-2?10_kv0wo5D1bYcalndn*?E0w9+pr)-<+bznp?JdBt zO)9>P4&F-P3PH!NFBESQNA{q+{ctAezen44zi#=x>}@+Hpe^%x1Y4qo36W^- zXqr%~u~9ZWH~j_j07;Q8s}53IeO&*Sy99$~%KwL|ZTCEMxGqd~!FGralY~v)G9*s| z7SCgSg2nCWsdtiSVIwMc7Sc=_=4W?p6%P_DH;~-6iASGeTHCf(9o#0pa?OjChXBR* z=E=FZvvvvbh`Cz^at|O52vQsc3||o1Zu0a6p?gM=am@mbK2lF?4V(tC@3TOgS&Jp`m-bCBew3t6G!&aD+NFCDn-9ch$43pB&CHPadDSpN(y z`o$OqK!_q`_(|m#kKhF@n<&=AgBc;P-3sRn^&%$p80PIQ+qg-kRB@Xf2^n_HSLHdy zXgdtjnLMG}Z!4^K#LK_ zDjo(F`x;(pPwvz+$9-z2ESJmo%I3GAvE8$lYgk=Rag%J`l|kihe}gUo)+8yqG-2TF zOOPP1439;8w=+Yy2o&F>XE!M=@HUUOO)73@^5g_*0%-U)6$go0-3urV!EDx#iW;xe zzh){3dh_J8ZOnAgkD(dcuC|OqsO{WNazt9EkbZo3aJ(PX6Cf5o^ri#qw~ zJ*#sbYlezZ%+Zr)7Gg`cGIL0pvi!)U{7##KI!>oFIa;KyPIJ-W%ThE;CufU={o%El zrK@wrbF);x$4S0rs%K&ur2Mj&`VHs)gT;~jiTuvr4Rl{sAB7p%HDUMXG4*q^bJ4?i=G8ur9b27jfVm97qJ|*p@aXj zp7hne^gT$q;|Z!f(?Vr=bNIRT8cuQReTqBQ{DH&sg@FC7*U@1IUt@?VdkhK>-$doH z5ntgj@&d$I6+mw|Vgo9?50WtvKjZH9WoIDaGTtIFi~8Xyec3=i=#FB=y}#h@kP$!T zEs9$5o&fu?evf-PA*grpp`Oj1lP2X42J?PaFNI7Q-Jihq{ z9AMKM;Qrdb&{zCm8Yg#GJYhPLkC?$H5TbvVz*4E@z|#5NtC$oW*#G%hDw~Z zX4m5y$-Kx&W(TOglk^N7=^ruC&WLQTJbz-OMlxsM{SSXa|MH!^^d&$4l;s9%h+d3j zW|0330ipMoewUHVC)reS1x}X^A4Tz%oim}!v?m4-vEuX;1`8Fu_YWfa&%G}Eor)Bknsz1Ti z(^kI@lRsIekxVn5m7)YNZY#-h+XweylYfUjO+#Y)+C7fqgTa4b9;{X1;5Hf>6(Uq7AalW(?$*UfUL&|IrDXn}8iVvzI!Fig% zni2gV#q}dMLAJwPIK6Ks8NmlWfF1?cg7?m&xV-?yd<&jJs&B7G@`&*qeQg>?9cySi z{bhV-@E6f3v;U)1zI*}p9uDpLw_c^Vf9OvL|2h9a$qxzd1D>L-p-Vr834x^)XH5pH zl+e){$z(&WZ~q9XMquf~{SQ+d`N?99WZnd(KYu{ftUeA(=jazIUmlBS{yTVZCm)|P zp}z%;?ptpo>B?hBzC<$|@hO1_z^6b=%shZEc>1Sw?6;98?dBEkt~j$1kZY=eWi3dz zEx(6)hvT*_$CRDi3Ob0Qb(KWmV0^BwGnZR(X{L4~@sp9vSh)Af*BQyoxWY)L^sq)U zos49j{3*p9pL-W~J41Z$E=0SKo5YQPqiI(oELx=v)IX^RNHL*FpbY93pMzBI4Y!+Y}GFLWg`~H2p@~ z_TKcDKD@8()%8mDuF&@;*uW183Tz-_DvW%bG|J>F6MI+5TqjwJ{e%S7veO0_KIx~f zHd0snl)CvVN&x@i(rjbz?e+E@G>QP3?UKd<7wN*@nuveeeAq6TG`?qPa8X^L%9 zHc5g{af>c*RBqL%RI84>h^Rs{waRq#Rz7-=pqiW}lBE_dG^r;!$8%`Zj99;uWK>y9 zfD#tX>)F-_wBh+2cQ-%Nij9@{R%c?_z*)4_Wwfu5$9o7TT)9Z@@1&5k?3jadk7?mKoIGiYIi`H5~EOY&r1 zVOZzAIsFP*pow(jp#Ew$P$m79fBBw8#s05if}R!R9DIfcP1nR{^D<$+gK>r}LIC?A z8L7RbwmwjeSKT11m}YG~>7!nrQ>Fuc*JL3cgVA=vE?~(aB{m6y*Nr_Aq<4<&?LFHZ zL9-?ej+i{DoQ`Qto|`j?LS7S9fb#C8t0;8v);vd4Xn9@2P;f}tMsYmG^0r!xTL3^w z?%9}|OVv_R6Z1CdZR+!2o;VW_6Sa21L?Eh($c1psJmH^Gq)Gha{R#Bm2gtS!@$EGj zW%mQ>-97H#7E^1BA=51pyAA>L3M?>;rV0j%6t`}XZ>-3Ig))g-#nlW9Jondtz7b9M zP9)j|2~P$`=(!l)FrY)UKyGyVEO>i$nfX&Ww+KayyW5Rqs%7BjeWL45f~(sft9pYZ zFBLP6#A3!W>G0_~L@R}q_Q+&qsMqik6Ce{^)=4)`D+eW8sllc9T^_AfGJ0xkZ^;A+ z8Xg3?Cu<3U(!JcAAf$u9&m_n~>=DA;#Qu8uq}E3ZOH6 zsZwqW@H3Dn$?@F106j)6_LGp~1VdjnIG!mLv@gU#j6BObl0$rUj^aUrzl9deK>X6) zlDB20fW>V$z^UrbNd48J55x_QSyl*0~f$e!2YWmb{oQT|73+5<`pdD-AlBNa2}U4;>=6DsX*4dgn-;hnMqj&|*raioLmR5^NeCY6f?52l)w4!wzCRqW)ttn1Yv2A=2naBG8 z_7fSXbuHkZIe?E4g(=<#>1^X{fc=ZmtvW)ze-TZp$0-n%3bYo$x23`IE0nqWVIPsh z7K4}Dt;=4)(XAfJ+pErgZr-9z5m#$P-K)^vPWAQ>T8FQ7UB^{44t|w>I|$G4-W~PL zLXjYiz)c9Um>8iOh7|7mA|@OmwK)*QapI_Z`Idc@(R&WGP81P)sckbz#uzHL4nZB0 zJDqymllcM80$(M*m`A|Dd<$&MG}-HW5j2>!Qd3D8z+^&Kb9bL-V39`urq4QRGz;jL zOmE$7)S{LS+nx;ToOCK6D18COUdwwh^*;CnwKiEAZI~;erusd}0E!RFwvFAJ}+Vj=K$g{DOl&bis zdw$Z4sXcA$CedOU%rwywpu)LSw5V9gy204)FE9)qKFnwjLNa$*^ zq*%C{>ga?a6Bp|Oy?S&$5QMbodn9~+={YL!P095 z$c8EJG<|gFvV?Tq9V=_3QKLz8m5;v)BEw~kA=u7LkaMP>i23{x%^V_hqh8Wg*r}m96%rU5Z~)Fx!U+LkUY3tshCqdqzHW7 z#$O@ADo7{>KPOg3%9o>sm}Z1c6jGUJh+-o3=*)W|c#@d>n2PTe$WI8hyxnrvuI;GX zWDvE>htN$J&l@>3@pLPuZLRg`%{<=`w{!eWA(>8l?t4onTzFkxhoFJ(DV^Kt(d~<& zt!QiyP1y@`#Pws!yjEiZZ)4}xVr#vaxEe`p!!zn8_Lcxm8x*;FbOIAzd0@0O9QMaokuZb>7ExUZ~zH3fuNI3hMc^z%z7? z)K=?m!di)9l65Qdod-5b#gZK5;Li{lZ+OKUI9V)2n(!1`EO_-^_TBx5I-szvAu!nv z;)}M~#y~>gj{L)R!X57$5bPAgQ5lt!$#RRLirWjv)+3)u6|#Q0MhJ#BQ7+4O`|V{5 ztI&l+y+P&ti>!uEInKMn6$ttufQdjfx5=K{6{fGQH#g@x(Ec1Q;ekM~MPHDMiK6P>3ltxSkFPOp=JA zomNb;L#; zr1Cl{cdlvmB7t_Lpx8ZPVACu(vQwM|7TXHf44YU}`7|NK$d#i5)T3=f&9o!@v4?@os!_RHC{!(T<~oy(ual77VQl zCAM9(>)C`g0t7~rh~l0o2DnEGBjJ?^1xhnq>Lg3ZJVE8HP)>$`MvpX28eB7Bx;s_k z8W)ICgn9QdMPQJ%9mS~ZcG+NoI*LW*LhKewOdJkj;%3QW+i=(j4V`+Pnup}ySE}o6 zQQ_Vx30FKySO-1grXdTFkcvW649L@RxkCq9SV*tQ+am^3t zG#t0ig%ZbV)#%v@G^|~OxzD;T{q(aq><8k>Vg1_);q9lweV2xscIRTk$Xh9hw^h1t z0&TlqLFN=9cX2OCdudXIEsixMkA5qT|9J$u~^<{ z@wT`2O`8OpSob8-z3W*BX!<5CkBpw9Bo%McdtX4DJ`?lbdjMPYSwNMAqGwhVZI4y9 zVbaBkQMB#$@HV4l)Touvx^Sz~i9q6`iIZu=rf_mdJUwO!cYE8E+om!s;1>=}Qn5`x zUDwLadno26ojRbJZQ9JgcCm`}EAB#yPfrGi8>nnA(yYBuL`jK4z$xkNZFA5ebr|1q zw-gDI8-}<9&~5sYCvp##V!1-VZG@J2NRID5kQO4q+!}oCk7^>1=Y8&v^~w|n2rw5r!Xp68ZFuVZ%t|pgwcvI*_2wiR0M%Lwq{QUP>C2DPQa_b>w4n-f4*>mTfGwQw-$2Nb)l z#mc60O_NGW%i*I|0@)H5vf5G9mJ3h43G6;?X(Dj%yWbIWnQe(+8JtP8T^oTey&hOX zT-uyLizY;-PE$;Z#N^4d1wH6bNaVWtb02^71JsJSiDO}`dUiV$zo zmA83Y)@H6jdAHmuQRz4n2~cmy!XSl)~mvdK%K>v-3>4&MZnONI1^e46>5 z+R7(9nSXMXhdU^xZE+GUM1j6-TdC}p;bBbM`_>mjGUWTYdwhI5k5ab`rBXCu=Ry)* zK~X2x+uH)ahOEF=h_^LD*V{ZHRH^YPvFrl8uxIc%droFJugH6Py=c8fcmF<{lRF4j z_RZZGWREzw)=j7pvuQ^qt+HgDgLFBS)lHK_x+U9D2xJ0Y28AeSZG0HWTRC7tV1pUF z1ha$-g~3C)P#?W8xo+U%*3dm7cBZNdJwf-rOE}$yG55?qR}j1T%3N8}baZZWFus<%6s$L+ z=MtTE7IURNFmJNuTFHa=&;|BRh2iovo25 z?mgTZ7skqWg2T5}+d=W`dwZzjl00mQ3uA7{>Ne!FwTN%u6Gek(q5RgN>e~aLwR>aM z^sPV#H(YY>(WER?md|QHdfJVHbSCB#REtLMZ}1p;(efLTnSR-|jsx=bw=h zk~K^e$KoX>3e@g?N^mokM{T2WzDYrG$cvCk+fdt9+{@dHQ@iKeF1$Ua<3ZlQ3T7F{ z%!La{TBc2R*ox3$1%$(9%1idf`L+=7jdoYS?jz%e` zDan8~Wuk!F%D7xY7SVz}Kt%CHRonj!b8y*!}aTLJrw< zxQ2k%3gU4v(S!{oOx!E;#tMr<^^o2s$L@PdO|o%_g!W4wzQe@bTA=5BXi&Qb*!B`C zckl4iVXAUS_}Bj=sU_0jx(b3H$~M120K0Q8Rlc?afOZPh*IZ882DNOHE^ouKwh6A- z)~j6Dtf8;%5{jKZVCjsfq(dgqHnk=~?rim5oC%qT%I+0`$k`A+K+3Xi3B^Ju!>Yb3 zx!h%AeKD3k2g^BUu~;Ixxi_E7T3W<5 zV?a^FjF=FmO&DlVq(xdq5fcWaMU<9C5k(YHvPh-^m2=guu`qnl>k?ig*>NK^T9iIi0})2Cv~0`y$|M&0 z=VV(~xJCji6>h4Q(^QlX=EdL+#9h$h_v-cE_EO|F!L@h+Lj1{8QZFHwAeNx8N&>o+(e0a6QRfv>fan%#ljcrLk-qwd$|}nIDf> zTsJ_Vv;B_EJlKqe%bbZxCP{53eHO`)LJTfeT|n1Q&OwI~`2%+$t51cSIx$i_UO~&z zr2u*JRQ@iMfPR{cWGy!08QC@*Ya_VZ5tx5TP}Ic^{)J#9M6No23lAHllKdt7G>*d{ z@)ec%Mk)A+>edKLu!^u1Z27}-g2CpH!X&sA*6d)cAOf;WUtJ!+c41*JS^loAUR3C* z=|vbDA&J~9VT_dgZjxR`NT@fP%o}*ZX0fIl@;$A`eSvRLuB|0@*sz_k z=vmTB;pvp-cS_tgkz25X!fm}Po8%=_>h@6>bA4gQWlLGdFO?R}eZQcbYowBiNWdAy zB|rB8vk+7ER ztER!x4dz0R%FxH?Zv{i{Jkeb(vpdgP=h}7P_)G*D+;niJ^i?+>!p>9+=_L@Cq#7#) z0l$e4ZR388T#1Vrk9@B04S<+7^4``6_mjK6>5x3l(TP;{ql`^g$N3*Skt(G^|HBML zf_A*!mqQf^P60b#NZ7Ij|G64(8+Y)vctYDo!vZXtj@YiF5r_@&<%*UesLrLYK+$Jx zMElw2^X&9$xv_Kza((5_Gl8t@8xIa_io4HDb*{1=zz=-8818aJz{Z})m?KZ%aJux@ zK@Rwb6iVwMzBfsUr&G5tk*{#c7_pgiFsqs|7l+%h4E@Dfh`S4AS?|P_-@2aSuAn1W z>AO{(s{$EcUx}%B6jYrIf_-TPHS}LBnd|OCmkBt0<63l=NsH>EG$M{g%GBLgJYLs} z$_7d{*Fe4-DrjBNQz1`xkvc@Kfi&)==T#E3_ZBj}NaJ!g(auSg8Kbe4t$e zzWkT7P|#5{>HmeKe2J>^KR3W;<@(N~q~6Nmyz>ic@D+LLiFKkNH&lEbCqozt{9I=M zSEte+dIwbLRp46GCHqNC=Yl(BHyBrO; zED5PfKBWEuwe3VI*yu2Rn&8k|fZMF5-p+LKpDVjg3j1az;`>D({vZtgqOj4gMcsY_ zMEkXJci##EYozk;1rA?Uj2x=4QD3IQH5uy5gym{vNVzcP8zrV6Vcw>veVyP3xn$Qb zLKq3l;Y7Puf++_(Iwn!5cd?Y_vLvuLDKSfY#>0rqQvb(w?9Sg~p(+?D9fL~T2>{cb zBexXXKz$Z;mm)G!(W*7NtYp! zon;u(FOuI8+?zjOM)9R&Zrjqg0TjQqn!2|x!!q7Bg%%+P|FdX03VnWy#BHNIKTeT5 zZXSiW%izwpi0D#K=prpB8C^jdyZ5ms(c4y-(CIZ!Q*F8ugzRj5kc&W-YeU^GODtwc zO{oUtwt78d3qNi934pTuwjIgNv{0|tDqimby{?yp+V#L_CP&40cS+5S*g)Lg`jvRZvJ1-n3o_2{rNFN<`SVT3;BatM@oq@-MR8Jv%a0dzD`Aiv7S`xdSd~uvs62vfftW^m+VtSx$7Vl_S*!b zd6GbXstp#e_a7x7_g{g)vLxXkFwrV1QYpgjhts+bMt`$ca<1kkn~6I@J${Gm^q^$7QF8cs9wK1o4H49>2Bl5< zsZoyMs8*^EEf?gq9mRzuHVxTW$BAPdNu`-5Xx)WcbM<$E#FSeL?2`KA!NUa za5k-h_m)w9h;c!p0IlP5y@j&=66Z;qk=H4hlsierr#l%@nAFrqIl6t#^+LU_w?Vi& z^3n}6_Bw3{f(^E*47TW1?YKpUzSXdQ8JMgTSkf&tt`r~X3xScw$6TZkl8gD#IUw+{ zU=A9}NJ2w@#Vq|lk(vMW3UIqx!zO+o1^EM+Ft-7>=GP-^*F}YGHcfMeE2Ygxa@zUe z8y8DbEf{fd0JhpQ-$ZGyyVwbfW$Q-A@0G)2`2wn}1_HXKB?!yvftn6Rr*6AnBZ}yJ zz6z&EQWAs9SBP;$@^oTny95*-NI6yTaBLIeQQ^9`R5wHf3>u35*q+FwmMq(KmuYlG zVeTY83`V*8qDh_YOLqS$NT{upb*lVAnrax7k1T4EWInmGfMZmZ<(^OYpi`qn6&^u7 z2IoZT0g4Dr_n_nLQs~ir)=<7*rm^!wWb-Td@_QDLvwJ3=?6vFC4u_kIzcfi#~R&XkexdZ{_)J;@aV@REhi`C|D_N7S6pF+NyJcGi{ zFmIYDNk_z3oj{q{3klRxmoNZnFApKPMU1`#z3zeuIMT()Ml(lb|EO;g(1HIEE$fDI zoU6F2jF&GyBB0c7;qL;xim#R6Z6jr^bqQ+L^HBGb_?nHNxk+NEfl}Wrdv(XFRqhV? z&Q6l;QHxlP7aY1p6;hroVE3U0cd|(9L*1@F@^ig+=^WhO`Ox%+Vjmth2y^xoACAPy z-8BzUd^`ZbPI@0-G@}X_&wU3%i%WkcdHYgA<2Ank@X}k+{`PhVrQHi;H?$iHc^{kt zWwZLj$48w2Z&NoiZuyDqf}B|(de%1LxPnE9Yuk2G*Ob!UM*5Lmx`a(F+qXhR=?^!N z`Wz{gjXjUlQ)-dQXWvhoa{X9{><}f@9;8+EsRwSCDr#@|>K*7i>uF4N{2@}Q!vRAF z>2zx>+0i>5r?Pa|sm~b+`RDe;O|*;f;=m~gDYWz=WO`@;p~~k_(Q*plNZ5A@Ei2-~a^YukkdtxbH zPriy&>$N8oaCi9^zzOGzC({S-N{K?BN zQ|$&8lK1{OlkD`bQIWsm6H;?#LDj+cN&O6I;YP*$nG0)q_y<6cdlq;LC0-8sG3)u6 zx}EHpzaHV|gGVT$E%ZfhL1Rt#-ptp7o&uOx`OtzH9ld*K&o@Bg@%!N8HQ(g@^X=Af ze(I}97}y=-z5B4eg4?>AZ>7HTlJQN|wcx^b=nQQGlpZgns;i#{7vqlLCvPsi&ijk6 z%w;m+(Yv7C9U?pVWe`(wHs;#toEsr;OwIcFk@qu)VETOT@W?4Nyj$OpPeE&nfo zsPzy{M*b8)={Rz7LC(lSG9VU@{^Cwcr zRg>CxlvKylNPRejuXoRf=H=V4sD1bH^?UvJb6*5Ic|UG1+4~c_;4q(eYNT0Rzx=7I z0PvJ?c**?{vMWd4f|wtDnADZik>ekakQxL5uH|%*HU@2mQnq0OpRT|#dhCR>&6Ba6 z)384lI$R7GH!r1~Tg-MC+gX(J)wM8Ns_i`Sb>Bke8+ebzJMQDp^HkV|(WHJnO!kQj z?(ps(*FH?7*$0iky%Ry6RR$P82xEUP=IkhTQiJ{Hkm>9tBlgT*cSEF@twv zX8b!9i%@)HnLShXedDR&>wTmaQ$y3Y6G?r9CP|UqeBcl8ih@E&Yk=- z$#-K(UDJ-#&Cmb)*_*U~E~jiQ58s+gZ1PqvivRr-F!=x>MnW`;?8Ij-M^~E{BChfY zD5PjU;=2Wr#=D0wk!jw&dGql;9Ql3^G?zBM$#-8tMDaFiiNtRNCDqp>vQO32mXB>B zISZ-O4EYyr!Hu7TmltTyG9KPdcJt|3sPvU3WOwY2&hB}LXJsI}ev87Dx~$_6+Hf&ed!*%CmC#*s>8N0|yA~Xw@il)#7acLjEL3Zpo%8hg){M1yUOXg2+ zGaxNCx0z44=^I2qX%eB@p#~_mMGJ>**rAfb?dnlGzrV`i;ITH(Hr&FZX zX89sTxpq+-qtw)G6Nrgr_~In}pcZ%eN|k)TE;=;&UNM*4emy0)c68 zKd4UUakeQUD*45isbj0l;_Q}qUvfSAuH{cVE5+B^xWayMn}5eZfH57j+JmxunPl;+ zI)uJK9fk@>^;PQtLD6ig)y=b-g@v@pcuFf3%h@WTWiuK|otOj_Hn?g-dnxo#>?Tu} zE&-(tK~!lrL>WhoE)E^IIJzw^E)UI{{342@y`6t;>lRy7_ffPOoZ`uDGwM#V9swG2 zX={Oyo#2?PE(ix~lbs9YE^Q=|&Q{^3v24KOvNlVh>F^ZHo!DZhbY$bwQMyYXE+|GE zMaCjUZ?iKAV!2875?!r(p~iT(?LDsJUQ(KiNmUM*1hzlG4K{3(;S@t{LwP*1@2hTc z;h!YKvmPhabv2TEVj({l@T}OKMRwI|eaP>#pj~bw!|j+o3X{}S;T7jFDXDN{ud42_C4#s(j4NK z?Q?pguYO>A`zfhp*R|rDi(CIhWgT87Re2GqAwx+WIhpK+={G^@hkxg%wyUZciDW+L zMPdFhq=i496!tS*%H2!2fX=FzV*{RPFH6+KaMvy^9$@N^CiM7uBBl1zpN_@Vo~q!AhqT!c$-RDG%F>x(-jc zmsY3#Wg3s7iFkyHPieDUXdf_-%^OGRx2>ew_a(KTP>*M1XY`=P?xXgYb0^ht7O7!0 z&E0>8Gk1xdX;!knyMWYmTwT`dqexAdyY8PP~ci zaF0ytVf(p@ys;KpjY=o=ReL16g4Px%NJA2SZU&y`LEKL`DUF{CK8C(4PgCLb-NMiy)SD2Pp>=(QU$H&BPo;muFp!>hG57 ztDgf%M}C@z-OVi|_3}Ho)Cb<>o${;6Zg}H%s(AZ#QrCjL*7v_BbqRHcHrJ3{G4Cj3 zEZxrEqkcpP3?6&~WVHVM_c+DN0awfmYSm-@d@TQ3&rnAFR-qvX@$Nxi7BE9Q4-J^UHw7Bf+zU9^bQlc$rK@;uooOCcj+ z0pM!)M*x}n#vpiz{uLpAK+n=e2B7N|1*$!K8Xp7gbOgV*A_cKVvPKF8p z-4&3v5eMVvIZ|tiNe#jd+>`$z`^ZTI#cjs^#+br)LF3~l@p(G0yA77pCYv)N@5NM{7p-z98?w<>=jq7D?_g|ohefB> zgXVvo32Dt9&)KvFbBY_ji$6buv=}#$pA8!+JaI{1^w||o5^jgIHoIzoR_P8HD}I-+ zs|FSTmLjBEd=X-)Td)bEnExw<&sD#pijD2K3 zLpdF;J3=b_1lh6UDk-u%vTB}j4U&$jho0Z3^XHaNz|Pe^bbRxZKK#-zW5Lee=Pw5* zwc8o~nUDoI5V5nU`Dhat?nlCP_#F9PZNE~K0%=93zG0yWIVnhzJL?P|Uer(OTZd!#tf!Pno7 zI-8%M1+BjmX!*tn4P{Ej$CUHow}>Vm8z{N-_dv2qz})`pA#kzp21xCW{x%PS;j$cB zv3>LIB)jg`0wIQX8pa^^(udolpoG1oPWV=ZFWrYytKhb77!Bi*+*RNrxr`dJj)$@K zi#}mI(b3%e{?pf(@$51Ttel0f>Uuv-x(nTs1IzDFS@iwaVYwRwG#wA0E%#hZ;nmyX zso~A;6u#<9e%ir&ZTKe`D^qs#`%W<07XZ~f=*fBQ5#D|t7wK-?M0VAivq}DXJvH1$ zh?Dr+C@Q@DJT$reW9p9m;X~+)dmn;&eMTyl_9EqOpr3R4!(=B90w>uArU0^U4wG7h zaxx2PiPOJ%QrRD_;KK`gf~93)Qm0%8(nb+Z*Zl6!D*SAyYQAqbs$E?NPpysAy>L9K zHRX_2+b|R`pSKd8W}@Z%c6gEcff&k>14U#vyvdtK`#uUg*8;kt{3$TIlJ>V~zV+2T z_HcCba$aoC=NlRyb~2^$?9nr*JN)rjYPkAhO!W$6)jS&`i2a#HB59+7k%W^Wy3HTj z@S@tpwj&-Rdljuf)_uPxJ974`Jp7#OxGqSo?JY1Lw;jIQj+sGr%3V)0FS3c|IxU-b zdR{sOiNvpCTz7vrWk1F@ou5A*sa=I}bso;2Y2V|VIz8Nx?9lcIKd-#SPc9M(Ul&C~ z7h;;6l0kOP&)paSJ_jT2FoU+g-}qAi$^*wSy!Yy6vJ>J*z{IJ4!;*e_oNAvfgwI#{ zAfWmeQCe4N`ldMlN;xEO_LE2bcKG7Zeot7_duw9VjFxPJrml zU-(%{)0?<(iL%##hsbe+4)Ldy^5M|4N!myW~Ij+}}4l^y*I8|={=e|QPn zIP|9t_ywB0b@U5D6KAj%#qXpOr`2_s6Oppd*R;dF{%?>{SIw6&>K`lmUA{0YM0u? z5cs=xR*p!^&d1o2tq04@1{zS;qFo`W^z(n9Ty;9yT93xaNE?P&n$(xjRI;y{M_~zF zN5glab%prGA6}1A%kTOMvBS5oPsI zJ)v2m+$$n;$Kh95a|>hH4^VAw6bQFL+(Bo{&SFflsl7gubdO2MvmZselNLkYX%L_T ziTRpN6OtVdy;{6@Y~30X@Z6C=-Al#Jnd_bfYj*fxME#`{sB{UlC^um>3T?h#W6)Yv z;G-agAqXI?33}f^-P*xNLAKJXp%XmT3j~eAO#MuyY@W_GGHb>$IH=aHvMEfVu;`)_ zDLg~k>M$KBw4Fh!J(700BE@#u)Q|I_F0T#o;kggr07h$iKE)RkhzIsooPe}7!e8}w z+)d*?v6SERCGR}d9c2vt6hiM-OY+hz+PC%%WY>)O9iHc`)xhZ78!(L5Uq_PN$ z0H+dKS%J>)9}-xzZ+dPAq}fqn%{O2GkrQcPTh4~an2M)K#R(@#;dvBs`w6&()83@W zQy<`2=l(o_PpU)O)RVmEpeh)2$(~1~&{|oXf-Co1| zUF`43ZhaG6N1vVxX}9F^^%od*xRNNTx%GPr59M~?VfVj6?+jYd=FF2R{EL%#=XML- z_kDo?ue_F@-B0jyBi7}9>(0+)_4(wia4UFA9iGHJV8A@)K_E%5lI*t6%?ExDeG3|v zzr@dj`B=f106uHi2C}Us?6PqNF7m3$%(rB<&`iY=g`fD%C@60pNL!J91hh0>h8k0T zfU)`&WG8?9rt1FZO%&PK2dsCjAoWWuso6r_Tg%7}&0Y^_8i))%T%c48slUj-ynB%B z9!DNUzVY|dblg!2^SkiOohOci=64p79hvhuoNW0MvYCqL62o6Y#r}>EHK04B9q7FT zm%5YS-yV*U6{eOdE3q2c5h*mDKCgA^)B+q{h=-a>uRTgrzTU zq0PyI@4+Ac1U>ynGLHKBcCr&@F68y&>W?v&^3s{e^O++^^4z&($Gq8rdajHkwPGi! z4H#B*K|4}+egbJDZb|a3cb<*8xztf< zmtO&|ZDFGBj5YlEO(UsAU8yH+!dXx>V*t@e=ufOMdF37S_DUPN_X{5;JKPNS(H*Eh z{EV>pByKrW$Jf#0@2Bv?-&W$DHeLa7t#ffoYKwiV+KJGsBS<~Agw)cDc(ZoflgR1D z7fJO*xrz5|!Sa?92PEc=rJl^{(Nu8-YR>F_!m%PUFaJHZ;~wYhpdUm;!0hlgipU}) zY5Lu6Qeg$=Z&H6KuVp9yRiriGJ_@jbiZ{BFy6-uVd-@nsIg+}2=5%222jZjF8OU1W zw!Z0!MCzV1fYNua!7$pL1pDWAZ$%o(Q9PN>po;&;%Y3b4xU84&;%oZxP5vLzULME} zpI1kA=*_#R;hy(My>}g{No0qXrIWf*v-&mTE_{rh*-~<`QqK~Dt zD$&JCzma~~Se6J#5T#KX_G1@3l|zOvQ;Sx%2X|Gria$%$E>)awQlJ40Hr#Jhv#a&s zE|w>A1aNiHiMX=?gLm<))ABZOLPPMJz`bl~^gf{$7 z(%nG{_q$hv$L&IkFM(8lbS8fnjp9XHaoCUta1Uv~*Ea|rx-OpSOQfcCAFhM=Zk{Hr z4drj&ApLjkj~Yjc&vw%9iHg0Ptyk@4+R!`Qq0gmBbUo(ZN4DLEoG9jF6pbWK!##2M z5-F_&bxv?_BdJk)`*eNTzf)4EuJxi?IB^3i$nJ%d-04CcR~?ca5vk#K0d@;N!IfrF zkf}%w#!e9B<7N|@1(*J5SyONXZ@$>8#N2|D*nLg-Spvvmw;@neW(Q695^D4nCZRed z8JAK499C@Nw5xxlM?{NdZ-HZB$c4Z&4t8ng2WaRg_A_FY!e&tnqP6r z`lj7w6p>L36IoA!o;HJdJ98+Zuj?cpJ4$xlAi%YgC{4)3dGOdMq*PM4ZxrsUa16d- zGa{%$fNAyTc=r>8h9<4lWOmsiAX56;XnwF#UnUPxDwkDm@~Tq__2RH*hw#=NG}x}S zsKMxHA#c`xh#u0U7F*ja7o~kt5NiAC+f9Xq)Cpu~Q&(`(4~LzQt&P z;JCIEMx1QZDfOdZj;&cRBM3JlB+e`a@Qqq%X$Kq!88@_Y~Usv+Uv7=t>5TJ6f zjw9AyQBV za$ry!`Z8woVfT(wm9*d86l6QgvNoc%#Oaiig?P38o|L?bi-0nhEUSpfNRkJl{fHot zYz{B1!mSe)5f0KvqV0(2qj$#FHnYcOI_Q0X8g=}&3!hK$7t&fUXWOY`=AyP1e3DD< zDS?TbkZjXesxsZ_=o_Ta2AKB^yvS*~@(YFN$$X|Sl&Z@>Ua;++{k&vX2`Cj^`Kdvx zey_yRp*N{|EhGzewD~nq<OW={MFC z8Ykspx}XQ5UHl;wXa^|7+vHoC-n7H~(uI~x>CO;D4~@d(xA3>El#Q>Z9BnaF7nlq& z=5%GDixFHSD}`BP6Krz|Pm+x#DO#Hf!7fPO$^7fh+^mTyA z7sw2PLux9OT`%b$75TBxq8ICAyO>=RR3+J@!MCrInWm0_huEGX%{Vb(WMHN)Clf|e z1j=A&!j9EzBYN4LfCSc4xVC68d@RaiCH~kcc#y_nd=n9UO;Uf;V!-6T;$f)62)Sx& z;c71@F^YV{Kx1<|zb%^fw>6W3asCflUF)ltT5p_`dSF{1S2YY)ZnjGJwxsoNS`YNaG5}&w4`B@ zJU_lweu@^0g2D?Bn_X*c+X?L)x|o3&B#RBmj*&p^qNtb^dgzd1ELv7LLw5WF6lo5{ zbsY`q5@o%#6Bj^Kj-C-YFnJf=sTF(hP2+)3o&1Zf-wvE&GsfntB_VFt=I!aKn+`VM z;>X1xWZ&G0!ea12zI6-NC2I!jl#X;l-d$Gr|6)^8IL0Pw`%i4DefaOONv;2h&520i z7@Jqad%)%$7|DNP^R>~(eE^RSH94;u{qgB z%rx297StUhL^*jMo=H~|@IhS!p)h)IU5CR{oSZw)$hPY&6IFPnDNvrI#c*xzq=ABb z!F+6^4x#%(T&8cJ8ed?lj3P^40(^ZGxN21jskP{;#NzT*kmmQii<0d+#2uGW1jmB0 z0Yxyf?bZHQVCDBofWJuHHB$h~j}~?6$X~xzK+r(0|8X%yYg3Kq8dNAB0Ly;Q4%9I~ zI_bU?d+9!xw|mMx1)Isb3JSV$C^(fcgSy>G7I$PTIS9!HJK;|qB3~l=I8}Z!IPfY* zVzP$OuK0+ka84mn-TFFLhBXjwgIg&FAsd%$t)FuJ?2)et0>m;EtW%JD z`DALWN#V~5buN6RbW*Cy%B81b3Cxb2tCs5h54-Sb`$2~P86CKxdfQ(O<^7CGFRygE z1b6~Oy1`K8Y(e{p+S1_$%mZxf=%z;`#7Qyk>@Y64Ynfsz597XdGi`uZn9{lls=ScE zGpzUlAL5WE<_z+*)cp|9^9RfLVz+oTy!|GQj#{(63*ZJJ+9n*QFP;rrcg;Xoould@ zq$6l&XLQ&;HvBA3uNJyvHz=HG8$mC2LSr_famfvQm^qhJno#Cagdkg0c8;<~<5%sz zS)JnrANv7i5b7O}_zI+;B9U7nAo$8+SR$H4e`2#*W@!t^s-!=cx}Lf_%PiHkbe;8Q zY81J4w!k~<3INu}fuO8e{PY%Sb|hqPjflHM)oq*P6VZ}uD^w+J0C{a?UBQ`#v1rYu zw;#i zCs1oejgD>F7~2GjXeL{SVN2#z>)MLw69uxKbD;DDk=C)7a9ckD{g;o&T^nFM|wE>r-7Pf?;k~POD!VWlU7Ugl$Vw~AbIUjJ=SiUn5#GtNMmbg!%K z;YIPwaamP*uTv+8*gKFCI0=F3k&LfY^WyXSQ?<30#mYWNF}@V@_6Jp!-z^CLOTOcX zu=Tmf(e`iug~iS_V*%KHsdk+>x=orM%O@_zdfe%~@CHsUA$AzA~htKD(%{# zBw;CSFse$K01LMCGIgW|BBV;!9vKFw@!2p&ySsQ1G>~m0opFHD*B4TQ`fI*P^fbOJ z#tz6{cHnRPesQ~9!rwj&n+XoCjFw4P6H!oftBm?F2_H&*vpkw_QE6%ueT&Lz?T&|N z0gmdmVKOdJvn&-?_e_@&PqhS=*h*( zsCBGMV5M9HCZUQ99Zl#u>L*>wvdx^?avLrKiMVuGdM<6C>nxUQ(XZ|fgYrXE>-T)9 zt0JSj$g*we4MK0dcr@HLNISj?vOH}IP5d;ZSOYD-VkAF|SW!>9sO*4LUFJqW(H?}m zwn)uoYmmvsv_PeurI)Cjq$?mbW)pRDkSp2YUMQ*s*!u=l@2e!~Rv_e?=PC03p62?= zVLo(YP7;Lfl`{5A$%Xo2`(8|8kKF7b^>=-v2#u<28*pio6$((kDUP;X)3-FBMHf%|0k}!2@F2#uo>i=rnsf(Eg{halMkY>ja8)@3i>kH(;s*t`$jL9w+W2Gi58EA=AYbSn`sBYh=H|y6U zmMEeOB2yc+sDD;|Jp>$_DJvw12B~SXA!|2{9oudnC##T3g1|e&Iw=<9q(M3B^=Mnu zq}3Pz=h6)IVZnnDKYrxa!eGt0fnXx0yNu6<*VUQzUN%mQ0n2(Zc06H}Z<$4rtN=k- zsMZGTeN=xoS`VmN$PJt(^cZW$V zc}UtCsOSVFS#D+!MLWI%Tl6~8-j^Xi=cJc%Cpdt10*{Vx+X>vb1Sv2EyK||l75K(M zhD+&pzPcO3+o-4%DvY@1y!DmIUM;T|}ZPusA6!4BJ4wI$FMV z3VdtJy>B%ymjTPxzPPya`7i@Ba1kMj%k!W-MK+cvfn`vwYbSHc#ILz_f9xN&wOZM-G{g25u|k6OExES?g?U4ng`I2%Vog^gK#u-j>F{ z?c6b|tkpy7pw&^0uQ7YIA^1v(yk#f@(laa1~Llpk=DEz2z)agS1#7 zCKYyqxI9%B7ApX;_nqwjp&Ntp|DGGO@#nyeiIe~6#wO4o|DU=su;T+aX7PRC#)Qtm zjY*+_8?zqxe{^HQ|NqpD<-zU$)s4jpiJDywMkN!e+lBAKPbG@6pf6!_pf=>*r5Dzo zCzsU8U~Q95Nm4}Ohx{b1;cIOGa!Fx))}~7W`QELvk>1k2QhS|9M@@$b^5qm<=a=JBurC*fOg4Z-xf1=co&Y_pcZ8KddL z(0=t}2nlDy*LDI*{B+o?)rTe1kYRYd*c>X)5!OR3GqC$BZ1&Y94pi(hO-(^7y3$dP zq&6YlRt%j@zPLaR`*BngoO`E!mW$}` z^kv9U=leM88EK7*OH(`Jf+U9SYy>4$;g%K-4MjpUHA z7SZJ_K?0ghaXE@3?UrcoC1jh&v-P)jU~SlfHJg#qm`HHhSAwLLif3`iScOZd>R-Cd zu}H*~0j5k8s7Rmei12BJhX?av!g{c!(8+}eTGHwf%7Y7%eCTfLEFG09ZGN{%Fe#r7 zhqkz<#wN|1Va~UpKi`Z1e9K76YSy1EG74vN7r}}PNx{i0{QDW%@kq|uOn;nok;aF1 zh9`F!%GbvVbav=|ya-K}wwn>VMt)EbMv4qoS&3XjqjXa%EpaR)*_BFGOCyakq>@Dl zC5+g8sSS|1iIir0Chd@yRyET$qthxO`6ymY_CX2-`8t8YZsX++AF_)e$ZuOh-8SyE zts`wlu}a)JPYq~Ef2!Rs$d-2Joo$EsDN~8t1P)&>(vHf03EfEvc@6R>MY}A}e{D3c zBN1P4!LzEq(bOE0Or9=B<09eiUol-qfsob69`8;EwwgFNV9tzd<*pX`Y9CNG)posS3x$mSS4|n z&m)M75oAiN_oObnjXJyBnP8+2oVzTMo3KDKZ-+u%;#@4Qjrh*p!q;g^wNbd`(!iAt z93r(DeCFh+seoLUyVX`Gsl{w_J#I|S%XP$$`a;=#*ns?=>fzA1W0P$zuAjWWnzG0S z48}%7BJuKQ4%2l`q=#U_IVmPI5`8-!cQIT#J-g|4ytG0XKz1(NkuBk)UQ@1l~jX=t(XLyb5ED}+{2 zLZ!&7RZbNE(^YJPcU1^S_ViH zUmH!UK@l!vuk0C0xolRpkTwhCwm~Iq{(*Aq>a-Dr(`qGi9YgJ$j67I*w*pJo+_Q6% zxw-$~hIS478d~iN45i2pFG3DY6S1Fo*$?weh#T9K6~MO_Ray+hIc72dLwF(NG99bJ zyW{p)9Q5zhV5ka4nnzmpUHlzdsfY4EAq>Dd>48O0d>wJ{odFZKp3D(na^=MM&y_nSDb=a|03BFWfsgwN56d(Fv@d{tNRDGjUGj$38 zQnQAXn_yr}xVOQbR0%9TMFEQi(ZNF3cacMaB;DpQB%2_mx4DCM)@5EnvIkThyOVSw zDY@=ri_=oUr=6bWQY6n@$rEQrYKEeXBvF+p0NN@<>o?m9jRec?*b$j$iL~S8$FdQN z%apZsQhedU`Z^X`e4}{MN(!&>Hs37D{bDr6Nkp10Xzq=u6%zPX;XbN@Lh$6Ha#_J> zR47Mn{#K$`EtB!Bl60%dw+MoRr79K0d}GHk936s+SjEWKwwop@)i-uUzD>%pC6G<# z37VkIw_s1cN&44HQQtI)!a0DPuQmL|_ChQzSQQV#yq^yh$<`u=aF@;wPp}BuhTbAm zP*IX~N^S6`y+mtMs(De=Y9~iC_>F|j`D*QqOP7}0;>~O^jBhqqogkK*%~m9~BDrQ8 z3a*$s2VXqNyCN_dx}?n3n+s82O@;bhoe=p4Lt*A0OL0Hjtmke+Y0VOOiyRf3lPJ0Z z#Bh-hY^o`7@x4*M^&XPW2Vl0Y5R-z?IOTBcB=0~8XJ{!UYU#s~9ds_B#V!cY8Tq8T z!!?6w%0^deKak37zR55&2*QN8WGk?9dHij$plfpgEfr89TZeI3zIg=nwjy$0O}Rld z$3g*cplIAT%Nv;Q(8-3r)uFFeNt})g**bgLg?B-KlkbQfsJBH=j4V4yhGj2KVE#W+ zqC6gRiWI|qoU~*Y)x=#UPqRr~)T9XlLK60pvI@v1%TwC2rc@_?Xg1aMHQ3?sJQk7( zd57{LlA%6GD;SABo3LkJxg2~Q5ww~nVr_Dy94VQN;9bYzQi-kQZMO(nW$42km6I|H*>E8V*&(4)Yb9)vkJHlZ zUu*ULT4Z{FM(~JyX{(Zbb}smzv*>RD6*DwsROEM!p*5 zU~QzPU2H|E3r|PWt*Do+KGv*OV@?)y7mP*P$zaJBN}P!%uL?CCaYCY|nO&UfYUmBa z>C%-~YZ-WzWRaY%mLzc<9)frjJ}hWWSx8QbvTf)}hiGa|0NHVpu8qhim^7tITZhQ~ zR%Eka78yPUz^a75a-|x@NWBd|*pa4;Vo|PyT|Jv>2x>YEX_ z%LT2vhk|T78NWeZCIN57ZcKSFe57X{fWj?aXvWp}=s0j;y?7hIVo@O3g)+W!YV=2d zR&XejP0AIGmshC;7rqi_!O0c8b67CRfiX5RDL&M&MleKWCyA%Zxu!{rNh2lmY-AN& z)}q0?;F`gBX)H$`%>@%tt0^)pQDy*r-y-~{EAVwE6avTgz4$o^n-8WIdZQATWzl7X zx?OlaqLW9{a5fr~7F*Oz_$eUF&p!VLKwF!GEDK~BMKfXNC`I}s(ru%;#=0mEwe(g; z6oXuc8}WuuYf$`Tm`H;0V^gRCj?Dwbby7cALs-EWXe_h@i@amQ@W`SMvJId~*TcOn zl}bR0E?F8(kZTM^S#1GmqPVqHpt{&gO$pf!#Q9oL-h%#viPNT$dIpbaRX+6chUGro zKhha4cNt+*;B5yY^|sz`7Zv*^^8&v}Qqfg#{fmsYDFtEwqU`1XCPQSj(m)hz=XxhF z7o-f}_yU9gM1zqAYsqW}LZIkiK400f)3Ejgkmh1#6T#T52DNOw#x6nfwepK`dDKw5 zLnw-Dg|s?-lWG>$bu@L)MhJn2|^OSC8fsy%-#S8ywwE3Wllp+WG{svJ5S`wRIuB z5g}^Q9FW;~O{(Bkwu7I25*_yen?;dbF3lB4lq@I5#ATlq=-KD1^W}>K*Q|u{{x%0Su6h!p|7)O<6H`c@$;NU1`*uGOw$@C#PL&NgA>`90+96#>sh6^t{% z#uWy#FkO-1*HE{w$4>kgHaKAmQ_8~#xg$jSMnLHzXw_wyr`BEZZV*HV%c@Ql9L#GB zQtatc5uiH-VA~+8J4L3L2^FsYat*+Ad;kTHgEZF#+_(!>&hdi7`FU#2;*oFAru}az zB;edMece}R3C~1yeN{vZuy=j$pu$+Q$9BSqW<*^(Smg8#UTim%AGZ4Nbz7mwwhd~> zQE-(zi&SC6Cf=DOp`c9|k!_nvM_IPq&BknO&C^>j6S`X0jQC10>Z|6UrE2M~5_|QX?77w241}a!=T?P{YV^QQDFeXOg zZO9>DOpL||7peMT)x-bE7zF+Aj1}|sF~%Un{l6J2qFkSkX0_i9b2j}Qg7ST{H3HQ< z7)GnWj&=kdd#7asFEVl^%Ed(WyK17LS2q`a}E4f2nXX5CU-=xw9CZDdUy6}va^8i_^UaVUykJ;KA zxz_?~-y*xRc^jsWNLf!JDyXCYFs~tzYbt-c?gT9~}QCGErqDZr6L6z)7L#1@TrC^#ku7vZ|M(xC!8H%1Xw64)%)Ug~Etaz0h4uZ*D3{}PaY@`F2$m%! zt}%wR&}*ZSFF($s1HtxXY8k$ga5QFbWoXOrji{@93reM6d%oOOUZ~E$hOW{MrV{lla@1zyONNTaD7A zQJhK^QcJ6+5lXIJ=iSa_lj)(Qb*+~DwE^Tht0!}5JtWuvfp@-8{~+NhaCN zPpOxkit2P=c*c(>A;Xqu7bArau7MfO8Su6D#G-~TaPYNfq>ydL_Lk-qQbQ)Z7fvJ9 z_Oua@(_=A(XKY2BO_vUWijUV*!<(>~IP6mZ(^umq>1ESsg+9H7zjvMiA2&2n_ZbK> z_qU;BM?#1%sn03U`|*#YdUBFM%)DNtrW_%=%O3_1?moYVwU^T_gt1*`Bho96!|SJO zP$@F46R)?Ro90L|)sFoPE&a7rfW8jO5B8aWrn4VogTv;R!OrqR-25|0pks6&Ux$DH z4ByOp037Jp<;3sHVfN1unRMPw;`?f9OsvWQjt39Ha+_^9%Y?Z!bQMBgP46$k$*cDO z-V3p_!aJkT^UYt8+9(BIq2_*&G4AhF!}0RIc@=equY>?QV1;Rbn+UOv{}=@Sj-T=` zISr$J$42n}7KeTK`dNcW#*kCj+L`P=uj7fA;TNlxd_#8g(9OuaIG%?(5T^EC6ljNE zhD*e^pYs`jAKD&;)?kLQi7R;aeS4nGv{Ou4G9bF;2t+PK5h-8KAm^2>fagM;PpD%` zTxRjTurr$%Gq1oX;@<~B8K+JmJNBb;#8MJR_G$N%`uICi9|6V0Yr2yile~z(V=328 zl26(?jZX%l-|)Jx$&PCTxv?*dhx|fe;?*x;cK5Xq@bslno0-WaP0u5B-aTX+hHJ;*E9xG41Pwhqlfs|<2`PPi zG1-kTbtUKR?)*Js2Hn{SD62Z*3Z4xrC%bj&uQ0J;A(S^`M5%K(Lds1K!hB6XC=XA* ziibBL#hCM8Idx1=7)idBRJf4r_@Zw>_gxf`GGK$Ek}{r^T*SBI>XE(;;X+nFiDs-N4zH`JZA*Rik#(WDh7K`*0(vN9L2=JQ(;^-G^Pabl^jK zr)5J6sqWK6dECu#^6m}1IQ1$u-tZ89>KvXuHC~h-xl@fd_)L9g6cTv}wWZC*U}M_F zlD%{e+PbY&kD1B$y#1N zoTNrX&C z!F_oe@5FvVc#slzq{xmepR42f`~4OA$aNhajt77xW?NoqW8#x`Be-4Ri4M|3c? zM|K?I_sxrF1FeslfMhvg9Z^Lrdeu&2*H*18X^s%E<2z0m14kr-z}g{5Z92K}yviI!H^D#!?omG0?;0J&=;D z29)?6?9}a8xt+4y|j2wU3GaD00r(8FX*InCQ2*3XX-flA- zZJ(kk78@gQ9Ci^@YdevHaD`JHsJmKZRH(*U>CXUU8>)y=Bw~TEek8QiDz>OcH@~3t6{!WA}+d~7aMGp#LhLh-4qt0HWT*P`Ysoz0WF(vaxu?dhRCJp z*o88R4^a?Xzo0yXxdf{vV)|hzHWZ;vVJ^{N&BhAag0W~Mfx2DBD8yjgX_GC+lKI9Z8fgH-u>_W9 zn3PMh5V1i)bg)=^Jmyg*B5fY6tiLAq?Yx-^)onBPoPvUTk%lT11N!xw&{!Fj`L6}Q zl2sJ;GgSE9)}1Vrib}vpux|W_RMMIR<@;sM!QPf_)abItQSJ`(?M{?zDr^9!yL4NH z9D?f^6C~O8kn7qT)^swB>maqILb*FZ8X}%2)XZ1pWoJ8ZG7Ze<69AtGrNQXXi4!E2 z9I)o>i2Zgg@+T^{oiLH0i4E7eHwD>!(Ve94lm~d{xNIuS7^W_rW!u0;XNfXX)wIJ7 zT?gywMx-JwoAR}Y&Lxe7<>*8t+b|JpZ`~8dqYbb@d9K@Kj$A5P_T-W~g5(~^*L4>YZP)TnmmzSHZatH3>j9e`Lem3L zx|DU0+G7nbA{*%ziTP61sI%~Wsr1$aJbWp@NvnU+hjJ)CG+`~*#rOl0afi0Jnh8o4Br|>N}7GKwNkFl0+=n7QE-L^*(E^67VLYQBGOiNUn=a-m4=UWv5*0q zLEqU9n9%A6E_lH3oq8Is<7EItfU-3Z{>YQals$?RUU3KZVk^<@0;;M!Jae7E%#H=J zYxYuE<<+ADzaDJWXcSt@)@fwGmNRMbqRr9}NsH10$eE=d)ZK0?q@|%imq<0PgVbPG zr)F9g%Eln$5t{3WZ*$hF)*}mFv`2i~E$_CY!Iqrh$BqG^ajZIjRfExNxh&nL_bNbd za9~w=g|u(GpK3NhD&s4VzY6ivH6vw|*U9_WW{OehB`!yuwydba`ox6(=u7K;dTW@3PN>l#C#Hr zxGbeu1kTs^7QO)lF7-nUK)XR*qV&LtretSe^S+@0cD$fpE<}k^&$Paz_5cp0bsmfb zDS^pm=UZ*;^#{3#g8<&{$MbuHqkJg!-y4lo0aydRe zO^8xU6F|?ISFuBVLaTw94ThY6^j(Iy>yV<6phN*VeJ0tq3o8R4yM$h7J_DP9?@_#- zt#|B(`D}?U0;R5`Cgf>@Q2MbE>&|+Zff}8y?M{1)!n$Om2i8hc zc)Vzd5n8l%)1~!BboCNlFdcM>#4>TsZ5XRPt(1JZqSyNskm1XDJy;8(sa?txsqy1c z3|rzz$=p&mYCt0nVYw9LBo33aK%T*QGb}|UyFK>h@@8QNoe`kR){C9@!V`n^D6bk& z`27n}X8H9%b-(mv%f$`?oV11>tYSF2!iUc}TSovMI_^g7`MOdXjPrAW@MYr&+g4FP zS@8nOD)@aDiYdYXN{iQHQ=PY9ALl)Ux<9y;?ER;nj?zcN_R+O%SP1c1FYi|!qAO83 zp*Pw2CyWLI98~TSKR^^a{y7wwe_RBE=N3zxZ=Q`FQdf`~aRaHf6UoUJ>aP|?b>g}A zqsEZ>=?-N4-c6`(!*qc92GPFDxa>K2eQzW={~8Jb*_YF9u6zY@BQH~T&C_e}neESm z=x@{D>E4}C^~3Yv_RJ^AuH11PNnMv!KH)EP^e)~`UEM!tlXPDJnQZy)(d=}cz+lfBI_t27b?1;;% z?#J7C`<797^vNpTi5UUNE={4vqjzINf27A17u$ocyG>od*hXbvBt3E|t?tb^Q1Kht zJukYK?3B5A0O;siQYUqw@NSj3jgS9@eY_e&cEYqfrL6J%eC_#j3#p_6PVtQU`3bVy zcDxGm2VD$zy3ozNiUe{7%p*1LBeHEA#hrB_lwZzI`m+TPd2oPy(iOC4uY0%graOF0 z=F&qlk%tlS{vKgux(Pt-?S!hL8Sf$NCzguMsjJc7U6WAi*MBDU{3oQU>F^zx`!(6s ziNmTU@U%z)JecUv4viH z)5)Kbot1^I>V6>3DjETeyDEC4)3?uuk`)OQUiskLsPeqeu)iP5Na@n9nqG_!?C-Rh z>{=aPdhnkILDt@V%;i6Y2h1*<2+dbrB+_1a5c^w8PRkO!Km+XU`{QL17wNuWe?vG9eS}$Y5`~8h%H-3CP52tRX zv|4=L{wsGOh6`Wf*{W8u>#lnUF%O&(C;Bi#+u=ia7i`J_+AQs$UlsJ{(kq&mM!>??RXHJ>TM)PS$nzYTQj@ zGmgO~Ql&U7cg16TI2ii483n%Wa>L-R5M6WVHqxZ{PkSB9K6bAAMQ31KyB>g`G0PEZ z_2pD^*;EPjAIC%LN2#Q)If9FMpdVj<@(igjYgG6<7-;&_7-U@2d8F>fVMQu%ENO{q zUE+V`r_FOfGyeSwQZHj{@%KMWY85zg@ss~da>iDwExR2YWg^+|o%q+RCP^(^w+Rbu zS_dd=XJDV#f}?`gK5+NpAZ%u@55?@BRQ^Uf-Yd23r-=l zwsI_*9{w0*-7=nshjRep7!(q>Ig{*$qaQ)|6AmeyOfZlTE&;d6Kaxs*iR{WD=&#o~ zS20-n_v>gc#~!9#qd@L;9Z7cG`#`NJi`F4>pPJ=)L*ZyjJcLe__f0T2(6V`BKDLwfckY8kezV@ z&1?OlpHai#mQwD4PN04+C<@2!COc;*J}~?xi-MX71 zbWO)`m!3ms?P=__>5Nq<=+U%#jHcxl9S6K51s(ktcgfU-FxG~@C84cYthnJX^$7UD z9#UWa31VWKAHf=zv{L1wwxk~Y20D&M=((4UR`%aNg9}~Z5ZesQ+MJroi+u)@Ix(5- za0^a5+zAcG^e;e`x^yAtwk%TT0s!}4-3HUT39f?MV2D?O+h9Tz{@=O{CR#wk8nbHWRpqErw9_mj7iIu8_NO?{KpkdMU}gW@jrVcgTvi9G;T zN+YDi)q={WurUsz0r+cUaOFkK)ikrETXBH<-y@ZE9@&1!nf!3TJPTuJiv8i?{Os&# z&&d9dH_@DZ2rq3n(OgQK_wF zp7_EAK;y-=5WV4M0J^`H)U#>mp#-Pt{g}fParZBz&VTUVsYwr!y7P3hn?JsvZf!*i zsSgg2dIZPd`rx_?-&REAPU4%Qt*4NRtAMYQZ^Np#b%v!^^2rXr-;RP7HGz>=Ugyso zImEp*;jt@kAUo#axgf*CNmCR4sO<~iqL)nKg{+B^d1xZuH}?gzUE;TNE*PhXtIfGrNtFXn&m(PSyYm18}6%#t# zJd665CDXx~zD&AVLRkmi#qGq$-%4ulXke500W6gNfE3Q?OLpbc)Eas?k<@K(p~Vs4 zxbhVs(fagC#F_L}4ejiK$0=<{U*fIiVWhtJmDJWrWQWUe$-ewAq)G{|J%H4gzmT1F+N&hj=kqWQw6;2${M2t>g|s!3$xb|(U@rV1 z<+eW6$lIT-1n}q9^DINdc!wLs?X@=$JIBvMA>TgAvq|NYc-zPp`AkyeNy?l?R7&aGVgx2Ih_3zIg<%N!iz5f(aUy{9qu)Scbc%S z!s-Q(cKvANzNCSS#FHs}QxwJ`m%l;X&tA`=TfdPydg&tmoZgRYyJB$9noDR{|BRd4 zH3n2QoJA|MEBSZIuH1oy1iI2H#O2ztMVew>j3SFj z(zv09D58jhfFdvqPs6}4%=5!Jvw!c;`W|Xz5C>DUI{%#4KHvTQ?uWJ3UTf{O*Ivta z^t%Le$4_1XBHso2{)xvKTH!3#gJC%+E!=th+4po5=syE-#h>D9f6c$E6jNzFrS*TH zy8UZkgZJ&*OX)k`rL-*#PQLct%yZ%gsgCdd7erR^Gn6JhNNHRXFW&t%KyD{eRy{O;!Ux5Bv|*B4s9?f1QXuLUqS4ZspBUK-%4V z4%NT;Fev!>I^OwEl!+B&=YY`NNQmC~BY3CBg#i9HHO%gl(!w%cs0I#?T@B!?CO{Kg zo`V&xSx)JozK(wUAh^x{A|>r<+@rIou7IKf9kXos!yu3K3`j{4>>Q zv(RgD$L(hLB#n1jFVCQfCIH>Ie-Er=j}I4+V1Of@OMAgewysiOo2j=r5@CBtJAmu*rb*l3-?&gRmG7#E6`_mlp{GW4KahM_8$QQ6|xFBh^ zw*BhXWq2$gwx{)wqj^=(yK6_rHSDnIWp1vi1M{n)ZP;l>QlJ z(D*$@-n`~hR3G`qcW9Trqpx}-o4@adt5sb+60m>nMygv@LraNoe-*mO`T)!UWxe7$GBf{Fsr4>XCt2B?!yN@DD*I>=so+>27h@ z^lOnwO4sg`+uIdfx`($f0mu0>k5L`lemS#!YCAvkj#5+hQChqp0blyBc~raaoy|MF z&trl(0iyZMV@48QX6En&{*2AE4;Do&{{yuo=IfH8Bk(uj9QJR7{ z%gXx=rT49%G*CyAEng`H{PSLBKvPkCYWloFi4CQ%86kg&w*MXj#*Tp(^UA1w_HGv7 zrSF#wwFlK}pMH)v$9<3^o^SpWr1%h2`24`nLz&r(ee zXA6D1o>w!So=f>U=O4i`z62oaA6eaf2cz|IcbE06wCI#RLn-`&@z;Yc7*$&Sa-Zc}XD{0;}@4Gt~g|C^wg7pcMVM0Kz1&!wbMO$qp7tLX*6rx>} z2@ztAHB}$h`W?8GQ zX93T?l@~t%=n5}}&-Z=~3L+0U)zOt>>DGg>#+nCN8?phkUGXIAlQYX5I|Vcoy8jrIq20vuO*7o}Im&}7{}N=N9X@(d>C*K zy|kT)WtTjQnQ$5=hQWze+Ff%grxzC;f`9(U7ODrWrJ;hg|HeC?g$QyUV?m$s2r@Cd z>L`u3{p(MG)p*2S+w&0Zk@DZbh4LPw)T1RMuk~IZjz#A9(6?Akxy_evPd7I^5U zUx3AmFJHv7!4aN4uFb=Na7(a_@f&bTSie)$#mAF#3!QRQr4uzGWm1&1+^J86RZvUD?8m zM?_^=6X90pKgTr7<}Ts-i^2tzE{0l%d}p12H1jC{a^-A3e6LE|eh*OkAg8i^C2co0 z?p-YRjsHx|C$E6)zq6a4tDj``JSj^>ub)k0ypzslkTKZN!Jh+$mYhDPc5u!YXHvQvxqr^Z z|3v9u5$xyml46#26&xc{J{gF7HWL`jFw{Qj75+XW>G_))1j^6GOogr*F8BpZXaYj~ z=)`p}k78b`uDu>uO@eE?yiX4R-V=`?4ceg5<~i_Yxy1RV>szVLSkB6BeF36K?|@Zh7~ovg@9RH;`axvYV{F#0`xav<%_30hw=N?*^>>k5o6hm{ec`|TkfX5CFozHC zXVKK(a0!7@hd;_tMre+Aeg`_sxeo~4J?A{{J~+IPyCqk!biZ{b3*Vjfu4CSPr zm#^e`_sO&9cI!n9@*^`Q(09{^dwXBnv=RtC`Rt>N=xNs5+z3kfuNNG`OKjjHfN8-a zy!qRE?|_h={3eHS&wYp1ehTMby9$1D-t*dTJv(+Hoek3KPhIQXd(Ri0t=R=yukKBC zV)pmF``UlB)9+_Dcz466zwF)T8b*;p_E~fZ_n|$I@`z9UD^Q$L4%MD_yLVSU3%N85 z8m+Hefu;ZMB1&y&E6N$|6D!-{EppTpdJzP$y`V){s&bL-D$cLL~{L+88 z5S&c>M>^Ye3vBN?#Oc5vpvL~;CofWc#@?Uc6ug^pa>qz!eCsbW^8Ql{ zD)fsnAMl|?kDkwp@3#R0?wgGUd0vkil$%B9K9$2J!73kw3;2CMMr@yX(SG`=WI;8Z z4fAR$nfHdMd)zW-$`kz=swx~o41`QLB%y>S26Qp|GeSBRo=`Kh$44R9shVTPJ^%Y^7 zn9n+Pg}0nZ@z>Pk-}7G9!N7lm(0k8jJm1nfZoT*l*0tyLV@m;Y!%M7#hW!_S^cg5F zzTt^BitigwiSO{|hg#|voX?7^0!2!5b?8m^b;HfQCXZ+KAsQ=TG0LvGCizH&3=bQaZUQD1^>!--MC>VJq}`(H%@40ly?JEUpsOs#eZ1`JaaxI)_N5H z`|TcHoDre#Xa%fG20Rz#6v6v)2rz9o9I<2NBEDAIJ@OT;0dzxI7RLD6r#wUHOB~tx z)_Xzd;qmVY_<=vT?bl%O-kYJq_gB(*DDn?9anY|>t>20Pkr^Xsd>-0Mq(QsZ+!0Vq z_Wul}`thgXPZvVD(FckF%cW^lr#;Q8&s}>q4|89pw0tq8e!rviz-+2r&5exm`~4{$ z0<|6A{C7%~qbXgpoa&7H8b&#rHJ4t+`aafDD@O5YfcP_j>Uvy~fC!c%_|N>qETEKi zFH&e5WYwbw8xiNPI3Jd*MB2izjc9+jF>pUhZZh83>mGH+l(Tk_$Tmwv0y5GEj~HU^85r2Hg<0> zw(6s7c;rK*V7snFPHZl7#wzwLrM#ZGaXKZr{SfDe(p5j8TCq`G>E&qLI4h%D;+5u` z)3yA9X-G2?M4VRrD7(@N#&rp5%wsEje zx$F?liB*foR1ov}h@80;+Xmfj%5+LNg$1G1jxt&KBvSpHzOf6X&oc-Azd5kI7Z|Qz zZBD%whSWkI*3Y>G9^h!qp82v>d|j~;RVg}@)+*#p6p_HIkgHPId|w~B3$Ubb=iw?% zOzy{i)$k%#wj9ZPBOkgD@8GFJP3_>VKuwLsV`6U8Duqz~e21z&)JGb+1WMec#)&uruk0GjJJKIyb@j6FowE!xp1ICE71ZN1ynCh z7p=7cMQ>pU%;PmE&EZ92GEr?Q6DuH1-vo903fc)S@<9lim}sw-i3MT&j%5LxcD++; z{*>(t8JhWfDrts4x&d0V%hgBCanPbZR4oM&14QSF(_nWeT(S9)7C@=4pVnB$CbnGt zz%Lh|u)35D)>%R(@OAwm6l^O%q*MUP3{&cF&U*@&;4Wddu25fJ0&H9l!S9msECUN- z;QHSLtz||5Jdwuf2IsdR=C~XC7~_otCO1~IYhKJK#*F(H0~KFe{0DhEx(GDka&Lh; z6z$%HHSffQYc$FoXQNv75zPdFB&;-sVe}ET#253XixxvO76Kv4M1ug5K}j_2_oz*U z^YnYvWZHO^MdeK9ZDDRg&o+Y0M}}&Tt-M-(FB~VhAG}wz*raMoLKGu(xgv$ud-yyD z>i5m!PlWNHx@AXh0Tf{75x}Y#Yj7p7HgCtXp@#h0SbKRfWLV zSjj4@D4#@0AM#9-5C^pwROa*SRb8<_BzKppUlEOUXy~sn$KX!ID|ZPy73krvH`T6O zGwH2`T0Vro#ViEfeW$v=YR(?T`1^~{hN`bJh{O#w$Ku>OS+j-uZ{F=ax9YE!HDR#% z+TyvUYxY^!vJx%$gsvWV;_6eKa|iH_&w!FFwuXYESe#DW%3wYgHY~VAVV}2tuE)B}s$Jt^g!*rgquO0A+J6OC+lX zVzj%taVGY}zW8V&$y69>;Be4m?P-siv7G{ocb4W^H%4E%Yq7G=36X;UGl>-lc zMSAdYkzIx^*7`QVHwW0UbD@bG4MkZZT($s64s;&&6#qCZx>EpGi&W(DO2wusr$9qR z0MPH(J5PhRfN9j={+yQ*?J3co674C`o)Yaa)|6MH3X+RO!)MXROFSQx>>OSB=ih;FUY7Az^3#Zir+ z{J3xy!K*kc)?sOfroRBer{vxo(mGOFM@s8RX&sYNTF2j$*74A6f>V8DDhJ?hu#SHQX=i_)1mwmQ zXcR-R%DZ$sAS>ICy*^FCS?TZGnTU43PsRZZ`-s;D9gU=+!vx#=+G}`Q**$%WojT@7 z`ZmcV&>~&KV%O*pT7i7|x~-gk96kEBB8~u1CMcPZ4}wx$$5@mb!q=NxfqrDkNnOYc z3Wua6g{8PuSJH!>u37-5Bc>R{b+*x7!m&|0nu%BLL6;_lC=G=uCHYg5KPCB7l0PN+ zQ<8tl-;?BDgX|9dlQL3S)P1isH7;@$GFI8LO+XU&SDsRqRYvKlEt|0qO4?#~SoF8( zc=p(q@5?-{JeztKMe@r7gs;=IYqyA$5hQdFRxvVz9>V6e6S)Vv3!yhC&l3+N-<7<^ zMa+*Pyb9+E^QlpqF&Dvi!6~skSE(v!JTwh$A%aVSrDL$%a+Z@PsP-NkKrnnSWgpGjxIw&23U?S>^wETDC_zbsK3)Q9pS}sx2%$DXedu=F zgyJY?gw0axjF;<`*mx51HwvPSGa?>hoAHUeL>VB|6YHL8OVpeYVxqFc;1YYI{*okX=iF*02v@uaGE@}xZmOTu}y7aBg z2BVn1ZbVbX36XDZ18UF%KjoA7F|Uod6j=*|DgyLzp|wN9j}OHH=Q}hE%fsh|Ck-Ho z+S_i*u2SCl_DU>pN~0Ed?}UspM%w>z9(lyi#ricV{u=e|Q4 zMK+3yZ@*cmXOiNC_oq4y{mD0@0V(4WKldQj+s2_HwA;0SV=CG`irvyo>ZE*`f4)Vn zCW0Iaa(WgDhXwl0MaOp8=v^*d4$Ce}HBr<|myHr-2|sl~grluH>|jrQn<8sD*OdR& zw@*QHis*)lZ=ch=zn)%UZS+s8AqywA%z%x&<$=5s+8h(#uGVdj%%IS4qqfulrDqm|V;RY#V zyEkiS=u3X5lu)}~a>*lQY39PId3X6F=(*EecTvCBYPIy;EG(zG2u?NR;x51BFj zAJ#F4#At@Qb_gvRGVEDg$DjTQ>1L)*KOvpUOPhK^ExP*^m}lEE3CHUI@3vyb>}H^) zZx<@TA}A>2zZ;C|<_c6gV-%3gZ)0R<82T;6@_E;T#h$2P?mX$6z2?y3xjMjfP8jk+ za*)2e1ln?fm(za3^;r*oEgG?Wla5`5d7OLQr644%5NJc|FAdW0 zT`GmL4T^XDg-`ZtG+s8INhm0@E71TfAavim=sQ;`tIJm7%B$d;D<(0g?X=q{8im#Y z%VVO6R$$}*0Gi~%?fv7!85&wS)z3p#L45G_Lb7Kfep?RuFv&(~+|HqgsD~n89^{}d zp2pw7q7IiQnD^G6FP{OpUTUmx4nIYl%z|q5lL6YyUCePshC^-9Rk8294CN0lZb3^0Cb*;0Lum@=xlUB}@3( zDzNCJnxayS5{zpS(6^)Z3re(Y(6TIM&~60db3OI;=+z?m*Twb6M}g{d?UPeY=sYhE=X>sO{oH$TV!#&kf8YS%<-W1RC`1@N>Jc; zE(Rw?zZIHe$ov^stIIT@WA&Zl8LSY$TH_DWLoWaczWE?{T>#kpVO4)yRIn?sQA`C8 zmMYe3Ciz2ov0cE)y9Zzeso7F@|5A7VQg{FSU+3LF7=pJ?}gLZ*iG0;#b`(MkjD3H^8EUd&O2-4oeR16_cbKF%Ii-nQF5C2v})`1Y-Yd zP*>d0C=5qdL|1w{%k7%wS{;m~FyJ0vzf@NyG)tdyOua4W2_hVdxd z3iCGgF9eSzE{F_!3z2|g&Qh)!+Durb>$egfJ4?c)XB$Ji8bRhV7fMySM9f)t&D=P~ zs~oVDVhBHPM&$vEm;{23OJ8@uXiXs>pvht_<0gSg9pTm7E14B4N^L++yMF8~?EQ8ycf>m6I=mQS?>E71VOoH2*0zvY&CSs-Q zZOuj~CAh7r>Qmg-L~I_vtvLe-o_JeRL+##eO?{2QmLH8RC?%f(toIiJ7`e>33AGSk za9a~9Kvs&pPkLJuQcd30ymBfz(-8Y8=JlZ1w+VQb7UUS{BFr=jebX#C5H#-u)CYu_ zX36mbOF>Et^z09qm^^;36IB6X?0VNzv=H3&yk`<)E9a-;Hoi{Y^;B!8xa+xui3fK* z6Cf(M>nW;D-t`n*PQ2^6hEj^QQ?p;ro`SH7+QYl{hu4lR+bX1y)DLO}(r*Er7 z$EjJjoVb%!KKw7kZtj z3aFu4fRr7;SF5H@9GUb@RM$h{{+Ouh*g{HjF$oeRp^Q`gI~w;--%s_5D4^SN6YcyO ze(6c^PIZGwOM85mA;>BigC$OLQPHiNQUeIi0)K8QKy||wGssI}Pwql(FkF8Py0>Q1 zpEc$BX^9og1Cr&&sR{(?a-p$~p}DeUAm`;F04PHY+`n)wjT^gqUhmXv^=!Ev+|>!F zeoGkcTL(n^i_^gT5m4f{t$?dk32Oeq1T%R-um2WYda29)_d}V@qjtvhD8ulNUq-uY zwcH;8tNvvewtvu$DU@W~zr-T;^>?vyw+m{mqM)tX3i~!N=y!;?I7xz{c-sxoAP)o_ zTCh6Chpt3TC{?ht{Ts(ce9g%sO>Ab+egoBQjDnz`^w5^We$%Dvn@T+_UrRg9BC8T4 zk<#l>ogr9-3?+hjcmjQAhYXylr=w;f~qB+T+GjhV#A~UXSkg8o z_%O%~D2O-#DEQGTaZUha630s-v)~ygM-)Y$xRCP$YJ<2N*e$F?b&!(|u(;l3U{!gD zUG8Ia6W*#lUr&~xODmy?o;eV|NgCxFBc8bgcE8n(1;qeEifF>JFQo~K17%+b5{?%$ zG)tl9qTtdW;YHsreEXxKN}YlF8a-^e4anBC(HJMr0;*$FcEZ=J@hXX+*Iv=3+wLyG z%6Dn21hvI1#@HI(j$Fa;qo!hI&f(2a79VD6lwb_AEz>WX~KUg!xwNp zmElW>ep3uzH9#%GkMCsof>yd|_!7(m!`B8#>uoT63AL8?JP59GnuaeB>XU{qm_gF; z#TriwHLjNYInnS1V@(>q zzPbW-(Zu}xSK*4}R%ac|*R!oA>kiz24%!l9`gVY_%>ne1^CjWWZwLVP zFG$`L!tnei8Ta1M-W9TQSrUc*kTB6{@2WwbV($u3B=@c|L7rmoYRYP`ca_8l_O30I zt`@h;w1b#S&`I9V-gS!R{b%i6JMNR{*=t^1or1f-Xa0=fF8VqM?gHEF8r+2?EFbXX zwt%An0QeRJcUdCwzFJ~$rVnY?k`jBMWeSQ%r}Lha*gNTYPfF|+q{QCqo%f`~-fNp| z-|1rS`SMBLEx-R7Ot!tH)}0vmWD(jxm%t}dJ+if@lk2NX20qaaEC_tUUZ{J4PuMHU zbpx@dn4PBzd{Tned}81el%yc=NlLDblZ(Yk{TNbmEhX1Z?#GalYbm)#E-yBjosJ^^ zX0~(RkX`7Bu@%U*H!UcLcBes55H@YHTOmJ|%3Z(>job_U6yAhO!kYA^ezpdcxUoRY|==~)Opr3|bo11pxzQ+O7pcA9^^ zJIyamV})<7z6FUKp2~s8DXz`xw9Z_QI$fY;x0gJZ3a-s^X5u$I4O$U2TexO>-TC9q$0fmWvB zHJEO@b#3-CHq*hiS=oNylxwp(Xb7&&q6KyD+N{pExQGc#K8?{?p2ncQ)3sThWd+w} zIT3nuuFXPMF1R)uhLQZ`ugwZ$$!oJB&`y_SIqiPcwb?sZ9N~p5i#DBIg|Nz4fcP%g zX5$V}z75xAS*%^J%?eLluFY!D!L`{{a?L^;bbq=w+q}bvzO(s8AKt9ky8b1pqy?Cd z{L?y&KQIdIc&CWtsQlagFGVujMQW?n)>`$qaRDwL56KhCSI&Z(w*Vhs#e((EVSMsU zdi$ACs#Xwwzj+uvWaynW49tz$Kq+qC2O)ZZ~e2Tp`BEnc-s5;3)Mlp%Ro2 zHW#n_7Hm}+I%F$=kGYsWER3?9;LbN+=QDB_6=OP!Md9*9HaTsm>qj*-mn(i)vyz`f zj5oLv(VD}=*BP_?cQy|4~$uuShfH-ws>RZPOENE~9B z`7-V2AjCgMkAB}y@ba9x#rcbpHNKB|`)73)y^BHnjpck@tJAEfbQ9gT3PlIx&QLyI z%U#?vOAN1Iu3TTr1dI{O07fyxccZtnPwFd}U$C6Rt{@A`NWrGBUc5k%?W?`i$Xb2f zS5O=&Z1lZ}CP%8KuihECLB1`0^nWaU83UZR5m@t>G@RGq}Ad?xH*;P{aSHJxL*i*4k} zQPTyn+~II;C^E@qOLlcW(zu-uPa4Y|Z^*pnovGE8T3zoz(dPQ12Do30S4QtziO^0q zz%gQGMO0_WJ6t!Ip?Hb>ZUqJ&%Zou^;4#|Hw9=4oM-d?ny=;JzZ-2A_qTomt2f>&% zpi=~6uFqFz7Vk+qMd>Ks4#(bOfMI4#) znqU4q<_jW$?y0=Od7$#nobQ$-D@FIURo>69 zVTpZz0ry?rc;GU-690M@7^&ld1!fhCCNzpsSyb~bfy-iVr3FUY5Hlbh4`Bw&>STer zA5!fSxJ;|~RpH9`B>9Z5J6T}#P_YQSrG50;?Q{-Yrv25MY=K$EM(j-qTqYGN2wcWy zGiiYt&VKq$4qWyp3(SMs-D8m-;U89GzcTwxrM}+P!cS#A4|iS8`q*bm z!(}p(#HN7h9=zjX(*R9Sn!v8Thn<$4D>*+G0Jt&Gy8Ch|R}HuW8{04V);HT^1~sNC%;X|sno*WQKmat^%8rvIl!t;5U3S$j%m1cx)0{# zbHQL-aA*bGzIg*Lw%^5z7NOGDp~Crgamonoy3AV{sna0SgXMe$*vZxSBAA3+?qnvG zMvE@T%sY9S$=M*n6^j>}ZL1j2xig073bvWg2C2!uB7%X}&`lZebEP9>{pk;$f>gET zt00+THGU?@arqUx9+=N`TspL8_uyO>b8`7Yj@>}Z498{LEZ^UqRhRw%3jwX!r7CbjnKdGoMl*N=Jo zW?pnzf_{O)Jbyj^`)X}u2^jq+^EaDMT&Z+&m(3_$>1}$*Y+b2-vIS$Vxl^r6^o|?a zlvab1pu|#EiE6WQ0$)n=c$hD8%~5r}oYP&IYR(HV6EP8pa2g z(D(;OQ<_-M_&&(;cN4dYkb4M?g**7L$5v2VsCIi+YGT3ASY9p$o?NCFfnDm)0zfV% zI(JbOBf!%|SpmUa5n||2xZqr0{;X)cq6R1MyvJ9a1ZKJhi*B;;)p{#$^w z-*hebwJVsLg1%Wl{t#gC9YXaX(NTve=+FW!LAxfoZv&JN7vrj2$^uDK5BtLsnYv%; zj|j_l&o#s-oaLMAag&_>!q)-h9-5tJ_^yZehT>N$W{avX1NGyyvMduSKMo?x3|ijXYP(twd9-w`bx^9UUTqA5I>JHro9F`oid$g7iMoT9*L&LfI(- z=nofS<^rp~KmdL5krM;x-{iw9W~}nzn`aR+!8ZwQBbb-pE*O^y@z0NetaI#A&|SiB zU$OH=v!JD?1kU9TGMSCK>FMuYz<(T`hycX8^S?xn`bR~M_lRxyT0N`+IsTA{r9u?$ z#V#w&|5k~!eYI#Sr;!01gM?kdO^{>r92v{K5g657O2$oh!oU5~T3EXw3IC+lL#^OU zq$|E|7(sETiKnkGcXn=?R$J z2vzr;kE}RPG)Y)5sB8j#yJ7U`#$`bqqg1+x$+|ICeFjU;oiUV&aW5QMtG>&$X3m!U zbLEhPJ9ia89;gq`XQ{fPQB=RnxM>f-;?AGSqRCaw#hO@?I=n#3$G^yc{F3@nAnyom z_`AjU?da;KMa*`M{;rCNMQmZf{<$2$ze!Miff4v;HSevaojfe8uVa+{S2xhkGpl*$ zr*KNY#{l@U8m|}fR_Wn>L0U51jTVbgpgBOIq4wO*DBrC)S_bX2jYSm1Shi;hqeTMX zU=ZVa2||OU+qe>;%+gDjEM%e~`gX-s`vy~93LXS`%`14Fs7s7u7*Na|0x{RwsakJX zgdIM)o}#$@cLMGnBGUcR%3QkWOewTo_8dOUQeS3t$~2lj#GCHya@yS^+B-`ScnRuu zrKX+KLO4OT%$hJw#2AG?GK!%@*XXiYi;CifzFxD_RbyYTuR}JpeuBfz$8+CpS(#pqN12ro3beMHbbpL8Z!S zP~9vNuQH~d(Em1}B({X{#f6vftu%(q0(}=6RYDWabfi)A?KkV45?fCPftt%z6MGiH z(V7{#uYyv2<9c1u+ZP4C^|g%UfJ#RtEDj2E1Zkjpsc2-)0;uITQ=qzERM7b2z|a5B zObeSe9zstOUMwg}dZhri0Vr{9$I!x}t89?&$eG3{ZUGEYT69^amBr}M<%|JLF=0&c z6z#axWvg+Q4`sNbHS|yb`*J-+Ooh`W^3AAM4xPI*<{^L1ln^QxR?kvj{RK5Q^g3EA zE1v`+;N-6FYz# zsPVG|mfetyQL}$a5NQ^8Usx_CHFM$sAG)$)65^=SdEhdk6-d7-x%)LxznycZ)zjKr zmfT&C&k^#PnZ)tr?u;^-+?|;ule-H9$>i>$_Sa7Co{~4B(UiRLDS4BUH-ESCW;v2P zDQ_f>0(m1@cM5rPlO)fH@&?JClsA&`rzvl~fUFA*TJ!_t%^SIRH;cZzmvJ05BAIb~ zG)iW-GLAdc+{30zv}s56niua{PY@_85J|xXW;`0e+q;309vW<5T2PkXp&OXH*u?}J z7-=!5*uY5TIK>7=bbE>o%m}_tZeXMkoMHpRfR1lq9!87&%Wq($jRqT-wP-rY4NR0` zuz}H%d&@R3QnP~%jELzi+rS7r$qfuE@>Cm`1t2fjz(}qBOKo7JOa~hnZG`^TH!y>t zuwVnDt!%e8Fn^vvxnEnu6E`r>dG`bggG_E_V7s_q|=%ZN4fEwoE!f8f+OFPPp-w5`|{0d&ljqQljt;+454N@T9i9 zlqh^1Ti!cE6kZ5}4!vgr?!|BDXh}xkt!b9WAAfmQqJce@914f1Wt%bhIS9 zgKjmNcWw7}W`8bo=4Ft1ub23u6{r-77)!TX*yM8qQ)9{5T`k zHzQ%N5rO4t>69>-ZRBa*d~W@BA6CAr$hVa|(eD_8RboFU{=PyT>|Bj;VK){VUttb5 zd+y+2WjU6%Jqc>|U`h9tSTp>d%jMj&L}uIqVCsKd%DQ`|1Q4&lQsp<9_s%=S_eaAI ze1EQmth?p=qgw*FK47A8m!V~QIAyZ5Szfz`HQSx|aRhh%UeOw%PWcO-lVW_zuki^WBn09DfOsHF&ioh{JqVcxD(6M0!Xhy1|bILe#K-{Xpg zF!`cNnKZSNDbksa8>}}6FJhuYHQRFW>#~DXpC#wkAYot@!oVNph)Ria=$j6GtNdaT zm(p2cqb`?qb`>$OfsNO271_@)0#^|{QjWWdK)+pFMc})~T}9M+;3^_961a-I5qUoE zB(5USMnGVB8PbJ%CwQg<7Vbs7K;G41A;w9iYAT{VW=rA2*m-83oj7o zE@R|jxuW{F-+M=NZ{9nhujW8FN6pi};d3ZX=$dex<4(&?J7# zZrR^|&m_>(?s;bQ zyNY2HiFzh1WJQk@*$oE#e*Y?%LdSFw=z6>T*fCzxSI7)XVo)7gEIeZptzroBEB0-$ zV;4ii@o~*I%N(U}1^=_%>gGc!fm_`gmSZtw=NrtcKgOu-I**GL(`~!fg@uS~+oWtK zl@$zi8+i40^=GfV7v0C77d6=>7f^vL=*3v#^l1yY) zG!euN5Za3%OIL>AbG<~rWyYok&SYhtuVpoGEiVoihcX||;bWBst(VYxNFS0Zm)SFD zS16z!cu=oCH+g?VSnhWxpsf@CN?& z+0Ng08UFfB1BUNj3()${qMb48p~(SE)?Kt+R}Cze)CIuD^=HuTf?Ieo=T>k$MxKJf zC4_<%be1uZnsW+SffYI8B66Vg^>7fM&{C+^6r1(8@iLhZMYq15mF`=I0y;}N)dGj` z4HB=sNcASYxqG;lH{JRdw*tQkNv7vCZ{H40`c_RR;|{)Qp)r@X5@80tGqi!A(hc7P z37oBo4Hcr#-w%|^#5P=JJ%dgcz_Mu4MO$bm3pBatt*W-`xR&2ID(I7ik!N%$Sx0q9 z0COV0Ou;G&zcFuN?f9sUXuKWQp}58Yoj(Rpy;mu|LlUC}l=up$(;pq9(jtiNnEvjy z8&;K?@>A=W{LX7BZJ=?#XBh;yUR3Y*2)|x5j8L;?@7U_vCOw{k6&pRz@;uY}Xvh>M*yE~E%SbT-RayIP7x2xJ{AbpJHT?!j> z>EOkcK;14EhUSVBl=2Pc9~USu5ovyqp}A=>UVPtV0Q50qC1+kwweo4W!64m@V#;pt z?VxQ`j=WOyrvcC2Q6Os~B&buFQ-ylez z4@YtTSAC5*2zb9N<;7)|PvUj0v7Lp0o7-rNkEQI9NX$D|qq=X2(o}YG^|hC3X(+2k zuO~FhD61&x>dW|(uD%jz@%fr#reN0wyo3EmmsB5&+P4Y8nGodLo9ZLQ$y1!X4MvsX z|5iD9hq7I|Yp0(LC+{uxe`5BE#i%G2?q!Vp1g7rivT10a)dqs*>r&x80RinSAly20;zfq;TV>kM>yQTI% zSoL3rd#W}|jp+KWr?z#ZyF}r)JiL~I*WwwSR$wrM!yGIbfqSYpOI;E&Xs;5ur?NwK zp|R{U+9X-xX0Uzb3Z%%P$B-&7u@UmM+WG~-*hW9jrlwUw<~7|@QE;-~RCn5*Hmk&{Ww@KZjCA19Nm1uW}UggflHNlzP_ovA)v8|v50G0=Lh53fR5 z3VnJ#!G!$(Vr%sc8tw0~^ZG->psGh@={~H3SO2^y^VnLZ{k(>JYyz}o;aG?-4SLJQ zu%5@tM9^O#9xF%Sug@Vd9?##uso2S0&> zBgg%Got{9sFB3|R2xEh!bngaAE>^B!DbH&_SXyee$X{9Z7BsDx#> zWip_sYhXY7w`2Gl3by3FSYm8cA znohNdsh4nRQ9z1BOwor0^P&pi5Cxacq7?NM1PX+v+){=0=_Ni@un|!98V;ljHM?G= zR2SL_%Fu_|*$ia7!69L}8XK|*NOG#o*Aqn3DGU50uP_Qi^HcsKqCGpI@=ABhP))o-M5vWM*z1Qw2HpAne)>**8%B`^z9~! z;Lg=59#9Rzol&9SA4BNY4HMJ`fE;&03orH;F^$y7UBCT29KL~xmT2B1K(#Bm3>1vG zOfqQ13INs<47!mEc&AYC94XKh*xCH})euvWzMgPB*vMbV*W>2_Xpq6zG1gdc-M51& zy4>5Kgl2)ILr2~nMJpfo9UGLg(C?!kzg6pW&m5hBYH>c#vhdXgp(c>$+x63+nz%@+ z5txgxfzVOmHY~7NsHJ>St`|Av87Ju2!JB>bKSN;ZBM=a|no&+vQ2gSXn8dlu`EM7F z2MGj21(woT@*X=&0PQD;S=_kZ>g!za)W9NE zr!mg~A442Ye2CILb6C_pf5?n8@14Rj-Twfk`>H7I&7<_gPg1JeKy{DLH!#r8zATTX zo1lrEABLMg_d)F0cb`dh+EM`DYu~qF*q7YFJ3E-nksLK!b2regIy1`GZJ&c;@`od% zKl~n^wU2=s+=WyxoV^%9@aXTE*NY$U{)Yd74qE1Z-uv&AmQn2|H1Ou65fJj%X(ImY zB~Z}!w$tLL@288L+h%xoRn5nto1uHWySnB@?=D;VEAQT0J%sAKrq40nD<9+Q&H222 z2_U@d%?j7=nd;H=Bq5*~qiD%jodV!BqDh{vG;R1tN;xwS|Y<&O>5+zZ{a? zHk!uUMnS@RSH4V-H?9WU_dURw`+8oiF9Y&^@!WR-)Za3`uWttFZI9mJ{kQl1l*RU4 z3Gu2zYJ4xg?ilqA7-D@XOKRi_7UYnh(f6EV?}b`I{J%EALP}r7Li)^Cda{Nd7)141 z&n#r*zl1!_dEl$Co;6=&mHw&^Jn7?$sLoh5jN;dCf>plzJXqL__rfN^IRZlqk>~IC zq2Vyb(Bu)A+59f}iGKw~W|8LklcskE49 zn1X|O3zHg$H~573e7i9#Q`W0S=tHsm_#&i6v6x6mTp`~iept-4*L>JQrbI`tTbEOU z-^rN9JksH1X8&?AJv0fQQ9g-Mz$h2ObF$~bSz>D3q*|6ZYNj+D7rlyyX>=ASzjlV- z0+aR$IVSjnVvH>;SATROL{~+-zFyMiu-rN9AjUUsf-kjQ2Uz4*>)Rb{w9)k3{pc#b zW<9W}($7J-p|6rKI%K@H{$}2+Z=;8L@#A{LfUmn205vK+nom3i33mu|jbbNFjX+-t zu?tIP+ukVHEvDrIfhpuaY<}rNN z5Ao-E2rMOXOJxD@FrU6%&P={B4K!Dc6RwZys92)5G;S!kb3+86IDh?4a|ZG7b>9Kw zb|F!MhgzJIPUh~AkkpQzJfV12LY-e z&|i4zD~azr=7Yv|$XA8|O8VO&!k|QT>qcgKFaeDuBxiPrn)}d=uQTm3pJ4=GDnuGO zONpAxi!M_NpewDXXh9iEE~oFlwcw>>3WU*TgM5RE^bKN_5;W|3u4Q6HfXelh@@a_# zU7C7m6)`DvZs5MtW|H_qhU$K8j@p5ji>N7EtEO4q23{_0(j?_F@8g>Ni2?1jp5A0H z7YIeM#u=nv zbgc1ls!eSOoI#4=&0U;9N)RJ~Gl*nIr;KHTx28B+ile1CT8g6`=V-ao=)ZTHLgW`p zo5Qmj6-m0kdGZ%ZjUq}jI$8{!-KCYG9mT|*B50(^WNJ0ifkSAl&Q(lezaS;@^^&Qn zuQWCZ9*<_ePG12lR2q%ohEl7Puyoq-t=cKN+~sV4EN}_=OxXCWf(`c^iw2$anJCpx z0u|Fmv~SbJap%xgyOT`qxX*-GY!K3G3`*|F6WCFc^qJ76Ch0RFwjB6OpwjzVMip#B z4hCC6G3a(FzCi*zZDRhIz%I3&PHm@C+v(JHI;mX_5=&ilmx6cgM+t3P`Ux+(&5+1 zm{K#Dn8lPznHN*$#gut5WnK(++eK4hCHLvHGMP+NYLqlQ-@X&hlLp(6*9RQUVgbo| z>cVWN&5ruVXH*BtL^q?#5?UC;^C&o26rSXqPDPy=%7bL0@U5h)m3Ujy)k?}}NT$t* z?c%NGTB*RJt?yM*G^K&s;Y1MB=pHy48ZwQCq(KKkjmE*=Bik9iAB+~*C8&|AJ0%mf zgVe->*lW`onNwWjmHKVT^IT-IxjX65qg%;D7XUiDS=lD|d6}(~$wXIUS+?U{vpL5l zCK7h}I?hCW#|;RXle=2UW}#RHzC$W-r=Uh*nXOOiY9*UD;O!FBXyIW{ ze_~K01NU&97fIGBt}$)$<09b1Cxk2^RQH6bjxOXBCb1NAepD|8u2xZXW`_O1)kiTfy|pzF&FT8Mc=u2z?W+mPDP zJyF7LK$V?bt>QX(3S6yt@wls%PB~5tYNQ3y#ntLIpmqvZD;-;C_(>wrF_4$*fsb>t z;Jxr9UoYpT^G^Gw!&G;2wUU}(X>RO5D9-s-YD<*b5~a38sVz}*OEePp?S6gB0OUZ9 zcySio)a6MOW=QlEAcJs+5vz_#EQc_`xEKeJ&dHV(lkoCc^18}^x%rNrypt^pQafkt zE)AjRo0U)1r9F(N$j!-`E4N>eY;%Q{IT&ml4n=tcb8LB$58xX?SnFOEbfoh zQ%r~JxikroT#X`I8bZ2UZe5mYq8k~0wisRqJ-EzL_+hJ^dt2Xe4LaJU;Y(s!u%3vi z0F6>0WODO$E9fSYPv7+>D{N>MfT11nskF1y2w)r{@Ylp+6mjWa;%hg4IX^{A-W6&n zSvpjAdD06wU#Clh))1DMZ95R2|-b>`H9={cx!s$1$Qd^lubF_w z0CW@x_`PzX2pp{ymB#XP`o<}A8@mixq$4KsS|e`%ZKJ(oIsy(PII*!r8zpcaZaO;O1X1e znNd{04$-)a-YL_j!fUx~v@Dl4A1%Og-9{&ddZr><& zWkC({VZJVZFtf|KT-)Mv#kkHu(zxNX;Zf&^dtC&BQI>70)5S@{@N}Ojg)?PqbJ21S zjVEr?*7SbK+V?!B0Qs>|FiZ=YnIqU`z?57Do%y(=r#aU9Hp${vfb5TmhqbJMaW#u~ z`_@8)PD>ek(dMyy)2cZ&&r_*dBC1+a@7VqPRA~fFpwS=9q10xwx1a^_({C=UzL&&W zUj&@%gNSlHrV+!w`5f;a;)~Fg3f1Fx14(}f0pfQtIbY3tem4x+A6hDTc9ePg2BzST z4yTs}pyF!;-A1`u1<|0|>fxb{s3pNIi})-ioPCwf^gE=hD5!F9RF=VHg7dQIdt=JVm~&Qt+5%?z5aEihXW#9W|(SR^>p$5V<+WJkiHDx;H3HJPLFWvHDV+rd(; zMv-k8s1tg#kbk~fLZ(SbtVdeolP>bj67*r{&9@B~vb0iw2+zz;F2`2u%r~lscJ-yC{u&Zx$^DVDnzolYSR3h5&<$ z8dyx@%-|arQIm$y7%+Dc^@aB!jfeH7?$&r8WjZm9uSK7vnO`p3blxdp-lnCxd`8;7 zksm)t3$gzI z*1hefPOh)Ag1PM#g#`NQ3YJu$uWr|Xc8Pd*ECCH0E{CG) z=CHUnj+Jk7+fXqX)~|07nd}su_5o?Wp;XKukY_ERt_SL@%U=zSEn9Sst(g<$nP%5n zo@o+c^_VCF7=b(kEIwgu?EVT~f6p2PC60+619^5o#Pi1GnHWkS&u(H-oI;*uA%jjK z&t`)N$ukswEr{6)2cC>OcMA(RTWqC^M=qF1dgL~;Ogp8RzS%r-#g9%AcTUS9h&xva zMzgFjkmoYYvfL)Pl`L&{ zj%N3ktiw_8l|ZjVEF|?xc*3dl%1-Oh2nRYo4ZZSD>+lJ&!I3ozk9?Y!&{*nkpQC6o zi#%`?g_R~9MJ4*u%~A9{rzP-G3vCC=hpxlE%0M&gFDUT-_7|G_XgS>D+Dh%-YNg$s z82hLQp`L~M2A5YcESc1T+;t&w{!_$0lKR^@_7PvdX6&QCW#7s9FtWlsn0%ciV2*fd zy#zhic~d&E%W)^za1Z{K>BQY;5v4G zKPkP5&9IS9ZX`?K?_JzTR*AtdWgVx}E7Fvr^qjzrMA~3@2AhnqSXR=FY|T#BjYQuBZY1p1lWrt4 zQPtzxlb_s;1Wt6^jYPZ1PH}p)-RbN`B7KhVK%l;p8;P|p6h(%!F?!=}Bw<)%(v4&q zM4=QUE`}*1=|-{w{3qQ=WN5HhCY>BgZfBE2!Z9dA(Rf^%s5F!>JC63CUf_?)g!xC8 z5lYv9@E~@N)QYgE^3RRfqrSrnfqdI!5y4=#OMk^~Bt{wLMgkiC<0_KkU1zzmDmDO9 zyQ99hkhyLvfr2deNnmA+V z?OZmnROy7)g|wt1IkKZ$QokI?+@;C%l(SAHhUT)(C}nrnOKyN1bGL$vVzrwssoO_9 z)}@KTWL7Y=TJh>^O{94zW(lsN)AwGm=J!3wUb#lJz7wJgay`6Ut7B6f>G%*n;agOF zSSo6C5tBbEE2ABE`nttD%!PP;6YIg{iL^qEk5iqiiG}Ff#ng9J0*GX;#G0C6{4jez z=l2N<+`gI+1sk|9v?<*%I3#5>H8Be1F3cL-p_8=zWO zO#;(%yc$DOz2@k|7mm%5(#o>QgS5T=Erv!YmqfG zxw3(`3kw8xXdIFCC=Kli`j`Ujx}%iVxbQ%0MLNKM6UxF3yJ^w!B@6bH3wz=KK0IMzuVg(&f`zR$on{LgS<$_P zP2b6d9leJouf!iNB))IXJqlmv(v5NE15cO9>|DV@IAcsb6kGvY$gpFIf@Pp2V;Da} z%Vel#BCoO-EnsQB*45&?=16L7{xz>nzf*KFZN(^5+8C0}#U!&`IRq7$`LBY?((S;x zWd_uNgA*(AdMz5Q&33d4-*l@qhj*~x8xL1;<4kcosZmEFGHKM2>d-Bt&O8`Zr^}h6 z9~TH{C~4G*!8dBP(EfNi^G!7BNaG7G#7fq?K=m@sK^!C-mBZJ}hdsR&7h41?>cI>U{ED6KjJPO8pQd%CF3V&sA9EIp@- z>I{&&s?K^dP@T5}$S$h0*z74(=gS$|iK;X7VE7*X&k*EJ=P!GYa1>LlCRzHOvOm1A{=nD#cm#KB0YO zmq5P~RO#C1bh&-9K-^;oid+!rH`tA@)NaRWn0@m0iH0AS$84~{3vQo`P`3^}%3TEj z@lm1L3l8lS;=^j^)wfTW$rug@6c*QopiiIRLl+*c4j+~WLd$ikS%P_>31JcNy|_e6 zi(Q#|2g}qb$O=4&LU1k@+Xy!7-ch=_&pXXAaAJlL6Eaa`mwYBpfFkNoGh+3F@{)K= zZaIO~OB(CR?O!6MsPGinzb2!Zdl9zmUwO@6_aS+4g#KwKH>3UG4Uj~QHtk2WK8~OR z`NJY(vuOC*t)RDFd}|Ll_6H0?26!wnFJg==fldc`%2>iXFfb-3gk_0WRTHHq>X z+C)3`T204LZGw=h=J?idkQZxhx6{Bv@<$)zoyPJ>l*A?)8LoU5fMJ@jmdkWp+Hy3$ zT$Yqe6B*~qToNS3F3_MYka&TCRPv@ku8S^B_>|v`E|61N1X-I_kS7b43&&t6IuUar z)r9HWIq1Mew4|KfjZ6bz-m|1!cn*dV%ZsH;+t9^Ld?-I5-@xp&#{nV?GDzp`>ot?3 zKru*EYSD_CwO$X4(fbxrS%-6__;ahit>Z4ldZ6$zGQLVkK8vdv$ zxoslMT1jov%km90;oLofpPkGY=d3CPZDk@y%cdHEeJjPLz``(Gh7fEv*i21CH%MKk zCgzIPv$ZN;1<4ePFb0Z(ORD)PlV!f})Ne~K#(pY92(k}WGaSHCy&51)Wz#zlA zc@TGrQ?Lv=d+j&{6TMU7>d+@PJOfX9We$T%*A&P!%V_6v-_EI{ox3he1-VmT(}Cc+KEyw|h= z00RnU7+~nciRu_?;Kr#Gr7lYhwtcgfZilGM1Xx0VjNb|pJBBe_PGqQV97nrH#06{D zQFBBSsip_tyihke_o`dDLAz`%k~8jrjG|@mpj_jo(bX)cyzNk3YzUam6YXa(MMrj2 zh8BbO1u3uP#p`mi7@7qg%GT8um!_fG%1INd(iy3XikQs{)rEHQKV%Pu^R&D8%Yg{P z7cX|{0%g1YTQ+0`t+4y9J|W|?Z(lDEh!QQjWy|gK5FMkxx)|ef7D7;20(`#KbB}ty zt`}qrYyl{!z3apHdy|3eN=Iu)=z~^wA=&DT1d~wo^BX2d^y4ROfG`CQXbW_fDpmRV)b1hj&Ef+Up9` zP*hD8PG%IAsF`HhKvvq1QasbRU=tnzmNV84fcC|MI2i-xSF8BrPC0Sad8cThwP37S zlG03LU3?QChV0@-IIouz7YSz~!ewf_Z>w)&iZ67BFM!G%te{TS>kkX_Vv(!c)K(E|cbnBzA@O z!_ks_frWkwzVInzru%%iGOR9zTrxy6C%1gxE-pSwa!kzaO%AFFFYMwgDGnU?N~RWm zYT^HtD+F)*!oPSKn#8}GlXNQfa2{N+P5!j@B}kCf>nW%_v2OdppFVT1H-i|lF<6WVhy$o&N( z8QwDqB|>JGNR*$z1ziTYHy+7Q6YJuFE?uQdB*W!w@PkN(+QpqBk|ApJtDVuY9kpSD zl(#gc1%R$wkqnva@koXVl5NS`jHgo*b)ReS#d|z=h_^#lKB5^+XqR=(9^C!{97g zw%GXPIv2Ks5X;vTL^71!(t+wAG)b4UWNptnoh3V&<`wF~C$Jj?5mJIkhFCDWo+Zm@ z+pS23Mj05H^O|K7fU4dBhXkIL#gEyx->=6 zaB2EFjjzqUIIxq41?T833KwDFR0gl$w6H|H#G+XDy_UuXjgw8;qEy5n0pIW-)tSra zTS38m^Ff8^s2a?=+augm?ZiZ0(L&838OtlKf1GO1|G-x>h*OZoi^rc)U2_%YsK62r zOYLllw=>ni5)WPYqw_He2bOsKORntcpq4-sZ_d9-IP*`39!|!ccGuU}r`;_B{D1 zfE`>0rvU0K{f5`H_AR1`T>S*60A}sGR$lADDZoLPc&AeU8*Xq4z@SfI?R#GtxP5k& zQs@NdLt9z>G1>G^VFzXvy@nl_Ev`!g2g&mxO()F&XfE@d1oB@!q?t^DNWD4k2Zz!} zv<~V;dq>8B)_P&^h{ZTL#>9PJxg=Z`X~pJR=Qg)8&a2iuu~* zwIN-Lb%yC88nxmhx#&jd*J3!NQ|%L4x5>jjP2$AEy$W)SHkW$$KeQj3IR<3-V`{-t zHrJOh_hVC`>xOFJbJXs*C^v^UUk7up*gcJN6kO&yu?P(>SJEZ~bY0B0 zGvQevFjxPHTTJOPUQA=bx?IDRodlN1W~=+9u9%^?7=62vz*Q0!VW}K_jX@h?@l97S zo**G&^>*P3*5m6m-b!e|H`VgD(vF?%vP?5D^m2&xP2w%BwCEen{ooxuv8vfjbpJ{; zmO23DcL5vU1jq2Z#CN@2TB@5TbPHqlP68D8I^8!64xH_s2v);oBfB2p-ze zg%p2mgK!}7YX-kA4<_j%#qtQrE1yKsp>-)EqbuYcOE}=N&2b_oR^CI%%9PkBVEHGL zLyEcij&0SA%~9rd zh)YMkioOYR6HwbEJKF4UcA-S!kF+8>2T&0dy97|V1^@;r4dLC%04fhN?>Bx(DHJ7d z=Lp{23ZNn!1St*mNiwD36y_M*&WS+}C#EzkOD%k8G_~+k3;$0GKe&4R)X*kG;h&j( zuNcX6Vo(*hv{Pnpysi$iOnXSe;mge$U%~qAD;+aeZeOm`<(^U=cDdYR+)F&`BuAVn zk)9IiDUseqq<;mq-u>S@vn0}y&p}dfyQiGH1jW)^*K&ics?=8ep-~W|(4fpylIoCd z+a`wwW6t)@sh!}*o~@BTTSO{p0~(f#<)dvjNS{qZPx4KsaLKjJSEHA@7~H^Dwb4W$ zv@9PtZ_$W0n|`*`Ec96y=AGol2pLISrur$G$?KNRv*&d*)~Jc~kOu9Wq~#RQw{ILK zy;(|bHKUM+E7i4S>{lXEBAcYw*GS)KV)DLmvJ5nVhiNrRPVFLewj+$f*VB%#UMg*_ zJph2rE8k+5Q{%Mfv!Pk#8tW#bVe4A zAa~IaIAixL{@NpUD|eB2=BvWbz;xa&i@yvR-O61ASMHw0f7TSZe;13tY!9+Fzbfbq z+wWxVqB(HeZsjh57k3RhqlaBA{!$fB)8elUXfo&wyW7B@+Rs8r>Uu=i+hek#*d3y@ zs{l^Bw$4e5zXYoen!4EjCCpye;*T&xNb#e~Tc^rhv={*xSp1QmuX}qeu=ryh=$^$N z^*m|ux7lryQ8&0fR>|z73?y%l8D*HozsZOG4fQ|tsn~M%5mgfD707x2(p=g)tiQW% zMF`i(+1l@zjJQ6c4f=MCiGbFW_G472Q}hqtEJ+=2q^4O~LPFb$=BPj#+{r0o#&$8s z6=DDkO37#F3;9{BsZpYfUPIwTBc}0okMWpuM#}F$h#8-85IKKrC#pfVP}M3DZI&|P z>ottp|KHwuKxtKE`@X)fbB=@tx*M7-L2^GM6!TD6PnK5bWU}D|N3x-8RyOQ&RX}b_tqQM>cjrduCR0MQ>RXq zdMrLp-H(5ck3YN-vi|4^zsS+WZqt*)4+S&CmpVJLVoyDGP znVTFc+n^cA7ih8qBK4+)pD?D(i`q4~kMWc7dWNH3e=N@!g%ZDcqTE9oWkM53k+gf0 z38_|KqHhzjSZmM&CvY=n0C`+6-S`fOaa*%QjntMZLFmf2TTPe}tWbFJZRi@Q>Dcq9 z#vnyo0uLrgDk)Eb5#OOq#}@LtNQ}Cj&iBbe+%(pS4CP6sGhj>^p3M~Wy}UhfJ1iDT z-zI4>4Hb%~W+>F8kYJfwJ)k1iEzJ+p40{O0wS5kxMR5s3#wJwHjkat$QuKWsCBw)? z0>1t7bKM6G`XJNhtM3XVV)GSQDwXi%uhpk%n?*)$PcWP20Aak)ETF#+yx2#Veg-JVe`4Fk|($SJ z0)2U6&qBY=*O1@>hx^PgM*_@t;mm!#_xIBxG7{hV>3>Jge@D)LN6vpo&i~cOnMu_4 z+>*hZueo5O4287Hqhv+@YX{6OAq4-Eok2CQ`5w|fVvzdHMj0Ab{{Dd3W(*_$V-IO$ z5WnsWszK_%xh#uEgI||rjUWN}`vYd5Bf;P}SB-0E=YQ;g*?;LFjlAdlLmElRZx5K= z!A;)*v&sSf$pN#g$c6s(ok5kE{&yeJoH;X;|Ca~MI#c{z4{0mOHQhtn1QwDuM_0VE~B+a7h|nfdGv8*Zf=K6yFqEKe>bw}4f9CN z?D2j;6{L!lT3BWG(~zxhi`ncKN0qXW{YsoG+};Mc`$=nK$hzDSbS7NwoK}__sF&<^ zv68`hN20E=>8IJZQvHMPsL&uU3 z%eRlvZvG^yeB-v}E0S7%RI*wsp4Q>i=3*t!$60y8$znthn~p?GPuW{-nlP>PGuoj$ z5`j(J2yV6$y53A>)H`V+DPB*2y?ddZR`h6khLd4;6x0=RO*e-Q5Kr!7M0etm+YTdu z+|&eyOKQcU_l!s@ZUFgbq>UobHp4CaDW+R{Rb}Fhk0u67dwTa{NAT_ z(zg=|Llmc2(FAGQ_0^UhBxMc~w-tj(s5z(+Axb5tLjoRHgW60Taoa9UrYG4n*IG9d zjf#9r#G-Q1hBh>CM{XM9zKU)q$9Ipz?TY0dv}@We#YEWMMvZr-NgcCnI&{P+y#}## zz^;v-6KZ=2)g43G;W!m_Shl%XVquLJwL_<)A>>v>F{k<(3JahtA&< zns#}fqj7Ds;dX%ZeI#$(YnQsZ={8fWQwISB&PR7{@uE>Ek;PF*+DMdaD)Cj5NEut{ zC{IKO)}4aYKz3BY*k%%C1HKKzlX@Yh8r4a_8I6~~VCpS~#R#`5Dol;Lm0P7!NfdTt ziyLEgHPV<(lL3}5L$aI1ymn5Idr`%`wAssV{2+asAhdCSZfurTWSQEbFy@xQ z#LE&=8X)6HiKWRPG3ItoV6qNqFjTlBPCWxa`)N-K6Cv}6$HYyf&~aicY8BnY`leAt zMleeE9aYMnX1Kw0WE|jZE%Fh141pk%pzz5#w){khHpfkbXpY7f$y{TmaS7^%WZf1X z;d&aj*=nsNq}pAwI@zN=M&I_d+`yL4<#)w?%&a^giBu>O>4Y^~?jGsA^keGurEh~M zO&zRIdtG2Ms}YNJ8#o86$7+R@Ac?n%#&oC?d?(5U{g?DZ^OBmj(=|z=)O%1~aw!D) zmM!*_wRp3beCUawt##8!$+8R`eP)v95nyVLBEMvj%bXUqV+R1bBh_lrxZ6I+gd!@N z>tycz2qv6`1rDWgmWfhY#^bMrOVvGSxB8U;9fj<6b?~D#-)llYsoI^C=H019g}&8( zlARku-M+@vBQYkoEU;!3pr?H^%@=sXX(LPy6_S9^Bc2d@2Z2JJG=ii;jY=lyG!_^p z17HIgpj3r%ceYmiM&#tS8;aY2uAMK(X32&cNx4m=`=+vjMmW4_4v!{ruG@oV3auqI z>PIYB-}PioZB($f6V^ono(Ter2Et$Yhik=k_|#FuE!$<|=_*2+j~8iV#h9kD>x zOoKiZfN0Oj_|6E*E2CgW3$&H%W;&KB6l_#yYh-*Y(rMxwMZ0%LFhgm}F<9e|McOJ! zHW0LLe`lsCgRQp@1es08`;nSdqaB-%6n)#3~d8m%LO0pZew?efFEbj z?oTMo{ve+A&y$4Ki>j^S`IG5z|NREaypQ476K?$fc`Y~34WRg13FGuaF8NV}=+0iH zCWRw#j5c&;+ydzC@L*!n5KGG$nkbEy*)^S@2qaHz40op1IjkkcjRy%%c~4-f3985r z1-N-J^b~2D%0#b(vC>r(k6erz^&VU2+tRBWMQitkQT9-xZ<=uDOq*B<==2iVw6>wM zB{QKVYz#&bDod!0Cwzn`Y}!mIucZh$A>VH<2^>m=$ObOC$ZrR!+6>+1dMOwL6*i|M zeFeCQ@iKv03v17bCmk4I&+*fE9BK8DuW5MYN0fQQ*Y5aI&f=cO^j5XD+0o0cD{4ZJ)1FwYBj0gVih*ENi%(S z#obI)2AFP7P$xotb(cJuH3l7nz1m~)xpG1KWO3z`v4gC57=*3<>sp9l2ev#`)^B+x6zKzpCQ0vj#vOb~jk0noJ1 z!Sqf8^2`G5_r_@i`|>i-A*+B^H3k})4zxU0jX$)|LY8c4&euTYxj+j`xg<0LT_fpquU#Ib!uRzBx10A}$3+S`!kmdkL3EBgtwVG`O9nc4Vj_kP#?tUJJ zt?b$av-2)u&}nofd}pLWTI98D*dA%i5Q5pX`!ThT-zT)(_h-;q?+_1jH!MMZ&y2&z z|MWAM7Rf~ELBs`nBKjpri|>b!H&-FbT_2Lazq*33QS~M0imc9r`=I_nbC)Ao`ykL` zO&MMR$1 zf)O-PJ{7xJvys}l!$OE1Pc`77GZMj{XgwGi=OT;31($-(PJ9M2Ekfyek1ocxUc86e zi)N$aVK0L&zBm*UnvV+>Mh~O*pvClc292E=D2;_TZiQrL!d*%Np5*EHGm&L%22`wm z5_HVAceyk<0cj5uLq(f*2rTMO?7T#8#LR&m)FC(-b8070=3bza`N$@^26SNOm$0Kj zG#+>l+?vN%z<1?+6x<+XMeQPV`!7Hn5wArOdULOGN@lxKupu<=*vEKv%RV;%0C-;$F6mtYPMc+%0o#BYpuUFN=?AqtR&z)^)_#FaU@WTi! zcs=2uYSc9}6wwL)uXq{{DW887lb^PShBm@eX<7vhmB-*KWyusT?Ox2!c6mS#egTwq zJLs&_5qREG^nZ2$zFabXBKi-*zbh8Cpz-LkWxOApI1z14M}YM%3r7TFTEN8MJA)(3 zKd^qC$Vff+x+BB?%n0jtU$jdZ6YVd{NC@7ajcWI~rxCYTXDyeeC|{_}=xr-Ie}f=3 zq}xIb@s}{x+OHU%eMe{S-kb~++7omdBfc%vAhM|rmrNxVn>4Uw_0W^h2_+}Ri@BT# zppH97`nbkCzS9AHG#|&(m}h?`U$BK5P!H##H9t*8xcxHS0pzxfGVUm4QaYfJSFurw zqD37QWBlJ@4AhcFUjo2EMF@$_2MHERFM^eKM7W9&snwN|A#yNvd*YUH8q8}Cr?I$= z7)vcBLZ&(6xM@Y+X)b~t)1p7^v`(X7(^h(NxD`^(HORr-jciPR^>uG3m*hE!#WWCA zK@86(Q3JI*KqYY@EgH?LnR*`}Y34-8rJ&E&Ea``-X<$C?o_s?G4##y#AKdxD_HY@3 z&nJ~RpZMmgWQMent+d2_SEOk&z0*f5?qj1&vNxNlJR=KId|5P4t)NRF$d;!iYj%D*wzowY(-MrpekN$S z(yir5Lhn^Jv{&rx75b+bRm5=ZcP}XMEmFxWgzS5UNXOtzgb2uR&w3FuMYjoQ@=gqe zE$&e|mZ`;W!DOV;j-S%Uh9hAoOtq6Gh7dilnnW2_h%BLj1QaThX|M{h@O2L89*Ez> z%W^M?rwO-aqNgLknPOZPb^Kqz9O|^Cj1De0@yhaQa`m}l*f~Hu0$Rde#fd7F^SUg* z*i9dW42P!zS7t3r=TLeoO8t3GYP13KQnk(_w?&etTMkj=9=mjh?ew>>ou!#L>_S_qG}upMQW-sRMWBNl`L~S8)Io|SP-W`if+kQz*FlITV7;P4m?-xy!&0k6P%JE3SA}FlY3{;=8KvFp^H+6F`=l7IQS6-1L-_(*;(6Sy=_kN0>=?{ltqlta;HWT8+nJ+g8 z6!kfx$+>@7E5ZhIuGB8Wz)W-iN)8(arED(l+Nz!~=02uBC;u=}jmhFl>vZ+X4|#^cIw#QC1n|U=rHOu@IPvl;A`ANapc4g^83zw9~bTN8ct$8YA+6_DANv;%+C}=(*6R zWl|=}%`!C4LiP2;&P7mVYRhC>sfRch+ydy^c;_Lys*N%sB7`lM6I9D*>=^MqbAUE} zE|!GOC@5_%C<)TZx3_+x-Ghv4j{d$Vwv-4cS!s4V4_+3}dwn$IlBB zfi8$@H)9h(XT~2|On^%BZ9A9V3NX$1mijgJXR~Flb~h@p1q>5*cQLMAAd_%&rmTfS+DXJY)1v{wBT;dqhhkK+ z{I0vglZm0YNtV^cs$jC5I(84}q^&}`ts)Y(VhHX9PJLY2BoNF@OqRk^#7HzBGLs&) zIlhUjMwNx>_!R3=A+1uXt?nuP$|H5Qh1Rii*`t}<&xI~&)Q`b+Iz0TOir-@=_9r)I zwy_y3Z#_&z-l&`3s+|q8`tR=7BS@~AEPR#UZa&%VAE4c_?K%0OEs=rY3(OtEyxlvS zZcDa9zAr47Vr3@y1BebB9GSqd>K-a=8RlU^rSuvuzOS`dsY>60)%abKRFV^pyQj#< zk0B)!)QCxDyITZ>4o4yfB)^{|zf+1b$MqK3ekGZImOyCS+aJ*_mB#ig<=%Tvp>8Sl zZqbq{s(u_sL6-ylFh-AkRS3i8VlBS+wK-_jZSie$owWpsoh%tsE$LFL%uT$E$(NeN zA4d)W`7%ZNHgS@G=4?%bUP75#Dy}o`O|7;_I!4^pLbWEwO)|Gwk383yN#CZA46e1jI5`W0Yoc-1G^1P7N>WQ6 zO~npn6ik#cwHM{4{%LNu-2tNs;=3KHHVJ)ELA$;(h?)b4dZf<_sjgYYrmONAO(G!b(!87)E3LIc(?$KzQwJ6tk-ihk^V zsI(`>5x!hoJVyhJWTB6|gD>0ES64V@~ zWL=qu-maRu5&>t#L!}$A>&$?O)*tcPz1B!k6D=f`1|yXuHJ3o^OJWxZs;$0SqbC#R z<|ryF~O9_$X~P-bbJQ0w;x-wEGBRfZx-gka?y<)A;`N^# zc=aQ+1x!Lr7NhWe3hGtW}8n%{o4zFW6pfx~#uiqP^f_t494` z9%X666``?;zx~AOulGP-@UfK&8jQ`lRE~2fKv&9UtSp0XRWU4~tKK32v7Hv^su37P z&HX}4C;xbRHSGB}+pEdEf8Pn;iWEl9TrKflc*3_vV_tKUsJIunRhWYN%*|FgabtnD zT8{|E>!{u?%RndgHy>Nc5vrw_02Tbl>-ePBAO!$FMeqRmZGd~)^UTFPwGi_EaKh26 zQ1$mGe2b@mg%Y4u(D<8AthhH4)sq5(_8rxQDcCAGhwo#njd=ZUKDI(97jCpRQ<2Sg zRM!i*MMZxZfh?m!VbZ$6rgEipn~OyLrFDG8WIHuDgZ*9W_*_^TN_?$y$%Weo=C4O} zU9gT%(?ag}Ra==3Pi_%URj9~U6ID!)M3%)`&*cd_t!MDY+epxkkJ>=q^(s@s#~0ql z!{+f-gt<#%iCg<5!6cGnJ5hsUh{h#31m~p*TwZKW#V>{usJ^-cbikQe1hQMhX!6p9pzrO&Ffs?B zlsxnfjpt8>mWr`81lA{?B3(*23FdlUQJw3%9CXpKWGGK<19X5o1>;^J)tI#nrKIf@ z2nL4TRET1sKQO-_m#OJz-=oSHWG#w5~kW*yrK%>Qt!cxCC_Y4e%Hg zc^mX)T|v@uEsd3yFC?S+IfcfnJ;c<~4O{4O!RJ6<&4kh+XZd+mSI~vs{(u0NVIlcP zFX6KMeds$el6!d@L6@YggXk?4K<)1ZT9*LSJPIgHVWZ@3#9bUa9!8J1;%5GQbo%x4 zw6h}}w9Q=xV+U8j$(?IpEI$jO>P@4WmJj7|!zO?U9t3x>Lr4+yW-&bQ8AiA-yMgw; z{2`a34qTo&%B2GCyf5h;N^vyk=tI|1{B~ZN#6}XJLp$LvanY~PL;48Np$+NG``VSX zw(=&R*mcP77YTZ0lxo*$$J1x|M2Nf#x64~J0?dA_BR|VU(}$a&mlqLJ^@DSOUcC(H z(Jz6XQ^x)M&tGACHJH%mB{z`0?7%!v7j|X@3t11kJcNq*>vkgPxGRxD?-@{a1{2Ba z`xt%uZyN@<`~Z9pN8iP#{bhPlh-L3!(2W~Bf)}jgT~ht!9tO^BmxAs*ho9juf)45X zN6hsBqEqlC{lPT21E?hy4@;m!`vS)I!GywIN7C_g$6;1gh_`n5E1*5IaIt{JL^=1u zII!MIx_z&lgto^P(8=ww`{-*osvru8;xvZwS7jG~~l69f39JFH#x>`$3MUw96> z-tW9Q2W^;(vbOOG%LJZ1hi(U1xV`isoCLiFWAWee^AP@I>eYp_ghv}e@2$5}`?{Bb zj#4IeU?&2Q=aXI$NWx!@Y-#t(5>Y8jM1U3Ew064tW=}f7N5qfBISse;f%#$W2&(`BV5# z*{903iu!zme{FsZXw#p7ZrK6U?^d8R-T~T*$+%kiI!vx&*yBLEaleYLp8(}ambM?S zH0@;w)LyZI%jG|E>D!-6ubV+vEh8Jyn|)iBcM@n0R$4ibAu43*FwiBDTcI-QY0!^7 zjP3)Lk^motPrtPnt9I)=)c(pz>NJ^zWzS-8sJY>F5Hs{@w4^WApS3s+b=*}(z*K|5toNVfcUmt{-t;|cKgWw*?}NUBufGK;HN~OMJXK0K{t}xz{n851 zxe4nrpk;RfHKmD~$^$@sik*%@SI^#sbNEdM(>#^Sr#M;75h7hpqiaFilfAh7#DMBs zEV4TT=*(I!yWNyu76Xgjhr`?KNG|&sUu||ampvLW?Lj(Y!3mdNGP#Hfdn$>`JG+Fg zK!I-9w3Y7z z;<{OmV`vQ*^e6@LftxM@@{)qt&AKNs0R08r&R}E$I&t&6dO$(A43N;FV*o$K5=|uI zr2S+!P;3_m{RNF!&KE>1tm{T42uSGoU4WlI1XM>7+0X;1z9{`tGd-rB1okDKYZ4U^ z(?}dl!`eXKTrG4I>5lYrzHj(8Ci;Hvok{}ji48${tv&-vD*#$^FA-pTIOwttpd;=; zHZ{TI5E1tiL~61x0eW028Ob6PHr`?Q$P2m&#~Md$&s{QXXyC)3i@eE{98blf zkQ|_nlrCz}n0B^3t=hAua5JcqixiP}+)MntkDD-{S{C~oUB4hAoK7HH5XKr0>w znwm=OoNm1_wdf0FjH1FuGkuuSJhw?3G;@-E8_S zanf`-8v1@8EGLk^6fc|1R7mhi(B+3h8CMn)xC5TT8S9QmcL6I}a4&uojR$?mlT+gN z3+N_o6wn{57^L3%7y@QA0v)+-HNCyp4`|J$Ku4|vTBx{wP$Nvz-b(;iLT{5+F<9%D z6*RGp{4cnH635WR@1wz!uOQ?)K6@abq8G~|QW=tA2WJ~y zmlp4-1YNS_4Zs5xKpPl+ogbEbJ`8B@Od5Boe)vgb6L>Y5R`_R(1A(8t3p7J7UA-;F z5PJNU0QDZv09E`oP){iJE4u}B_&O3B6BrA$_6DG2FySkf(Rd+)zKPkI1&CJzDJJBR z^7`)O%>o^D6$6&Vp-tQ=3Rc}r!W6M_H{9Hp0opCT^1Fwe*4%gqF}(c+dfBxc0`d;S zSZ*GpV_^An%&xtCJ+*fd_HurZT633y&b~v9E28)yg{NO3&{YfM)rufh^KYaF4~_o{ z(RVHLCewZ2e}k(1@xJ7|eDbi(_n?#-k3=@}Q%mKF(q;uO~`ZWu2-@NINLkx$K+>T!X zD)#1fk;Pp72GAuqWKH!;ppQKK8ekZs(UBGXVK^)i=x7xd{Q7v%h0pXL3>?Fb1M&&Q znWu(h9Q)9pXY*VLt(&tNGnz#Hln{Z1c%S_Or~xE<-qaIp>C{|;Trxr{ec&ii66!6h zhq%(SKLA}3GlwQK_R!7i_2|};F2p?WIvT%ffuz99qOFhnvQ1Pi8V;Gm#T@0rXygX3^N=}8 zNxy^#x!Jb?H7okidct*d%YskitpaUpK7`%oHwH1?Jmpa?OQs{~Oe9_QZYJp5gCqm_ zcb&o>7Hs13EUI@;$~o`gx-79u!TGe@QG zvlnKU8B!P5_%Ek$G#5#Y_o`Nm3_9S80 zYaR#c(u3M_&H_ajfcBgF6D0=|Dg9r11t@GDj4Zf`7GGZj+FM8j30zW6!H+Kk8bRdo z`wud5)$E7U}6L5?s0tejy z^uJv%T(&ys3L8-0MwqwBf6x0uU4Dk&2 z-0m6gndO=5dC~KlXQ}5y&zGJZp53&OXquX4rn$Muv@)$t8`IXbH=Rsp)5Y|F#y;js z)6ZOG`kQOajb^C1$qX|$n_J9qbE_G__SXIFCUY(P3^g5SOIJU0qb&i7H@DhscQ46Z z@#@*(uo!3^N<8vd^3Zyw3^W7bW{@p_yKG7%IqpWB z=g?P6*PFTHhtwpIB;@of*Zxoz?^?bE+z4Aisq5Swapsdy0@@rx{o(xGicCh>%JW(e zMKf*x#$M|F#a_$>5XH~e(EkktkDfAqv=8>Y`9R93=~IV{nmlFf#0e=qQaZNn+_rs6 z>y#-YQ^${;aOah&Bd1QBoYJFX+m7A3cIwu>OZ#qJJ9g>Pl>(DeM^YfA#{((TCQTYO zd1UI8QTlQB#5?#u<&IIKQ>TreN=vtoN}cQqc5XMLb;|gO_x|p(cZ|Jr?9?g0qtQvz zCXAdqEp@76;lCtd;?yy)`#YK)HGSl$NorJuXFz=Fq)Fq)jy&H_%2fjf+<3#_>;E`- z;Pqm2+Su`TT%S7Oj)`}}ez%TYyLaWT{1oQK4#j@_)mS))M)?! literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/montserrat_bold.ttf b/packages/SystemUI/res-keyguard/font/montserrat_bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ae33a4538132c8fc174dd53b3ce771009405d7a4 GIT binary patch literal 29560 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml new file mode 100644 index 000000000000..dbd70c43e492 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_accent.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml new file mode 100644 index 000000000000..fbe12b4932e2 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_mont.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml new file mode 100644 index 000000000000..83cc92afad57 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_taden.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 20e47b6e79d3..61dcf998759b 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -49,10 +49,13 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_moto, R.layout.keyguard_clock_label, R.layout.keyguard_clock_ios, - R.layout.keyguard_clock_num + R.layout.keyguard_clock_num, + R.layout.keyguard_clock_taden, + R.layout.keyguard_clock_mont, + R.layout.keyguard_clock_accent }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 044e4b06d8967e68fb80f92e7a61725ca1fd67f9 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 10 Nov 2024 07:53:06 +0000 Subject: [PATCH 117/190] SystemUI: Add NOS3 clock style Signed-off-by: Ghosuto --- .../SystemUI/res-keyguard/font/ntype82.ttf | Bin 0 -> 39724 bytes .../SystemUI/res-keyguard/font/subway.ttf | Bin 0 -> 70804 bytes .../layout/keyguard_clock_nos1.xml | 68 +++++++++++ .../layout/keyguard_clock_nos2.xml | 108 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 6 +- 5 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/ntype82.ttf create mode 100644 packages/SystemUI/res-keyguard/font/subway.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos1.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml diff --git a/packages/SystemUI/res-keyguard/font/ntype82.ttf b/packages/SystemUI/res-keyguard/font/ntype82.ttf new file mode 100644 index 0000000000000000000000000000000000000000..761cfd9c2030c590a604e3284aa0378ab0d9a0bd GIT binary patch literal 39724 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 F|39J~0Z0G< literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/subway.ttf b/packages/SystemUI/res-keyguard/font/subway.ttf new file mode 100644 index 0000000000000000000000000000000000000000..06d5cc037a2409477829f847381424403803399d GIT binary patch literal 70804 zcmeHQ36z~xegD5%lF0%D5&<>APe2F|GTGP(CIrJ$5Smc5Ed?f%mzg1xnPJ`=l8n|q zZH!bo7ABeKp$`i9;}c+`8|fwB+bIkO)bGv3m9H{yHo zoY=puzGKbc{Ik&B9Yh@?gTuW&^sXbmNHljY()DdUJ4Pxi={meW7Uh$no^91HedE~= zQKjQ{q9dLh8Lri7fK~#&L4FTNQIu%#>tA1U{SoK?1Z^OLXwTZtq*9su@+8_AZyV<%V!)8w#nUjMq$SMCmplojY_pKd z^RqgqucG=~uju`Oem9-{$8E)F#w9Cnf9b|;WVOTXryiE1tbEf3wCMh-`##|`=7Qye%zhOg5bJn80Q8 zFJTJw12%r(#a5PKw7m#p!L1l2Zou;tQRf0Y&%yuy4$;i};8rijb1fc@KS4BWGtun3 zFsWDqzkU_bVV4sfjxtA}t$Aqo$c02l?I4P;b$bM6W?!!q2TluSLJdqsqHmcPP7i~Ui2{0`UON6pG|bh)p(vGx@;cN zFQLuLFCx0)Dxx-S>n1~_{@i05HENN)lzy=bF% z57Fk^h^h|~Z9%-hi)dgO(T#5=+S(x6wu@*8;c!3E2-?2sY@*Qw58A4spW5w2^?5{N zmlNHLHn#)j9e`;ZW!?gS-g*boZ>}PG8}fe(@HCzvdOO~~<7b!@eUWGv^56Lk(YqE9 z-EuzB?`$S|_pLLB{S8lpc&nGZfl^r5{(ca9MK3HrPXb?%-^^kKku&nlvipzKGG|7Q;neRLNd zl>g{6ME5SkgZlSAM)c>G5Pb~wKXyCOUm)*3e$dXxQU4RDfB%Vi?jrgm`u-H!{uJ7J z;0~fsH;DdvkmzslJoqrt-=0tOcWCP~$6{){gy^%E6a78l_}sNb599d;B!{0!Gc11o4^at6((pVP$ib1FYEaoYD+JjdRLhdPM=PhfuCPBXK6Q6(_{p4^R+ z8KLCD3Ds{KQ3&iRjSA1<^ZP&}T#HN}&zn%v6=^f>L(-rmdhz?1Psh-!={0l$olK`u zcLw!hYDQ^pF%za4Cf|fHtTx@GoNmRVYK)`c7U#$5ymE&z0*WS`Gz;>>6nPaLN3W&V z(J7$FN;(Jc`A3Y#l|fpks}6 zIqEq|rDEJdrYxBrK?jz8g|Ws<$1QzKIo*l}XM^j7X-T1laX%iEWBUS~tV~#)oS&^B zR{Irf0;3BR#~5~jL_&qH^pDWxx1r~TwK^j4f+e!sA$(356wgXuEC2Mz-Am6te$)%jW_aE8|KVPdf{Ewg&ln<$f z)IruIU@?I|rlC+xtC9e!$ixC*ki=>hZ2ovf#$&0a$cw@XTT53%FFU$xTI|-PR2LQ1 z-2|HFmhsWuOG~QEOio!FIvY|wPmM=G-#&kX+LlKeDzu0k8`tm(Tn&RJLQ4Rk?#B&p zozLLLNAVa=r{%;uGQN)aaUWELvKvKJr2;F%^8dWiK=E&^<{7$Xrn4-N)6P6A6l!Qp zrTlcBh0XL``zhux2<|s95ENQU;;q!fVCH$`*Ud z49hENk@Edc&xJ~W_4ulI$IV&!LfmM8@C03_-U?W%OfjD{N~#B`*n%vcK{kfi$VM?y0HULd z%s=KE`A9Hw=gF){hlsGjHCE67cOqcygu}dpi{$dqjKM{4l2Gy`M841wp@@K=ho+qD z3(t>+kI%jW`}i#T9#eyOgr82L%Utisj@t~19y0^vJ^fH9h~C(cQP1|2LK_9d6>If> zv<37fPeSB+0CODkaBX9HMWE1M4fLdenKk#F_HOmKp4KC_C&#@A zdSpkF4AbI?k}q1*K|~R=(40svSz#rF*OGe2Ui$wU6z$XdB16AP*@@A7RTeWSh7iysah!1Wg!i zgCS?C9%_VhAyvB(ttT84U}O8{2%r@-f-u)&2pp=kkYaMThknRF#;j3Ut?1gR>>AHE zvT5N#fR=qdUOlPt)piRoNYB96(>c&$okyqiT!&ZQ(iSzgISow{=>wtVgQ82ZxGVfE zJwjNEg*UD9dY$ma8#@JMOyaF5z6d&x-6|{4&czTBYi24vpTqf;vgXXb*UD^p3lu>q zLp1h43>4>mQ^%4Jr?ph3<|je97G%as9b_gL7vi)dOkA#5zT9{nu4l1)ok~kFBAp#j z%F~M!t-=g7R13KxauiOpgR(4h*6&qP91uqU6wJe1cSX;(LV3SZuJo2@Cug6Ua^7~onS9%nMtN}pB>YJ`k`MvN9~`=YK>kCTfTpGQ42cY zRuBpv7UKqB$)A64F>Jvfb{$8=yo%SVpPff1` zE^bk(d?(vulNK%`7}-Y8ikx36cBe&sBvkE6KPgj}S;}r{KzH!5`v*M7Sb!5g3+?%_ z<#Yy}7mat##szOCqDWCN^RfsR=~#{~sk&2h)zYI!O7DcA2xAg9bK0E#xpDRw&@{pN z^J47E;EY!@5=QTRi4s#{Nw-@Y6`S^DXZ!`v@zcm_QUy{c%c6P4%F8$Ndgs@Qjbbg+ zXMIk?83cu~rGh6>HV>1UX#J=_&aA*Q@EHs3LOjLwsmWBh3?gK+#PZ~*nfy%6H)&`w z@Z>gRQ6Ls5D`_bu5a)$b&V^|nx>>hAdFpY<=a$FAAiBREvv94cN*EOZ)>HGO1!tj6 zUfTE9^x3ZTs?C1tK4{`C4J)p#Ry!AvEXza1%gINce*}4)Z}4qKA6*&}W2IEJ>7_hr zYKD9K-ls*UI$_Nti<~oJ`QY^{kq`+3dB{!#QerR-7CjX;py#|hp*f{?Eo6;jKd2K+ zOQ_?zZM$=#G@NH4k}qMyr>`m%QaC+c=<|S!u=^;d8L~-{lM4t`-vyYmltgs$?4sx2 z$1t!R!~F7O+5jzs}dxkw7@BrFDBN-5$i zB)xMg;tTj^sHb;z(()8t+qgx|a9g;Z;-dg9NFen2)CA1 zmi8sf{nT9QBP`HNc)}@PmlSZzWp}mdBXA$4t){;>q zFVwBb%$vhnb6adeXLqTEl_dmrU~s$IT;y;WAm+HGUc zWo<^U4CuFhT;9j^bZhZURe8u-I$_)7E$RmGVENwJKDR!pzax9Ny~(U_#62?er2 z$u35kv_UF7WD6iIN&SR+>*Ogk#U_&!G&o=bQ&hrrpG#~qnbGQFXglER^jz7`DUUIZXEYG8SK*~2rPq@UH@@=z_bl)3490%()V`He;St^c zW{83|riHAK3vJ}RgeISrf z!6+8C?aoE$^+~w}jL(~&jPK(g&>P(*F99a@+cBC#qMhnZWP{baVyhLOx)jS5?#_5L zK(9KLP32;1=FhveKI=Q(_{^a`uWvcwq$-Sg20iv8z&xYbo+blW$rVz0!e`N!GB%7E zZ_-wQJd$Yf@=SNpG8+ioiE~sq(-hnrlEz#6l~5vkBw58&8p9If1r{jmV68O!)prB* zSH@5Rd9WcU=H0`Xb;C1?a6^_nhKQCFs+$ZX4N26EjL@ZNyXz4!!s@VY${v8eX)iJN zEH9;1xXs}Ll<|eZkB1^-Q(h5Ox5<|n?3YG8L*@}l(;h_d@)Sul;Z*K)D4&p-`Pnea zhdk!4;XfdelnzzNW~#`1^K2kST1zNImBi_anF7P9d0iIz(>>qo}AWzzCq(>!D*GuL9)h_Dgr zOFr)bLEr6nxDQv~GUJV9T8H;>F_Y z@YJ$4*WC5Gzj$(w(TE!QEymOuKDCUeCv#Q;b)JVY`fetTu-}E}Ulkp-!BLo)!H%b) z9xYwmtaF$<#AbWn=WQn2ED^r&=}ygmH+UGUqzJ|0VbZoP01K>@B}$MRm*QK?Knam+ z2RHj&i$lv53N|bm{@jP#@gZQ#83}+l6#Cd?*>c8E+7p;1^m_>`x56}^@!Th1Zb0WJ z)R?Nkhoq>EtR-OM&3XHGFSEKw+TM$<>FAS32qoQ#5QSo!fJf6%3l>* zJ-JQ^f}$;VuY5jlFUo#_3wnpxLcmh*YXvw3B~?NNvD!dzS$MyT zBCE~9MvF3@*G_)@VjG*+p4*#~wJv{W%V&=tXI^<}j<(>n<-uS*Hp?xhs!$wJ%(2w3 zW#xHvWBcYU& zw1ko`VTmVy->2kfB)>0txjmR?OUl08+%8jd^b$d)zb1A^h1xcH-d!WIVv@$s1+zrj zH8%OquDeT1|H6rrVs8X;nHe!PJaOB;Lm);NQ(C*vvRjuC!>tT5uYahTHaDx3wCn~g zJqW3qV(8rxL_ju20KJwX!&lgXSIVocC*%Myq+R*Fo|s&2PT>1F<@b8@ysOw#Drsj_ z-m_w&*sae+OSy}6vZNiG`0%8QldEF$U()Wt(T?{mu!pVA$MF3`yob#vKr;Ox?Qduo zL1u7j3j~~$U3G6q*E`t9>s{70@NY8GoEL2guOZ7in5x!!$Q(!`S`tr=x8}-o>|gxvX~pY{?o2IzN1$NT^oJrg z^p-;22%$#`J9-pNzTH7cX(=s_x@F;n4l_yxp1wb~o`IXi^#7a@Em~YA8-k2aqWe(^4_j28jnfxZK%2E{3%I04*J}7vsQU8gc;Q*G7Uy^n z>=I9*s0xK&FFHTV^9^p^kSV?xJB6Lm|=RqJ#1QtnE* zYkoq!)LBIV<#X=u3~;(=$KOA7XFT;jK(YTd0~Io6M&dE+x84l}0;vh@w>t^czi4O#MAUFX;RW8(5IY9NGT_2IVsOh_{)YS&f}VI zwPjUFhnaJr)QDpI!%hEfY*Fhe}l_;sk^gF9Ssqt-;*gt{|JJTblaCBwYF zatp#_SnuwA;X50{I?9x>pol;~jJTb4ZW6c2a6jyQNi9gQe}QrA)fvxEcdXjLoirL& ztVnyR5JD`FYbXSA&lTi*>8ABwBc=n+46D1x7(c$pa()1z1=a$^Br@Ku)jmaJom(-a zGi28^K%OEo>RKe-8iHHXEYCt%I)Pq#DX-GPoklO0eGcbaq-Eyhsom-exrU;%d|GLa z(?;4;QXiKyY2BL0IYLkRb9|m}~J)Uu7OD+IqlmaQ%f}FFn-8UndKqhjODY z-Ut^h`n-UA^z4*ixA~Tf9=Pbxrk@p`)U!UMaxgrIbDw zuO1xS-xo;pro0a<HF{tq4jK*0B!*?jzNYI0*$0p>T7j*Zi0X-XnASRP^nuD zJ*UE!&ce}+(5>S5(m(5x_5t}$O#Q32(|u!#Wg`e~5D!#N z7@xlXGd#a^T1%s>6<{mAMgT1yaXP{)x16b$!dvhaJmPd-e!85q(P^KPy^J$(&zCxC z=#a!I#G+5^bF_FyF*QeMg+|4G$P3%C&!^!pns4%Q{XOb%PQZJL`ZwTsOe&UPE#EDu zvo%>TFOTROVXQN~AId963t{f)Q~qvs)?JX!erf;6eaG;4e8-T#@zdH&DY9s}tZ%4s z%Gf4rxASb3&W}s)*7m%qRflk)x3tYp<$K~2mrm<|o2#^<15LdZJ2uBynzVf{_B0B; z#aE4&d7qaLs=4`Bddw=%l@*UpY2VJt!+~4hGZJt0B~CY!l~}6xH|=CD?ZY41zeVnJ zMeL)#vt*(AMv9&rquwL_r{V$f7BwR%$j2#~5FgQK+y(vIU$URq4nK=`E5s@Lu$EeZR8Oy1Y@q2oD551 z-`|Q^FzE*h-CVK^X zhtRten%Q- zv5xX>i-NU;9=B7x7bv8xq$SMazy1ZVa!vkmQ2b!-ogHfSGc9Y*Pb=>+U@g`Q_@%lq_LS?*tY z{L7D8&gP8o!ePs~i1XqH*m93+Brm5g1M(=Fy(4hkntn72=vmydRwjZ2SN<^E;4f0@6Vm34ob zvlmT%Z(KA&-6AJy;h82S(4rnXv`S(Om`(diV;E)?q+zCR5*kI(@6oO{k&>HPN>0D} zx+3=R<+Xq5Y{knH|EaAXG^{X+n1!J)z7R(%^ioQMGB1>JE)2)1a{m^_#_sDC?dLhe z;`2fAN7fB2FNGVP0m(8ap-DqZ>czTL?%&F9{FeK-<#(zM?!5m{v!Bk^ls>-LUl}c% ztw0^Rvf$Qn%5x*KgCG7zY56-UlT-Q~m3FL#V4n>)X=oV>)<)n#Dc!SvuJL~h)+lh7 z2kx1ez`YgRo^8$gbjEMvyC~F-zJxEJ9$=hM_kaL8GgsB!i9=_RFb?2U`|3dZ`c4kf zN9oT^k%eLgPdo9A@pd-N^GX5mi_`}&ad)R+Ezu&NB8&w1qhO9PP;c}}LZ1JG(CZmd zNFzR16SePpi{--l?%*Z2Es|&RMPA}il@^A3zB`WzfXCeN!uO~gslJoQ=Sg#+68O{^ zV!QgQVX^dxAVj_guYp^j^r)0UGyN82J@|o-O5v^?#j9AKIGg)3a-~yVeLnLncz04) z;r%fasAsU82IU)krdriQ4V3htSG60J)r#hw_&u@~@2){>>NF{E zVcG`gzoYhL)tsrsU!T$xWQw`{_|SjiHSumx()*TS5VtHxuKB3KEq)6TJ!^g#sStTp z`P1=jL!rIu1+7ZDnNNiKF(aFme~XaDz+jRm=hkw^kVx8vYLB8##sYVSL;q$2|I+2k z<~k_c@I%mA_0ndjz0ZVkTFWu|FgP_UXUm)RbT7hNkLrSB!$_W_C5-dqw3Kt9x=XZO z=GED>I!k@0)+fY%#)WP|55ZT$=M0_Xz50a}r~gzjbdmhLa?)O|b#i!X&5h|3r=@(H zAE%|93yD^?-Kg;9f6HvFg70)nstv!$d!c#Lj{K`kyaVU+H<@@RezA9#iO;}YTpuv; znRE?(%*4BB2|OeHnMFrcmYDeLwk4GdOneUA+%E9UrP201CjT(Hs$+wRA5PbH+->6X zX+y{JI*zvIbzTLlq(8@Y-eBStn$@|(#M@|Q=i5!Z9r?GIcn2NX`92fxq&b~;oA?as z>ikO+pGl{6e%i#l=;+QpCO!-8{GW->t}N)BH1Rof-pu(XK9|m!`9>2zj26#)mx&)v zOJ{z<#OKo~GoRLRAkoz|13qGZ5+DQv#`~QZH>qPL(#$#YiW#0&=$;e;a8%QX6Ru zGPa$1P;&!H^&vfo)M&>0bd4mca}8P;r5gGhrXjSx9KR;HOts|qvtGTeBfpN)V}Ouh z9YDPVJtr!+hc=_FZ3>dD$Q!0D_@7mCDe4cSBx76$1!{CIEPFS~GYoa4T`y8LMVDk@ z8sB6QdSuElT$=#r04T$dZbz(+{<7hjO&0-jra>S2XL=+Gmm1(?-i)BGrq8)}vsSgv zSPQ%f>Q(W#m6jm)Qal2~O$r9)C(~#c|A+9OTkA!6&g)mBBqC4=GW@ zyFSpYswh+ie2Imau^mH7=Wc>rxRy798qC-AXqjWZXrJ+7Sgt^=A*4CR{g0|#4to?7 zHOPX-hoKloX)UFUCDW7fz&Y-71)Yh0&p?k%aj_Mn;LRpjlu_g{MF$Mk*~VZn!kpmRTxJmOw<0bWX#Heu@LShfENw4E9}Gpes?GAvJ+fXgq&xBb z_!@d(4~{KNHzZPE+=LiQJZnpj!AR@CIO>l83bv69{f&y}%>Qjl%305LAjOo}3|rX; z*e^qD5Ut<1Zg^zp=s;h8J-M*jyLF(t;o=LE70Z?{-?%>6IJRkf&(00izOlicQJc1r z#JsttI$9eT9!i!kUAA$(uCso4sJ^j2I@ViH25L!9QXlQvT;0|)x-}WzlDL;kdxy96 z*Xtv-bGy5@ph`_u;am&LS8R7*O}6*cl1*a+gY{(lK)v5<=b~zDpl_%;nryDtdPfIF z>Hz25WG(m1%_e=->eeO6rK*9O#s+$~CiUTDXt+MmTTSZy)nu?*uUAJ0hWZkq!5Ot` z-?r*dy}CK!hHJ@K4be?I)w_*7L&^Hlo}u1>TJLaj#n{k5az(X1I?PB84AqvRpL+lB z7$~|V833rmV}qz$O?roi2f5$=o|~%z7^qS2@2M|M)(&k(y{$vT+f^SU)zK|g5U+0P zFxHz@Ye0NyHpx5+0{vQdt$%oX_o{Q2uRQy#?%I~?Yl@jos}7G!%@t>!b4K?jB@?54 zo7Rn1d+Gy#eBJQ2ZD>AOTd$7}Y#L)yCmZ{FMytu%!GWz+?f~5EHO0Y7F1>Vkw6A** z + + + + + + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml new file mode 100644 index 000000000000..4e0ad942506c --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos2.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 61dcf998759b..31e6cb77f17b 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -52,10 +52,12 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_num, R.layout.keyguard_clock_taden, R.layout.keyguard_clock_mont, - R.layout.keyguard_clock_accent + R.layout.keyguard_clock_accent, + R.layout.keyguard_clock_nos1, + R.layout.keyguard_clock_nos2 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 72813bcafa60c36c5c73084694e42ce6ea50a128 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Tue, 19 Nov 2024 15:14:23 +0000 Subject: [PATCH 118/190] SystemUI:: Added life clockface Signed-off-by: Ghosuto --- packages/SystemUI/res-keyguard/font/Cotta.otf | Bin 0 -> 14520 bytes .../res-keyguard/font/PlayfairDisplay.ttf | Bin 0 -> 176736 bytes .../layout/keyguard_clock_life.xml | 57 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/Cotta.otf create mode 100644 packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_life.xml diff --git a/packages/SystemUI/res-keyguard/font/Cotta.otf b/packages/SystemUI/res-keyguard/font/Cotta.otf new file mode 100644 index 0000000000000000000000000000000000000000..5979cec2f081199a6386af05d5d0368fa5ff9859 GIT binary patch literal 14520 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 literal 0 HcmV?d00001 diff --git a/packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf b/packages/SystemUI/res-keyguard/font/PlayfairDisplay.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0587b8821539c5d6197573a735734de9c9684ab1 GIT binary patch literal 176736 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

Wi^30mew_W6*-Nr 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

OE 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�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 z z51hF!lFrk?tt-tu>#?dP#_zn-hu@w&JaK>6 zIeEx)*{5kZILn58Kb>;FCi=x21qA&|9$eZhf2L7tQgFbJw>K3-0?+EQ*Zpn}q3G}HB?YxsF><(^_Y z9(`2&aeNm=z6c{315(cTIRn19)jaJ`k89?|=91B9Q_|Um<--rQZO#u^^XN*o|9t1p z&+R`qcFr)($twsMU16Ft@Rh%AA{Wpa6av(=qf`&{LgRv(qM~4+yOL1!Q*GApaC4f4@V0qx zBDU=*YWAs?qJS<*U8+6tkO;%7DzQfEhyqhx73J^{5qv$vV9e`VU4My1pFSb~_Y+HR z?TihJiq$#&ouRh$lKW!MjS+?NKPT(#(s@C^nyyfAsuI z@-I)+RY#V)Z(;eXHW#&b9%P1RQcZnBraAffoMQ9N{MQyxY~c!)Pc>0=9}!D;fNr_4 zdYjz!SPRez;35j&c)ENLw&|NF3Ia}fV4$ZYH!m-*I?r8NjN$<0My91w^oD@NJ=C$F znd1v?q(Jl%$<$HG!o?Ske|qP69O_Yd{Oo!8%E2qbboUSi6%X7qIQqb%|i=|E$ zd4lQ~pOb&YOT|M!zx{B0l~O()b;46|3rYV6@DasIOOS^_x37aq{+NJ5*tfWDX#eon zfo1%03H_x3Evgw|n7|nfs^b6V$X*x^EC}_rmF4&uCB=CbD-s7rO#1X#cRuk_C0n3_ z_OrQQo)jTfAo5g!3S2}%1Wh)sCHx7piVd2THDvntU*x*(=9ZD_h)Uao4WqV%%KYRs zgCo3YQBy?Zp1Z48{^VrpV=ZU<<(E-q6^}FaZBRy+aS2vIozQJ#GI%mtC3K{8_kFrM5D6@JrdA$2qpXJ!7&Iw zK}{eVN?1}%?}y4>@RJffzpQHN`7kb!bexHy)f+F)K;9AQ!%KkOe>vJc*E`)ci87YSXoW; z093nSa)iY zg{K{P(GiU;eK$K1(wcLQ7xz7@79DuW8CX2G@!5jM=bUJ;CQl$7zC2^3w7OXS$akoM z^i0E<8z$&^!-?`v4z99J-n>g2dsdd$u4!yoUS6}hfju>JcEzfP7YyCEa@B=V zd)LXOYj3Taar@FWN9cHPJ~i^M*kSM+EC{UH5N)V7V6>ga zJ0^e-$8n_#p)n0?No8Bdmc9LLtqHXaOK&WCDZ`=BI5JDvP`NAIAtso{pHP0^JK|pU ztdK-rLQ-NV>W!o*7)oNLfzh}S@CPX{h(p#2VMNM{iM7y5CQXP6mxi>IYEjklvB+oZD_ zAANZK(bWb{1DBj7_Oj82XWr&XQ z4uwL1%#%vvucM6b!ky;^EGYxMZT8`1bJi~Hcdx8I^N$@%pIYBt7n~FoV9L&{&TFfx zSZGRSi)IgXwXKS0xx-cSPRuY%-FrJa7f8BLzu3q?F=a++Ra-Ft+EIQd$BNIgFCl*u zMbVvza04cAp>K;4UINlTNViMPcxQ_3p-zRQ6i!fL)zPMKAl;)DN25paMHI$R7qWZ> zb^}pNllWyRw$Q#_p38pc2um?X1e)bf4zdn?4_ zHIg*{PC0lUNadgn{CUn`VmQ<{`cfc&R z9h%oQk`x=l&gjD8<8@2gbutR%;OEJ&i*Jh;(W_*kDOxG?uB_4+jS?@sq=*9s;tr}O zq*nYT(Q8VqHYO&<5@XIvvuZJ(4V>rmwn*Zj0}SbFQRzBCp94l{h?@Jhg<1F1w(aSf zednA7x3r)C_&VL5m5aN})+es*5!a8tcz10@{hpa~_csrm+wct2A26E7Heyoa26t1+ zrQ1N)WZ5pRW*3A6s<%wQB|sY$NLCrcM7$^sK&8SwUs9%}OL#jB(L+X_XW9!3;+Tuq zPhh4dig2RDf%CIOU0R~awDZwec^AJSu{dt_8`doU{r3l>s{`UK=}EVO_x;jCLAB9t zT^4x0N`6&*M7$sGk}kM}Tz7VEN{Af zASSAx2wd4yvrkth4JM{ax~Oo))pMdZiBmCdQC{z{*)wkL*|l+KMbmX1_3LWqjQpys zt$JZ+Q+3}D1`Av>>kS=^!*d@W7%FaE@6KP;G=Fzd#rE!sks?Q5dHdS(SBJ{%^|!VT zG?tIaw&uK&Ic?6s>}xs(HV`l5%TDq8kVDbnUfg=9erzcUlW;$KTSlYNYP2}hEW`;# zF2y9C*baEZhGk_x$|jimr^(h`)e0w!9_jeXC)+j5Z)3c;uBV{Ca8b9s>Cva%rS-dK z&OOlFeQM>!k9J3xS#Nc7T*BF#3)T*9M;R`?Yn!xEYSHPSHxmB|y^$D}fPurFW<_7} zgLO%7z&cY55Uw>IS?ARn2UbC8>|deNbse2M|IV46x6Ye)dZxYMhS^6CG~PJ-mIL%I ziQ;48MeQH{4cQ}1FMCdCVo3I^XRnj&!L_4BM;dbd{hr$0HwL^+7KlI6%pKoiIw z@nwvvF2hZR>`8&_u|!33*@MZZ|Ag!*P-PDp_fuq#-ZokE=pWip(X-BV_op|kdU8$I zjAac|B~Lu2)fW#JF1WP~dU)GlU4L;XyIB{S5T{$zp}S0SM_LPaH%Z*^u`yGm4SV^WXp16jPR~7Y{`}v!PskeG zqfdXoyx4th&&=JmPu%z5A9qHW$6j2wdEL_Ds)Tdel<6m;rrae;FD58QG3=Nm)K z%u!Mm8-H>qF#*wB6xPE>T%vuE%NBIdkZY_UavBH$o)>(gOoSVxlLd=s3NUZkEy~x4 z29DGhZs0x8`v2O#`~9OQ-`l?Xw@2^1wRQip<_a?8X&aMN#R65Qb=O9`@2E47huNI-x_u zw1DPM5jG)4TI%$Kd4&!<0sB(3;)uMV&K=gRPUqJNR0u!5Lr-!X+inwDZ8l2=28kI7 z96^zl9$PncQrVc-9pSTU)s*>!0X#t(K88#@dUn?8hvr=S>W)Kyz3odYM=Eo(Q9BlK zL;I>bI@%A-4Ni*=j5NkYh2{lbvtiS^&O?a>wM475)UqcRYhP?VFniv;tK+nT_g}N? znT1U?-D>?Fx$c{!wmc&d_ z2oGv-<{SUM_(}>BW%+Az8T-zp4&5)=HRiF3o2BgWS)31tadwaB!jORgR!t_bY8)Ae z8*7nJM@#~zr(cAc7-|&bO_S(QAiJ95H1HA7oI6p69$sj*rKP3PD^f4HNt)1al%DoP zA&5~Cqai7%8r#U=f=8tpA>%iOZ@6#ct2=La|H!t*CFK=4IqvLq!?o^PztXX*tN2j4 zDL%b`ZEva%Fl&EZU)Lc=watBY-^sUDSElybyod1G@Ap2`ZF7W(kN9OFUI2P;gq$&g z-f5`R0IFajGAlV|YB8qC*03iv0HQ#iQAZAgVtX?K=1surp_+4uARDsxwme&ImV&6;i%sjdxdlFxq#{<4=`YUxW_2dhv$obe{99p*LEHM(aL?Fd}&p0Ze4Xw zR&|!mxF}b?m!&_l*>zysvf>SkjHW+H#}~hP>z!|}In8o@{wdr1T}NC;PPV%&3qwz1 zCNA~@q^0y4$E*PVUyqtpVgj+cW-lBlHo_dNG2alq)u1H8;5aNd^dFw zY3Yz(eoXXHqaKspjMr5%M$&!q-|rCp3LRO_|Ng`@{y}}eQ}5J;9$=l9AJsO>Idbby ztji*trL{R9vrpJ9E^}9X^}HGLQ9?rcbP;GC51J>UYrR;&?B+aEqH4P+wq}lx5Li$6ngNuUcQua>yjPs9N7BWj_=(+`?}7RPj6iM z;G$E9<%hekoA>4G*3>R5XO;VV7w+HnZpEJY;=AkaTD0zw(T-EA553)5z3x!ooFm=U zJ7%u_dJn6;VQJO1uWh@vXtd_jYAP2!wRYx>{}{e~HV6oQnvcC{Cip2M9=xQ)5nxbp zs46fIf(be}o;0Qa+@+bM(kAnh1q?;}8vB?&$80n8bLRb=^yrl_akIEz zNTRd{&a2+&b;JGi#Q}c1)lis)n|^+>#qDod|TRz&tLI; zK8a^cR~p2%;ud`VJODX1Bape96`XV&SYsOHM}(gGgV zY)hoDrc3&b0})V-!%@Qg%ZhnmQ$}^Q&?IdYH^a{Ve<7~2EL;%$E(z@71u|ts zVgHr)*b(guh(RO?X>NFC>QE5wi(-B_@30kwq(n8YsYP|B;)p6uwXpEx5Z6@vZusK) zESFEhROVqu#1s_U(u#8utlAf7rQBk{(6K>!n@`Ob#C|$qNI%_R1dT*z~t6SXq}}LJB17w zrphqb*x0~C%Yrl8W=%;_ccQ;~C!!KMhBr3huIaHgC8lM_H;8RMn_F`r_x2mgR;JjT z)j7_Jkrn56etX5=U$<*tnzYlfE&FFa`PI~PzoLx%@(O29b<4~zuf6|wMa&_88@j9P z$`X-gfmtZ#rpIRBw;&mUT`2tqtcO~ixyB5p$}c=OQ616wVqw@Rs@96#U zY>9~RvTS8Mey;t>Ap1zyf}i_0afavTVtqR@=W9h7ci~%zGWpyEYB;a2IK<^z1T3g35;?w5G+BG?u`a|b?q2cOjnt28FzBB!74=lEW+ zioTb<;`v_Y^S$CV^u6pA@yUtrRX)cV)Lgk)tkd>_C4dW{@(y7|00w}kOu6Rx}!BXV&#l)78|v_I18E3lD$0i%9SIcUE2mmfzOVp z7@|I+1h&T;H`5y{sc-!2YEIg7PW}>`qqSbX)u#Q14Rr3ha)e#bwuRN=X8=)9`5jol zn=KOGmo8u??n}aaMe@z(0tKJlEVk&ji>L9~x5C<#&)R%G)7bfEPfz|#5QV$1{F-6* z2cAuTlGMTdGOscXWkt&vt;_R**x&8GiQD^V3ywa zXmmuDp9^`Og${~?EcE%1Z~YA`j)l=m*mEHuIezypp+$OE^Ck6nCq{+P?~a2Y z1etBHSMd8?n(#?qeyaSEM&(zsrT0;P=47F~H#G-~T`CxXR(&z->Ut-A6g`;)c~m5g|QEy-4b0mTq| zs-CmCSoBYS`iyb+o;|ybnrG!hKm4J5XixlZHv5%V*zDaTTaU93*=g}_Tu&RtAXQHX zdG&M+^t2uK!;FY3Y9t87Spx>02<(g4y(j>^86#~w;Si!-+G4TfTkh0sOSx->Wz9^_!KjQ9XQ#!eo22#uP!ySDBV%m-qF8$!=?FmytyRbUVF-iyTv2D z2bOKPDK&jCD;ZFy=Bd_!2c;d4r?8nLPS{_MS4AOBTHQ`m% z3xEZnUOpHbG?bHAU}+O(>?Bqy&t%b}~tn3Z7&)3AQ|JRc}p+rX!J zyHi~se=K!>X{|HImD`(}RbG^9aRo;vwk3_8K4q6H`2f*Mr#e40G3%g|Px%<|s9o&U zWJz;zFXkUvuiT6E3WG8sUt^Z~wJ$?%gu6on`~f7cMNUu8>5Cj-^;OTdw$|h$l6<1cSB}^`zM?D?^$j zjPPr9PSARfu*|)9Jw1`#t(ECwQE#_(BeG!@8dEK?wtX(~C?$BUXn%R~bnU!G;QBrPj^YBBtUC{7ft1OH7k|Q!c5_ zxo}bb*$oSClYjM{hvlC=bj|JVg-g56tlHh3X06Z4tg3YS_m?!Rs>mvwrpO->F4S`;xmXGu#<&lQCMK=gcXq$a8hsM~8p7FsQsDKBFf$v(6gmA5s*bQCgJQ zQCBcZHGANzW9)C5lQ06oS7VV1UqyOshB@-%x^HL)!@viC`a$i%6{qy3_5y06ODQW@ z3UHAUYH+g<1c(4Z3W5>`oA|}_|>kVdug39TFqXKsz8dbj&H~S`cNX@p4^t3RiENrW7+}`=dnw~(bU6fyA zIofUS-Ff?;_v=v;@|tKj2X-%)FOSa7bLNlP^K!CREf?o*f8)@;A6y$Iy{ga+kf^W| z44~U$*`(YNlX4%m!WNZIZnU&?3p0fg;d=M3Y_uupb6HS8cS~KB#y@E8V1HM0y*mYz z8=2XctO=G3Apz(j)#xe(|DeDie@rBmw82^l!xi-?(boeM8ju0nfWSUVLZ~JA9uVj% z3Pv0gt46`2TiW#Wh-;icAaOGw*h?n=z=(}yi9uw=ZVMdDw@-%4sj(OL2 z&DcL^Hy1kWxtIbH9;tU?%HE0v?e>JYBp7>t?d@o9wxlFy^?%si+ALnTba(H(TLwa& zd|cFi{aazV3J~gPU3t06ca56Xx+x#5j%IWJtw; z(6RW1wT+`rcH+|p9s6-y*7$pt6+JbJa^!BD9r3Yz5HnBS5mLa(;A2dz!u$?2W?`ZS z0iz&c3d>rr)%#J78?Yz>x<>SRZD#;H*C1rMkW3)pUQF~gnFPUUp3i4h>Cpm$8uj2H z1?{e6J|Jm5D-uR&zx&AM43~gEosqUFS5V6^X`Ek=5m#gDa@*bMM~9T zD6j!rG|hy1(I}I~h+@Luhb(#ihUJGh7#(4@@-7E~?0Mh&B^v->YVzmkLMjj*B;o`EHwU;}(6GH;RLrmc{!j)XR z{jA+?+YSvX3cXhk#-R7Qin5{tn>ACf<}?+hM7x_>yr60lo0S8!O(?4g9|db*232@zMWmK_ z_q!G4zKTH8N?ZC;XAc)z=U!O1Z*+4>p0gq|HD-IM!Eep#q&m0!?2KOX@Nhxe+=tih z=O0&Qrp8`V66wEYbz+^Hx%2Erjs$nP{9wJk5dSD+z2eXMdy3s#vvd0@TDLdb^M*3r zrI~33eo;xxZ@A4-TbJ{GW#0@uk(Gxhwl&*xW~onrdG*8XnYDG<@5=?|K-YCG0@*9hDgm!2nBtCY!2v5l4bsWdVgS?>jFV= zowhHCX@Z%ATyh@_7#13W%N80cbdo@WLD1FF)>2nnQw`|miV~L9hoZ8h@U1GDAm{T-Eg=<&DGrRmI{omTzw)WbZyVg`TG|cYy zW5+5MR2R)H&72n+mYNJ3(O8|7QoU&S$6&|}Pi-i1x*a72>8W`^Q3(rAZVSTd(5&rC zTRO7hi!v&@LQSBh3pFKA;m%eFGu{0z#)8cpio&50gIPrd!h+>N462V5E$%DB$UR`K zW5nKc5WwYSG%FLx6qZyIYEB|S!{`+1uH>=_Rk~hsP}I#Uqi|0P>9R5}C1(j`J*mS16oM)_A5|9SQ^uqa>0o(X!v3tW_Czb?%#)=q~= z3EmQg$C*K>Q+ z$9^nn#>SXa>K%K~hR!z77=DnZ2K?$KtPAQF1ojmvS${n+(SjfZF>o3(FoXf1k}8|< z+wfh4T#w(b`gz2cm@nrl%%^drW<#VQ!fFXO5r1;!IW_P~!AuyNuFqgjQE$?lFu53i zvZo$7*>G)aMnQ=!qhP_{-s8VLY&dzFs6F}rwD%@}RTbCc|IFN%7sy6P*cS==W&&B* zLD@k>K*Y5!1PBle2_^x=eP65ArGC|_MQf==YbmvsQc8=KDz%D8DW#NBN-1CZrAR5I z)++D!IWza=-Iu(CMbNhY|GRJQojdo;nKLtI&YYQhX6`eszWm4g?zym}U$5?iih8xU z;haZSHq)=RIC>T4=Hpr)-J+8ty$bZuZawn{=UzN|=#78&n&A}d8a7VonvZ2# z-ysvuxhmu6xY4;egNyRczF^pS#huyePU`IVad)qc`&`{1av4W$(7EA95IaqVr)8nq z*+EoS22o<9AtOUOdbb-dO>Wb&X~vnYGMZ+$O3R)!BEM}?k82+4(XnH4<1VdRweL0e z%8S!1y@lZ{ot%6NWdTlC3JlD;_&AY0F_gnIvNM|Z&1~MRO>%0->D}6fC!PO5m%hm< z-C82^`YaiiF8MD^dIEe&Cx|bte8 z2kAq6+8oa-exPTE4n10?_V4-dy_aT$ue;z@wsXqP&h65A;K1CrLR)yGHzRp0^Dt)& zZ=24Zb|F2KU}aa|$OuoXQC&My@k*-3?E}b)2e13P4?g(r(6$5D-hJ0k2DJ*m`^7I7 zHy<$Zyn!u=&>=j`8<{-Ujcqg1`LcMDU14!WmW*xLiXY)L0I@{{wKQwr?%Os0?b}nX z`(WIVbu&_v&v;tvP8R7h)!UN!GPh(^I49OjVQiO~jBt zL#4}ChiYb;>`1yWC)$y^+YJLcb{xyWd^MgpR*2Bg@e_p&_~MU{emzMkzeTF{|o*5UE06@ zWkq~^vFGqkT}JfmIkI!|?7V3M2VIz#cj2Ic)AD+E8kw6nykp1VdATDyK@w$kIrTL! zc>wnA*qpJ(-@^=*Rw)CQ#7p}+b?wwimiD#w7bt3K5J0n7gmqGD(8&w z@R@@PFZdpMamiI2KSAMliy8(x2WGVwd>a=YfO zyCG%aA4d)N{^cp*>mHsw`Qg0wZA2!vX>arUdCJ7Xq~+vg%kjy?gXCk;?ok6?yCUU= zb$MOGcRdo5iI2z(A~G@QHM2SF-rt;dnHYW{Did@1i_o+(5wETH<$6+h#ix^r;T1Ik z5s$5_Uhrm09o|eR6IU+!ZcrvBZ7djDS0)xFJ&H_hb<#4i;=*gYIGNb>-kYap>mSD7 zC;e~nBOT6Y)1r0Ht{o;$7}R=b{4H6s{QMCzQC0}H8qP=Fkc6^9N4g%)Srqx0I86Ge zu#iBBbK2$MLcKY>-^#=I@A<)H@)kmf3Oke6%V{Gw zhhJ6AL=GbRQChMamV*rR{3Xt3ptT|fnQIU+h>Bgc_`;&ge>iGx_s%VyuW(%R&fyVN zJ*yX`%&zLWaKP{sDc{(5>lpnM;{ZliZd^sY*f=6sml?sjl%WH|20LJ|b=fm1(`y^P z3|n{J{I-^?*}jd|e1WVEWso}@xDM(JUeky3`mnxz zM)c|4^`0?f?(Ql^E#b?oy~>1>@ze!|Li{9 zdoP_b<*U7grsVsF74{z4;k@%YjO<-FY)D}|RnSELF+aSA)UYY2MU&5oO&P#`8glY_ z^#1Dk=YO?Vk35QKPc3r#H)c@Nqj7&%NLpPJx>dIfk40Xz8{V3|p!ob}W20+^c2rfn z98smRF8>=lT`|;jp-JAKj zI(U_y0|2e92Cvn@d0#4_iWuIX<25=f?f-5yq(V=O|pog*K4aM zg!X~A&G421dP#CN+Se7eu{vs>=rL|!>VQjI3D(fI%oc@xZu(}QMu*Qivzt(RhAt<% z`(3Sm?fcn36h3j`tkcqW`+iS#(p!9gh|hGj@%>3^u_vP@`Gr+W?+xFdtnx$b0Bpk> zspg?)e1Bv7JAHquYMYen`_u5>>ig5xL*XC!{tVS7`F7u*rCKGwhb+~h#^C)G&iS>< z(W8C8VLEBC@AuSb{gCeu0rR2nPf`a=Z{HtQdETSGKUvN2w)_4@s#oY5-`^PjcYS}V z>L2>M?@v?xlg{z|>H3VMXMBH#DoSQ*-O`z*`X)ci=%GR_P)k&$DphmXTe({0uzGx^ z>dCbLySaQ^uJ0=#YzDA7#41z8YN4vaKZba7R0$a5?gI58z7i~(s_a>?mEa!^J5=zb znlwtRE4Fu4Di7Ea!h5PSiN6>+1SW^?cS=0LyNL5FLdJrj+)_A+)aMhL!#5SCSX!2V zznHj{gybnTs$#*C%F;P=t8;qH?3q*0H@|N#r-GarOL9h)6)&vH88c^2No7^e1wXDV zDX%Ii&*@Q8HGFnOW%b;Wxh2Jw)m3@rCDlF8%voGoJvV1+NmWVZqLNuTV=Kz5b0!tf zFUjdXrL1_#?BdeO?j%;5f*ds(N~$PVDJ86nqQE($ORE+TQQ6GRp#TM-lG~Cw3(Jb} z zvL@9aFLz?JPNCv+Eakz_xj9MG9-k`#sIk{yyMFlyzmG(RiuI!tWdu>jB zQBLmSoKE99<>b!H$tB_1RdbBmK7lG0rp%!fr93OS&Z6|C7AmQS9LjYz=i_hM!uftH z$SJMLDbA^`ES^;|zqs=1oQm0ro0+ss%k#%0I#j}zdMYr8n5(9F4p!`*O#RD|NA4RI zP-<><^@72D`Yc|&IIp&-Q1Ilb$2rY%y3|=r^j%=TE&*xx{mcwM>0t*|&JTVS~2;)R8UfOpKl1bX2P!^wVl$v7NrHEsZZ!Vb^lC6o$P9CKk;| zI<7@Dfj_Hj)ph6u*Q*=V4QiEo4vX)b)Gg{(wO<`jtJw)Z2}21s_R)>iC+a9K9#b&^ zN>>rB*lnkY&XiROI!kBkX1Y0(=Ph+B-CDQNZFM^|uYdCHRV+wybVuDuch+5WSKUo_ z*FAJk-HYvb&r~0&tvZ)Cj(v1rov#b9Coa#6zzJx%>feXRbd_UH?-p}3f@ zz+I{@V+;Mu^%eR`eU+ZBi}egWQ_s>RdbXaU=ju{DPyJ9|t;_U$H2y!Sy=s}RU~`FU zbfvD+)p{Yi|1Py%{g&_X{+@4X?bM6Zi|VWDf7Dm>VtuV%qOa4}>l^fq`X+rdUwyh& z-==TZcj!CyUHWc)kG@ymr|;Ka)DP$f`I5tz^uziQ{iuFSe_21yw_=~rPwJ=iSNQDU zGx}NmRlQU{r=Qnf(=X_+>t*^I`bGUsyt#b52?r0m-PYt zf%<{^iCU)*>c6sg+~4$J{davt|3iPs8uX9!zx2oY6Ma-iWWHNIn9VmAk__h1rjcoE zQcNlOsd)83q6I+z^O(R4DMO&8PEU`K6w zn4YGW>21z5xhBu_F?~(GDKLem$n-P)%>Xmd3^Iew5Hr*aGiRBz&2Tfqj5MRnXfwu) zHRH^9bB>u{&NUOwd1jKCY^Ip=%~W%NnPx6zUGl}|5_2hQYrkMFH&>V|%~fW)DK<0A zOf$=rnAv8InQKbTJae@vGxJTksW1yzNn2^EOto2P7O~R$TC>DlXRbFlm>bPa=4Nw? zxz*feZZ~(BJI!6@ZgY>h*W73BH(xXlm

6=1b;b^N4xWJZ8RZ9ykAEo-j}H;h?XW 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 + + + + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 31e6cb77f17b..0c7883c9e9af 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -54,10 +54,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_mont, R.layout.keyguard_clock_accent, R.layout.keyguard_clock_nos1, - R.layout.keyguard_clock_nos2 + R.layout.keyguard_clock_nos2, + R.layout.keyguard_clock_life }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 9fc05dc7ce32c5e7172010e2f39a95a680ceede7 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 05:41:22 +0000 Subject: [PATCH 119/190] SystemUI: Add Android Q clock face Signed-off-by: Ghosuto --- .../layout/keyguard_clock_word.xml | 34 ++++++++ .../android/systemui/clocks/ClockStyle.java | 3 +- .../systemui/clocks/WordClockView.java | 78 +++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml new file mode 100644 index 000000000000..6b295877b6b8 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_word.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 0c7883c9e9af..79599498fbc5 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -55,7 +55,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_accent, R.layout.keyguard_clock_nos1, R.layout.keyguard_clock_nos2, - R.layout.keyguard_clock_life + R.layout.keyguard_clock_life, + R.layout.keyguard_clock_word }; private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; diff --git a/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java new file mode 100644 index 000000000000..62211f7b34a3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/WordClockView.java @@ -0,0 +1,78 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.widget.TextView; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class WordClockView extends TextView { + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Runnable updateTimeRunnable = new Runnable() { + @Override + public void run() { + updateClock(); + handler.postDelayed(this, 1000); + } + }; + + public WordClockView(Context context) { + super(context); + init(); + } + + public WordClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public WordClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + updateClock(); + handler.post(updateTimeRunnable); + } + + private void updateClock() { + String currentTime = new SimpleDateFormat("hh:mm a", Locale.getDefault()).format(new Date()); + String[] timeParts = currentTime.split(":"); + String hour = timeParts[0]; + String minute = timeParts[1].split(" ")[0]; + + String hourInWords = convertToWords(Integer.parseInt(hour)); + String minuteInWords = convertToWords(Integer.parseInt(minute)); + + // Set only the hour and minute in words + setText(hourInWords + "\n" + minuteInWords); + } + + private String convertToWords(int num) { + String[] words = { + "Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", + "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", + "Seventeen", "Eighteen", "Nineteen", "Twenty", "Twenty-One", "Twenty-Two", + "Twenty-Three", "Twenty-Four", "Twenty-Five", "Twenty-Six", "Twenty-Seven", + "Twenty-Eight", "Twenty-Nine", "Thirty", "Thirty-One", "Thirty-Two", + "Thirty-Three", "Thirty-Four", "Thirty-Five", "Thirty-Six", "Thirty-Seven", + "Thirty-Eight", "Thirty-Nine", "Forty", "Forty-One", "Forty-Two", + "Forty-Three", "Forty-Four", "Forty-Five", "Forty-Six", "Forty-Seven", + "Forty-Eight", "Forty-Nine", "Fifty", "Fifty-One", "Fifty-Two", "Fifty-Three", + "Fifty-Four", "Fifty-Five", "Fifty-Six", "Fifty-Seven", "Fifty-Eight", + "Fifty-Nine" + }; + return num < words.length ? words[num] : ""; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacks(updateTimeRunnable); // Stop updates when view is detached + } +} From 57516e0f3e1d8c72af66a60d7d5bc582d0a94915 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 07:05:42 +0000 Subject: [PATCH 120/190] SystemUI: Add Encode clock face Signed-off-by: Ghosuto --- .../res-keyguard/font/EncodeSansBold.ttf | Bin 0 -> 160700 bytes .../res-keyguard/font/EncodeSansLight.ttf | Bin 0 -> 155272 bytes .../layout/keyguard_clock_encode.xml | 47 ++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 +- 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf create mode 100644 packages/SystemUI/res-keyguard/font/EncodeSansLight.ttf create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_encode.xml diff --git a/packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf b/packages/SystemUI/res-keyguard/font/EncodeSansBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5aec42814817281c5166c0af20d9a87729d54768 GIT binary patch literal 160700 zcmd?S2Y6M*);B&gd!LhDCCy+ z3W6sz5kawB_1b$E1?>IW3y|-(X77E@PQY@n-+jOL`TujCHEYexnl&?P`s|qrU z6B1J*;eAlg;b3ee)O-2jPEsW_@MNyz-?>7z??J zF`vN7lE!+t`y+f;VE4-Eg)@K6{cI&;-{mpBw0>4aNxAPWek0*O3;rpy;NV;2?E`x@ z>`}97nihOJI;4WJUMCpqxWBrttYq64b}KRje~;3dk_GkF0AJ!qdYG-Yq^6?j$Bp+Q zvl{>#>+2eu&ZVr_#@Mtg8H@c_eM3ck!!1D|7l;}8r9T@(C=?yh^fzL3 zekR+>*l$8l+vCFB#`tyQr{-Fo1hRp9)DiY%K~W3m>nA(-{qD@2mDRT}H~XQ9_E==D zFVPvEBixhS!`%Z^)m?WOZyjolwuTDn;AKDFajDNz_obGl2;u=2Kl^c(q5q_d!m?YU z`08;*j*QhsElPQIOHZ;LbzjQ3nz6d95RNQ+GX63=N4clEN4j@WRqwt>cdUcCn z*#|7;R+dc#|JVL_Ki-dh%sy6kqFf$7_p^Ga^>L)v$shD)(kb5vGc>p-ST$p zm#|N2{rr5Ana`RpW|{e{)5TPsPlhw(shiu%Q#ZGjhg4Io$djq}M)`WGL8^@jH9}2< znWQE$MNL-IppSv-AeiZDHr#X7Nid7lsc@ODUIKHIx(lHNi-fL+LFNdy80MwydYCt| z%`k6e+hOixM_?XhufTkby#ezrb`s|Myc6R*fcJ+P&l6xK@d+>|@hLE;p{;Q~n=gZT z1-}O7b^K14yZAAf5At_lejs`>E_w^_i)o?}X0rglSRrnLd9&CH^IowZ=0R~7<`L9Z zE*=!mAoU)q2TaPjf}Cf=%tfvgnJcaC`m%kfm*9k^o>KyKnzO#*3!N6sU3{R^3dZ z9k8(kIyN=C98I(caHl(y!)+=f|R2sb@t9=<}KsuF(*2<_1Ygivf zb=*}e3uIlOS}+e3wIElk8-uVlveYBmrN+=p1oTgTG$EkgsWY{HiB)$`;8)mEW<^cH zbO!Bv0J{u5>Sg>IzL9U^4*m@Pn19ZH5TT-Wt6;<(8{FIJLs1l|0 zR$`TS<$C3A<(P6J%p=S%tV>w8u<)=RVbNjz!&1Wrg^dUsA2v0tDy%MSaoCEmYr?Jz zyD4l-*e^COo4+l<7GevtMc8`U;%tewfwpX0y=|dwjcuE4hi#YbZrfhle%m42QQL9b zL*bpm1H*0M5#c?FA2XcLPS_2{31F=1Vn^HghfP0438*|sOa(3Zvyik`tyOl z;?R<}VvL|%ZRPj!$M{G5GyX6BtLQ7@WUd}S-~PCGLt%;)x$1yiMJhep!9gwSFXRgYTE1J#m)*n!uyJYTIYc&5z zqs(S&==nFzb{@gQc`)zDJ8*CA**d7TS8JEn&Ss9^r_ThO4kh0+BLJAm!t8Kn)tR@> zM4di!dM|#LpMD9m!Z%O9Lw=`^Nsfu9zd!xm>90<|e|pX7cTT-_3LK{noZ5YAJMbF7 zHK(pPReB0jp_8%J_hfx!jAoNx@FsC1Nr3iyD}9xLO1d&c$yHV>>!s@kWh3g@R%N%c zpX?X@DR;rop&V9@C`VBnA5=~#k0_5RFDdUUrBh-^v{Lrq&1j)_usbmu+Q%MXkFYl}3wn>e&puP~ zl>}v+>ZRD#I`$3sLfLlWp*#^aJ)LLral8;SqGDdkYxoA#{OkBFm;>F*ALmb?T|diT zSH`HSGFI86)G7m%2IWenNm;2{lvcF{OM+*WsmeXd6qT!URRt}(yJAykDF>DFN}@VP z2@}sL)0AkGQH(N}1z=6#&APHqEQEE#tS$q+WC|P1X0S?yd#U?zATytuoxc5vM`rU zR3SQ2Z@G7>6uVBl09lMcV!M0-- zx{F`W?&deLyZ8<4UVbY($hWZrd@F0=TiAnqH#^SnX2&p>dmVGTm-z|y7;j;(@kiN< z{6Y3Ae}uim|H)4C=h+$dIe&wF$=^a-d6RA6m$Ik%5sVpeJOpD;J)e$I;!-}Fxv70c9+|6%d z_wbw94mbDY{!wW*PqtNOEkybFuvK`aL|`D`A`l6iNQ z&j+yyJcEtrLom-B&g%FyHkV(*ns^CY#4Fii%C9s2aE*=D|q-ON|AJ$w_} z%Qv&b{0?>>-@zRGHnyMN&W>R2e}eC25A%KO4gL@I3V)cL^i=bUC)=X8~AdzkzdZ9cW9AbybD@lA|Hy@IEk0>I<&-9 z=u2+r-|<%ACAx^IqEfu11S%02FOR5GQ2q_-G4)CH4NHZuYv@xH3x8#`|4l+-DsQ(mW{PUW4NIQ;AmjmAm{5;& z#a-^{@_LsuUB2$}Taa5&r=adZ(Lt#}SwVF{i-T4LT^DqF(1GAy!HK~`gGU8V4XzAs z48A=0s^FV~cLg5^ekl0q;MarC1b-d;TZmgor;zR;TS9h+91eLT^tY~VU8i=vt?S)g z4|PlH_F}hpyL}oqG3@!Ux4X~nerfkL-EZiAXZIg%!sd(l+uOE}ZQq3};eO$v;l08W z!-s~C3ZE2S7G4*=IDA$3ZQ%zZx?^s3dBjVRgCj>oPKYdttchG0xgzq~$fu%WqXtIh zMHNMrM_n4VChCT$ZBcup?vHvt>g}k{qkih4^ziEu+M~Y5k{+vj?CNo#$EQ7h=-H>I zy=P(1;-1w#xAffI^JveqM_-dcD!>gI?eC&gh-rds^>Vy?6IM z-20K<&-H$*_h-F->?8X4_6h0Jvrl@Tf^HGz`{(s9>R;Z! zzWR%4l=Uf_Q+B58PkB5wJN4-)5*8ff+Fw zLo)I+N;8@=R%UF<*pYEK*$@XFP$@V$+OYLjyx7hdE z-^_~0ip|Q%%FC+AnxAz;)~>Akv!2d+H|y(cp6!#}Jv%--D|&YhlHm%A+Y>fGCN_vD_+{WABLVP3=Xhm{U% z8n$xSreXVrof!7gun&g)INW`B*zly`8N&;Pmk)n;_}9aK8{spe`-n3mei-RCGI(V4 z$iX9T9{JlSpHbaM#g7^`YWk>W^8E9P@@~ufJn!6SztN$i`;8tjderDiqgzHlI{MYo zACLZJ%!V=B$Lt$(V$3sRPLHh{yK(HJV_(gW%>RJkDEBIbX?Q8l?6&cKtZp90R>|VZY#LA;NgOo3w|8mYy5!mqsC7kUpIcs_$SA| zHU5k7=L`J{yB5Y4W)v0_mKUxs+*-K5@Ug-dCXAnOaO?`e^!nCq!&ri2cUp0OI^zSbj zd&%lcUYL6K4aw$4hQwSU&DRpYB3t9qvD zm8y5DKB)Sl>iep*v(?$&vpdg@o1HN`e|GWg`q@ioZ2TvSvfgo|@w|Z`Ax$>tCB%JFj+U?Y`QVYJaKgTsNg|Ufs2I zKi8Mlud3fu|6KjQ=6cWVH8*SSsJWGM>*mg%d&S&q<{q5;=G+hG{%h{JhMo2H+PJE5U*j9*UsGn&*59b6TzS)# zU#v)5an*`fSDanhWo7Ql(v>S$zOwS{s+3h@R#mLJcGa;}Kd-)e^=qqtUQ@Z|;F@D= z9$WLt+U{$Iuf1jMxvLJZyM5i`>%PC*|LVl69aq1-K4^X1`eExE*Kb+>%{A56v|RK2 zhO7-$8&+&MxZ#Bj-(PFLcJ{S9u03|`%h$GU?7MO1#^0{XxUTfNPp?nCe(3eXuAg)L z_UoU%q3aEo-DtV-z8l}Y@t2!=-IR4xq z=Ib`^+x*YXAKudWmaJPEZdr6|{;el&^Sy1^7OyQEwmfjV+wI}EkG}ny+h5!ow>5q1 z;;mb^-nI4MHji!5+v>Mnwe7xbZ*Kc-`^|T#ckH<1%pKqDuYaV=%)fKZoe%6(c23{9bLWwrPwsqc=dZiGcXizrv+J^5dv|?tSMXgqch%js@2-<~ z2j4yJ?(6S9c=y-42kjocyK(o5-FNJMc=ykHBKHj0Gh@&4JrC~r;-0vB7Tk0Ep6B=W z*qgd{{N9Sa8}~l6&v)N|eP#RB?K^(2<=*&vXWhH$-be3!&(Xm##BqsZspFvI!xp!e z-Yp|q#9KG-8j}Od!;N4@M$Hp9c_4t_MS08`o!C?;` zc<}v)QXhKgL>glb7Gra;2}L=RZ8m(e@EteGVI4oE$dMH4=smf3rfuznB8TWvqJ6+9 zi!2Qd4|lN14we;}jeTgARc!C);4uzc@yvb>5o0U2IZos|)SgrJ_vW5iIb}HxYfe$P zL+LTOU}{l#WO(S>BAX*WA5IyQLv4;UqS7W$wzX*7CFPFZaMEoyM;!Ub5$B2gA{$b( zw#4S}%r7d2i;es|iAo_Vr8u-0UpYb@JbLovNC(R=s+c^vpF@eU<=7l*j}oNJl9gZN zuteG&)<`?@f$uWK{TymcB+_LoZ?TlxZRA5K4%JdXrb8{xDRU@&!{M1_TWed32wLJS zJ&?(9MaB7{B?XgpaaHsA)V%nC4 zrzV9*BMC@*k78TS+QY2AK?h(k8mXN zM>q=kBisY|Bis}DBisx5BitMLBODWBOP9^3UkudRq1cv%vMr{%Py+0Tp}HCyE0n>BNxcS4LU zS*AA;d_3pE-TFvVyUC29Q}$uY5A()&45 z{)`nhxD4#6C=(Xg!xn2BMlBz@9I6-VkNh4l7w3^rnu+SiM!n5F>jvuqUa5Ofr=q{Uo^e^ya`D27a1`sCP_ zSl$^8bZ8sjg3$anz8P(Njj(2U?L%b7IMSlc3`QU$LVJv3K=fKvJ*r=8(WHMbZ79%K zM=X+2Ru#yC=eMv>lfpkP}r##KX8c`;Ol9iyPw(J}a9k_}=E2u{SO(i}_O(Q-@ zO{X}sz;+46NyH3_lZax9lZX=H$pukLJVcZc4-w_WLqvs4mmSnhnJ%I#Wx9x(CDTPz zl}r~=vt_!7nj_OiRJBYOQ8m!jieCsHImpDK+YgdzlN`xG&$y=q7#IRZ;iD6C5 z0S{FelT~}P!@a_xMCC6qhUtFza;9Rx8GjsS!wA&VI4$&F2J72Q5U6`uts2U*eh(~+ z)#4D31Pg0DPj(k(weHy(s0onsUjn5SvY$m7{{Sq}NLO@gtA>eYwZdXR>rr88eGD)e zuuuaw68?bJ4cEpKU#3UBwy*EQ49C zn;l_*F9vQsOz!`p}Gob$fln+?MUj=J_$P8lx)Cy+#J+O3O zv;GuM6i_K1Nf&X=UED2Gq_D-DFn*Ve+v@+P5u~hFLO7?`#W?g+y#KJ1pWyd3@Fv0 z^?v?I>jQvvzyb}VKX_W#gC`T1cM#y?ta!ThCEx;`KBC03 zK7SP~LvTj(Eu6Hu09p?kZmK_<)(*zBvroggaB;XWY#Uk1nbvbCmyYd0dA9XS^7~7W z&0>}I@B+&5Ce%}c8%>zn@(XORItD}Xe;KSkOeIhUvU+6?%lSP}*RXm3YlrLIgO~)= zoEY%uD4DFEY^VKfd|m~ z5bz*i;yDD`2l!sVPk_hO2*?H~%PtNU=!g6$9%WhUY0D?9i2!Go>VFTcPnrDgMc)1w zfHf8A=z(xhDZG1GM2&ywE>+v1Nb}I)nmG1Y`oT0KEV` z0sl|$Rv3~iCC5z{9jtQ8+as%ug*$4vUeG0iLWhjd> zgW7?O0z~3o!6-naawpQG1Es?N+(%Hw7RaoJ?0Q2sOGdm5r@Sej z+InpR?D(Vg6V1uwI;xo^(7YIPh!oshNfP(AUZFOi{IPzq1Ou;PgVak{l4TIP#2Uqt zJbf@XUIq7afKHmf`#|7%tuG+HxqxN>)wcwI9Z(7A2Pg+ny&9$CK{_6*<1}CjLp#)g08*aacNl(22p8>J})K5&-AsBcAfOL}r z0Po+5s~2SSw&bx=%LIUq;Wpe1ZbMNIu49GhQ___kY?$>D*c(}q+b(8NuEv>5tSygD?SwC;PP;eJ&Vy0D!_!dMFO~ zOB&l5XTvnVv)8EuMK^UIUx0bo8-RVFb9J{HLBFcQ7Wkb8d;t4g4YO^aXM(;D@C|^% zSHlhAB@Nk32VM^PS-|V?zXf;~;A_N#dt&UQ30~2|KLIxiOPGMdlKp9o=Hr1I0mP5A zOZo7ZbQau;G(QT5bFJ*`?IyShw7HzogPkQeGZ;FT7_wf}ed1Ow>vGKK8nT0ld`znB$GAeV5jeCzCn`dLE$K++(*pY&tYsHnuBHt0&-; z_i)@ex&-%F8gPSeHM@>&VRy5Gw5KO?WWh<=P}URY+SA!ER=}p?*3n$Hgsoy5*=_7D zoHl(>=Tvd-HiY%SZH+-J7boDS;a*EUZU(Mo*W$$ZF1DW?*Etn@tPN&StUvUYgF8V} zaVw^dEn+L!26hYEiEq8Qg&%t;fKbFR_*chB!uVD*tVtqZkneAZrvZFYmk3Y%Fa0)n(h2x%1DzmfEtO#S` ze72lj%{Jkr{yug;d#t3Sy2+CFcg(Vc#xhIp-!e-YD=Zmhf5$AVskbDRkr^dTjs7>x z^6IKeOW5BsYwKz(LFIqLoY_!PX7T+yCNfcCasOLpZF7x9RR0~buB=);iyPwA>W|X= zMw*{V^8;y~l;#`Kd`X(mAi@Uq@xNo%A+UPlZ8utGkv$3YEUcKjU zna!n*>dwZ$W!5*UTN|7IbF+!`u=($p=)ySqE;2E@f$1g9AZZ4`6c__KNn00b`bg7V znr_mhevpHYOp7$JH3%9r3z&*DJ*C-Mnsj?iLEr9=UY0m~r5OU#iZP@c#wNP?6b`%U zv=bj@F)$-wYH{@lu>1Y$=kTlc8;M_v^B3ml?bA{aC2)M zQ*mpn1~cMLN>^r;x6S?KJ^NtXuph*taohZMjJ{e~xFdWRBzDI9fg|KptmG4k!dRzK zf>rTJa3PH%*TT-j_y_z$+=Kj>f5Jb-ZOG607yL{975|!lgPW1x;&#XP{0IIc|B3(1 zf8oFK-}qU6j-SV!4j1qZbv={?AUlfS(qEJl0-TsMU zk|@Hx)hW2AGEGbumxvjnSd@rTQ6|bog_w!kE3-tEm@VdrYEdI=t{(Jz}re zhnEK&qDAZ%2k`p9A#qsThxZ5W7e~bdxP5yZH&q`JC&a_z5%H*aO#B1)R-X`0ihqiy z#M8K$n<~aB70OJdQkkVxDYMl;wOO5~&Q}+x3)MyHVs(jnsd~A(R9&VnSFcd7R9C1g z)m7?hb&a}Ky-HoDUahWIuTeLs*Qy)U>(uMj8`K-so7B71yVc$59`zn|ueuLAba?v# zwd-qCr_=m2`$8F`jAvismg710ovf?hqptR3=Wy>UkqfG^+!rln6c3j_t4) zs&~N5Q@6sjtJ`2^nq=&h9B7Z=*daOYY{#(~_BiYi3gl)J%rNz4nB7b=wn&cKCC9Cj z<2K2`(Q+ej?=2TS#pUX9gwWa_=e4+RHjNkX;d}^gYlfg&H5%$9c*1m?%D%e)5E2aMm>4UbfE(5iIdI5EblUfLB zHA>GLDVtBUIuGVzr=;<)+0_D=nd&&0=}w=qu;t^s0QrxWJ|uY**<~EVK@F8umXpc= zm9NJ(glKgzOuJKNf7rs*SeV_^elUZaKGCqnsC}h>AL&D}^&-2BBMMZgq`Et)uApM{ z*g}X_gJBYXXVl41#1x6UhjbG$9eueSJ@`nJc|O|50_b(AdL=YNBbh)A^h8QHRv4Ub zlx4<1%aM&Qg?mQ;xd`lx(ik(3hidBvsD;ZxY2mH7RNM)~u5~vAi zJK}lq5|b~2@ZR9)?c~tL&?3n1jFq4>6kb?KL0oC*|8Y@WawnNm!?CwR`J@|{Hq^UF z)I2S}acaDppwfsyBMinC8J0$kT!1@d`C=?_$F7UO9l!U9%fu6*^T=Ar-;+EiVUFYP zN=_W<;UrtD+X8E>zW=2WQ-m3WT)X(F z_P=9H`~TXA=7&4S7Io^57@veg$@3j6x&b%mq;AU+fyiI2r6;#2XN_(A+A zeiA>6U&OECH*r>+6X)@U!Tor}-~qg2a2zigJcPFl9>!}1kK#Rpe<+VDPbg=U50np; zkCcy%VFO)BpuavKqZ=cGgXqtE|MypWBotSQ@nU3oBC<)xs*I z)@DK5eymCjPy?_Z*H(9kf#gH_8{$pzmUvsdBi= z#ND7Y;+=+Od9!2DN@azz5-Hq*65gt8Q?}!ki5+-n zVyChT_r34N3mJRxw!>a!pK>qW%4oq|@B?_+;Sk=bxbIKv9CD2Os;UL~^+PT(6Qo)J z-JxDUcb|*%&|S7N8oFDh+zL%SshokPK2*MAg{WV@L0|r;kriqeXx^-&A*}1gbc1~S zaV~yFZN-QR9e##|<`I;OC+ht#EJT(?sA(kYj1r+yPsJ-*3a>$#V(h}MsW$3I>7)LW zaKm>8m^4~r%w$Wk5~KORz6*R*tlG5DJ@wGk)?=Na1>;yzk~9k6wM}^PqtzY7#Fbt! zwJ{8{#@3IdTc*h^4kNUSBLcgrE?Z|gjypqR2N$xn_HN>`%Dm&$tA10{gw>i|=U~g%!ah%vZ8xOtR#7A=_mch<62IExmzlmJraLExs0} zeye_tPl5L?YJH5${VrjT#FUyWmv5?U?n?K6l2X>i1qMrtd`TUj*i0` zI|{4sAgs*2g%wrgSN=V89fH!K8R1g=IBLKFxIKuUBKIUzxhG+fdlFW;C*g(~vK6z% z-KZxts+>oA%TzM))i+PcV~J?5ds!0N;Tt&Da|UgHrfmC_(Bp4xp6n$T$(Fg8dSA9g zj-Qvw@pF|NKiA2b-6lD+qczpNv|EBzC#}@>s_zOWg|k zM$E+p)~(ZFUawArc^%#!6j;wrfqAVu8RiBRvumt#>1D$8>O`1Vs}o?ZQww2UrP4ZR zty~AKk?WwHMa=gtbu-?vw zxkROP(PFHAX#JK6bD^36a{*RO0_$^HAI+2Nqh_qSI7SfxU18*gmhgTj`ZFOlDA0%g z0{aty#{vHUJO+3a@Ce{xzzM)ZfCmA`0eHJmc>r(}a6jM(WNpDWdYtz_uPiVYd<%1r z@-LXX73@P{j6i$DSn(CiUCNg*cPd}Nyi@rc<__gEn0F|j!rZQW0&|=4G0d&XM=) z2AiAG18;?O!2IV^*sOR9EkyCheCQL{EJ~yjtoUJm^f7EIT6U1)i}}(=uqjHo(nayX z{OLp3gknRB_eKl<05*=dmI4$n%&*SC#?T*hRy;A^I?b%2Rq2K?!vmu%^1=6rTVZY& z8^}@xzNML^5c3n04g4KfE zybJtqt-1c4HO9qOUl&90jZ)q!sE?6Ip!ueyt!tn*^a`Z~e@O6JNP7NH#8Ha~3 z7rc-Ci)Y}4u`Hg?(`YTubLHz(Bd`{)<`eN2-0l1lyij>J-@y0acQdWd`DW}!pW#nI zd#~|lXl~74p;a6|Nh>M-9<8PLDa^Mr_!-Q*?EE9>aTNa;Ytr%jYpktqg?=*Jgo$&TtXRJ9k@Bo~Oz-~Hg z#a=`p-s1{lz3`f22;Kk-#Y=hJ@GYl1r}srGcsP&Xk$6Kaiud3>@orad-UqLV_2toc zB`yZM#Jga5d^8_}ck)iNIJ}$}kGUf4pA^z-c__6a zypuN*dU=6Q!E1I?vF|+{WjBKtvr1lq*9pt;-W~0+%;c4P7O%p~&2v!3H7MgcUXM4@ z=Hk7)2HuEw(;PUN@+H0!B=crIkI&}|_(HyjFJ>ux3Esi`0I#Y2#xLhf`7*v7FF#+& zSKwu~m3XaeHC_x{%df(2#w&asUemi8FZr#<>w4Fq_FOApM!cTiz;EO?@lB{noAFNK zt^78e7#Ya7pq_2TiL*giZ+*_Uvj_Mcd`@!-;Y-U58!>mLwKq1K7Ism1>VnQ^P~I$evBW-`L+jfx@0Tf)yu)k?L3C%I(~vb zj931KvCsIUxEc8v{|CL(i2eV1{v`h=-l=QgPs>*upX1N-7g!^I5%0jwOcG^?V(#=zYlM;lmOc3Z1epOuMXz(U)i_|zXD=EtAH&ODqF074V2>tTcUjnWQ*`^@OQoj zvTY)iU8;Q%#EG5p_ySVMCba!F$R^^;SuggX=*`Z$z7gU)(xkTE32_4FGLg)diWIg? zq_WHZ_?scl_Ds>g8;)dCan9%qF^U~EeLWneg4jq9xb`<8|J+T7lt@ObPIvOvM_QP7bzY>SnRujqvOBPg(*e6K+cCy^;(S0yW^c2%>f9g1>Ahl{_{+eF z1aGYV_tSnM^QRp{oaEb$oq&hfKQJ~Q!b!pV*kixHi^#6T9fRY{jrJ1RPWIEq_7m9( ztn+Scw~zg&yS&&p`Wm|tN0^OWgE>QYoLsyfCk=P861IbFWVg~TFuoz*%jOR@F^)$NT@DSebjPKG-4hGwmY> zV3yMbyBWc12)<3;uXd#`ao8>Mz^TZU>?)kMT!-_7GuS7%FVUSnf^T%;xGfQZ?{rb} zOC4@ks(r8n6^$>H{je9+A77p2cRPIdOUC|GD!%*;p#Q9Jk*{kx|G|lr8F;<51n;$$ zVTY^&uXa|dv+#23Y;_J^bFIM(u65Wwn~S$R8}W|o1!o9w*70q2ik)O{;DqEmI5GJi zdyBo#-eqs9m;DFl3pS}YtDDtZ)LYftuzPpAy0z_TnfB)oREOH4?pF_}2h~IBVf8-s zh&pCMUY<3xw4q{Ng!b<3=)tgEe<Cv$|q| zXI6P#Q%PA_MQxLeAwD-=!&ynz?6MLF1Zt=&k-qUcaeDgVWWM6!bG7*6vy!bjGTYW1 z8AwZYyv}XcwUDLfDoam&mac^?nS10S$up--a^iFBx&rKm0#e*^^o+{f#OosCliYH& zjB8QbWzR{WF6rw zW29P7VuF!Nnbi24Ea?vaOt%qwl1FGsw2o-1sxGha8tIbkJ<60MiY6{TE7h7O%gvf6 zbKsq4Vo~#uYArd5QU&1G(~+phpJbFxrge-aXN)Fij7g3rB1slNd`?n|l2co$M{n2t z(yU{hxsA`s*2^wik1bm-!)#qdww9U%t)|2!BuJt0 zr0HDQx=wTAe8$ddMtyE*uBk3*mN|!JbKJ(7D?_p#I$6dQhvw%swz0aTah9q2C+U)t zv>Kd{kYz2L&dg77_Q-z5e;8o_*gJ+pDN7iy@$)n5F6A~vgfF4bYC_XF2S|RgituS%N z>)dudyIFejv-D(U>DkSa*+g=aJu95?>YC7VYBzG4>Q-Sc|9D+we6m|bn=V&I9hZ-4!Lzbm)CuwGELYcS_nPGjsHH6-*;?f+RHR-{YC^6@l`M7K zW6>D1^xi8=OH4wX-K|Q`M3vqNS2?R~TtZ^1*KAi@-g8We^qy1MP*G7^T~b?KRc5W0 znSDI+8mEpZ=OX$C*HPqpsvsr63S+yovnwTWL zDkh~W70zDS=)MM6>zs-}udEkuwjN`)Ud-9Lh-@uI30h@BudJ7CqERjMYLk^~sdM!g z*?PKj3~4%7wqCh%;(h8a>@QGJ>&*2fSr45o<3g`&_o};~SJovbX_Y)7A=`@k5}3PK z8#L`Sw9^i{Iz8#hdRF3%XmYKMrbd>cYak`ntI<@#jqT*2SJvgF=yFm9s7=%hH^~H0 ziNwWgeu!IF%mBS9r0B^{$+b2+O9gCt+&KnLwKTi5o1>SNHsgxV$uYF83(Lv%X}(aq z)cVZYO+_fZH*S`_akH~p$LFLDu+G;i+GoLpBb}_-dLGAqY5w^Q0+Cs)k}L-xp>r)*v2vsOw3U7 zl9I{<wxRCzE`1f!Qw7B`|Y<%hj%v)Zv5lE-`fH zVj*{r5~(Jaqmd9A5Xe;@6C|k>7f5$GGJx=L4H2+;l*l3kWho&wcNy$ghH_}^gR44| zj7r^~211Y?l}26m&_+eL`I-hsSpCdnBkVq|;Sn}13kjqNl?uy^MoO1E<-*rAUb=iJ zcC`wIht?!Pb!y)@rLU1d->M4=hK5@3cvqP;tS5*H%$vj;1F+_vMG5k#YOCuk5<>ov zC)KWx$>gdJ%QBQv|JB*B`kF>&Sk*Z&{LCXXIab5)&<1Bo8THN2JVe9CjYer;Es=>v z(b8D$a+SI?bxTsB9I`;EHBQDPs*7qZd28dkq?}QkM|4*hS%qYGmPD~zF|_NRNJN&4 zkJ95&>(oRYS?io_UK{-3=393`VbOHJDJR>NuWVP-$#$h8+o=Q04iL<{u3Z_^Y(W=? ziIPil7IAyjwJTw&4(@Uep@%_gjNlmKOi3_W4H#UFK8NJ{x0zMwa!_R?n?AR2N>6r) zOg0inlMFC;X!8tEzNQ(6^f2_Q)q^BgT9S;kAlu|>siRI!t%u>>M1wck=8{2;&qJG~ zNXl96=D7-dwE7-zq%A%t+0X}O2&TkS#A+jn(&jIEP|QqZ|Iuj3*OHt<#fB-3v%~i_ zO>f}kXP)7>M7dIt;)*(j>WEKeMMDiHxuw;V29+9#8%=bgA5WgA)Q1r zT}n)md17p+F+y2+Q6)3Jy**U^}C5i z*@0CibrHp-P2(hGR5KsfoX#m{00ov);lu%QhaoO8bAYT9u*Wvd)JV*qnr3QT_AJfG zo&_gMeHBdiMHLNov2~3z-LOq5anpQh_h_1hg%-KE&#Y^1(1@ye8c$=@0&?(Z#MWx9 zq$;W^XEjNt+A3X;l-xAaN`iq%F5=U~B1KXvVG;^|k_mrJBnV9+7^J|anI4)vN|2OD zgp>)wQL0Kua@SRF`R%1f`h>JROFk{llGM^H8D*L!CG|qr zxn&%Zpg4%o;~-LtL!&4T$wzTWJ}nMOYH>(L83$3CUQ(rA;^OSuDn8CG^#Z%DmsDLZ zsk&ZLb-kqOdP&vwk}AWakI}-#CntHJL}k3Qyk$vhbfXv54W0IuNz^SJ>nj?n>dI-K z98=8lSUj!Kp`8=DMZ|^tGUUCEbXf zv}&rBh_Peisk6-~OQpKdk0z3WRFu-~DFlorEFi!4spE;hDh?bSBG$N>ITdl5YDA65t;o3$%Hqle8^%ac> zuY2n)#V!}im`&<&YTXg+$xxFU6&J97o9N--Ro2{qJeDm4?NMHb6;lJTQ@RLK(#U6K zbsd(}pxp6pLv0xd>dhcL!AWLjgLI>0JDu#AE|R)EElg$#NK^pIg459+?;=T%E}r^N zuSQg~YE%lRv+P!_3(@@{MoLFQyq0NwpCKVWL96}=aoVmal;EZF$$qlFy17wyoi$ap z8s%NnT-{VvU%il=Jb4fKhde@?!8Wu0?gq0v!Qd)IIqm<^aJ36_1 z$j4=)Se+62mDJZa)GcuO`j%kUqHV;HP~Ymh%Br%GYD6j{ud1C{Ra@1hCC|u;t|po> zKoK%iC?PEh=*^iWDq1Z^@YZ64w=+KwV`PU)kaE*B)2~ft&=_5LfyI>-v|4jc&^}EW zfz6Z;#Och2q?>2~N)2C@HVVxkQEo&+LFLV5RByc-oAH^dsY>=Ko(3z}JV8Ytmh&dg}a;lF?RkJ_?B{8{_37fZ5ov?Ul3I)}ntxDnIYf>s%O=^YJzl~zy;Ad7X+0Dv@-N&U~*xX7Q z8tUdZ*LzAVJ1!4NmeP-0G&W(u zihkQlio)h(V?|kAZMnvm5T_+no7+N}Q9wN&WLhEo6<1%%>OiGQ1Tx6NzNQ7%z zC#UO-_Guu0Bi%{5q$CPsqT@_-yw9wPDZUh>1%j2a)|J6 zTbG=E39guEM@o-Lt3^&G+VR!Xoe9$0Ssh#? zjce;mD)MhzJ)EgB7R6}$#)bfO2MFQUrdk*QP$`_GD|4t2PS-RSnd2nomRKjP)Lg9x z-g-}!LM3UUjUJ5L2bkOwbsAp`ocu%^{6ri4L?`MkNy?YC(d5vkCCQlDb0nG_WSs3C z+Q^Wxx98Ag=!K=F!&z8lH5HbYhBm&mnV^T&;%{R$h1KG2<4a-bj62;X^2C|y1x&ty z$ilQY5OMMle|RU4ZoDcud+4$=oPfuFYM)clP|MOZv!|RW&5i8)%R!`vIBK?u)t=Gc_!Td5f*x#&jFr z&E73t;*@rn+~eSHxEp-!abG>0!Ed&k`vh?#!v1BpE=xmq-8aL@bcfmOLX^( z@y&x@J;1=TP4;rohW(PRMO_PEC!E&~=jwI?xA!M|#$O0;gfqu85MjwqnB*q`VokU= z=mX3MMJG?`;lm8a*neFCqMtH;Dkh4h-Png0R ze1^ZoNW)=D2Y~XjrJdbyr}zx{25ut<{$$?>yiUg}_4wQKnbWlt@ht*00cv$Ts~tAn zOW{7v#AmiognK@K@W?;G24A+$XV@v+5CGW;!%qECXc`~^5YrC#Y-cBbo6cw8kkHPd zzM<}+ghg8%a#ph&nEXS2yf}ZzH{H@hJ_8W`0C2LM-Eh}2#b?Mhun~XA8yCiFq%Y(p z$bAMt_;DSdXlI8z=_BN*jt}a1UpqUwLk}158T=+3au@iv0|?)G5qzWWzoBbU$Xd`> zhFli1K*tR_t~R;PgumHt<~PC>x8pb4FUCK`6c5QaD|81kOFh&&2s_#5#Vds1 z*X`@uxr2}DTkvWZ4qk4uF9EIFgXaa;2hWC`aJh~R`z5-)NXG>_Hr(?}{JADMMtFnY zz(zR3A9ngt{1PL6i7CDeq@DZ>JWsa+2M+|8?2<3ny4~P6upVER zvwA#6dW>`!{yzrUx_r|uy~}5S4*(~1{DzKS((yAU_s2n>02~ESIKl^Yyidn>ncTO7 zZqIjXJ3ivS5wJnWYcGPY)cmO*qVb~3WnC6@Y0z znbY?c!o31`4)COoA8CgT_hWEBY~nN99dO?bAiU#Gu)()Q=QHdSZWDm)gkh&~B4A@X zysn)c{sAjBJ||upu&7H)KvMwWxOTX&ZZ|Ob*ZvOwEXXYd5S|8@*v@XaQ#^(|1MBey zp|R*~vWt5ToOsI<~d5lY0oDJ)gmE#+||E3n1)%5iB%+e1&+O z_G0-v_#c=s)#u2CzH{(zr29A45kyPhZ=~-xNP><>uy2Q+sXYJ`;UM!v#L z#^NSp89~0vWkf5NNzNjfo+8O#B>9UZ|JRfTQ6xE|G)^)tPmqgM%S*8gRV?|7rTk*z zQLN;ySY=pzGXU*FG+t(eX~BOtLymqV9_}eq?n&=6{x{sIicu9Abp*l;>&XTh; z`C11PZ5=G>G|3q&Ic<{1CMC%CWW_iccARt{C*2Dqf25=%iB@DNMTS!3d-9@}4Ao1@ z>?OnYB7S_E0YBc>p`6}*9^V1@-Bx_z;dkS$Dwxv!Tl)T^Oq0IfT1lVZ(sv=4;8dnb zo=+tI$3!au!~+vNFo`FCd=>eBVEDI^^Z)Sn9e`C-+23<#=FNNQy%&;KQh_9dB$NOT z2%#iEqzOm~C?IxGu`4PnDvOG1N9=vqW!1H#ps3izwcv_?h}g?2YXLIz{m!{FFY`j8 z{P+95|AZOlow;-8-gD1A^_CwgrD|ZeKe{5Cq#BgJYH+m5pm%YPd~ih$>a%(?-E%$< z$#?m?>$p_a-X(Z?ztM(LReN^hij}r-y_TO|%cZ@R>vb)E{aSu{Iekl9&hX1Q=Nd$& zgjPdOwL1RdYn*aFUo9%F_&2}xZ+`3FoSQ|FF=0_HgvC!S{;tK}wHRX-*P@v66c)XO zl^Lmo7G2e3O5=&=G|BSH<;kIZv>bWQ>9e?wyeE(P5v5f2da(|s8ltDlUP=!L%Bk$7 zlHdwV;)?v0y_65`@s_w$rk7v$zD8xBcTtiyT#M*kT=6ch)Cxshsv<6VsyPNZyiIk% zx~){Bx2@}N{fhDP6=UEl#=uvMfv*??U-5Up;<^mwx?r7V)OsVqJsSz`*~sNdbhXUc5zc)QHN=)TM6Ya&F_dw((orxjftX-5p%2?OdMiT%PS*o`sxp z2VFHEMLxCr=pHzsx@U46HPiD0K^R^~ky5pe;dKPpwh#nYdUuP2aLSb&iM@>Txs20a z#`#<(pE7(U=eCmHeVk&|>PpUGo=n5nT25KZIn;6vwG6-3^DZ!dfS!s2ln<^bgE&BG z!~sroHNQKFzcPt)n8Z0?pGcJUNB)-Vk2mqtoA@bqjYLUq;PT(VIo!ZG+(2oxkzCFj z_}wuwC0&gg#{XJQzm`%OVM=4{q^H_6f@{<0T@AZqqJ#nd)^NJl-{W^nD2?D&(09{Q z&vH)bS!vG{%5N-xV6A7ue5{?)k0jxct{~*{P&vCmFxbUywY377yMQ$=RcG0 zS98v*Ip@_3XTGG);M!rQPJHWfda7N{-(`-Wp3HBZ%wIW~o+2ILt@09nrs#xnu zS4QJ*EY?(P!mf;e#_ojIDK}y5q%~L#>d#o4=?Ud#Cc9+Ih!|Yp@dDSy&108Sy9a zEY_U6Q|wkt#PitCX(m>Kn}r>e24DxJ3$P#4#n=UDHugaJEB03U8+JCj7%NvlhFz1M zz>Y>Mv7^!J>ebkpQblw9Z=IT$g^yt*fa??jv}ylVu}{MP*6DoH`9G>0e}t1iD5vOP zj@Cha52J4)qY0WYnt*n#>CNK^Ms-H##WK*3ighaEL1EY@2o$!KX>=`77$_|UEB;Xl z|F=#BYf5%^(w;j1TW5FQC)gk3;19~!@hRc9<5R+I$EV;Xs^X2b5smn$(+4Pd7)#Nl z^g??bfxUHTEJc`GjaGX0DZc7;MQu54Dcih*bR8=oW&+wu?WyKt1%28XPO;WuHM(#Bbe&)&1+SerPq$MBF7>bI|UFY<)$e{qfN1D)yPx zk(*G8FuEic+@%fIW#A~e%H8eswZV2^8MS!PPl*+^ zSamc7d}T21j~7?sx*B^d#bV9PyOn&btN9R~z9e2#(y-d)>q>@rL%fgs53s6b0oJtq z1W$JhQ}JLiRYQrx>X0S4mZ?>^&Q&kKb-B76*FUM(D@outw<@XFJ?$AK9s8!OS2DqA zURJWOTiWYNHdZ)%3$*=#`hk**^}j#H^%JbB9g7`VKF9r6Sl>IARu5ESusVN(5|4d2 z2IJbOH7W^ML2w9m1)){%lW8Scz$a=GF%s=C?J!&?X_J&(Z8G*mGqfq%OyqEcb_CYN zn+2WOhcyCc<9?1d2lsPfwZ&t-z$2AJtQL3_?iXu|@$Sjm$+#}jmY@u$Xs6)*H0?C- z#--X)K+e+60%WbW7GHk?Yofwj6U>^)3hFq*#7mIeG zF?4GG)dZ@~#_m(GSoKe#kv<0>)|b{3)*4Vqm&4j>y$_oD4c9~q9Fz|0Q)>;<4z)hB ze{bdg4eJW)3Opq(!@lDGebz2(H^%Cr3^?-fbwBD9yZ>S22IFunSiUOh~&D$}c>>{Px)9$#TaQZG|2_O}An z#xm6krdsR^2C5ZIudz(8F-)m3Orx<(RUW1)4^tJ^qX$)an5qQRhexB9_HavkxTOWR zw1->T$1N?iGqf|Xn+mnJhg(!|i+Z?4J=~%mtN@SERA|%#V!7hQdX3v#jeNA)^=+<+P@yS&Q~T`dm|W9tV@wT9ry<(t-aQNtQGXkdc)cTXd}`mDvj0) zxYD1*ZFlrSmUs_`LnZFV)@Jvw==<){N2iGX1^#x>VZDA3C<3s)iQGnRteuhD=o^7v zc8Ni7jcH9~TGN@hXytVG}t00;MxY+6N@#LC*w+7G@j`(p1EiiQ)MDk zWx9)trZa6OGXIQW{+Yo1GlBVM0(7-TaL!@sFkFYLBXDg}n{aJbn{g%Xn!wyOf%$3@ z^VMYLt0~M^Q<<-3y7(&XtAIV?xCg{@4^X)W#B&c&xd*7+161w-@!SJc?g1M2fOw{T zm1#eoX&*}s6R|Vx(~80u@dX0_D7Saqjw}iTKTPyaG$6YfxGz8hib5js{EA*Ihl)fR&$lU zprBmvPTD8X0JWw@lg#h^A`WSNWWCWPO+N8R^i|?teAqmbIPH%%SM}olORK|4@dy8p zgghUvRD&mei5_2#{RjoDfdS~tTa}ZQX{f>>|9qEkxiYXVxY z;{2!e3g7R-y{i@yv-ITu(~UQHq z8_DM=8NbQBk=lK$g*{H`pfckqvtGs(`UtM^XnTfAJX-&v1lCVDf3|6Y01EU(yR|?C zzjW#>+~V?UxH@fQzgfm5$d5p|Cfp17E0inF4oUy~ncusp(2IT$(5kT4!c3(cUXaCD z>Hk=94%X1WNIb4w2G7U)*fnh%Y?BXocgT;iX8J#s?T~uAlpVC{xbg{0w@+D{+l_s~ zmMNcMjqFF2JuKI-a}xYBq&?KKrkc;%Lq0h29iUyZtnyiVsATOSpS6cl)*gDZ_RvQm zeYK3WhrX;m6k|V$I9rF17FULCaW!K1Hy?I#7@>r)o5M)llU5NF*vA3)q+0~VMC|0? zV+|w78b*+IascFTF&Ec%aU9;2mfKaBK@+m=w`;^rxF;(vh4qgN?1b>5Qo>qDE^8rq ztRd&JEms75Z9{sp=rR~FJA}k zf~>?uwD$nmbpz4rA+{J3*|qnGVQgY8By+l`5AHzu>)n8>BXF1s72srLskXM;D8k5LIOx^G^coL(9YZ@@ zi55UwL8%&3s)s36EYaZfg0JiMGYDKVhzd!TFI3;#qMDEo21p9Ou#Gkqs8 zeJ3(~CopAeOxYf$Y!6dvJX4zwJHLY3m`XfMCGpG^ROSk?+#d0aci0rL?;&Z7=e2#% zIRoHXw?KB%j?hKWaiFVOS1GfUCUDaC@v9zB$WrqGvX8W+o(`K|H$m&GfX}r8=^(p$ zIAA~G58wX7V*pZ|Z>OUBWe!$D{F0N~A`5~W|!H?xg?1QZ}NXf2; zb)&Th*6VG+iKx{&AQcE zkGore7na%3quxWVYpj>87Xe8J#^P8X?tmwc4liIkAVH&aDLcnQ`HyJfpu_q*@L7gG zXeqFN67kjJl&RKp@DAj|p2!E~<%9A-hj#l>>)Toc-LR~t|9_=l(FggF{wD{%N#31Y zrf<+Vu(uM|4bc3cjQ}@4!uJ7;T2I-ngR%fB{Z%g`#mkffXaU-=wV7=Mvai0QG5~L+ z!P48W=4}setsxBge(H@UJD*f zdp$pcU7oQ^qOz3r0fC(^dSMR|@?WV+DRu+OvONojDTm{JHg@jFQrhvC1qRk zOId`Tn8o^oid{hN!aZpbD&jtW#XWgxHRVbCWkvZTo<}68(B+koelRcV6+Z00@jcR$ zZz6*^dUiJ+3eqmJnOpl=yU4bE7NlKd+nx*3F0yU^1@?(ja&0dL=@{9pV`Q_A;kW%7 zq+|G9oY>DgMzQVVARVI@^WbFW!KuuHlWl)7=@@?7>#+;_%4RV?&Xyh!VF^pgv;81m z#L@F?Z%8RdMdaB&k!tKFlxKTJhNwevFa0B7HH>@dB^jxX#J%*Dj8aG8UV2PMt8;KK z{U#Tx7vY|K(+P-ut;9Wfs58|Y)EjV5e(EIkHuW*3h`lN)>{Ur)uSz<5Rq|Q$$+3Ma zq+j}3^Xb{cLV8FxOBg>Wu}#UceJ+c%MYt#5Gb@|wr7#$&L?W1z=92S3q^ z=ywLZLsMAhrXt3Y0X{=q;VNYStko6Z{$Ikw{25{>>4=}K0Utlbx)5?i4AWs%~=OHyw`fqGMDALej-HbN|Kn~ZVd>x7x-x&wHjDG)w zC-@THeBSyP{BK{Bgo~05wuGhpej@J4--7kl<&fp>0;Hdka7SgZJ#^L$ z+$!H7)jqTXQG;Rq8Pe!eq`|wm{>CNSg6B_5oZ|PZDBBji^PcrOr1{Hep?K>}w8@v& zz3_Q#MQv_Is+U1ch9l{1D|#=!z*K_v0KL%xFABBK2Z;SiEy|&TQ-G%dI2)ncK=T3~ z>+ydDItYZ{cr zk2d#1d)sAwh+N-*7xP7?(eHsp_O78lK112|LFbcxtcOwKW};x=Z6n^HcttwO?MJ%j zAo0Hk9u?emxV#9$x9nvTJLU)og`n5w6R(836^;=EbU@g&c(2liedQ_ z!;&e{C6iKECZ!%ElQLK)#X<|J0e7WHsh9bDHuL#7+Y+XzX&iI=c(#7Cn8U|0htFo- zp2J+->#}t-UAAr}bM-jp>Iux%|Ax2J-=vb9>2c2>1!G7y?SaGBt5qYoL06oHBJ;JLFQNy?=-NCDp?qIO);8jU? zFjUeVytF%^;#0?}6X2zos3NkAF*I$s$NsRmpQKL0{bcAD23l?zzIy?pGn#s#dLg_Z z7eUuZ;&UGx`8Y zx=uOwGwje!h#7u{XTKrxmX2S4N1C^+Q^83;1n1g}zkTR)=xaC+HC}-Z((R$46ePWx^kMjSt}BB0oGW z>!2m=MH_u<-LF*PtB*lzz0EofUw)F_vd+c19er*$Vmo=(0i;E5Ln}cul?TccRLa3S zw&MFmu&=2!I7y=8y-3@0?^7U|N%vy?3KRyakF>Y}PYK?lTqz!btE?&e9Y`C)A$20s zkUNkvmSK@}^o#me_lI=@!6`}>(bewdl=|VZ~lPuReqy~ z3(E8u>IQDr-NBk3X?~>l{fU1)iu-qfgJ+<*y$bvo&_s5C^X@{cd|e)H{OFSfqXurbo|~Y?$qoladb%Ht$X0HON4|-2hOn~7ry#4cp>8v zcj%v-v*aZjw|b=I-TC4^pW7}4ceZAVJ4%}_e?#)DXN|N|A3X-#rb0=Er|up^q4MD$ zYlQB)7rNIp#HC)fE(W%)fM!<#FW%w!wVF%68l`&)Q2RiA12nn`G4CsIuC{JeE&=Ub zfgGLyreES{8`6@_E45okx3y`?$;ip?H=72312%U%G>CV9?3uPYvyd_C&flGPZoVz= zyUJ6-`~SZKlKl;Qw~W33>i7jXdI-H0zpd-6zgdrg`mTWlECvPqq)Z1D{sg)Nb)tnZ z$^o)=BWPtcC<1XCWKn_k`W4p5CfsMrI3~FN9cal{ng4I17$H;A4)7xMYEb+O`0jHq z{ryGe|1Ut#--PeI&6Hw*>)ik=;Uz%-0-19Ue}5NB_bU1p_yT#Dao9YM@9hhF{KzUu zMJmWIT&XX}$g~2>em%ZVyu+~tj)C<}oN6`Gp913A^mzk2Kuh%{o>t596m$NtwC_Pq9ItFM7QugM%OlX2Z-L3UsR zdtL{y=e2=7uk{?)ZD7x9Jx9wL*z;P?KG%AV>o%~@wSj%E1K8)9jOZ79UhH#C=IGY| z_PO?R#dQa=&o!HUuDR@U&1Sq8bM$L~E3VtXHfJnbPlMU=48eK|DTCQ!3bDmBm@TFd zET#s)$;UGvz|$!AN)&$erd%bz;H_NP)LxsW5tWlS-BU6JHUdpsLOl8ZQ!JXGBYdJ1xE zFPUR|H5^H9udvFJ^ z&oq^-=rs10mar{d%eHhX+tO7WNiJZTF0g&QGy=JRt?MMVt`pg3TF2IPBKu71*t$+- z>pGpS>(qmM!^sDY%1h?{P=Wq%rcz;#&^uc@7x(9D=i_>Tb}_Cr{;piRMY{!&67ss% zYPV{);r@2*c4Zj$7{3GecVe#PFzhIP5AN^P9>D#B+Fx-0SM9I3r!f)(*oIGH8$O9` z_$0RBlh_ZP95p78{Lm?E)n~C)pTJgqJX`hgkr9K#utR#GqOm7Nj%BCVN-6f~E`vsz z2F}?R_f;4%myWT2qmgn9{&bGKc;K^{2A)UU$Y5?{FgG&5jgCf-#n=$^*kkZ#D93_7 z`M{r+;Cc$UlmRYvKCa7@+mZhr_|xFsxfA;6UHD7n7)=a&dg2hHc@kPVjiis^@g4~Z zafc-44$0sRJ77B!cSvRKkj6eCgZW=N_rrANe+JWZ43Ca5n3@HTf=FiC6->J^OpC%E zxlR-l!@SU7UKnGK&ZIHua$IISx4p`3?~F`77j5rj?~B3h5yO1ZV7^GZrN?3(1Nb5| zMS&c8D7hBEP;*h&BYw}0Xt(T+UJz#+@{uTaq6cWN1r zi98NCeg-?&(@ysrl#R-3m<#oW@}}~Z@-{I3FYI)_4Lh4}2lq|KNQGK((&NPm;zV(h zI9V(ar-)O7~ukVvKC_$z{?mZJ4!i9ZN@AuqN=GFpE(uLvcBRl`SaBjl+BF>j^LN9}#Ptn?O&cit$=aC97+Ct@O?P#3GC|Bbb%8NM= zeL+Wv6)P6T6O}4SqDgs7j8bNbX60@%T6qm4IbOp!j&-6{86n0i7mB4wb2`rJl@{?f zM(zo5IBM!uOxPoqwOy20J4GqZRwW>o;{VgFofwDFgkPg@Hsc(Pa}3TFoRn%j&Ivf% za88DAg;G}`brn)qA$1i}S0Qy3Qdc2$6;f9rbrn)qq2wm?F3Z{~bn7AE!&zc&K?%0t z>s#>kE#eHE*Wl6y*g?c|lcP(3MyD5E|USaDIey z8_th$ZpXP3m?Vn}ny2z9&fPf4@3aT!=Qzn4hfSwGYkjIdhjRnYjX0mj`2x;OIA6s1 z63&;cPqiBBEv?=9QJaTzKF%Yd!O~nI&1dSZ;4G@jem*@@eE|HY2)UEDJWI)i_4Yl| z!HR|MPNQw(p?ey5en^M4jgw^|2ht!(OC`H=1iqdH&)7S^n+aKc`VOqtp_h)3T8x+ z<+g_3zYX6<#2lKnd{Y($SN@vj{FFL6{j&ZE*`YwbXoNY9 z*Z30u$1x-UJRNaA&>eom4l$%^)v{>$-5kI8joa-#d>`py5z+NmnK9awtT^5Et?q8qxz)XL zj5{p5ctnI!U?tZyZ`qSHC7Z%Mf+*oKhNX`MB#1lF_)S<^~jO{-9&xn5qa9bD6AN2kfEh-FC{ ztC9W`%lcC+>rb()Kc%t$l)?H_66;SHtUtxG{*=M`Q#|WW8LU6Wv;LI9`cph>PCCzk z$Y=dY$2^GVlzi5rbj)DE2p-m@bk>&Q6afsUQr`vE-ToL%lK`#KtO)}a3bntAahRY2 zy-+IzMbJp=L{MoH-Ywx+7`O(gbT3i?;r6z4vl**CoH1t zA=L}L);bVL>*{HoPv?d0ZvF{=2`tJ!0p5Wlm+H@VzjLwn<10_d1hQnne$TwtU=M;} z=Sy?M_^^)d=@ZON2V(wEnM<=&rPr)XX@m+Wx#<|Tzj zf=fQx-BTMTano}GALbz|UE1B~XZLFNY4>XnP%k@ZCK$~u!dx&t5xu8^d6vp{WIXe%IObU~%(HTs zXX(tde9W^1^DK>dmX~>!!8|LLc~%zlED!T6AM>mP=2_Xyvoe`yrLc{d!Zub%e+lz-sUBo zD>|@#H@N0Kunl!!{XyLS1s0;@Y95}~<-xZ%VV!&$l^n;MOl7-Iu-zy0M6^g8IM%Ps zv6gES(8GG6)iD}X@&{`%{Fr{|7)44cMpBkBeHY=*4@d=QJr_QH1Kw`;@f6e4KsSi* zn~}a5c{js$oQ-QIQUK|XdJ!B4($jO&?C3}Y7T$sXa62Le@E~WRgO}!xpWi{=+8RZpYde6^V!b-Q5vc%4tihmG8;c`?m)G7 zem7Do)b+2{lPITw9Qq<+b|2cS8u%W``7|PZ5$bRpWJVt7U;ywc540mN@+&;i*C7hq zA6iu(=(U(nk2F=vAdD!=LVlQ3t%R(z@OCloPsKYtJ_`N-(xf(94?>o0$K6)o-@q5s zQDeMip_jmu4ILaU@DzS;hb^}R|G&*2pKiU6x|M-n8o#H?=3Z)6DAlGc( zkC8@*&hNEj-vU0E{}Ib1ArxE$tinattR zn8O*&;dJJ3$;{z2=5V>p;bNG>6)=Z8cm{qJbGSU_a4F2;;+ex0^q`ylPv+s1->Q)L zTnh81Lgq?^tZNmrmQ~37uba8|%!$&O6Xi1}QkfIM`H5NkG)~6DwURSN4CdPg^KCEl z?KrMg4A&}_Yh`e))E>q|dYEr(%(r#s+Zyw2o%y!m(l`ySPb`<)!+hJrqaQus+b^>I z6stUEj?r4Mu960CQwGK&1>h5_Lxg__d|)}@%#lL}4;oP4zoINyl$({Anj8z4TWocu zNGVS76lc_C1Tu>20@&-7|1Zr5)Y;J5I)0W>j1+udhZOYZr18>=4YgWJ$Eo5w^DmdI z5YtwySn(#V%}pZJd;s?=n#}LSv^SfZ-fR-n%m>7@rZ?%yRac2=?_4#)X#3??pYQUf z)~2S`7vFClH*Va^xW8(^rPzzAdfr`Moc-n9^X9$V)ZWz8PWml0Tz#i!GqjLcrFXbI zR_H25DMP0fI=m~Y8ffuost`fA2b366R=g=GDTXhrvRK2PC>Ekv)1F>r)-N)5z)Aju zxaPpuV)R{NmAS;TdEWr_a`i0ify7_&9s|ycoH0XYguV*>)Pc{Bz{k4bKFmS3)8FR6 z%{lr`uaiE2om!9&^6Tln+nx8QmcEuqes@H{hehCby5X+!HoM_I$QZj^cR6q_ms7va za`p7y-OhVlu0fIf?umkTmg`PUDKFzIf_JC9nm*h~Pv!BR z7fC-Q3cfG`pB4qLjlkOoPE=(!dt&ficv6&6;SssQ(6PXvPzCz@xKts;STKH780tbS z#S>`J0138ug~y}J#rtInh%F}@Z)bwoG6FtdPNmY2VFdgob^Yrb0ygCtMn-xq$WMQM z^rRUVT|9PjgPRO5n76!REGW@@zVNJ~V~-rYq}HLy{)>mr*EYN8a!aKAnUtkDL6LaBxtn6%*-b!mxQF?k&MNxTaD7{yDVR61Dy6>+AZLlmt@w|3DxmvsU}*YID7L*OWJ>&iQEZt3o^XjFZe054-=U zQ)~L4+$+Iv>Z z@e9p)Fq#B(m2WlE^AXni=pU}}3ZbO?R29|IRE?G!Vp^)iOweNU(CBFVR(9D z9SS25i|&zx*iE+3^enWEF|K7@a@+JUhEk%n^pm{8Fg%#E=Ruv24Yg127Py+#xu{~9 z+L{-}QVx_-JMWK@&Il(Vi@r*ql3=hjy~O9styH2=5Xi`+mJIky>j*8>lz~89NhikC z^EwfeIC;kT7ax6C+04>G1BVPO|Jf1__VgW{h-!{Me9ZJY1!pBC6_=KT?ky9GFdb21 z6LiNo0S+~#pK^aVzX}WBRVS#buQXBB)HGpe`NCu53Ekshr5Xm6M>9@}K|^EiqOu@X z_sfi zQsR6Wl|m~{!vdWy+WAAoX&-DdJ8q~r6zqAAF}H}Sj?)eahuzR&a}*ttbSOfrYx*#z zLzTJ0dT@m}bc#XY234q-)I`!Aodvu=wL6f9xd9ls=q*~~VM@ej$7iOe;IbIf1O zhj0CD-i~LU{$zgKfef8A0&ptdhbWUDkx1fPU+J)J960wef(IlVbr~H)USriV!QrSb zNvPwv@aP2ef*b*b6Qi=UagaQQ5MEU*04Zp5eW*}nON{V(xrX3u$uMt{i;@cq^7C@D zGm%MRN@^&UxSOn&zcdBf9I7d~85jmQTn6*BS$6&IefXr4UR{5SUCUj&Mh^1)D^kgH z^_o}Iw#xm9c4f^Kub9)E`s(W=^+J90#Z=!&ec(fp^@k*<`q_Aa=5x9Yzdi~+$AJTn zfzI^e19A)6cnla7c0K_K7hZj|?2 z&jMmNvsp1jhGI6D5OhN+#0;hAFI3Jq_0T$Xo7=loB&R0K_5&|s+5i4#3W&Ov%x2_z$Y|t&jfJKVc@Dl zxkxd*9OmG5+02iJ!;e#-=hf+tZs;A!{$W}=GEhkq1UZy9XYaf)pfr;KO>3{KO`H)yu90X zY3{_o-pqQh#Q$9m+??ZTN78$#T{NuF=V~|Cdv|xj^>!cWy)wUhI^mk-BfVF`?{&jn z^fEOHPMl4qZ|ng+%z;yRSs#|^xxCaXy3pfrCq2VSAC~E-M8QcPmhfp&aMFh*yv+^w zt&G4Y_W++naO!zP*C=PcGCVxg5Q<9LjXDlJ(C~T;=3=wa5ka(GuR7Z&R24L9>jY(X zcZA{`h=E$ElbTG5jjcYrDJY=zowqz}tIr*O?If5sQj#6TIYJ^4OpyjGYuM_P7?}DI zC5}-7;Ud&P)r3lQanxQ-3u??Jl0DGo1qvbmv>!X}#`3+o`GK}ve+dlJ>;Op;4n~5b z9|!F&M`;WXjDzhEPqr!c(j|VbLo0xu13qNysQ@eO_#T)!t~p9heIV#JeAzI`jQ|O! zgEYMq{kR8Ln=8zf7_)HnotIvI!EGQ;#ho#EZN=6xg?x8mxPavf)nSG@X zoG4Dh+Xzk+1k^tRM8+wl;UIqN&)=f;0t0sH6i(*E&cIoC2q(_eu0=^Meyl>@0|W;OYI( zQ7QD{Prr`CykOHNmL=wRuBjhmlBh*y>yJ3HM1O``O2V5RI3y`y#z{Y&;1Xl7ur@>I zfPV*&KnrO??D$}PPlKvMI0A0SvmyI}I)=YMBQrq@S`mGjJeO)KdnqJ*tPS4;_;6SH zW*a^O@ER9_5#TCQ|$DzUc~hze3}gh{?y4{qCp97BRDXcU^a^lOe?U@pj49muc_k_ zf!487gCMh?OtVh2A2gB*&PDc_+vEq{rMZ8m&8!BCa#;AC#yvF&@hrmsUs0YJY^BTc zw94{WADMS^3&U$4PK*&4(?*(PYq~(odSRy0M^tZUa#)nBOD_KC>^=ABYxXZ(Z*IAT za@}O!ZMU8++E`{AY-?3Qbv{gE4b|bLa?r5=Z)5v5>?M7Y1;1 zqj|Xdg3e zFdVcU>V(5D>cXhIQSZ&6_Yhb46p4PtHxrbSa3CQ*F4k+n!vWSr zMh(z|G;^fhnvs&MfzntUc}p8W01a6l@vz!_;OfzH?-Au+&NZJ9Uh_Bggo;P!sVADl zRy?|aC>t>eBLiQESIWYnc(1Nuaw~aHAos+1a0R0gY#&cbYHCWS$P)p9ZxjptesP@n zjrrua=9A_(;y7_Ek*^*&@Q7Bo|39A12M$N=mY8$+8-C>!uJv>e{VzHDTljX2QW(yQ zfk?!sHU6Z{uTt8BH%>SvZSJehEt z6bqz^aWRHRQ$}l$_JZ3l$WlI8NlwWl!WJp%hT$!(uWyj`P0`<~KK6(ghK^3|_|U2I z{?w~Kd+WGm({7oe?c%zZXHly<&}B~mx$YO8P{xt{tvF1gpe~87fbaezP7846{aO6rmtV>#0#hq!XzGz|R zdt6^94_EnyyUW4SRF-cF!O7!=`tLV>;IS>ox2(%)oBj_K-aj!ekNyXInXyqCbeHHPGs1~^d3hCi<>l#tU6$kw@%X?IuYZQ~McHSD_Z9L)#Py%o0ZYiiEo{>rU^+?0&|q zOfNC3Nz6k2=OA|8q;Bvf_#^A;6s4}IZsd@`{c8GDmX-L6P|xgOC>Sq22ynUk4R0Met+PyHX# zM<++o^d^Tsh+}w3|6Im&Gc5{E`lp1Whj(%~){`ZCau4uH1eZQ&?%8=trBWB}=eE1C zs17Vlc$TrX0J+fpwZK@sX+>TgS>>3zwtX45i1b2$8q z_jUXX#vWnv{?kTDOMJw!eN({gPmOTJ8XL1$+Xxt2+iBtv=m?8g#%N{N&w7cZ_ObK7qP0pIF1~JwkuHvt3!gm+jix zL%U9hf>V@_(g$=-4@m%tz>+{yUEH0eQm52#(mGNjA_ja{1e{ukg~dET|QVx zY!1wv5C({<;4k#U8xlVJ0B51c#pFdb;6?S4PklD=^FWI)1|b9tl2R0?H58@rVNi_k zBqu3cJWfh9opT!l6We)bMmP=c`tx&9l5E%)#r@~Sf*3;?#eQ$6^CPp?an0I|Cj(x_ z){asA#-34Vt~GB2Z5>fRr>RDLq~jmrb2G=>*4T9Bt&^tQaaKze+_xUE_VY2T;}pX~ zj$HF{?LA}u$B#exaXUuBvd3W>JkALy1?)Wq22f@OoHQ!AYcQr(7mLVALjhin(7-rJ zOiav9%nGG66_ckXO-2!j+DcQ36^=P^AmM~ppBL4lxB0&LNkMhF`GGoc)yh8)AJTuc z=Wmtf0rOjPzxi)fm=n}nFS~xtm2n-Lebk%DE~DLE62Tu4lO@@jm8oKU9AblTWD9)} z_&NMk9+m7Vuv~VEBqb%~BxQ$E`umefR3Z-BS*nbvLW%PJ{X}0;2~WkY(n)ry#?&`7 zc{Xn_xLCiLKk1)~cUQ(}dy;@9gn@J@O}erGvWOWUbc@J%klbwOuTaAMDK%ac7zQokB*hdA z(las*xGbH>q2H@5H}A`-X*0LI_8KN0m5M7b78e}YGHUSkS6_Nr%XrnVcl-;D`OSB0 zw$^t%prsj0mYs8YJll9^#f{7Z?m;V&u5hB%%e%lwJ8)PbR2x5LkWu=X`a|8OpBhO& zCo28)sPrUjWIod(>93DUKZnwTp!|qMZ-iD@O0=Je7R`c92<{4YOID}jr9_cjvFK8z zB-mTVqDv5Ht?fs_v(nNM8s@hU3NB_rk|P|8(DY%?wmx#tFTrW!kGlD9=lt^6 zlv^6l{j<5JXngaL4?c4Kj}N33=IQz& z@!Ii(Q!Xpxl*&#x&0!od+}96vmhh2oxGVi=H{7>0l70-q2^a8Q6D|fT7lrftBEDUf zhoPvsh{MARscP|XPry$ioJej7Ig2&*Bt)Q{@HR#1&>s=Mg~Qh#5|W2NI42b6{XxnZ z;aH$GB^WI8`tmA+hPT^D3NZbLDAzW=^;~pRW$KgTdc=Rno?eGV1y^nsg?Ycz9No1= zvx8gsQwWDnTg-IeL}zSG%QhP3z!BMSr=RYIN79pZMr}kfVA)nDSl=4)_)eX2X3M%< zh^Ul`kgDpH*|BIU_{an)%F!@17RfV8oNd6%q|XI2)xp!Lt;iF>c1eEJBk{`H*i@0;+%=))cu z|HGt3^R9m|lovSR7&y^Tjyq01XWf`<2MxYti1}6FnD9I;$kbnuyDzukZ_U$!#r@yD z{e&srv(7(|uDvJ>kz8Awn!GPL^`Sn)YO;#KsRu$2_y~BRRY&3HP6yqlfo>zhV*xrf zww>ija%A8VYL=aiAtpuHz4G&D_);J}#A9TXlyp@O_=9RnU2$L4QP2@gZYVBPYqcd} z@pa~}e|yyY&vn|;_@mcfH}$cJQ(v63`;1XB9nbnko;lm|k$Jg!@&WUa+iu31&+75F zK0ZD(luq1CH1ZdFNG%!-eGO=iqFB{i}$*pw;EzJ#wwWGsJ&l zt+_})0_R5Za_t}H&L>Gh)Yl^*HaV69ZCLSQ$0n~MDQ!=Fvd@PxEXa=HzA5nF(^YRb z7YQ9g@B_0$X!H$zqj~P;j?2}hn=vWWOO(&$Wv)VgABudpG57G8&0LQHr8ztjDA!_f zk(B84e*^U9nGKNy<;hFIj0kP<&@rVzDWK8?gC#K_ui|dIzY}>-kBRQ!10+VeL`fY4 zQMn;u2+w}ZaHMk2q;fNSP85841b%%Ke2!huqd{T!BcC*NYoa*f=&+sTX z#ad-PQwR>aMmasR;1wt&|9TGkHv8ALi)1hhD3kmL);N+EDh26*Ksu?DdKAuh3=?@c zFg&db`i^dCOMNS2?v#U(IA-#Qmhhb8F3dXcoeP_uSzP+pY-~b5AS|16xRicH!l&yt zoJ#LXPkT<=tpYgJR;Hg41t-6vgtvCWqp;Q#g*DPsB-W-y!O13;@HRKxN45~b1Mr3c z%WMnX&$f{2*g}MP^hDW$4dMQf*eWD9qR<>a1hLwtFoZO6AionL&$#s~{y=(00J0Zx zUZC1bUQBlYy04p-_4}{B{<`Dr81&HQ2eutAX&TPyQ$91l>tA=;2xpJ%zCMl z!aHf(sg0(ObJvnBL|OY*H=I2J57FCN}vhY3V6{GvqV@E2P`$E<9p-fT`06;`nRF73wM4)Q+dsQHafPu8fzR5-<8B z!plMM<|sJfMW%1;0Y1!ugO^dMo%A+uKPdmW=T@K%C@ zqDqmEz;|Ov7RNvq!aq#oK%jfr<3Etf8H42t`_!>O7Wb+%50t2F9ZzX5k$k^b&lTQ# zso!wEjIXco9kKvwnU1=`M@PX4Uo!nL2M)YY`5gFky$|Pef_b+-p7W;&Y(PfFm4LE9 zh16%d4dH)P>0^DPEz}PO+b&lYg;VlSLw|8@DmV+&4OpMjy^5K1(6wk7VqZAFF(7lU zO^kVPj(0vnjzNCT%m`x@XM|IG^}+=7@?K>m7%GG5WTwIomyr@E4Y0<^1IioZm__L| zO|5UJW0sX!tF}Hn>)XtPlJh6cykh+G?_6F!bHd8V4oe zAvfIH8iC*7z|AIYq2Z(t=yllFlJMEndpA1o5uUwsBKh4E1s@!N-|U9F%Ioih>tR>9 z);Mr37e$4s9-ZZKy{FyadhZtJJ(O#=t6Xbc@P*#aa@}gfOQ;^Ma@Dxg^QcCN$7}~q zK zAJI9<-ppF1lcu#{O)XQFo;PxWuRFrN>DgJQ&Ky*4eqs~2`FyXlijSX%$rIhsIMaUr zV}@4Ms(LrnenJE*C2njPdn54DD0pE6UdC9|&}+W-tkS2$o~%=bi-%g)#lqywDD|r` z214kb0T|G$B+`(7eH;QY@%BSccMpqtezUtA3ySk&MqNeqzmXSlkl23!; zh`)#!=BtJJZ`_~f!|SpO@MOpljL3v268Z3H!4rxLhFvKkavyW=l_E-Q$Cz`qw~8T; z7(!l{Qjn8{my?2NUJO^zFUi|q%YK4UuzopPS)?t zhym%6%%p%%6}tuCr*LM(kgvJ+pcyd%(fenScwo`2F-`N94y;=|W%Q9Jos}WdMF5`M z#Izrh8ZTdZ&XtWtKZFQ}REAQo-B|Bh6*c=@JK2qP$XrtIu{WSM0{z2RdJW+=t*+kqI3nKKg|fIr=}`O zYOmBnOe9Ic2&GhZ0qSt-vl|mVUn-w){-ww$Dh1-i*uN~o2#tEPA-0}72ZwzXz7-ZfT6lv2=5<|z1{3-OjuiU*WY7a0s@5oTWpl$e{N$v6L8vD;_u<46yeiKa}o81*%U+81hsJ~dP4|aQ4EZn zO`cnLGJM#9o^>$H29cv|VDv+dhmf2m#`Tz7VEiiH;a*3gKU|I+k+p`=(EOe&>n>>_ zJ`+O$Kirh5A#f9#0Ez*jopuQm3(XMr1SHE)SogEu(k|`=+ehl51`h#Cc9M2fC`T?vHOsW)h=I=1L_C$%qJIbr#zC-8kfgyX;8w- z^Wn4%5JR6#)$jy`FD3v6u7d_sKvYAO7{h18U^)_dD256xg2zSo#OVvk>lA896!Gz~ zbCuXwaB|WdK}uCbr98c{%nLdjCNuD1#L<71!;G+hzyPen93C*Nad2I2?~2lpzbFUs z+@wT%LQ)c$%25-NTutDf<>-h+xb>o2Ma;P;=26e6Awx&$kH*~nCvlgnkqo!2yx^{h zU0O{4_%DB7IBV9zhaWufwXMjxW#`?Q%fEQ&P+HR8*&pYQt}};r)=l^E;Ye<4#X@tDQbO3QjUiQdph? zr}CmNIdCqoEeqZG7dYwpyRc{Ne5#}1uxD*}KR2B0SsUKl4fj4_!vh%l28xH*cm)2R zfYIEVSU15L%}sK4RX1secTH<+Jyf3nY`pQ|agsOaA{lCR3N#>n96YiJ6f2lf;`Opd zgC0R1e~4)G2-0K0x68EFU=R`C%FIv(7-%vDeL9Jij0xBDr-R6}lX+Zn)Ibd1AoI9k z=wqKA--RY^E>eH$h^kt)iia=mBbM%aEkcJUcI7CrXl*=}5x!90-q!b+=5z$}LPQl) zq1R$4R(DZFse3r`IWrUM#Dg8?=HS)zK$?sy!oZheSOyh&(4P}iRI4Q$RI4J4@j@^} z^Alo-`VruS7=ON7n+V`BWWk=lCJi@2m?5{lr>dAxcFfrVUS)XdJLTGf* zhCSqar_mSiu-I{mC=UzqYmVMlYpwa1HIZMQUxtyCc}i}mL@sL}M+el`)j5vBWF-W9 zm15Cw5xOYcyEG(M&_e%>ru{hr7~OwR12bJNcCuA zn5MvhD$)XJL5_Q*IfgIA^|7b`#;T?i&xgI+d)bT&?=ZIw9aK3j*W6~bbzC%jjsn*nc^b7k5c z?m8b5fUxeoTW7zEHW3ffJO(j>^UTwz8r(|aQoR}4iJa9|(!;E_`68q@?|+I)hrU0P zzaVoeq?`~-c@pp{)`lcJWW%2Zytga8--e?Os^0~#vEgR`eiF+(nNPMG&T@m`0iEF_ zWAMDIjOn(vDEknz$%Tc0F0r7&PcG}Q3MjeMRi*+7SB3-rwn1`ImZ{oKFY8QlQo{S$ zaIE8@4&*)|;k^kC`b@!$H)@4ArATQC4|n94I<5eojIjtT%F#@qJC6qpQ`ccWbP6$$ zU0!Yubg%?u5F7Nu#7?CE1^i^R7Ka)o$B1FVyL-bCN6eaqbMPQrbd8)47NJvT>U*Y5 zoOt-*6DLmFmn6j%oVe7&aM)67at#cUP_hPPcFo>2%WVu&1Pb$DxMsp4IMI=N?p%pI zcTSUY=Md`cH+i@SopgzLKjzM9*Bv-xlljJ4`r6ZGxlJQ7SEdn3oq9gnBVMTq_eF1Z z&7A{Y*b*)MC|)N#Z$kxY2fCs<^%5$BxaU z<W}OyQa}=u~s=A$mS)*2Db0|BLVI&E{PD`*u&f1mAahVyMTS7&O{`TW9yg zfy@sMM?qyzIEWvB?C#=PAq`o=+OKT8Of-|ljV^eDO`-FVs2`UiNvVJ>tdcL2CO8n= zrZql*XkPLpNl%Q$LfLTj(Cj3d`0A!baO@jrf$;E;3ag$rZ30o-glUB7N$7>wxxQ`p zKD0o&wmJgGTr2T8mpM=8GFwlWm#Wo~=bAp)roCOLV=Lesm6o&lgcqEf7Wn~zJ2UT4j7Rye*WR~4QN znrcSUga&G7lIr4_>+@of8T3>g)3_8Kot?`)m!2AUihVs{ivO$dT7O zdaIkR3{n5K*k!=0)r zDJ-F74O5{w%RnlsCxvt#&wB=6iny}v2Qwar91KYLZ5P}v59f1n|8%dpZP>uxQz5 zCw+qh=kz51WIlNgoaCh|JxR&T9g3{y2r*jXhZ#DDOMnpW7J1J(GtI8(48TkOO2Ld(WzS)DxF8n^ZFn zYXXjbVVu3zvF%xN%NpiZ<(?NP6$S8T6Jo)_fH&o7Ot*I=i+{$um+&ucS^*1XJbZ6_S*$(on& zTitM1J!+!hWX;R;*>1S^OdAf(hT!1mz4d&t%ASe1%ASeHxbV)4q|I}sU5d0c|759? zmg>v2Ex*V0jo`BrZw2mh7^LOM^wk8nY2$m(sfZetDz)KiISUc8*fr~rqFVN>LpX%9 zv$IRHOUl#z0gT^u<`Wi^KN#b5p`~EzMt745weP1-TxK?G2i;^f7o}s^-G)rF*+o72 zNf+j=GC%5gO;V9Z8#`%O%gE{bzjG53T9hf~d$a(pPFqQy0|P6BK^}Nu5O^>R59r0J zC@dWeL&m!L@`6ulJGwBui)`~%=2ry$-k1x7-|yat9LG*y8H z@kcLV@*)pr7*;)WKz*OyWhFs>uL2q+4d)F{UZepy?pcd8A{^5h<5S+Ygl0S^V|6MH~$sfoQ7oB!^*R<5H;g2T%kPq(~ z`gICsoMP=O5_4dR7+!^r67h{tmuZKb!x+wzvhpA%rR4BzMzT4wIC>95q@lzX22k}6 zn$9?C&Y=3bFk&TDOV1k=UjGsahl?&YZx1%rMFr={9 zW||FR<@Ez29bHoJ+bI81MA7mv!){D?R0`9uzc?HFTB$xS)&#&@#v~!eV6@n1(pBsT zJjpb6oq<>7=clIThw_7|d8xUjSP>|*5_2*m6B@y<5VX>1m<;AL+N)J!LL(Nd+-x>n zHLw2CQSIWD%i8NN9cdobQf`RcjDexSg{DtOM!g{BmR(lXt5;b=S$%ciUZuTCFk}UP$?;JhW8ynTzzJ{CFpbVoQ`ZUd zo^M^lR93DZxcIeye!l13t;bZ~wCJg~U1L)^J{gHb1`0}sG#HTaERM=La(4Y z!5>bVhpSE~ExQ0B<7&E8sB2Lj*J4jagkHV-LS5bSnH3mkcmtpKh$7PKpmh6ox zE8^=ITk{t#Yd!E!-ewvDgw-;!j-W_S!w3x+6Ep@WVl#Ed)k%(GT~@deF#sr(7J@#V zU5P7b%N`8?e{!)Jd{K(s;+FoRg0Xmik{(xG7m{(Szu7TrtvQG{H zLVr-8@v9s4saU@9NV~5h%&H_{$pb_fFx`6$`YQet5h(Xn>VPRIsY5rY38o8?v12>+ zZS+g{jK|Uf0bX7vP1-kE>pU9RR4Mfb2rNo`Kidp+|4W~jfXUMlZPI$b^J$jH+x8lnSnrMN`U5% zbe+5DM4BTLH+w>|mbB&ycYyiNXLOJ7P3!U}xrSujs8#fcebThM;@iuJAfJmy4XDH!bnHz9ZkEe8P(9&w?0 z-X3%K=(d2X482He7MT8zGQ_ImFzyCq09POdzXGmcSqZE=_ zLU=r|9VrrzNs1JU_quQw?SvC#;r{v0&f=Z$%7e0e55Ceu1w>y0mb66XOOOMg@nW6) zRluIv%-E|`riCY$V*FNF8pdw{Ycb9=P#QBIBcMi}cTgojNilOV9xICXR8|D3B3PlN zga%o8T(dzV4CNq1%YnY*f{jdWb*C9qfJw_(?9|gq=zVLr*$W!kk7KA_(_j2Q5B(6Or^w-KV(U3F~ajJ zXH(X}wN67RpsvUvj2K>Zw+IfYFPoYxg79h{b4S`gUQs{l7&38Wd6}l~e`?9a)Y4ok zZfV4vNb>6emRJ%_@oWhXMZw9>A>n=-K7nfI%BLnOJ$c<^`fLXdE=$;O;B^kXOFjir zaFU2JpK5|jtm;!e$3p9y7LJ29pdn;J%UDXZW+@)kbCK-P=;BH{&@Lkd7zWY#1Tc21 zw*$#MGbsksR2j9j^1heH{Xr{q;W*+&)2B|K$k@HY!S-=je`{lge)NSHL9pavH_pdS z4v!r+%f`Asm9h0iG}g@~u8m(vxC<*$>0MX>oUkI(ORT8Ws(=^5n>;}IFr1M@zNKXH zE%nXEjL%+(veQ^MBD^YvHk*j)!NEjJ#eynf_A%)Nc}*>nBDII|8p{Zb3~C1}=!l zq*q&AsHtyReSVeeUj4B9)?_DCX^6Z}@IsaG&HkPm*zQ$w9Dt$^Iqah zkD6xCG-H~vu*3H|cV>1MSS;`5mGA$3Ccw_@ojdoQd+urXlqOhA@~(Qnv-5p!ds6SG zIN#^C1G-PPCty9%BRQ7a4k#X2wFiK{KsJ0EJ1UN1Ai`nwYfHgQI15uNyCC{jW#UkU zIG(FEWAda{!}1{}1Lc!qxy{&-keYm6o8rEj>=ibcm0GZp4^bC8{nuijQ(2+B#X7u0hwyTK!$(A0{3(wpdTALkZ1>Y}5iuMP9bhmcy@E}7 z?d8{yck2@KePkc&zKeR6f*QV)^KBmDIa@f<0saO~2ac$8z|fgf-{$vYn%pO9s0*1^ zhistojF=#{Qw}jJ^=^aCO;C0d5K#^3MN4%GD3jwoXsMd)TuXO~u48p*7b#yYt|oZD zDd%4l*?vI$P2#(}M-C6-nZ%8JIX`ZGKc>ljf`{5Zq{)3cZ&dGx+wb?06pUEEpJ|xq zvj0eXd(4p2SGAus=lguWs`pcz?{nN$?d;MPE4)2r03dl;Zk`8m+X?{Pp$$f%}+CE&r4^B*D)9+_$ z_i+X*_vZcgM*p$spAKEgFW|KLJ|2A~kvF0|0(wsnA_U`wo2(vw%!Ca7Y;16FYK` zNz%`oGM+fQEP_L5T7I09zm5bsi5*tG7)R}0GAuPNvt$rksyXazAcrf}qjAa77ixXJ z^B7R#sr&3)3ZuOYm%nfPGk`hHhJFHHE-H&VCO#t9wBwK5h zy8%<@2*5oU640ycCdtn8chwTp4-SUh5=&E$3&m^)MfXBQC@ zr75ALyqWAUIda-TaB-yr1k-MQZD_7k!yP;AVAxpCZq^}jzv@J^1P(>87ZN69a!d#l z9#n(Ffec0W;x`b;ppV z)g4Vv)g99%DXB!+P>hJ#eD*vmuvMR5M1>*bM=y;K{n%GD8vYhzA?W)_Fd37)K=4pb zK$ivERZeKStlGwEZ?mk`HW%z|9@pE5PW>R`M1P~!j}RlT_nTFg&_rG0*SZ?5L&=Xif7S9W~bWuM1#eICyE#MeL3-sY~> z+8DF#ZC2=Qbl4L-Fh^{9oGo3x!ey{3XcBh>9Goj$nwnO)42JM&iI!re3+!`SWRP-a ziE@G!$}4QYo7scPRNI@ZZM)k}pH`=R`p44=Y^O$R3?Fkmga$^m2I=LR&>B$5X^lV2 zX8AejJ`p&%)~a1n20wp9h*(;$b{WodaB#orM9_+hfTGgn!(urvkr*A9nDh^yb z=c%1Op7PE9Z64S$YHbKx?4kVByQn0$;j^edT?Q8_N8#ft^lkhZ5np>@lfZct9lHlo z!V3h&JJq-hSDnaV+m|9FvfjdgK@t5Jud5oDS=_kHW+;DW-C694@pZM+))nTjnVx!m zX6h|{d~v+kgQc@hAwPr^KD>I}Lw(!$|KQ(-U{7P&M6icMt7bT7>6tx0x~AcAy_E`BzvlT87E*?sC|i5h2gE%9(jZ* zV+a)VAAv|+t3_=l-FVq{YVZ;As8H3*z<4*4Q`JiqD9&+>Wa^||bun72S@Ws9&SP)DlAzj#P4WYRIAd34>3&zpD1tFtx zi22u{k)p-?!-$V#P1L*uV@F1SS{NSrF+QN81^!2nF|30g?tfM7xg9LELsCL)GzB3z zBK=KjolBv41+6Q9Oxi?+E^Q}tpg1F03&~=;M>l(Eya=q2O|Bo zx7k0=gJ>5|Mh_PbC~huGS^B60y?67pX}h0Zp$+$`r;@Ve=k(F~i{f+a{e*@%ZPd!S z=af&VIuVB?-}fd?8qb3<8I&hw{179i-yz?%$nm)Dc}PkkwOHMze@zu)+? z=_h-8Nqj>dt+uE8`n!c#^*(sR5;=;~Cx_I;1mw$*wt$bhtZAt+FHjZmE`IvuX9SVZ zYgz_^WwAR%Q^SW+jKbLC%J9>tN}DZCZRYB2ocKP60m=0=9@@)) zXnVQY80NcrmbZaiugzsmt8F0HYi-uG+6HpH)@D^}Z74IL*5+2djq`fac%0g-Zr%oa zgYQAiH$v!X&2g?20)}ibj>F`!gGeXfi5juN^g^8>+#yUokamYm+`ME6Av*sLe`CcD zMr)~2`wdmx9QfVGxjZK)LCzQrNnV@X3Vr1IN`ZRhfWuJjq$_8(ZYmGA%xtal6v{;$ zgIv2VGh3TkpPPT!vhDko;xAt1+ZN_O(6`@?{_j<0x+y)}GDe}~l=3&_mA@#zEnmbu z|2lfwvg|7!2b^8j6$Mx&DMYU;3N_1-hgw>%m|)Kyp?o4m zNzW_ie^k1o7ge7A4YLoSS_fCCDhhFmPIR%BV5|6z>KYcRDr#z_swiBg&C+k{)`HtV z#|E(6Efc5CSoP?j`yXuQEB3Y>2|+E(E+}M?9{GJ_zem;{d5W>|pfDs=|He5-qRKgh z@hv!q+J-oX+Gbg+ZHRNIZI)a`o5il$z?+F6#QVWc-Y_0hw~v$LDYgH(uAb#{p&h8U zS=GFax}xCj#DNna-jnmBR+%(dtxSsb#0%|}NwKQVl}V=|XVyv-%v zO%AvxnI%Z01TPGaOmynokh7WaB2&dL<+O{posx&>ZG;4Q0dIq}TO_HGG$WkFGybUB zsM^Pvt$pBGNQ)+?uJ^%`Hsvv)a- zbH0mbbI|4&I@Qx)zNTdx4Q|BI)N!tJ(Pl5^TWX~Jq_$brybUmdS6StFj~Hs`Eex_7 z+*5d!RVdG(pRi4dB4{NSYk@^tiB%%sPN(od8VZ9*XL^a%t}FpoD2D}#@QL^iV6Y40 z8c7x-ZCoo_jtiXU6vtHOi1A>G7zc)G)mD*<-Mw!HOBg@z{+ef(NzXQvZE09cFg~oD z((t7($G;Z%qS28m%)6DiZB9+}8QbH=K~18OW(!Q#tkzwPR{lC%`Gh5mU}3DYvW=}^ zUno(aEM3?ewnMfpw0{scPVb2k3jjONLkgNx-!Cn|eZB4~2e4Xq6+9kI)VixCt?p_d z5sZ`aqYZ=3X*57uz_F+wpWyH$sCg)7q42c6E%7k0M4y8yejw zG&<-ASa)@FW+4rh011gNU|X4hFSUVEf=Ue3XoL@f+34rxp$^z&AF$wbmdOXNd^L3F z&|&zW&q7QYSWq}{U}50^K32%U^gewY>uDSudZ!E__WP39S6ZswzpF|6O^)``eY`y| z9(c?t5Ih0C(&po`rYmLyAdXTIoNBrPwYk!!g@U7|>s{<*!|%Bz>sZ2DCCazw&arb* zPZqGxmEGfZtftkRh4mtPZ>tqu?Oa2x=&A`DW*9IDo0#mw#wlkhpUYV`jsb%%jgTU2 zcZd}Y=jBIjJpqYXs1QJJh!Ka<%;fdLw=`;6DS0sNH?QYvr|i6*E6E)69{w`P%FN0a z%3$RTi)Op{QOrlSB)kXoSnZEvR;h|vLY^zkk_S7rS>mb<@SesMBcdO|tUUa&_n)uc z2Q8oL^4U>Mo?TDR(p+(xO?~5Jf8^ScQYV{q5vTZ$l^-t7{X;OU%}TLCIc5y zLZE4B*TExaztAn)sPwVV{_guPESxvwu4?f|6$s)78s-@*?BfR}$*<~|*JNy)n#_x6 znhLiS&F6((1vyNf4B5yFrvgq<)XfY;-rj=X9p()q8X9_9xPb@)GNo1}6HF|Q#8Kx- zbw@@V^}aJJSi~QHuyEz%xfwGv#}@NnzDt?!dn_Ae&)$dY(A&!Ur*^!#?#X`q7gm9f zgs&j{t2l?w;j3)dHtlVQGplWu39Ypu=|F9>xYahK1FLOz=xrRF8FtK3@??EHqyy)u zl8yRChXsaKMjgAOzo%UF2S`ujI{5MGp!{LCK3%(XD;zzwO~aEdcE&uu;!|7S8Gt%L zB}PP>W>9y2wl%2Fhm&lUUV*mBEwutItwvhQ z=xPL*f@3=h(T1x5*^~({f&#Mbaoi5iSN##A5vxW8Y~dx_^f82FXJ-pScAxCLp4~g8 zQ5wi7^64f2S1*zeTlIgvhSg;c7FgOArCV}4TDnA}7Q{ssWmtNpC1gd2TduV1h9ym_ z$OZ=b2PgqohYAi#eJ$%Cm%g#4BU;%Cm5>b5x#1;aQ}Zpt`e@!8D(|p|CcEZz_v-yg9B|@$AGYH|D@; z2Q*D5u4?CBI(6r|kGjq)^fhdbFHY-xv)-l!F5vWD!>OBi8z+upgzGgGDA(WV#8Fo2 zZJToxPVvthMR^zGC=11bx`UiV?#(iffCw zEjOj%SF1jr#0-q*(e?6&Uw-(x_%vHRe(DVSc={^8bE@GhL<+7e%Z7`^7F3p{jipwW zjnhR^wX&>E7UaoKKDOn-F3Ro7;1^VqU~_j61U6pzA}J-PKKCRm{z0b&D;;!zLq_h4 z74n6Q3>z!L(8VxR>p=Dq9w&`cPKHt%pkkbML5iT`R@G-FH~=*g1m75cvkAkDW)yDj zx}2`eBkq?BXb4!=HcN2-!2E|JPcR+hWz zx=UZazi(3acjI?0q-N5`oZBpM)dso|%`rwqKcpL_s8U;1vuD@p&pPdUvg|oOyIy-% zeJ}ALb!_YOXPxK@os2p^yNR9!SBQq)wHiF5J?)8LSb#zRZD>@^QOnp4P&;|u2h~fY zy^v%{v2ZUDt@0+Hn^+OiuTSs0HrbTiRaqJHyR4WL zoNMj<(3WYRZu1QZ{4Ox0M^047cz<)^<2UU(;^Xz5mrqW&=;T;`bIPNuXFf5K)`E|j zuf^X%k> za&?O&RUde!sb5ObU0Ih|v7?7Vhcxj}xlarUxcmaTTD`?OM4h1y+Y~{RuO97^d(~Z{ zt{CZC>Z;Cl`Ru4B&#u>>g`KIITW;|z>6!NupVoV@YrlL~z$d{l74Ofc+P2Z)gXWY3 zl&kF!hNva2q9|(HHo1o-CC_nc>{Qzpiwch$B!lB8L8v-rWO~_KdOsma7tdbZmZv<g5oZ~yT9u|fPFoBxc-lV^x$Y#9wd z(s+3P9M1Q1I4@}KD`js()+DvfvR2!WzNNNt<(Jj`U2FAzbXshG7f}ecpV_Y3fR7SP zcpK8;DLu9QS;rXW>(4q-CUp#CIZ^vx)4Yua^VhlM1a)VTZQ8l+EC;_Du?poI??t|7 z3g{&PQ+7o0W}2OC9x!A&mSSvfuJ4-xCHc)3q$`XXnI#q11==2Ttjz0tWuDMhrc=`$ zKr;p;XrwVB+7Ki|`O}1$qH^BzLP(RUmv3Ekwr#P+49&xgeW5 zCBtZ9CvXO{+-dpRGj{7h;ivDZ1O!J683(`YH0+kFOw}kbCDrH09~|_=vHu+NSgy~t z1A*aB{+0Jk#CxJA@%Fpt`WK>)e6%rYH1sng~275~asDX*M6ryOAGlsj3U58h`z z#VNK8>^5b(cpVb?J;@NJ#2Vj0@BZk$WsP4fjDEnQ*7&6Z9y-&a{$+nREyhr@87F*0kD&D2Cc*RjX}i7pZMlG;agkP6AC{hjF$N z7t9xg#|6`Np$fuSbEmlA``MX>t?U~mRw_Mr?mo$U?ml1`?~|{^8`@gD++sE&dE{~6gRlz87cb& zr7p4{3x`oO7;i-O2SGFn3r(;-NH7>p07nJE2V|C(k%AM!tC3J2OSIaj-1q|{|xJc&y8u<-#T$p z_qDw{r!uI3HE*ZJAif&r%VTwq}2#I>%&)?QHAm~Bakk&CBJYED; z21{vxf+uXQbjpu=2$YyWIo+95Zcptjv4LJ*g5VYI6^1iOsx4sjXj)sqUV)OhLQwh- zZ@vA}i*LVmAii^CWM_Ekwr!i9AP)Ze%P+tA_W1Ere@U7;VZ!vJr0EmJPfNlm;Cm&1 zVR!>JV58u+m+GI1X+$_sl`|S?ZSJ^)Bx|22zlsrRt@PI}!8eo%IChnPz}RC2G6o2C{J;BDm_6gggPC@ysp+cWSXFdl&KL9ot zN9Zd4)tJdvT7AA#7@hdm0sPfVI3;+ z`wZ_8X6>f*dVNIorL(^@RF61!?w#Q~58w3ok@E5*kKc57=Wv>6^bR(Xl`1=whj%ES zPb9umiTnwa14oqhV0st_vbG$>bueJ=OEA~7IweTt6cY<2Cl-+jC8=$2-18p8-OZ)e{j`k{en2^!zv>To^(0xt^F3 zX&Xe!ibz_t0ZNz}m~KB+MCvB~HO><;;s7Ba#_S)<0mig^D`XRdI0(ra-j-kg8ssxz zcAAAsKg>cRA_|+IaZ?+mz@XTAY@jqm+N@_|gn_~gl_(&Ul#$$|5GEdO)h<9KNb@I} z5o#clq+EqDYiYV*GUu7G!mW|c-(jd!{?TaU=NHJFWiM|;DsKR4HLIz-;2+x*!0B?w zf`N5g9%k?w{BDIDv?3=DnY;pbZWqHe;97!o|X>)YPiN!m6~is=@$v9lMpSVRgzzWtK8qx$}?e&tG}v z%j)Vc4&gVR0SOV44DV>t7DRtWIuSB9kTOb1PyBcd268_%nc1wp%16H$-np1AU;h9L zBlDV($+fjDxtA-YNu#{mKOp=q++zJoQk#6`!& zS&Ud(-L``SbPh9}7Byy9WN3`y&l{GV+wsKa!YBLPa`T$I?_aTY)$B$6?(Eg`nPESj zUpiYn@xpB@UmnxpyTIV_lPA^vJb6sjh_I0Ff+G$eShg>Q#vBc*l@F@rLy*IXpv?N4 zaL5C!1UDNG0AdLY20gzAfafFlps*y6kWf%_W<{zGh*{}oIe&+8!M2XQ4RLrR>lAfL zjIPI_OwMCceM1G?t(4Fky8=pE@x~A#%bHFRNR)#OZ-#vgT$nruD0wt@Zs*Id?*Oye zq9%_P=vV`JmhG@XdOncbVAU(4{I*x}o~;9d{hP9(hX^u&wN7%h@*O z4}VTfwvCuRW9HO2%N>vPCWyKN`>lYekbZuDZS}8Ss&G4R+ST}+l&JO%f{JLx^!a7g46dr{=*H6=1dtg`}57W zy+3p8WM$*DjyKFK$)7f^?uO0_XT}x{UN~Po^z5<)2PTerX3oqPy7qZy&is9~mD^`d zdM1}CV}|4m%unnzdq73?r9)$e_Z-?M-Yao=@#sldNMM>uam7xb3jIywhi3{8m(v=z}bvzE~S4THUO|hM%7x z_=Wq01+_s#pLnXRL#2ToOJG#p!&Ct%ojrDH$>P&nx1CzN_|&#Z)2C0GIAg}dz@?4* z_9;u2DEsy`F8%U>y?gJwf6v|r2*)6aiBT{}srs;kLW9Y=pwM-o(7(&Ym-dK%+`%9i zF~pMz(kjd$l;{jRhW%}7vcC&!`}8vdZ(TQ}3}f(BDy7*NL!vO= z8swTj7d_n>Y&xCt8@vk>5J-1`WT{;=R zX${DD|2um2cy{Pd=a-`D`%K8O{P3Z zKElYoDiK$6)L6gnE15y;i{l@C^Br-7x+x{qUBD4i z&nXWB%aIFG4*w5vgzx_}jvy)1q;7z_QRuc;We6%h`00FsgPEg%AGA@n!hT;YuGnjC z!Ym{(i{W4v_bNs?|FMhj!4U`Op9eS%0Gz^*2O*QVMQU`|Ww}LSVt9CBW@1KoLU{a@ zxCLU*Nm)b*D+-fu#w@;=vfvb$#oQZJX0h)5nSWI_abD4P>bS{s(-&NhR~*0ba;yUD zp9YL=kSn-EP-pemc?I$#h*16+DgT7GZCkL6W+Woq-atcKowal{fJT5+>pTTzs5wue zL;yNV$!d+KNDZ8$RCb%0Kli>;Ielf(?1R&1zqd+dDRS3Qc|#}36}I-uXl0(|Y=D2| z+m9qB*Bsfn>HX_%FHeiJY}%eTV~&R930OVS?wvTwRY?CxSilQpqdaVL>A&E@`QLuK zaN)Ot;VCJ@3;GW4&|!Gr09FUNUzhx6DI5Q&{QUL5d{J5X#cQvBfuwQ55y+pOko-M_ zSZfp&MdOMtiJe5QxaU3O|28<|<7Kc&L5nsU%$9TrChiRfSODaw<$Fjdhs-uAsqU^X;=Ed zGEfqCt`8uqwXO>wu8bKVlo=qDxl#ro+HzMhs02LcXR9i~@2m~N>dx-nRw6jwkKB!o+Qgx9mdl|qijw(0crqCu~(xLgog_P5xMAg zAdaDz)d^ZC1(I6}7QpxUDrf~WMs=Cg-q3YaZ&zADY`!iJ{0LiI?M#2;L~pLB%`=n6gl?Ycq_Rae-Gm7e4|cKU2=`*E^}*Jb$rHGYjO`S?B;*C%=VP=1|<_D}Hd;o5`m6Y%|4{Cfyk zxQ@s5Tl|{BrbVG+W1VL!bLFuMi>In$gU1 z22(Hepb2U)eyMIglLQ#rNj|oy=Nn`_3((FtbT6ntvYv_iZ1;WJHGSalv@uET7v$`G zk#)~o)9c7$)^G5PG;4Q%zk`1MYqu0_t~$)eQ71l+aU=+cz-bG4DH5j}2{_%*X*Eta zki1DUz*w%Ha!kNe0xPG9Q$UL_kXy#$><1i|*J!xaSS%m&w-@JM*ZGEwQNzndt-N8_ zkb$M?liH^&?*7!CvVm;E!kX2a+V1x8>yc~iwa?nMYnH!xkDvdlCFS#gR7?m5?0%7I zU>ECR%~TCzf@DDTSsW!BN}Ufn)H4nPBgi#3dIjS2G82jQs9uiv%hWfO(aiFh^4s?G zO}{<;KmW5-+-kd%g(>xTUm0-lHM}oG=x9w232?(96rF5{bm9ijU};=)h?rqhCdHOp z&Yj_*ooSdvuYK>lv}k3{Cf22kR9dbDW<;o z4&ITA_n*WXg&{%**(v>isAgaY=`QMd2Hp?1aS;x5+PQ|BW3-&oSkodIgn?Xtv4qE| z`irI6nMOR<|8)m`r$Y@BPO8Mv#l?ee9sbPAKYr3_WK=>?$2qeMG3A{+dggV_OP6zV z*hjf5$Bwvk&fp1wo3)y=qX(7@t?o`de5T)VDl=^we_1$`4Oy}I@6 z-L5#_C#ikEzHIp1^3tV+d2y?vb z$w&z?gbhmRo{QPXV`gt*W}!IC%(v#zZln_p{hU!fw}O%00PR^j+Gw^F1tBJ?>7fRw zAI>evo1D%tnjLE3Fo&OM_#F80)3nN{f(oo`^zgKz#U+C`RL{!mme*Of=4AAeu0Pc% zz;EqmENuv_th(OHGFse%LX2GJB1EMJ7CjvUTUr||0^)`x1R!FCu?m8P4)HlEBUwCP=b-=Wn&>~q=86Ge zXX+e10aZeSTyp?*#QOP&$^abaBNhrLj=B<;(2r35L`ET(Uyf0LPiV4<4@nLxpJ>7y z^g|M?IUw7FKi~+wb^aioY{DP_LtW|#5s;^%35U?s79?B+eG~Dfe~>rG`&!`*KzWj{ zh&Rl(`nTi_>e?X!r3q`m>S?FHd{>eNc_j`~VQup-WFqpBD=`u95KVS;J`v|U1O$eT z#W9uE*oSeD%07sH0AeI@swVt0q!|mpU`D@g!%Am zUG5eoPwiTje?Ikr6_sxYhCseCHR-&dtDY2B^-d`T9<1?38z1})}<4XYSZ9a z=jli$4h&_=GmwMkLI2_T*x)rL;FJ!S%X8T9d~8r#43J7u*XYAvZ0%PWKfaJi{a_x( zAE1}TkX|OlN(nT6@GDOlF6sRHI^!7^-+O7__iLo@tMEMZIH%|J@6o?0!Nv35_UGw) zdLDY8)ARcGOB?G;p{)QgfJ;yd-Pf1hXn9(rT*??JuViWj8& z@q7q*%LE6t3Gnmv_VS>b@1l=TfJXpgf7AeKlgxsHO$iAmqg){``5X!#lp7R&)5SyAIc+ zy^gX*pze)+;_54|XEbBemD^Y-n|{`0J%0$#Wuk=A9`Q%`$h9Sz2#8RQK{AT97uZ02 zL?)m(3CQ47BF7kef8C_=vPqrFy0Dq!%gQHAEbCGV7Yy_!DB@;m3|vH7>5Y6xsJFOg zz9YuNc+M70%-tTB{IQhMRg=fXM@HL%zLtZs;S?I@uh6i z!uhj*88doYfA_HiD#j9CV+@9B^v}nDb~D z#sJ?Y_1)wt1t;%na&StK)ZAqOM&v3-dxJlj2m&Y_mn3Sb%T!ONr~9yOy`Qd|T-LQL zty6h6d!PL*N;aF~tAvjmHf&t#u&k_Msq}7T1$zVV@j~T*WNV_oFJ%JfN}(u?#^@M= zry1)=d)w^i<4sFyF>n~rdmxp=jVE1755NdmZds>EW#yCVN-OsCRu0SMsaV>HKI>Z|NLwxLk7wT!W`fql(k^A#5uy;#_>?Kfu|L6KWIk|cJqp#!4#`+ zNEKJUox8G&!>LEv5wh24M;xg~+k%Ro+F9)31Ip1k^-5F|3`&4?b`UVg8>6hXdXO&- zDjJ~rx?YS5dLGVA)WK3QVE7|oscSIgL{d~Zqvj1@i4b9;B7pIBU} zeKZqW`%#3<1F>QL?K{fmZ~sVh!KxhpXK4Q1af9-nXrF2< z95H?v(fnDz)H>6Aoz96;H(C#Mw%0-v>~m>K6LNrq3dcbcS)t$njBpM$-U5gSXF35@ zXiZu?hsqp4((vEt_*W9v;YYdHk3sn(Pr2EvV0 zG_Z(B;%LV@vSPk?5#HeeJSkwy#q%fn)IB3B8RB11i!WZN zw)#ZD=K`i0$rB48X`xr#KNDn(9Cl^OQ-%tASbp-)P9xh05j(6ItY3f@Z|C?HYFvyZ z+7gq<&Co`eEdwb6I6aWFSu-!oLcTR-o;)FttX^jJ*CCaaLwfg~*{|ZogVwGcI&`%j ze6gb6%-+2vza`<}OC~DuJ#$WY?|YVgmt(EB=CHe;-RFHGr>8O(F^ivLHuv+{j7KIr zn7Z09!GPJI%#Ioq(w2@ck%JBK&BR~S>1aAPpH2j(1G74XJ?jk}U#sCP@q#5L1@j_j z7+54|&?GlNVpXR?GL$+ua>(X07Vl5#ntSlY@}gP2bB9${4$JL5tEl|NgSlPB)0l!1 zq)vf-I$>#e_|ge%l{!OZVvdz|+A^h3yayOzB54kASJi6Hb5MnzGaW36mg|8$&nf9V zs?9GG!~gh0DRgtIzwkQfBcM~rE;{w28b4?`C71n3I4XfT-W>hMA7Z#tSa0lJ|HpZZ zryS#1kAB>R0ODo>FoM@`Z4*F|Bd~3zZw*mRCfc0i@ z3Zs=Bed{gtDJkk|pOT`qr^c|Oa=_I*6@Yh*k)Gj?fnVxl6v3Nl%wyOgak04b%46pJ zh>`5j8N|ah+Mg4p;at1a`_Zq9gqx(Fq;DaWlifvIO56|Z199(>D}}1W z{qwHfPIy1Jlcf=H@5a1$KIeU+TUQ&WUJc?_K2EE(M^oDFf@Xu}mutpUk!$XDs!iel ztx#?9fR4>{Q0=W9y5=6(2ddq}N=(Vx15|t80Z{FqiE6*Aj*Uf6w<+&QXx5WSC*0qEFZ!OXmw%|gP>AQsF{T3aARwyru zKeUmMCO#NvMN)K-KNS-0x8o4=o?AEey0dH%Lf1~QTb@(;BRif_^(^(72iZq4?iV73 z=~i=CAd07jBfYf2&ja=YV&PucZ!yTaElX1X1tv}fmsX_`D2(WUq!MVQJkza=qh|;+ zD_l2GSvjJ<&B5k2@!YRfjo;VjU%PshGt zuQ2ocsq>b~pVxf;;wvYrQZM<3$e;h3!N#(DapIf{C#1+rQJ7H{W+WN1Fryl)KV~FL zG$Yy93)!peQwrsXF@%~Z3#>2il zLvb|WoP`?FD0L|Fr+%xBUzY6giSE=Ot+C?A%2h(?F)hfZPZ+z!cm&+rp9Uvg@<+V zBeWDcVT`6mhUJ%OQ-QeSPQh*bY_l#GQ|c^5N-e+jb~@4o3K}d zv(u$l0uvW4x|y%gb?dutD<0B2XJ>v^+>o8K7ku@Rb!XSaHW9vEb{gK0JuXGnoH)SO zD=5t46!Q-WK_ZcNo<}mvY&JRP(&r*O0Wr3-a7@mVe*j-g6^Pn|1qDDWiEU^0Mn)V@ zH$+ZiRr*J?5m_NBlEab-+m49S8%g3Q*ikC(kepx%4Yu(3bFyrbfXJN=>#y&zy4Srcqe}8? zx9?`YpQW5XAx8vV42!HNOYNN*7M=O-{SSPUeBrnu@}i&r$e|s2b_$8^_ReFFu(Fip zauDQzP$5A`7T&Y^!rCk)gdvkH)H-)U;b6==Rq*g)o*tf~fw&fg#=$wGirh3{sC08v z_hJ#stf^)%#7KmzcE0qhc?=Bsk9YP0F<&TNRTj^Jn!_XX3wA`c~D|vVsc_~ z`y@+j3<84vEpcX4uqU%052z=F3YH=&?wAR~A7GpAoVYbBo8gumRP#q0pXeKgOx`g8 zJ|Q8I;fNoPt5FsvCKiuLPcG?d$q%%hmb%MdUy7V%?Kk817ZDF`k=DQY@WZxOO+D{e zwB)v|s7Tw`Z@!_`&wz*FEwJUa7fP)|5&*qKCU~0R{o>C(_aP#Yvl6(TiNbj(l@u(Y zo}M1nf`Qc&0*%xAy9B{E~b?YC&6*Nw}eL7s*N*-n^c^^agCVKO0`a1@u}Ct2A9pt-8Z-t5 zQf56>q2xq?3O$lZTAgW3<;9C9J{^?PC2>OKz4spfY#=@v<_sON{8YnD?|tt1z1z?c z()v?V_(#4(3bMNxWFwUku?r>i zswG5hb5^W5#Ec{p;mK62LFM26I++2L(YYh0aoxjfl(WD8cK&DPw_(xhrS~5mUHR-S zA!)3QXE^)(LN;oD^hEjG$z9GJmtro()qHVi|M40T^{RpOAPvQUX8_rWOh|nJ`YFIr zG@jC=#TI$E0|!AMFn(mc03g+Wp1|j5Ua^LQkyot&P?_wlOun7~$r7it*(lt(8qRHJ z*#>c$?M87~y;x(rpML6Xur)IQCq43L?6^2#khLTLxY>poOk#gGW^l*0GsxpS7)&>6 z&I1iUlZ(8)NFgDGP!05r6XIfHqNCciZDWa52j_#qk=tTAryLyHu&;EtGaw{K0Q{Cm zU;1<>{X@Cl@S@G#L6fI0&p~AS1o}C9_UxtotMPGt-ob z6g-*PIH0iv8UpddlbuMzAMVk8SNZebPv=97)5q2Ey4q8GL_?om^J2dP{Z`_KZBR~c zKd^lzew4F?gJ3p;JrmG90n7~+Vyx|g+YpzhJTMZzlGr$MMgjl>0|NuN4jS8@)HM7=0;A9Zo)VtL+~))y&lZnJ(kCB<6wwj z*@hz;<%#g~;w*ua5ME`5R+FO~1GLf&X8{`JBijk(dk&){F@^>1lsd24ulPuxG$gQJ zuwv{)LxS{)0+>j`l*aGf&lo<%%v0e{+fxg44rU$(6nyq50%0%iAbN^j4Y2%mF_M7T zAFMG995u4_M&7&;rD$ork*qd~(i)jkm`}^s@kYE8?R(#*!=djh@J}6fnjiDTlsu&g zU_gdyV(-aCz7T;#V-*hIjK=sPZz4rCUXX3M7NO20=gQ9kQT4})1aEIv?bHR$v**=z z0Y-t;IkF6ifpcSBuG|?x+1l1(8CcT4uzR=cOiQYzV{#HnD*iF%-T`3h#L(?b9EV!wSR>^X z(Nlv5lfN%9zBn~)2i0Ac)w4Vb#Z-l3EvL1{3SoC`1F6yQecL|W+UCbqBhS&|aOJZ` z7S^%7^3mHbq0(}E^zzJ~e)*+g@7h~$&*_!j%QRvBq}Nxjxp~#fkwe<2tXZ^Z+*@}n zx^C?BDWj4)tYISt#d@Cg@I3S$`|5*ZB2xw!b|oMFD_f@Ah%8CUEq86Z&C_+T&E|7}9otfY=!G3_=low%hm=yXuePL>p}SnxUG5aY3D+Ci!*vknbqp6&_E%`=gC zLCi)5%_jR9QpGrL<^_@33wY`2>0S*{kxNH$a&c5tRQo6hM(x^0gmF^c+uX*Fs5Ofv zodMu7QWz7#P}}EZyO4v^of6_3#y>7ek00%>{PI_V3s}w0oo^jH&f4U>YumYI_3b&< zY^&@J8-3LNHS1{7;+voWXOdUnW;>xznRFY>N`3{i3KF8o+u4_Sm_k4T#Qvz^;^_v1 zy3qhR2b7YFe01b(sz$X`t~h0!f0Uf8M0$IphmYsd!?4zi5Ax zSv}?zJ&>kwhCXmuf-`Ijy{&gWJ#fu!V)s@18yYZN@x|m*-zuHO1D-gO&zd!Q;X>j% zAYi|NqCyVa-OdB2fh;;Gm|zA&A9ykMz9q!UM*d=W+``Z;{ib|e82i$_^0w7r9Z9 z$QM#D^4l0Fx)zSGXfG3LLY0c8*_6H34~2S?C+%`BZL>7xp;cOKf%sjc=d5-{-UZJl;KT^ zmheBMh6Laqthqe5q?GPvJlOV_(lAB2@l1nq;}oUgSlfdeC$Z#Tk*h1eL196YHmdxF zGL~rk=2D%!@luU^Kk*#a_%1N*0gN_?YVrmm;|_|8K?diDg@K}e4%a2LffBLgxqA^( zuJS>SS)31&J23r+{ed;cf4o?r{D89f1LZRY_KZB0{O|k9eAIO9&UzZ&uvvjm5W)Ni ztq$2<6#LT3PKQ*R0@mRpgTwWOYKr^g-FPE#V zpRE6knQyFHxCn+R&CsD4rpB?ptShVQrkSQNtJ#=UfFTWd0ozna0A%%0BmtJak)Z=S zS(DcP3ENbJ5N-|$iY1>7=r!;cCmF7VWeV!suw86e&n1;(X8(Tj%#YvvKC5=n?Cw(S zy$>rd2Zo%D^Z2|zmWBQJEenf3^_el^jIS@oJ`H0}#)|y6Tc*zZf6LSrEmOU*mYuMc z9R#vWh5m~zQ*kXUQ>t`x=AunKRzI+Nlcloa`j=qXK9+p$gcK2cF&kznYi7IHtT!Hf_>-h_$60{S#Tb~S zaymuE_V~-=Bw0^@U8>;!+oe>yR6+ImCU&WaOP5$&wX&#vdEx*&KF3+o^yPDId$xS&ee=xF6g)#E-3*n(>bm@KLXLtN>hnV{9EU6N*CdgU zJU$Z;{O9dZ03?$tls8h8A7R-E=m>vkvvTtI-&vQ|tx%KxlUArnmyVJavlB(&qKof_ z6{_dVmEGZ6QcXX)73$=f|2wTva59(U|7j~!-z^2o`KX?m4`lX?QqC7_Sq3ZA7P3OY zbo-VlpKNDK7C&rv($=74vIeq3rTqMhj#at9Dkm!xPKWyc<5nms772fK?G$EDGaUEi z+^t);vTv1hUv0y`dgk{f+p5{1sv92AZBTbe*ONXV3tfX+kRur_i8&SBBqw*(iLJ10%LXW*0*ZqSHn~oi~{g#k=s?KknU)vCKV305D z3Z1dUL%6jhMYonD0oLvRA!|uWRB~ecWvnFxhi29im;?kKt{)EwlF@0%&7yeo14*yk z?iCTHe0C51-M8VX4^tje{BvWXdvuD5vjocSDPbY)_usM7ZZG*R_2xTlAEhmuKYw|e zyKS#Qx0rOn%tC}1iW?@2NtkXifyut9!NjwoAx+9~AucnIFCsU-M7|DQkMonpxl} zL$Maw*!@vtzc3>on4h~S!+-(Zo9o@;IJk=OqytdM5z7Em70i%4J|U_dGJL0Z!qJ_d z2N|fYWNMMKm3NC`Uwu%1>@N18@|vH<+f$!>jGX6Go+TS{FyTyCqq_jPyvvVE%-L^eaU@*$+~35=_r*~yH)v!C7osMl@HkTnQ_aZkM;rt^8E-!>qA zfsW=up=85AKR?P!uVy;-^7X=j9Uk|HbElvcN4QTc%Zv_ea=pL0s-$F8O_5S9M>GY!%I@0RE1s#0ce1=@D}D859t`WL6B>~ScnjFu>c40IA3HUT&ys8czEED zL@1`yGyM2*ucKIUIzSX6F;FZk{BzQ!N4}oMhAJIze7{faSJRZ8>^*!MD^eE z?ZEdMFDI{ObpWBZQ&OJ%cje17%9mg6-P_>4f8yXUBJ{# zlu5^k@fc0D1kq}NhX{sS+XI_$szLS8^yWrHqm(-yGMcLSa|0`^Av{NYdRS-j{!&Y@ z$64Y$D2;imb(A+_?dU_7v*~HdQ2R&wmmK@VB2c3T0jcgh(yEgKR%$dJ+-L1GN314y z+=Ft02SP9>D&;`&SC^UB8b0FO>m8%dy*{(Ul>TB7jVPn*59w*@hab~Hu4&J{EH9Mm z09Zr;VVer#3K{_`#ZovTR^f|oHj_hug=2@brr|?5>}7Vi-Zl)>Ba5vymdUlCZwbJF zTx-v?wjmyHGhzKbOzsjw@X&iLd1{E&sBa>Q@UTowj6hDS#Pr0@9aAC_BI0AB@N`&w zT)ZbO8_%atA~n+Ia~CT$#XGVkLu}L%wIHG;N+B+Y{&wtXzEm%pw9 zBuGTNp*@iA6zKrL0qRj}uU?r6Lrdj_rt$k1_3xFDsDD;6yuWoXE{lwi&Zl)+x~p8f zYH;h+dC9Iac;>m;ptD+zpJf9t*Im2|{v_4}5E+8gG-Q#MK%&vylHrnu}*d z5r{w!0wr2LXU(|cBc6a;9Dkvy#v6<<#Z(L@iuOd0RI$sMCbba3e4B}20mx3F9s84C zM7py$#`Rmi(Sy<%d*zkw+h2Wkd!;z!JKIj>eDB^pd*Vmxr`@Lu`VqT*^TQ7(PyX=3 zH@3rs!s7Y)n>Wv!clYK64d?2A`2L%3zW<^A;DWn1&!3N5w0{9{kK`s0?}Z4XUsb4g z2FT$Q0R4A@A4C2u=GhePZM*{{Pfw}F-`#BTH8{f_X{ud3*%bP&p0;+oiU)o98mNz( z=;e~W4&AKQj10$GSc|Ov^ZR7<%IMjx%VpOk@EX=7%1MOQ{9gFl>D+|Y@rvsu&eRsp z7Nsv`0(LDRi*@3+zJt95?3E=8< zEA|q}sgwg_9pcK4VoaKDm3?{LYMl$!iZm+=q|OeA=QqRa=XMq2qwHE0v@D*KksO3I z)ye=@ThNvS(3VJhI{_K7p>#n$(c4obOYBvsX`mZn{{BA5h2!7Ozimj6kJ$&_l{~+I zv#O>td1qx!yYqa_sN&88l2ZnB(iArRQao4Qr*C;uK}t$~l5*tZkClmKebwuNL{)qE z25Y*2>uvCd1=r^qrE{PY*W^lEg>jQeud63gp19FTdlLcAQPAEpt(_g(8`L%EZOkZ) zqqm3*IXLmRifeQw|CkadZgf`SNCq8>H+zG2x2D5+H`U>s+Pl=@yj$vUsFe5*>Tv8V z3X$i*igV#z<@`sRw=_Ka@y8Aw?&|w(qQfbE>@3&eVn5ij`6K4`Zo^6EUL!dN9jm%f zLM+$KAwX3Fm$87ebbNdIhHnzMq ztxb?uicdn~inN9iq#;L*BPVV+{XsIat83f^%Pig9@Rn?0uh-kkNLv&BVl0!kYT8;CfAj!rp-yEr4zuhSQv151ay+iSvI3tG#WaYHnA?7VcR?= z|8us@*I?9a|C1}*=4SCG>6mntw$05T_L^<;ZrC=DJKHufXuEBbf+1THaEkziD*L3Bd^`I31Bs|ZEl8b^Vpwe z+dQV(Hvisd?AOzjr`XXO@u`Jvb2Dt4$6VMp`RH8OHl5oy;fu#y*)}%=r;q((woP>a zt=KlNwO?nxc&t_1=4OQZ9Q)I4n~uHJnr(A4Y@5ga*Vr~+t;+4%Kc1|E)B0sp_v%s{ zpL-qIFf*%rIqp~G+V4BrHly0vZJYLwcFsvs{btxUk8#`PpUoX#RY!MSX3Et5j!~0M zlSaaAn(7D5rr7~D%|g({YqMz%Z}>nCx7#!m*?hS~o}+QeX;ybTmn?dk!a_n@HEo8& zYIQ|MT49a&(|M`1d6w0RT0qM)vzAwsXrHq(mscn$y_1uBC#Ci1ktP~@_DoIUm-3v- zr)JN7s&Yzo^%Tb^+u>38S6fk0J9hZ+vGkeN_B(jO-TIH)K!q#XKv|M%_*`Tf|NNrD z+)Ofp;`9DR+z47y78&j^g3?uk8#jrP)sw8CRC7ZVk8&%hFRm%}QWW2VD8z&cSV1XM z;I&vmE2LL(hI(5QE2!SjpI`-*!=0?4CzKc$R!~%kkV)dWW-F*v1S{wp%8Gk`V(-ED zt6D*k$wT;WuQe_rm)4JR6joBJRs8R@fWFhh0-8TF=L!~3bDI_x&@9ZM7v?~xz3mXc zmm`d@mbYswN@8S$h~rR1?oc@ibG7ndHHj%A{;4b2PRESj+qRn!`hL9i|>^cefq4ZtmMDDb?@G- zTaO;yl(W*8%AHPMbnSsJ@TF8xx~aNmV`=Hen(9rZcUP8`S5}slRW%&?NZS3;rLF31 zwaH!Tm*thPpa|p@?0`l$Azv6G%(PBx3kyO!oNE{n|KVnA&t!L0BSS@VhG51CJ|@u! z$1isni@jke)Ij6s=_|Rr+jCPE6y)Tn;3*hVFu0^BCqJieZm+0rQC+h#I;J!Ml$qNG zn*u`Q7C-XfipMd!CD7RFiIX1H)zU<0URP4GZqz9LyL)bK_wH6}cjYtb%R{S*i&vHR zn`Vid+J88G9^QXyT=53wrk*{!bnVrvEBjjeAWbbH?rh3%cqV&`;O8cS31BMMNDITW(CEa8Jn<-vIHpPET z&ry@-{?mF6tc=J0Nh_m67Heu{+zcz@v8%N*LNe8@jCaGzc-+~_Xt{!xvqOO;aRyv+WFsMXS_z;NZRSph#a3>>qcXQUGjH^TuAELX_m!I_B0bSi=CC1 z){1?^K5LZ&yCj)k*bb~^c?}Ja$HxeJ2Cl#wJPpi9NVD z&GOdTkfNEMYW_An05?b$EI!KnKE?X1-6i(LD5R{|bYd9KfY z&Y3w$$T~@wEM%SRE6HSoBruQ#5<&=&kVM%x0R^EA6cC{ZE`U%r0YSZ3&?w3k6kM=^ zAc{OKx6Qq!6$`FtX1 zfx(&Qs`M5uj0FcH(abAsiMsx}x`8cTdHM2Z@IM{UK0v%IUjD9(U0@ef2$tSD>M?fN zI?tofi`GB*;6u5x{ZjiS(IQ&xKSRo)S`}ICe$cUlb@G+L*hT4ISOrliQ}c{=_?-_E zs)cv7JXPTX;KET}78wl3C!`;ci*SfG2)=Fx#7J~}=H2Nt#70mu`%J_rc!3IddGTO= zo~0*gR6n?e!}$TmcmWwJZ~{cp{sR>pSQd7DJsc}smv{NiW}A8Va6uSeJ-iBIw^@=B zlM>St5_}O3Pnr5f`0gWiU-uv>EQG>L&Pz%mIWj?MCg%~T(Sah;+sIToUDl!@V)0!e z4)M|oSDXzM?3IsBoU09pSu^3b^`lSz&$^VR>e2^GrQuK4?E1yT)YS4(>(xJByr_P$ zej~Gd`OaG>-&HHsJa&rhtInUZ%Qo2XzB)+F0k8e&k=hZXH%4`qi@cy#3bNhS9yMcYJBs z29B1BIEV_{@L;$Yg)m9XOixXT>SyGMu;H+Xtjh4tl~;nE={{bRBGBn=la`j2o0b!4 zN#}vqh7LzHUdutxkfkNdFc8g1wi3lq%yJ=1=1U@9d$0DBjjdlS`_a_wG@KilTNcfn zvtwOJ4Xb2<0e=e!2*1|8YP`B4wPf8rA+6)a&pE#O-Z%4xtsQmYOY?Q}>>0!7*z5tZ z#rF>>T{F04_8Ve7%ZM9!*TQ=-zY;vP7RVx=0xA%y{g}MW*XT+}Rsr8{zmunq;_usN+(qQ4x!E&yNcO%s(rHrC1BeTCg6DLu(=yu`I3rF@j>r9> zyd~14X+zy|kvzS4*^FVxMsm-BnLnD3Y$Q)DTcZ9UCOoI`DUq4`=M;(!lgk&)-Cdvb z2g*B=>6;e)$f~usSKdjvN8ZZ31Hu%hR8d+rnWol>3Cr31WVpDFK;COras7Y$-@E<>zUSWoQT< z%Pf5C@;)RV7oHcuBG#z~j~!#xk(4v|sx-whgBL@#4kSi{clAm*hmmUr zmPHY;-FV-D{m~$M8D>FpabVF%{#zs!sAEXRk-Xe&dTf)EfIn`FLdFs9D620&@NikcJEfxiNyJJHQT1b~gn$JRa?^eHf=_U1x-Md-nOB;XJviSVQP3IRc zKELVPrH|}*h?UC^{f3F^vD@-;XWwRc|1tH-kt6D#5MO6La)gCF_T5WM|FCuIrKL+R zZQc5ZrR-elg?HW&>)vTdOUDr;aA&^C1IPuEy3oPn=*>U!){bMQStUXM8to@(tAMMMeEA5BSYLlc$1>Eu! zEfn#!&7_?P`QfPsBuNzhK%%%iK3}}R=)fpliYUXs=2`4%DB;;)2=O^)vB|AdrnJtT z%YR$zGPCM(t#w&5Y9(dTQ*-7#HA(yZV&?dwqWX-C`l9-o&{*Jw%6^2Hd*|6{B998u z{7>OSt5v%o&r>E-)^y}L7u~+2klzxyoVcf7r-`8EM_2AGXb2wcyfHhhY~_-#{n~dj z{U$vqN7yTE#aKdx65AkOWQ`M%^iTq~LlSjJAUt}pPE4J?|Doh`j^F?lRU6N%^S^0+bAEXYQRo^UHcJ8}((R%fd zt?FInBa+kQ=IRmZoAVAgWes#+2Mb021qWiA3l9nK!FCrJK`A=LxB&{ViN!O~TQE>b z21KF(sgYEYAqWjXjih(5#!Z4}RiZbojzzEp@%6zkO1qbs?_40~Un(rc7s~aQEsOYS z@#3!{wBKzJU$?Y;9ijc+%NOjx@7!1KcL(@VLia+-Ua$FB!FXXYx|YQMG9ao3D_iFH z+HjP|{>5Qs2ZS31tgIU@n&W~f8CF7}*&ljpkfzB5iG}K^mk+a&U%t!oU+nkYi#5Mq z_5$-;QFQz9^Y^jM<>!|zzgS{=o85N)61(l>G4+|x+rO=E+WTtK{rjq=Ja%qe^TaW# z6|yx_yv%iFqY%fRzW(sZ&`KbQYe->h(d+TiW5(hnDUMm?O!Y6He2Tm`i?559Nh=mF z(}{+&K8z?0a>F z3sy8ASUPA;Zqc%Wx~c&+Rd?U}?wL8aCr?hBRUc7NDMvd2DEflzx2a|7!!;qM6Q&Sr zPE1--Xx#Qi+kW2vL_gnxoT%jZ{%pGrP2aTtghLap4Vx}!LR&TpNUf}`3*o%57RptU zt5V^OtHP%1tAcBP34A-C;~P%&A(R}+X1L7fVh!yG1Zw6`zBTeHKV6x?oqz^M&o&Lw zo|!V`nYpzyvg)k4by=Bp)=r2v6Ie!oRx~~nTFGmuq40|y5RE{Hy~ZRR(jW;P-|8c# z@ur;3wFCOxLCsz6Zd_Z-P1e>Wao@}Rp{#|0@>;~u$6|Lc6n5Go%^?AP2=Pfm{B{)c z_R42I-oA+ZK^j9n$x$Zl`UZIc1On1b3Z0pmnT44JdAaeKCSX&LDVTzJ6O0PNw2f5J6lo-p57`=u z&sL`?gj}UKC;3!9DkR~=sG@_Bq@^S!-h`1vqM(`1NCpTs$;rvt$ywpVNR+Vj8yHDP z+)014AwLg%M57~#IvuG#Uh&rQt-ro&^0?$gCjMi=lt~TuEJ!Y5A>n@t4{2*ZY*XJb zrQSU!c2rz8N}Y?%avDkK1Ax__1H%u-Ycl&?Z45 zX8pgH2pqy8d_>ET}ZVZ9@H1 zQ|`{$v9{#Cb#ILy&DdKRs`{BEx=V$c^4gA@N`?In`76g1V;W9&%t9{TQo z{&hIC-j!E{csUH68Rg+Zt>AT7WQS@|?$Gu5GW~N!h95H9go=nFdzEd{Dk8RL$e*jb zjX4;Wdu6e1{O~%Dx&6W0Q9BGj^5?GNVd(b>`Ey5#G7bGkqDQUYC0Zp$wnXdKey`l7 zPU8se-fuUNzkEYRF1_}de`HC=s!wg8PD}gD6D;Y9-iBy@iP})Y zF#iA{%|_N#iW_63kh_W?d-&yQch5bHB$!&2>gq;}zz<^XlCBt2N{fq2=|_E*k5#n4 zjDB|Wu_~UD(!eEUk}fG-IfV1s>m>{AFR%D|!=4piuh%BP8^=nVksni^N9&3}m4}?F zII9UZ@p>)=`BtjF)&6_mvzC~B2Ibewe{#7Qx5j<|ql(1$j6w>R-Y5)4(})CKk3*Qm zl>ld*dm4#kp<)Bp&(17b^zJXN%WdC&%T{@`l*(H|K`CqkutL&k^f_WvV51QeCh0+v z%nBA|HLLY!Ucdf}cNZD!)YWX&w?JEMT*^e;6_bESWfL@8iZMf=B5b2nqDn+W8!u2z zCNRpE?0T-;?~GaNp#3rZ<}A`Xx~rtPc*v0A;*tS+kIepze8~}Q91M$079YQbMlP&@ zy+dM%_+&hEkVzEjiSmS|c#~VCCk#i#)ln+P(%$&A_Yp%y>5vNfi1$+)=&QItlE9$U#Z883m|Ecz2$$Afe|Q(NLGaI_@A}`a+T5A{`B;sqUoij{CAqwnnpkJ zuL`D=luRkmelN_+%*fBr$jsBm_-KX<5Wyz+W=q-)5KvnLE*juc}wJWzh1_*}JSgf+c5I&WN>rEyRye{CI4 zuMJ+|VWFX^8K+Ueg1>kxB@tY}eg{T0g`+S+KY(_G#e}j<)KQiYkw)#h#);I-h1A;T zgw$FaPNZfXNR2i!9Vvz{uxs}gF=4N;KOwF+WF9}x$C;^*lUnb3oR3oW#)(3hFpfV(J{^@zM;-EkeD#Mag%lK!BGlX^Xn>)J31cEr zcgGdj?Dl^1(XfPMCtM&O4UF16rB6OLZqx1$3%jNL(TKQ+@X&@aBP~9BH%_HIx4e29A6M z9H|CKRM84(rFqP-S-?hpH@X*johY)O3L>W6pFTnE{!Sk>zYK(lqlD^w%Ozuj*`l#w zi~aDvO&gv8%2bvP8zvty>^<<@i*qMUoQH*MA0l5-A2K$fcU?x1xf@nxzT7-sTwCst z1lksO(_Ht6hZ+L-Qq<}pMXN$AawE9qb>}wf5LvTZWy4|`QcR`^Wf)y*oxyv~r1W{nR4H2g!Vu9LM|mGH zw^dxKKKRBPki=$Vg=4-l2hwdO?NS?l&)mRWhlwR~FE=(CE%rl=jkUECCe*SO^a4M| zikixbnwpBrn$Z>7UqRA(^?DeiCQYN*R^$woM1Y_O-KmjuCuxe8LBW3{7@Dz%>&a~7 z0NLNi3*VH2C_<7I#wpzmroCYo8+lqC+IdLI{wr71CvO-Qb|AhQUXNjy!}%9>pgR1X ztMie$Iv+_hR;YXUia8@zJIPg7F+uGQPoLh{IBi;^v7&n9i0bMQBdcj=-iFn;^8R3@ zbL)?GRlPs^Fmktr_L@> z=BaHg`z_if_&B_l;0~B5TCfwx(4G{8($bxP1zSRPt3EJ|#d>!SIPb*(yrtKvbeT!x($O?}+z^ii(k5NBj2pkONTi-p80NLAOy;h61J&6$NJ-6+-9EpzA_ zZFpjUi#C`e=U!1;b=siZHLznahLviAi#9O(M&o$LAf@^Hj|)}ns|ID;;tK22EKQ}E zwRzb?@jBhoTxuMTia4f(-q4N623uew@3!B`qx>N#0?+%WC#D^$Q8{_g&%}FuXi510@L}G4DaZ=b&5ID_p#2 z#n%h=thlzl;1NDIVU6URs}5R8(C2?!t02!?U5ZD3GlH0 z4uP)lkn%Fx#NK@~ro&I%EN(1k?0AjkC{a+Vi?`>Iehm?8Pg5tWdpL8*A9W}L8Yd}_ zT6IGlhez6i@IOyYSF2&%rBbqCnx>SfldCkPqzbFstWI{!*SQJUA@vaewa{rRrmRmO z!(A$h%{CKP20ZP^-Kl3c>Wb>*8m=Rfo~ZrM8oJmDayj*asq~hmc!Q<91C|mhq}x(L zyJ0Ar_fU#Z6O@p^$w>z>A&VLuK{RSIzj@XDbH=9JmOOY+>7dl@vl>S>O^#X~5D=G~ zlz3`BG=gZblQyuEKmoHr79Cg~{d8v1ar9XdUFc`VxBt&VnNIDQ04gT~=x8k$Xz+#Ea@MyKt%?s=ahN>xQg zm2$-Ax$WxcT^nijH|<2L+ti&7tAir`d(u61TvT@gnwS_RUy<$r%lCIOOq}?4$1wc` zKNrIkG=}MAGxfpPD?A-gceeZQRwFy?50gzY%a zikxR4jGf16`7FnK7)Wndl-!xmBV3VfR%xE>ynITWWgOS%7UWaB1&4z@fqAT3ricjQAQWI?QlAP#z?=r4y;MZB4@3BQ{rKA9BB)Q1>p3re!yz9Lu<*%eiaat$I zVb1s1Z25EXw>bZjbdhaf^k~x4xl03-y z9+x(f0!dDCzK5s4OPUTlwj`%$??E!aSEbfIS_@D|+~j5F(AJl~_m0K;?NDcL(+Sqy}8 zxC8T{&06H4o%-z1XVItddtTt#1J`ltt@8=iw3c7v-onhap+mRrQn@e~<_Fj)L?^90S>f+c06 zVKz|UK8(Xkm<_}r-?JTN115Zc(cF&F^kg+q%;`)!p_Fd-FdLwYjy8I*8&D(K(V@7t zv)w>z<(BORo#aqIp=7_)Pbj;$8wkHO4v}W}+HQa;cq$@nL-%$AR%y%;7!kr9jR~CPcGaV-(?Cl%LHA$d4S!zXFlHBZS zf2kTMp2B;6#0`65xah`{;fkj~k}|z-Jcnat&^iUqaA9ffj^Sd0ZpYc2=3>3NrnzG4 z&1o)nuqL%q?8kX7PLn)&ZYR40mX?!UOo;1;?qv7U&A6`cTXnQFn{(aX*r#vYXV!N| zcrpHS>P~5XkBk>%){%u1UhpOCPtx97u_t4(+0ETh#P|AaPZsU}0dJusr3Tt_kT6Bd z0SUJcm}a@@5oK)=Dl>!54iZI5Rf<PXGN`-y`0Y!-kDej`+52HkKl%%wN^LPwsB6t!>`@ zBrHw;r(*<=bROgBYrA?m#>wy(NfLJ1JiJ8+SCJAO%OEa<Q`tC!ob$*h zNn3d$)wl|)+s$RRROc1sk`8d7GRS&^Mk6omVTAP@EssIQ*Z`CmN(xUj#RrEaCV6>9 zI8mB{Dn0#9I?Z@bUS@=OhNqRa-19EY%?j}g>}T=qcSk?J;*$6zvne**?WW>%Nu^uwyjl8fWI20QAigdabuG=Wm zVBp0W42r8bLo!nPTjG%N1wB(CgPRv;@Ki0;v7>XcN@MfkGBu$%4S7&X=ym*r67_=T z^y@=RL4mQBC*xzOBr&~+R@b>i7PE&xqENw~D<48@RF)v`6@VxmdEltu<&tTl-16|q zN|&IgCt8C^<$vMY=`-r{*Iy^kM5RXkYp}ZdTbDQC;~uTTKBzBaH?6`i(V(?D_hHyJVidg zPf_%-B*-hf{Jfd0%_!OxiQ3>{f;l-%fzOUeV8)%ET+UFMR5ER}8+7ZwT5FDDRLQZVyOO&Pe`tDZIx1xOLkct)SzSrK9t>(_XZ+d+OdVf5ycI?OZB`|gHaAIov#IoMnpsaQ#gtyyP+rr_H=^L&NIr- z4996z+LRQ?uJg32iZ5_oT`eu}c4b0Vh;Lv&Q;@IE?0){mr2x|4=-N zg#MVM7f_M1dx@+H59`Q2fF#d<{BI%T$l-Zug^>jfc^S>6iQ4N4){Leh>L>cdM$^69 zt}F=(3h<9f+>j7SFQOASSR&PTNtA|`LzFNt@>=-VD-e?GL!KNuLh9Zl8cuTO5r8Fp z7TjP`al+*LuW#C{?z?`HtRr55-{h))IeggJ@?yQ^rlV*WAgQ@8VjG5iMqG$CAscYs z#M9$eeI}CjX}UmINmoc~J73WJO2;@X$xdM1uzAz#YP~iNCH$MW-N*5r*IFSjE2d9 z*l()fusu?nY7zyiOER__gW)~^G#oA(-SF5Ml^tnIrV9bg*m!Pw<*h>X6XSAH8%h#v zxk>pEdE;`^8j2H!=cHQ0Rl9z>?Od{hg8TzqvVK^<|3Vw=Tyy_ztOkc*H()h8-xF4U zkG;0$sjR@0>rfR#O7+Zn4Bm&V&8 zRms5eOS7}~RYZaJ`^-7#-jj%B*Vo_g{rvyApJ$$DW}bQGnO@G! zgmK1LD;O#Z9FUcrQ+;RkD#nyb#(2bl+!3QzS{B4GcGnulZhC6KsL}QbY~vZmdaYzE zB5=g09*K2r9~%$eb&yh6T;-_sJNI!BV_pLp^9(DV+F-NH-87mp@f7^`OsXxbnp)B{ zo3W51jQItXIqGZS9*FQAfj!GAXG{uM+~CRB$GMC*rk0mFO8hPUXW?HC|I~6g`2E?N zbjyN0s=TUU`j#;l?q{s?dB$4ruB<6`TnJlgLxRBXRaE7eUTgL7_XU3x_-)mWs?x^W z)}|9Yw8;=r_O(ru?d?Qi~F*+uC%uP-26o73wE}PamKG+WdXoUb+R$cn>m;T zp50h)b{9BS_yr-~1o?aVUKt2;}(bP$3@T!#@@w*JvNR!x7gz%9FR=dlPr4{9 zYgZIsII_Ume__Ecr6g-tG}-ofHZiXDUs#+EN0v1XfBn6Oc&2$qdbU?p-)?*P)%i~K z?p=aqJ!vVivMeh2-}pCx4ImBFjf{C8x_nW6>grd7*+Bg4%uYFA4N%h=yE2g6E$>|Y z4EBLnKf97_=CkICaxgpEqPz|B^P^rBy%GX;BR&7kE5o!|5 zWHp&7Y6|q>YA>}n%sy%s+;h~iFbmWh;c}CDGt8ChCWIC&5_vrUG6%6)FmGk|!F+(N zhPj5Vhq;mMg}IL%gL#~tfcXwP1@km-!#Hopd%#TKi7=D-XqaR9c$gE=);OQcZ-;pY zUk3AD{y5A{{CSu!@{=&ni)hA07Xf}TLDa)+6yO*0#e*~V{9YVbXCvxhj5~b6eC5ta~S}=ccR;Lw~Bi87& zYT~i55HVMGx3YMVr_&y60{>R0Jy}Qoj!t{AD85IheONqSpwqr4+K=1VB;DPgC9_1G z4n&XOsne}MF_zEDSp}L6x%x{AyV*)&swK zgsOvtTDaA-I6wuf1g?Q=8T?1E9MmK&7Lu1EGIS?zwPPrT>%czPC zL!MMZ2g-L%rz~hjd0&R~X=xqwlhBmf88W|quC$FbQ$E)rPO1%6GVUoz8I_lf<-$%m zQ3Lv?al|#NNk%>TX)WsRs(CHTCK0KOmp?bv8!-(-%o!NZ%)0*;`FTh~nanTBLv!d~ zuIEPGsFrP@7CA>HP$tXQh87i%8kC4Upt?=9qPgT;aM!TO@F_-K+Ms!(tTWUus9tq} ztd^zQAJQqewH8FJv5IwNRDazyj#5MIunzT7YaQ+uH5_59;A&$LEwnN6DFS(`O&X{N zYfC+W#Hyz^@G;iJtVj>0JZQN+*&Ot`bNDj8f2NeaaT)d1Zf?S6Dz;`>^Z6!oxa+#f0?;OAG5AHYjXV z*o|QoVKrg1!sdr93%fV$!LYSq-`jj_fwp$G5L=in!WM0dwWg!F&a& zYK2rqD$&hSCxhgF764{Hir8n!~FYMspk zscLNtwso}GWU3NvJ!PsIY)wejW44V*)i&EMr0QAQ^DR;}D*VRq<$9`GAypk*sVYIL zXvW8{{u{-)Nv4injo}|?l-ax*dgX1iok#F+-hsE~t++4uzS{e0=d10nwl#D7JbkX+ z*--L5HwZ9TQmxM2dG4KaQD@Jc-GSe2XWwA#?AvEglHb|qCC8Yv-<}O|BpIvnJ zu{R(Twu~Zx9cV1mu1frJK@A>7(>h za+HP2Qt7%}S%G@CPT8vLCi_qSl+EySDtnZ@%0AS_7nS|W0p+0bhH_dtt6WsBsH*C( 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 wd(Xfk)`3+2JI#rp>bK% + + + + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 79599498fbc5..6d47e5df87b6 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -56,10 +56,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_nos1, R.layout.keyguard_clock_nos2, R.layout.keyguard_clock_life, - R.layout.keyguard_clock_word + R.layout.keyguard_clock_word, + R.layout.keyguard_clock_encode }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From eab43f737ab38ceaa62d6e69f1985241f6cd34f5 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 20 Nov 2024 10:01:29 +0000 Subject: [PATCH 121/190] SystemUI: Simple analog clock Signed-off-by: Ghosuto --- .../layout/keyguard_clock_analog.xml | 29 ++++ .../layout/keyguard_clock_nos3.xml | 29 ++++ .../systemui/clocks/AnalogClockView.java | 100 +++++++++++++ .../android/systemui/clocks/ClockStyle.java | 6 +- .../clocks/NothingAnalogClockView.java | 141 ++++++++++++++++++ 5 files changed, 303 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java create mode 100644 packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml new file mode 100644 index 000000000000..bb0d2a05dce7 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_analog.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml new file mode 100644 index 000000000000..aaf8e92a8dc1 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_nos3.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java new file mode 100644 index 000000000000..274701adafa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/AnalogClockView.java @@ -0,0 +1,100 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class AnalogClockView extends View { + private Paint mPaint; + private float centerX, centerY, radius; + private final Handler handler = new Handler(Looper.getMainLooper()); + + public AnalogClockView(Context context) { + super(context); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges + handler.postDelayed(new Runnable() { + @Override + public void run() { + invalidate(); // Redraw the view + handler.postDelayed(this, 1000); // Update every second + } + }, 1000); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + centerX = width / 2; + centerY = height / 2; + radius = Math.min(centerX, centerY) - 20; // Leave margin + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Keep the wallpaper background (no transparent background here) + + // Draw clock face with hour markers + mPaint.setColor(0xFFFFFFFF); // White color for hour markers + mPaint.setStyle(Paint.Style.FILL); + mPaint.setStrokeWidth(5); + for (int i = 0; i < 12; i++) { + float angle = (float) (Math.PI / 6 * i); // 30 degrees per hour + float startX = (float) (centerX + radius * 0.85 * Math.cos(angle)); + float startY = (float) (centerY + radius * 0.85 * Math.sin(angle)); + float endX = (float) (centerX + radius * 0.95 * Math.cos(angle)); + float endY = (float) (centerY + radius * 0.95 * Math.sin(angle)); + canvas.drawRect(startX - 5, startY - 5, endX + 5, endY + 5, mPaint); + } + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + + // Draw hour hand + mPaint.setColor(0xFFFFFFFF); // White color for clock hands + mPaint.setStrokeWidth(12); + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(hourAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.5f, mPaint); + canvas.restore(); + + // Draw minute hand + mPaint.setStrokeWidth(8); + float minuteAngle = (float) (Math.PI / 30 * minute); // Convert time to angle + canvas.save(); + canvas.rotate((float) Math.toDegrees(minuteAngle), centerX, centerY); + canvas.drawLine(centerX, centerY, centerX, centerY - radius * 0.7f, mPaint); + canvas.restore(); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + handler.removeCallbacksAndMessages(null); // Stop updates when detached + } +} diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 6d47e5df87b6..0ca6757f5ac3 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -57,10 +57,12 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_nos2, R.layout.keyguard_clock_life, R.layout.keyguard_clock_word, - R.layout.keyguard_clock_encode + R.layout.keyguard_clock_encode, + R.layout.keyguard_clock_nos3, + R.layout.keyguard_clock_analog }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; diff --git a/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java new file mode 100644 index 000000000000..8faeb31fe565 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/clocks/NothingAnalogClockView.java @@ -0,0 +1,141 @@ +package com.android.systemui.clocks; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.util.AttributeSet; +import android.view.View; + +import java.util.Calendar; + +public class NothingAnalogClockView extends View { + + private Paint mPaint; + private float centerX, centerY, width, height; + + public NothingAnalogClockView(Context context) { + super(context); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public NothingAnalogClockView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + mPaint = new Paint(); + mPaint.setAntiAlias(true); // Smooth edges for drawing + mPaint.setStrokeCap(Paint.Cap.ROUND); // Rounded ends for clock hands + + // Refresh the clock every second + postInvalidateOnAnimation(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + this.width = width; + this.height = height; + + centerX = width / 2f; + centerY = height / 2f; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Get current time + Calendar calendar = Calendar.getInstance(); + int hour = calendar.get(Calendar.HOUR); + int minute = calendar.get(Calendar.MINUTE); + int second = calendar.get(Calendar.SECOND); + + // Draw clock ticks in a rectangular shape + drawRectangularTicks(canvas); + + // Draw hour hand + drawHourHand(canvas, hour, minute); + + // Draw minute hand + drawMinuteHand(canvas, minute); + + // Draw second hand + drawSecondHand(canvas, second); + + // Trigger continuous updates + postInvalidateDelayed(16); // Smooth 60 FPS animation + } + + private void drawRectangularTicks(Canvas canvas) { + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(0xFFFFFFFF); // White color for ticks + + // Rectangular bounds (inset from edges) + float horizontalInset = width * 0.1f; + float verticalInset = height * 0.2f; + + for (int i = 0; i < 60; i++) { + float angle = (float) (Math.PI / 30 * i); + float startX, startY, endX, endY; + + if (i % 5 == 0) { + // Major ticks (longer and thicker) + mPaint.setStrokeWidth(6); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.5) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.5) * Math.sin(angle)); + } else { + // Minor ticks (shorter and thinner) + mPaint.setStrokeWidth(2); + startX = (float) (centerX + (width / 2 - horizontalInset) * Math.cos(angle)); + startY = (float) (centerY + (height / 2 - verticalInset) * Math.sin(angle)); + endX = (float) (centerX + (width / 2 - horizontalInset * 1.2) * Math.cos(angle)); + endY = (float) (centerY + (height / 2 - verticalInset * 1.2) * Math.sin(angle)); + } + + canvas.drawLine(startX, startY, endX, endY, mPaint); + } + } + + private void drawHourHand(Canvas canvas, int hour, int minute) { + float hourAngle = (float) (Math.PI / 6 * (hour + minute / 60.0)); + mPaint.setColor(0xFFFFFFFF); // White color for hour hand + mPaint.setStrokeWidth(12); + + float endX = (float) (centerX + (width * 0.25f) * Math.cos(hourAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.25f) * Math.sin(hourAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawMinuteHand(Canvas canvas, int minute) { + float minuteAngle = (float) (Math.PI / 30 * minute); + mPaint.setColor(0xFFFFFFFF); // White color for minute hand + mPaint.setStrokeWidth(8); + + float endX = (float) (centerX + (width * 0.4f) * Math.cos(minuteAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.4f) * Math.sin(minuteAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } + + private void drawSecondHand(Canvas canvas, int second) { + float secondAngle = (float) (Math.PI / 30 * second); + mPaint.setColor(0xFFFF0000); // Red color for second hand + mPaint.setStrokeWidth(4); + + float endX = (float) (centerX + (width * 0.45f) * Math.cos(secondAngle - Math.PI / 2)); + float endY = (float) (centerY + (height * 0.45f) * Math.sin(secondAngle - Math.PI / 2)); + + canvas.drawLine(centerX, centerY, endX, endY, mPaint); + } +} From ed356afdf931c136e3f0a929a02b6d616c4fa913 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Thu, 10 Apr 2025 16:37:44 +0000 Subject: [PATCH 122/190] SystemUI: Lock Screen Clock Accent Color Option Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 12 +++++ .../android/systemui/clocks/ClockStyle.java | 54 +++++++++++++++++-- .../theme/RisingSettingsConstants.java | 2 + 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 1a200e3ccd0c..68c1198eefe0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -14649,6 +14649,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ public static final String GAME_OVERLAY = "game_overlay"; + /** + * Whether to use system accent color for lock screen clock text + * @hide + */ + public static final String CLOCK_TEXT_ACCENT_COLOR = "clock_text_accent_color"; + + /** + * Lock screen clock text opacity (0-100) + * @hide + */ + public static final String CLOCK_TEXT_OPACITY = "clock_text_opacity"; + /** * Whether to show an overlay in the bottom corner of the screen on copying stuff * into the clipboard. diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 0ca6757f5ac3..d4f76b78d13a 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -66,13 +66,19 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; + public static final String CLOCK_TEXT_COLOR_KEY = "clock_text_accent_color"; + public static final String CLOCK_TEXT_OPACITY_KEY = "clock_text_opacity"; + + private static final int DEFAULT_OPACITY = 100; private final Context mContext; private final KeyguardManager mKeyguardManager; private final TunerService mTunerService; private View currentClockView; - private int mClockStyle; + private int mClockStyle; + private boolean mUseAccentColor = false; + private int mClockOpacity = DEFAULT_OPACITY; private static final long UPDATE_INTERVAL_MILLIS = 15 * 1000; private long lastUpdateTimeMillis = 0; @@ -138,7 +144,7 @@ public ClockStyle(Context context, AttributeSet attrs) { mContext = context; mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); mTunerService = Dependency.get(TunerService.class); - mTunerService.addTunable(this, CLOCK_STYLE_KEY); + mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); mStatusBarStateController = Dependency.get(StatusBarStateController.class); mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); @@ -202,6 +208,37 @@ public void onTimeChanged() { } } + private void updateClockTextColor() { + if (currentClockView != null) { + updateTextClockColor(currentClockView); + } + } + + private void updateTextClockColor(View view) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + View childView = viewGroup.getChildAt(i); + updateTextClockColor(childView); + } + } + + if (view instanceof TextClock) { + TextClock textClock = (TextClock) view; + int color; + if (mUseAccentColor) { + color = mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + } else { + color = mContext.getColor(android.R.color.white); + } + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); + } + } + private void updateClockView() { if (currentClockView != null) { ((ViewGroup) currentClockView.getParent()).removeView(currentClockView); @@ -216,6 +253,7 @@ private void updateClockView() { if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); } + updateClockTextColor(); } } onTimeChanged(); @@ -233,6 +271,16 @@ public void onTuningChanged(String key, String newValue) { } updateClockView(); break; + case CLOCK_TEXT_COLOR_KEY: + mUseAccentColor = TunerService.parseIntegerSwitch(newValue, false); + updateClockTextColor(); + break; + case CLOCK_TEXT_OPACITY_KEY: + mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); + // Keep opacity within valid range (0-100) + mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); + updateClockTextColor(); + break; } } @@ -244,4 +292,4 @@ private boolean isCenterClock(int clockStyle) { } return false; } -} +} \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java index 2f2b8f734d7b..5df126e7b9f1 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java +++ b/packages/SystemUI/src/com/android/systemui/theme/RisingSettingsConstants.java @@ -26,6 +26,8 @@ public class RisingSettingsConstants { }; public static final String[] SECURE_SETTINGS_KEYS = { + "clock_text_accent_color", + "clock_text_opacity" }; public static final String[] SYSTEM_SETTINGS_NOTIFY_ONLY_KEYS = { From 5820c82f9ebc40d67f033d785dc859c172a6343c Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 23 May 2025 10:28:16 +0000 Subject: [PATCH 123/190] SystemUI: Improve clock face color option Signed-off-by: Ghosuto --- packages/SystemUI/res/values/ids.xml | 4 +++ .../android/systemui/clocks/ClockStyle.java | 31 +++++++++++++------ 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 84e84bd0cc79..d85b938ec771 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -311,4 +311,8 @@ + + + + diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index d4f76b78d13a..3716b8b7f67e 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -225,17 +225,28 @@ private void updateTextClockColor(View view) { if (view instanceof TextClock) { TextClock textClock = (TextClock) view; - int color; - if (mUseAccentColor) { - color = mContext.getColor( - mContext.getResources().getIdentifier( - "system_accent1_100", "color", "android")); - } else { - color = mContext.getColor(android.R.color.white); + + if (textClock.getTag(R.id.original_text_color) == null) { + int currentColor = textClock.getCurrentTextColor(); + textClock.setTag(R.id.original_text_color, currentColor); + } + + int originalColor = (Integer) textClock.getTag(R.id.original_text_color); + int whiteColor = mContext.getColor(android.R.color.white); + + if ((originalColor & 0x00FFFFFF) == (whiteColor & 0x00FFFFFF)) { + int color; + if (mUseAccentColor) { + color = mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + } else { + color = mContext.getColor(android.R.color.white); + } + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); } - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); } } From fe85ad58042fb6f37b32f75d511cd1fca868a3a9 Mon Sep 17 00:00:00 2001 From: Lixkote <95425619+Lixkote@users.noreply.github.com> Date: Tue, 6 May 2025 17:26:28 +0000 Subject: [PATCH 124/190] SystemUI: Added android 9 style lockscreen clock Signed-off-by: Ghosuto --- .../res-keyguard/layout/keyguard_clock_a9.xml | 44 +++++++++++++++++++ .../android/systemui/clocks/ClockStyle.java | 5 ++- 2 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml new file mode 100644 index 000000000000..2d093c0d9fa4 --- /dev/null +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 3716b8b7f67e..a92e96c882e0 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -59,10 +59,11 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_word, R.layout.keyguard_clock_encode, R.layout.keyguard_clock_nos3, - R.layout.keyguard_clock_analog + R.layout.keyguard_clock_analog, + R.layout.keyguard_clock_a9 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From a13f9d66d05d99f5f16edc7250973d2d840b21b6 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 11:32:27 +0000 Subject: [PATCH 125/190] SystemUI: allowlist ACCESS_NOTIFICATIONS privileged permission Signed-off-by: Ghosuto --- data/etc/com.android.systemui.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml index 7f89fd5fad51..4d8c29a816ec 100644 --- a/data/etc/com.android.systemui.xml +++ b/data/etc/com.android.systemui.xml @@ -100,5 +100,6 @@ + From dd7140dc831764815e9f411d4b3f50578077fa07 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Wed, 25 Feb 2026 12:17:37 +0000 Subject: [PATCH 126/190] SystemUI: Set life clock at middle Signed-off-by: Ghosuto --- .../SystemUI/src/com/android/systemui/clocks/ClockStyle.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index a92e96c882e0..a9b488546501 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -63,7 +63,7 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { R.layout.keyguard_clock_a9 }; - private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20}; + private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; From 344af8dfe1ff96a43cfc61428d47138df129c072 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 06:32:23 +0000 Subject: [PATCH 127/190] SystemUI: Use small clock when setset clock style Signed-off-by: Ghosuto --- .../data/repository/KeyguardClockRepository.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 6d4d4f2e1e1b..bc9bdde3ff24 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 @@ -100,7 +100,19 @@ constructor( override val forcedClockSize: Flow = if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { configurationRepository.onAnyConfigurationChange.map { - if (context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen)) { + if ( + context.resources.getBoolean(R.bool.force_small_clock_on_lockscreen) || + secureSettings.getIntForUser( + "clock_style", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 || + systemSettings.getIntForUser( + "lockscreen_widgets_enabled", + 0, // Default value + UserHandle.USER_CURRENT + ) != 0 + ) { ClockSize.SMALL } else { null From 30a74fc4acc9da0e043316e33b7b736e231cae2c Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 06:33:51 +0000 Subject: [PATCH 128/190] SystemUI: Hide clock properly when clock style set Signed-off-by: Ghosuto --- .../ui/view/layout/sections/ClockSection.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 6b278212c50a..53aff4c6232e 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 @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context +import android.os.UserHandle +import android.provider.Settings import android.view.View import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout @@ -121,6 +123,19 @@ constructor( setAlpha(getTargetClockFace(clock).views, 1F) setAlpha(getNonTargetClockFace(clock).views, 0F) + // Hide small clock when custom clock is enabled by setting alpha to 0 + val isCustomClockEnabled = Settings.Secure.getIntForUser( + context.contentResolver, + "clock_style", + 0, + UserHandle.USER_CURRENT + ) != 0 + + if (isCustomClockEnabled) { + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_SMALL, 0F) + setAlpha(ClockViewIds.LOCKSCREEN_CLOCK_VIEW_LARGE, 0F) + } + if (!keyguardClockViewModel.isLargeClockVisible.value) { if (keyguardClockViewModel.shouldDateWeatherBeBelowSmallClock.value) { connect( From 23bf7d65a537b521ce01ddd8e1e8279ad46b82a6 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 09:57:56 +0000 Subject: [PATCH 129/190] SystemUI: Fix default clock omnijaw view Signed-off-by: Ghosuto --- .../ui/view/layout/sections/KeyguardWeatherViewSection.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 index 07ee85aadd9e..e9898053550b 100644 --- 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 @@ -21,7 +21,7 @@ import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet -import com.android.systemui.customization.R as custR +import com.android.systemui.customization.clocks.R as custR import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.res.R import com.android.systemui.weather.WeatherImageView @@ -69,7 +69,7 @@ class KeyguardWeatherViewSection @Inject constructor( ConstraintLayout.LayoutParams.WRAP_CONTENT ) setTextColor(context.getColor(android.R.color.white)) - textSize = 20f + textSize = 16f visibility = View.GONE } @@ -139,11 +139,9 @@ class KeyguardWeatherViewSection @Inject constructor( private fun applyWeatherTextConstraints(constraintSet: ConstraintSet) { if (constraintSet.getConstraint(R.id.default_weather_text) != null) { constraintSet.apply { - connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, - context.resources.getDimensionPixelSize(R.dimen.weather_text_margin_start)) + connect(R.id.default_weather_text, ConstraintSet.START, R.id.default_weather_image, ConstraintSet.END, 12) connect(R.id.default_weather_text, ConstraintSet.TOP, R.id.default_weather_image, ConstraintSet.TOP) connect(R.id.default_weather_text, ConstraintSet.BOTTOM, R.id.default_weather_image, ConstraintSet.BOTTOM) - connect(R.id.default_weather_text, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END) constrainHeight(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) constrainWidth(R.id.default_weather_text, ConstraintSet.WRAP_CONTENT) } From c52b69be0351e2dc03f36fd16a5e01300457543f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 27 Feb 2026 06:39:10 +0000 Subject: [PATCH 130/190] SystemUI: Adapt weather view to updated omnijaw controller - separate switch Signed-off-by: Ghosuto --- .../sections/KeyguardWeatherViewSection.kt | 4 +-- .../systemui/weather/WeatherImageView.kt | 28 ++++++++++------ .../systemui/weather/WeatherTextView.kt | 26 +++++++-------- .../systemui/weather/WeatherViewController.kt | 33 +++++++++++-------- 4 files changed, 52 insertions(+), 39 deletions(-) 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 index e9898053550b..4a43208d64d6 100644 --- 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 @@ -53,7 +53,7 @@ class KeyguardWeatherViewSection @Inject constructor( } private fun createWeatherViews(constraintLayout: ConstraintLayout) { - weatherImageView = WeatherImageView(context).apply { + weatherImageView = WeatherImageView(context, isCustomClock = false).apply { id = R.id.default_weather_image layoutParams = ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, @@ -62,7 +62,7 @@ class KeyguardWeatherViewSection @Inject constructor( visibility = View.GONE } - weatherTextView = WeatherTextView(context).apply { + weatherTextView = WeatherTextView(context, isCustomClock = false).apply { id = R.id.default_weather_text layoutParams = ConstraintLayout.LayoutParams( ConstraintLayout.LayoutParams.WRAP_CONTENT, diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt index c6c3fba59ede..d8b151f52857 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherImageView.kt @@ -17,37 +17,45 @@ package com.android.systemui.weather import android.content.Context import android.util.AttributeSet -import android.util.DisplayMetrics 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 + defStyle: Int = 0, + isCustomClock: Boolean = true, ) : ImageView(context, attrs, defStyle) { - private val maxSizePx: Int = context.resources.getDimension(R.dimen.weather_image_max_size).toInt() - private val weatherViewController: WeatherViewController = WeatherViewController(context, this, null, null) + private val maxSizePx: Int = + context.resources.getDimension(R.dimen.weather_image_max_size).toInt() + + private val weatherViewController: WeatherViewController init { visibility = View.GONE - } - - fun setWeatherEnabled(enabled: Boolean) { - visibility = if (enabled) View.VISIBLE else View.GONE + + val stubText = TextView(context) + + weatherViewController = WeatherViewController( + context = context, + weatherIcon = this, + weatherTemp = stubText, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) } override fun onAttachedToWindow() { super.onAttachedToWindow() - weatherViewController.updateWeatherSettings() + weatherViewController.init() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - weatherViewController.disableUpdates() weatherViewController.removeObserver() } diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt index e48d23424cc6..a603256bf48c 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherTextView.kt @@ -24,35 +24,33 @@ import com.android.systemui.res.R class WeatherTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, - defStyle: Int = 0 + defStyle: Int = 0, + isCustomClock: Boolean = true, ) : TextView(context, attrs, defStyle) { private val mWeatherViewController: WeatherViewController - private val mWeatherText: String? init { - val a = context.obtainStyledAttributes(attrs, R.styleable.WeatherTextView, defStyle, 0) - mWeatherText = a.getString(R.styleable.WeatherTextView_weatherText) - a.recycle() - - mWeatherViewController = WeatherViewController(context, null, this, mWeatherText) - - text = if (!mWeatherText.isNullOrEmpty()) mWeatherText else "" visibility = View.GONE - } - fun setWeatherEnabled(enabled: Boolean) { - visibility = if (enabled) View.VISIBLE else View.GONE + val stubIcon = android.widget.ImageView(context) + + mWeatherViewController = WeatherViewController( + context = context, + weatherIcon = stubIcon, + weatherTemp = this, + weatherInfoView = this, + isCustomClock = isCustomClock, + ) } override fun onAttachedToWindow() { super.onAttachedToWindow() - mWeatherViewController.updateWeatherSettings() + mWeatherViewController.init() } override fun onDetachedFromWindow() { super.onDetachedFromWindow() - mWeatherViewController.disableUpdates() mWeatherViewController.removeObserver() } } diff --git a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt index 334829ee0d25..2603ab84fb41 100644 --- a/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/weather/WeatherViewController.kt @@ -38,6 +38,7 @@ class WeatherViewController( private val weatherIcon: ImageView, private val weatherTemp: TextView, private val weatherInfoView: View, + private val isCustomClock: Boolean = false, ) : OmniJawsClient.OmniJawsObserver { private var weatherInfo: OmniJawsClient.WeatherInfo? = null @@ -91,11 +92,11 @@ class WeatherViewController( private fun getWeatherSettings() = WeatherSettings( weatherEnabled = getSystemSetting(LOCKSCREEN_WEATHER_ENABLED), - clockFaceEnabled = getSecureSetting(CLOCK_STYLE), showWeatherLocation = getSystemSetting(LOCKSCREEN_WEATHER_LOCATION), showWeatherText = getSystemSetting(LOCKSCREEN_WEATHER_TEXT, defaultValue = 1), showWindInfo = getSystemSetting(LOCKSCREEN_WEATHER_WIND_INFO), - showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO) + showHumidityInfo = getSystemSetting(LOCKSCREEN_WEATHER_HUMIDITY_INFO), + customClockWeather = getSecureSetting(CUSTOM_CLOCK_WEATHER, defaultValue = 1), ) private fun getSystemSetting(setting: String, defaultValue: Int = 0) = @@ -112,14 +113,6 @@ class WeatherViewController( OmniJawsClient.get().addObserver(context, this@WeatherViewController) updateWeather() showAllViews() - // When a clock face style is active, hide the default weather views - // so they don't overlap the clock face layout (mirrors risingOS behaviour). - if (weatherIcon.id == R.id.default_weather_image) { - scope.launch { updateViewVisibility(weatherIcon, !settings.clockFaceEnabled) } - } - if (weatherTemp.id == R.id.default_weather_text) { - scope.launch { updateViewVisibility(weatherTemp, !settings.clockFaceEnabled) } - } } } @@ -153,6 +146,8 @@ class WeatherViewController( weatherTemp.isSelected = true } } catch (e: Exception) {} + + applyElementVisibility(weatherSettingsFlow.value) } private fun hideAllViews() { @@ -168,6 +163,18 @@ class WeatherViewController( listOf(weatherInfoView, weatherIcon, weatherTemp).forEach { updateViewVisibility(it, true) } + applyElementVisibility(weatherSettingsFlow.value) + } + } + + private fun applyElementVisibility(settings: WeatherSettings) { + scope.launch { + val show = settings.weatherEnabled && ( + if (isCustomClock) settings.customClockWeather + else !settings.customClockWeather + ) + updateViewVisibility(weatherIcon, show) + updateViewVisibility(weatherTemp, show) } } @@ -206,11 +213,11 @@ class WeatherViewController( data class WeatherSettings( val weatherEnabled: Boolean, - val clockFaceEnabled: Boolean, val showWeatherLocation: Boolean, val showWeatherText: Boolean, val showWindInfo: Boolean, - val showHumidityInfo: Boolean + val showHumidityInfo: Boolean, + val customClockWeather: Boolean, ) companion object { @@ -219,7 +226,7 @@ class WeatherViewController( 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 const val CLOCK_STYLE = "clock_style" + const val CUSTOM_CLOCK_WEATHER = "custom_clock_weather" private val WEATHER_CONDITIONS = mapOf( "clouds" to R.string.weather_condition_clouds, From 85ac4a654a532e6a7934edafe10bc6e7dd6827a8 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Fri, 27 Feb 2026 07:58:30 +0000 Subject: [PATCH 131/190] SystemUI: Refactor clock and widget implementation Signed-off-by: Ghosuto --- .../android/systemui/clocks/ClockStyle.java | 67 +++++++++++++------ .../repository/KeyguardClockRepository.kt | 5 ++ .../view/layout/sections/AODStyleSection.kt | 34 ++-------- .../sections/KeyguardClockStyleSection.kt | 26 +++++-- .../sections/KeyguardWeatherViewSection.kt | 8 ++- .../sections/KeyguardWidgetViewSection.kt | 27 ++------ .../LockScreenWidgetsController.java | 22 ++++-- 7 files changed, 106 insertions(+), 83 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index a9b488546501..dabb9342b613 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -25,8 +25,10 @@ import android.provider.Settings; import android.util.AttributeSet; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewParent; import android.view.ViewStub; import android.widget.LinearLayout; import android.widget.RelativeLayout; @@ -77,6 +79,9 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private final TunerService mTunerService; private View currentClockView; + private ViewStub mClockStub; + private ViewGroup mClockContainer; + private int mClockStyle; private boolean mUseAccentColor = false; private int mClockOpacity = DEFAULT_OPACITY; @@ -95,6 +100,8 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private int mCurrentShiftX = 0; private int mCurrentShiftY = 0; + private boolean mCallbacksRegistered = false; + private final BroadcastReceiver mScreenReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -145,31 +152,43 @@ public ClockStyle(Context context, AttributeSet attrs) { mContext = context; mKeyguardManager = (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); mTunerService = Dependency.get(TunerService.class); - mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); mStatusBarStateController = Dependency.get(StatusBarStateController.class); - mStatusBarStateController.addCallback(mStatusBarStateListener); - mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_SCREEN_ON); - filter.addAction(Intent.ACTION_TIME_TICK); - filter.addAction(Intent.ACTION_TIME_CHANGED); - filter.addAction("com.android.systemui.doze.pulse"); - mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); } @Override protected void onFinishInflate() { super.onFinishInflate(); + mClockStub = findViewById(R.id.clock_view_stub); updateClockView(); } - + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (!mCallbacksRegistered) { + mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, CLOCK_TEXT_OPACITY_KEY); + mStatusBarStateController.addCallback(mStatusBarStateListener); + mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_ON); + filter.addAction(Intent.ACTION_TIME_TICK); + filter.addAction(Intent.ACTION_TIME_CHANGED); + filter.addAction("com.android.systemui.doze.pulse"); + mContext.registerReceiver(mScreenReceiver, filter, Context.RECEIVER_EXPORTED); + mCallbacksRegistered = true; + } + } + @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mStatusBarStateController.removeCallback(mStatusBarStateListener); - mTunerService.removeTunable(this); - mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); - mContext.unregisterReceiver(mScreenReceiver); + if (mCallbacksRegistered) { + mStatusBarStateController.removeCallback(mStatusBarStateListener); + mTunerService.removeTunable(this); + mBurnInProtectionHandler.removeCallbacks(mBurnInProtectionRunnable); + mContext.unregisterReceiver(mScreenReceiver); + mCallbacksRegistered = false; + } } private void startBurnInProtection() { @@ -253,14 +272,24 @@ private void updateTextClockColor(View view) { private void updateClockView() { if (currentClockView != null) { - ((ViewGroup) currentClockView.getParent()).removeView(currentClockView); + ViewParent parent = currentClockView.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(currentClockView); + } currentClockView = null; } if (mClockStyle > 0 && mClockStyle < CLOCK_LAYOUTS.length) { - ViewStub stub = findViewById(R.id.clock_view_stub); - if (stub != null) { - stub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); - currentClockView = stub.inflate(); + if (mClockStub != null) { + mClockStub.setLayoutResource(CLOCK_LAYOUTS[mClockStyle]); + currentClockView = mClockStub.inflate(); + mClockContainer = (ViewGroup) currentClockView.getParent(); + mClockStub = null; + } else if (mClockContainer != null) { + currentClockView = LayoutInflater.from(mContext) + .inflate(CLOCK_LAYOUTS[mClockStyle], mClockContainer, false); + mClockContainer.addView(currentClockView); + } + if (currentClockView != null) { int gravity = isCenterClock(mClockStyle) ? Gravity.CENTER : Gravity.START; if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); 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 bc9bdde3ff24..816b003cd53d 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 @@ -184,6 +184,11 @@ constructor( UserHandle.USER_CURRENT, ) ) + val isDoubleLineClock = secureSettings.getIntForUser( + Settings.Secure.LOCKSCREEN_USE_DOUBLE_LINE_CLOCK, + 1, // Default value + UserHandle.USER_CURRENT + ) val clockStyleEnabled = secureSettings.getIntForUser( "clock_style", 0, // Default value diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt index 6681e9cd2247..c44fd2beaee0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AODStyleSection.kt @@ -111,34 +111,14 @@ constructor( ) } - // Apply consistent top margin to clock_ls whether AOD is visible or not if (constraintSet.getConstraint(R.id.clock_ls) != null) { - // If AOD is present, position clock below it - // If AOD is hidden/gone, ensure clock still has proper top margin - val clockTopMargin = if (aodStyleView?.visibility == View.VISIBLE) { - 8 // Small gap below AOD - } else { - topMargin // Same margin as AOD would have had - } - - if (aodStyleView?.visibility == View.VISIBLE) { - connect( - R.id.clock_ls, - ConstraintSet.TOP, - R.id.aod_ls, - ConstraintSet.BOTTOM, - clockTopMargin - ) - } else { - // When AOD is hidden, position clock at top with proper margin - connect( - R.id.clock_ls, - ConstraintSet.TOP, - ConstraintSet.PARENT_ID, - ConstraintSet.TOP, - clockTopMargin - ) - } + connect( + R.id.clock_ls, + ConstraintSet.TOP, + R.id.aod_ls, + ConstraintSet.BOTTOM, + 8 + ) } // Update the barrier to include AOD style for proper notification positioning diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt index 8402a9066dc0..bb22a0367296 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/KeyguardClockStyleSection.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.os.UserHandle +import android.util.Log import android.view.View import android.view.ViewGroup import androidx.constraintlayout.widget.Barrier @@ -37,13 +38,18 @@ constructor( private var clockStyleView: ClockStyle? = null private var isCustomClockEnabled: Boolean = false + + private val TAG = "KeyguardClockStyleSection" override fun addViews(constraintLayout: ConstraintLayout) { - - val clockStyle = secureSettings.getIntForUser( - ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT - ) - isCustomClockEnabled = clockStyle != 0 + isCustomClockEnabled = try { + val clockStyle = secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) + clockStyle != 0 + } catch (e: Exception) { + false + } if (!isCustomClockEnabled) return @@ -73,7 +79,15 @@ constructor( } override fun applyConstraints(constraintSet: ConstraintSet) { - if (!isCustomClockEnabled) return + val currentlyEnabled = try { + secureSettings.getIntForUser( + ClockStyle.CLOCK_STYLE_KEY, 0, UserHandle.USER_CURRENT + ) != 0 + } catch (e: Exception) { + isCustomClockEnabled + } + + if (!currentlyEnabled) return constraintSet.apply { // Clock positioning - TOP of hierarchy 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 index 4a43208d64d6..3e3319dccae5 100644 --- 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 @@ -42,9 +42,11 @@ class KeyguardWeatherViewSection @Inject constructor( if (weatherContainer != null) { weatherImageView = weatherContainer.findViewById(R.id.default_weather_image) weatherTextView = weatherContainer.findViewById(R.id.default_weather_text) - - (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) - constraintLayout.addView(weatherContainer) + + if (weatherContainer.parent !== constraintLayout) { + (weatherContainer.parent as? ViewGroup)?.removeView(weatherContainer) + constraintLayout.addView(weatherContainer) + } } else { createWeatherViews(constraintLayout) } 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 index 0e26d298d258..4a46d9e1e65b 100644 --- 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 @@ -160,9 +160,7 @@ constructor( var positioned = false for (anchorId in anchorViews) { - try { - // Check if the constraint exists by trying to get it - constraintSet.getConstraint(anchorId) + if (constraintSet.getConstraint(anchorId) != null) { connect( R.id.keyguard_widgets, ConstraintSet.TOP, @@ -173,9 +171,6 @@ constructor( positioned = true Log.d(TAG, "Positioned widgets below anchor: ${context.resources.getResourceEntryName(anchorId)}") break - } catch (e: Exception) { - // Continue to next anchor if this one fails - continue } } @@ -204,7 +199,6 @@ constructor( // Update barrier to include widgets try { - val barrierViews = mutableListOf() val potentialBarrierViews = listOf( R.id.keyguard_slice_view, R.id.keyguard_weather, @@ -212,29 +206,22 @@ constructor( R.id.keyguard_info_widgets, R.id.keyguard_widgets ) - - potentialBarrierViews.forEach { viewId -> - try { - constraintSet.getConstraint(viewId) - barrierViews.add(viewId) - } catch (e: Exception) { - // Skip this view if it doesn't exist - } - } + val barrierViews = potentialBarrierViews + .filter { constraintSet.getConstraint(it) != null } + .toIntArray() if (barrierViews.isNotEmpty()) { createBarrier( R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, - *barrierViews.toIntArray() + *barrierViews ) Log.d(TAG, "Created barrier with ${barrierViews.size} views") } // Position notification icons below barrier - try { - constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) + if (constraintSet.getConstraint(R.id.left_aligned_notification_icon_container) != null) { connect( R.id.left_aligned_notification_icon_container, ConstraintSet.TOP, @@ -246,8 +233,6 @@ constructor( 8 // fallback margin } ) - } catch (e: Exception) { - // Notification container doesn't exist, skip } } catch (e: Exception) { Log.w(TAG, "Failed to create barrier", e) diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index e35581bf313e..8f5e45198acb 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -33,6 +33,7 @@ import android.media.session.MediaSessionLegacyHelper; import android.os.Bundle; import android.os.Handler; +import android.os.Looper; import android.os.SystemClock; import android.view.GestureDetector; import android.view.MotionEvent; @@ -198,7 +199,7 @@ public void onThemeChanged() { }; private final View mView; - private final Handler mHandler = new Handler(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); public LockScreenWidgetsController(View view) { mView = view; @@ -218,7 +219,6 @@ public LockScreenWidgetsController(View view) { mActivityLauncherUtils = new ActivityLauncherUtils(mContext); mLockscreenWidgetsObserver = new LockscreenWidgetsObserver(); - mLockscreenWidgetsObserver.observe(); mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE); mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE); @@ -231,9 +231,12 @@ public LockScreenWidgetsController(View view) { } try { - mCameraId = mCameraManager.getCameraIdList()[0]; + String[] cameraIds = mCameraManager.getCameraIdList(); + if (cameraIds != null && cameraIds.length > 0) { + mCameraId = cameraIds[0]; + } } catch (Exception e) {} - + IntentFilter ringerFilter = new IntentFilter(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION); mContext.registerReceiver(mRingerModeReceiver, ringerFilter); } @@ -302,6 +305,7 @@ public void registerCallbacks() { mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); mMediaSessionManagerHelper.addMediaMetadataListener(this); + mLockscreenWidgetsObserver.observe(); updateWidgetViews(); updateMediaPlaybackState(); } @@ -327,7 +331,10 @@ public void unregisterCallbacks() { } mConfigurationController.removeCallback(mConfigurationListener); mStatusBarStateController.removeCallback(mStatusBarStateListener); - mContext.unregisterReceiver(mRingerModeReceiver); + try { + mContext.unregisterReceiver(mRingerModeReceiver); + } catch (IllegalArgumentException e) { + } mLockscreenWidgetsObserver.unobserve(); mHandler.removeCallbacksAndMessages(null); mMediaSessionManagerHelper.removeMediaMetadataListener(this); @@ -702,7 +709,8 @@ private void toggleMediaPlaybackState() { private void showMediaDialog(View view) { String lastMediaPkg = getLastUsedMedia(); - if (TextUtils.isEmpty(lastMediaPkg)) return; // Return if null or empty + if (TextUtils.isEmpty(lastMediaPkg)) return; + if (!(mView instanceof LockScreenWidgets)) return; mHandler.post(() -> { ((LockScreenWidgets) mView).showMediaDialog(view, lastMediaPkg); VibrationUtils.triggerVibration(mContext, 2); // Trigger vibration @@ -1050,7 +1058,7 @@ public void onPlaybackStateChanged() { private class LockscreenWidgetsObserver extends ContentObserver { public LockscreenWidgetsObserver() { - super(null); + super(new Handler(Looper.getMainLooper())); } @Override public void onChange(boolean selfChange) { From e48fb6d0735f7d6c3e15c50bd8f4d62efac08f51 Mon Sep 17 00:00:00 2001 From: tejas101k Date: Fri, 27 Feb 2026 12:24:59 +0000 Subject: [PATCH 132/190] SystemUI: Allow adjust height of lockscreen clock styles [1/2] Signed-off-by: Ghosuto --- .../res-keyguard/layout/keyguard_clock_a9.xml | 1 + .../layout/keyguard_clock_accent.xml | 1 + .../layout/keyguard_clock_analog.xml | 1 + .../layout/keyguard_clock_center.xml | 1 + .../layout/keyguard_clock_encode.xml | 1 + .../layout/keyguard_clock_ide.xml | 1 + .../layout/keyguard_clock_ios.xml | 1 + .../layout/keyguard_clock_label.xml | 1 + .../layout/keyguard_clock_life.xml | 1 + .../layout/keyguard_clock_miui.xml | 3 ++- .../layout/keyguard_clock_mont.xml | 1 + .../layout/keyguard_clock_moto.xml | 1 + .../layout/keyguard_clock_nos1.xml | 1 + .../layout/keyguard_clock_nos2.xml | 1 + .../layout/keyguard_clock_nos3.xml | 1 + .../layout/keyguard_clock_num.xml | 1 + .../layout/keyguard_clock_oos.xml | 1 + .../layout/keyguard_clock_simple.xml | 1 + .../layout/keyguard_clock_taden.xml | 1 + .../android/systemui/clocks/ClockStyle.java | 26 ++++++++++++++++--- 20 files changed, 43 insertions(+), 4 deletions(-) diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml index 2d093c0d9fa4..ae8523cb5091 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_a9.xml @@ -1,5 +1,6 @@ diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml index e9b816442596..589fc86a7a51 100644 --- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml +++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_ios.xml @@ -1,6 +1,7 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/clock_frame"> Date: Fri, 27 Feb 2026 12:47:55 +0000 Subject: [PATCH 133/190] SystemUI: Add support custom clock color [1/2] Signed-off-by: Ghosuto --- .../android/systemui/clocks/ClockStyle.java | 117 +++++++++++------- 1 file changed, 71 insertions(+), 46 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 3bfcf41c3eb3..143ebd79013c 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Color; import android.os.Handler; import android.os.UserHandle; import android.provider.Settings; @@ -67,14 +68,20 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private final static int[] mCenterClocks = {2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}; - private static final int DEFAULT_STYLE = 0; // Disabled public static final String CLOCK_STYLE_KEY = "clock_style"; - public static final String CLOCK_TEXT_COLOR_KEY = "clock_text_accent_color"; + public static final String CLOCK_COLOR_MODE_KEY = "clock_color_mode"; + public static final String CLOCK_CUSTOM_COLOR_KEY = "clock_custom_color"; public static final String CLOCK_TEXT_OPACITY_KEY = "clock_text_opacity"; public static final String CLOCK_FRAME_MARGIN_TOP_KEY = "custom_clock_frame_margin_top"; + public static final String COLOR_MODE_DEFAULT = "default"; + public static final String COLOR_MODE_ACCENT = "accent"; + public static final String COLOR_MODE_CUSTOM = "custom"; + + private static final int DEFAULT_STYLE = 0; // Disabled private static final int DEFAULT_OPACITY = 100; private static final int DEFAULT_MARGIN_TOP = 15; + private static final int DEFAULT_CUSTOM_COLOR = Color.WHITE; private final Context mContext; private final KeyguardManager mKeyguardManager; @@ -84,8 +91,9 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private ViewStub mClockStub; private ViewGroup mClockContainer; - private int mClockStyle; - private boolean mUseAccentColor = false; + private int mClockStyle; + private String mColorMode = COLOR_MODE_DEFAULT; + private int mCustomColor = DEFAULT_CUSTOM_COLOR; private int mClockOpacity = DEFAULT_OPACITY; private int mClockFrameMarginTop = DEFAULT_MARGIN_TOP; @@ -169,8 +177,12 @@ protected void onFinishInflate() { protected void onAttachedToWindow() { super.onAttachedToWindow(); if (!mCallbacksRegistered) { - mTunerService.addTunable(this, CLOCK_STYLE_KEY, CLOCK_TEXT_COLOR_KEY, - CLOCK_TEXT_OPACITY_KEY, CLOCK_FRAME_MARGIN_TOP_KEY); + mTunerService.addTunable(this, + CLOCK_STYLE_KEY, + CLOCK_COLOR_MODE_KEY, + CLOCK_CUSTOM_COLOR_KEY, + CLOCK_TEXT_OPACITY_KEY, + CLOCK_FRAME_MARGIN_TOP_KEY); mStatusBarStateController.addCallback(mStatusBarStateListener); mStatusBarStateListener.onDozingChanged(mStatusBarStateController.isDozing()); IntentFilter filter = new IntentFilter(); @@ -232,19 +244,23 @@ public void onTimeChanged() { } } - private void updateClockTextColor() { - if (currentClockView != null) { - updateTextClockColor(currentClockView); + private int resolveClockColor() { + switch (mColorMode) { + case COLOR_MODE_ACCENT: + return mContext.getColor( + mContext.getResources().getIdentifier( + "system_accent1_100", "color", "android")); + case COLOR_MODE_CUSTOM: + return mCustomColor; + case COLOR_MODE_DEFAULT: + default: + return mContext.getColor(android.R.color.white); } } - private void updateClockFrameMargin() { - RelativeLayout clockFrame = findViewById(R.id.clock_frame); - if (clockFrame != null) { - ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) clockFrame.getLayoutParams(); - int marginPx = (int) (mClockFrameMarginTop * mContext.getResources().getDisplayMetrics().density); - params.topMargin = marginPx; - clockFrame.setLayoutParams(params); + private void updateClockTextColor() { + if (currentClockView != null) { + updateTextClockColor(currentClockView); } } @@ -252,35 +268,37 @@ private void updateTextClockColor(View view) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { - View childView = viewGroup.getChildAt(i); - updateTextClockColor(childView); + updateTextClockColor(viewGroup.getChildAt(i)); } } - if (view instanceof TextClock) { - TextClock textClock = (TextClock) view; - - if (textClock.getTag(R.id.original_text_color) == null) { - int currentColor = textClock.getCurrentTextColor(); - textClock.setTag(R.id.original_text_color, currentColor); - } - - int originalColor = (Integer) textClock.getTag(R.id.original_text_color); - int whiteColor = mContext.getColor(android.R.color.white); - - if ((originalColor & 0x00FFFFFF) == (whiteColor & 0x00FFFFFF)) { - int color; - if (mUseAccentColor) { - color = mContext.getColor( - mContext.getResources().getIdentifier( - "system_accent1_100", "color", "android")); - } else { - color = mContext.getColor(android.R.color.white); - } - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); - } + if (!(view instanceof TextClock)) return; + TextClock textClock = (TextClock) view; + + if (textClock.getTag(R.id.original_text_color) == null) { + textClock.setTag(R.id.original_text_color, textClock.getCurrentTextColor()); + } + + int originalColor = (Integer) textClock.getTag(R.id.original_text_color); + int whiteColor = mContext.getColor(android.R.color.white); + + if ((originalColor & 0x00FFFFFF) != (whiteColor & 0x00FFFFFF)) return; + + int color = resolveClockColor(); + int alpha = Math.round((mClockOpacity / 100f) * 255); + color = (color & 0x00FFFFFF) | (alpha << 24); + textClock.setTextColor(color); + } + + private void updateClockFrameMargin() { + View clockFrame = findViewById(R.id.clock_frame); + if (clockFrame != null) { + ViewGroup.MarginLayoutParams params = + (ViewGroup.MarginLayoutParams) clockFrame.getLayoutParams(); + int marginPx = (int) (mClockFrameMarginTop + * mContext.getResources().getDisplayMetrics().density); + params.topMargin = marginPx; + clockFrame.setLayoutParams(params); } } @@ -322,15 +340,22 @@ public void onTuningChanged(String key, String newValue) { case CLOCK_STYLE_KEY: mClockStyle = TunerService.parseInteger(newValue, DEFAULT_STYLE); if (mClockStyle != 0) { - Settings.Secure.putIntForUser(mContext.getContentResolver(), - Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, UserHandle.USER_CURRENT); + Settings.Secure.putIntForUser(mContext.getContentResolver(), + Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, 0, + UserHandle.USER_CURRENT); } updateClockView(); break; - case CLOCK_TEXT_COLOR_KEY: - mUseAccentColor = TunerService.parseIntegerSwitch(newValue, false); + case CLOCK_COLOR_MODE_KEY: + mColorMode = (newValue != null) ? newValue : COLOR_MODE_DEFAULT; updateClockTextColor(); break; + case CLOCK_CUSTOM_COLOR_KEY: + mCustomColor = TunerService.parseInteger(newValue, DEFAULT_CUSTOM_COLOR); + if (COLOR_MODE_CUSTOM.equals(mColorMode)) { + updateClockTextColor(); + } + break; case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); // Keep opacity within valid range (0-100) From 0c4909e1d5bf0a22691ec2d4405da9a39d619de0 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Wed, 25 Feb 2026 21:16:11 +0530 Subject: [PATCH 134/190] SystemUI: Add QS tile gradient customization Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 6 ++ .../ui/compose/infinitegrid/CommonTile.kt | 9 +- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 100 ++++++++++++++++-- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 68c1198eefe0..892431779f05 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7828,6 +7828,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_FOOTER_SHOW_POWER_MENU = "qs_footer_show_power_menu"; + /** + * Gradient on QS tiles + * @hide + */ + public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index d25fc33befc3..196b6627120e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -148,7 +148,14 @@ fun LargeTileContent( Modifier.borderOnFocus(color = focusBorderColor, iconShape.topEnd) .clip(iconShape) .verticalSquish(squishiness) - .drawBehind { drawRect(animatedBackgroundColor) } + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedBackgroundColor) + } + } .combinedClickable( onClick = toggleClick!!, onLongClick = onLongClick, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 96ba63d1b902..e7144d066ad3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -68,9 +68,13 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -350,7 +354,14 @@ fun ContentScope.Tile( .size(CommonTileDefaults.TileHeight) .align(Alignment.Center) .clip(CircleShape) - .background(animatedColor) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = animatedColor) + } + } .indication(interaction, LocalIndication.current) .tileCombinedClickable( onClick = { click?.invoke() ?: Unit }, @@ -382,6 +393,7 @@ fun ContentScope.Tile( iconOnly = iconOnly, isDualTarget = isDualTarget, modifier = contentRevealModifier, + colors = colors, ) { val iconProvider: Context.() -> Icon = { getTileIcon(icon = icon) } if (iconOnly) { @@ -473,6 +485,7 @@ fun TileContainer( isDualTarget: Boolean, interactionSource: MutableInteractionSource?, modifier: Modifier = Modifier, + colors: TileColors, content: @Composable BoxScope.() -> Unit, ) { Box( @@ -488,7 +501,16 @@ fun TileContainer( isDualTarget = isDualTarget, interactionSource = interactionSource, ) - .tileTestTag(iconOnly), + .tileTestTag(iconOnly) + .thenIf(!isDualTarget || iconOnly) { + Modifier + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } + } + }, content = content, ) } @@ -506,7 +528,14 @@ fun LargeStaticTile( Box( modifier .clip(TileDefaults.animateTileShapeAsState(state = uiState.state, shapeMode = shapeMode).value) - .background(colors.background) + .drawBehind { + val brush = colors.iconBackgroundGradient + if (brush != null) { + drawRect(brush = brush) + } else { + drawRect(color = colors.background) + } + } .height(TileHeight) .largeTilePadding() ) { @@ -578,6 +607,7 @@ data class TileColors( val label: Color, val secondaryLabel: Color, val icon: Color, + val iconBackgroundGradient: Brush? = null, ) @Composable @@ -740,27 +770,66 @@ fun rememberTileHaptic(): Boolean { return hapticEnabled } +@Composable +fun rememberQsGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_TILE_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_TILE_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return enabled +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp /** An active tile uses the active color as background */ @Composable - @ReadOnlyComposable - fun activeTileColors(): TileColors = - TileColors( + fun activeTileColors(): TileColors { + val gradientEnabled = rememberQsGradient() + val gradient = qsTileBackgroundBrush(gradientEnabled) + + return TileColors( background = MaterialTheme.colorScheme.primary, iconBackground = MaterialTheme.colorScheme.primary, label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) + } /** An active tile with dual target only show the active color on the icon */ @Composable - @ReadOnlyComposable fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) + val gradientEnabled = rememberQsGradient() + val gradient = qsTileBackgroundBrush(gradientEnabled) return if (isSingleToneStyle) { TileColors( @@ -769,6 +838,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) } else { TileColors( @@ -777,6 +847,7 @@ private object TileDefaults { label = MaterialTheme.colorScheme.onSurface, secondaryLabel = MaterialTheme.colorScheme.onSurface, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) } } @@ -854,7 +925,6 @@ private object TileDefaults { ) @Composable - @ReadOnlyComposable fun getColorForState( uiState: TileUiState, iconOnly: Boolean, @@ -932,6 +1002,20 @@ private object TileDefaults { mutableStateOf(RoundedCornerShape(corner)) } } + + @Composable + fun qsTileBackgroundBrush(enabled: Boolean): Brush? { + if (!enabled) return null + + return Brush.linearGradient( + colors = listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ), + start = Offset(0f, 0f), + end = Offset.Infinite + ) + } } /** From cd65fdd886fc2ac531d26c083b34e842c9f6a62f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Thu, 26 Feb 2026 23:04:11 +0530 Subject: [PATCH 135/190] SystemUI: Add volume slider gradient customization Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 6 + .../ui/compose/VolumeDialogSliderTrack.kt | 222 +++++++++++++++++- 2 files changed, 221 insertions(+), 7 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 892431779f05..03a1e6a0bd29 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7834,6 +7834,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + /** + * Gradient on Volume slider + * @hide + */ + public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt index 28557c854e1a..54b11f04a1a1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -16,29 +16,47 @@ package com.android.systemui.volume.dialog.sliders.ui.compose +import android.database.ContentObserver +import android.os.UserHandle +import android.provider.Settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SliderColors import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState +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.draw.drawBehind +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.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp @@ -77,21 +95,35 @@ fun SliderTrack( Layout( measurePolicy = measurePolicy, content = { - SliderDefaults.Track( + + val gradientEnabled = rememberVolumeGradientEnabled() + + val gStart = MaterialTheme.colorScheme.primary + val gEnd = MaterialTheme.colorScheme.secondary + + val activeBrush = + if (gradientEnabled && gStart != Color(0) && gEnd != Color(0)) { + listOf(gStart, gEnd) + } else { + null + } + + GradientSliderTrack( sliderState = sliderState, + isEnabled = isEnabled, colors = colors, - enabled = isEnabled, trackCornerSize = trackCornerSize, trackInsideCornerSize = trackInsideCornerSize, - drawStopIndicator = null, thumbTrackGapSize = thumbTrackGapSize, - drawTick = { _, _ -> }, + isVertical = isVertical, + gradientColors = activeBrush, modifier = - Modifier.then( + Modifier + .then( if (isVertical) { - Modifier.width(trackSize) + Modifier.width(trackSize).fillMaxHeight() } else { - Modifier.height(trackSize) + Modifier.height(trackSize).fillMaxWidth() } ) .layoutId(Contents.Track), @@ -130,6 +162,182 @@ fun SliderTrack( ) } +private data class TrackGradient( + val brush: Brush, +) + +@Composable +private fun rememberVolumeGradientEnabled(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean { + return try { + Settings.System.getIntForUser( + contentResolver, Settings.System.VOLUME_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { + false + } + } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.VOLUME_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return enabled +} + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun GradientSliderTrack( + sliderState: SliderState, + isEnabled: Boolean, + colors: SliderColors, + trackCornerSize: Dp, + trackInsideCornerSize: Dp, + thumbTrackGapSize: Dp, + isVertical: Boolean, + gradientColors: List?, + modifier: Modifier = Modifier, +) { + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl + + val inactiveColor = + if (isEnabled) colors.inactiveTrackColor else colors.disabledInactiveTrackColor + val activeSolidColor = + if (isEnabled) colors.activeTrackColor else colors.disabledActiveTrackColor + + Box( + modifier = + modifier.drawBehind { + val w = size.width + val h = size.height + if (w <= 0f || h <= 0f) return@drawBehind + + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val outerR = trackCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val innerR = trackInsideCornerSize.toPx().coerceAtMost(minOf(w, h) / 2f) + val halfGap = (thumbTrackGapSize.toPx() / 2f).coerceAtLeast(0f) + + drawRoundRect( + color = inactiveColor, + size = size, + cornerRadius = CornerRadius(outerR, outerR) + ) + + if (!isVertical) { + val splitX = if (isRtl) w * (1f - frac) else w * frac + + val gapEff = + if (isRtl) { + minOf(halfGap, splitX) + } else { + minOf(halfGap, w - splitX) + } + + val activeStart = if (isRtl) splitX + gapEff else 0f + val activeEnd = if (isRtl) w else splitX - gapEff + + if (activeEnd <= activeStart) return@drawBehind + + val eps = 0.5f + + val hitsStart = activeStart <= eps + val hitsEnd = activeEnd >= (w - eps) + + val leftR = + if (isRtl) { + if (hitsStart) outerR else innerR // rounded only at max (when it reaches start) + } else { + outerR // far end always rounded + } + + val rightR = + if (isRtl) { + outerR // far end always rounded + } else { + if (hitsEnd) outerR else innerR // rounded only at max (when it reaches end) + } + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(activeStart, 0f, activeEnd, h), + topLeft = CornerRadius(leftR, leftR), + bottomLeft = CornerRadius(leftR, leftR), + topRight = CornerRadius(rightR, rightR), + bottomRight = CornerRadius(rightR, rightR), + ) + ) + } + + val brush = + if (gradientColors != null) Brush.horizontalGradient(gradientColors) + else Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + + drawPath(path = path, brush = brush) + } else { + val frac = sliderState.coercedValueAsFraction.coerceIn(0f, 1f) + val splitY = h * (1f - frac) + + val gapEff = minOf(halfGap, splitY) + + val activeTop = splitY + gapEff + val activeBottom = h + if (activeBottom <= activeTop) return@drawBehind + + val eps = 0.5f + val hitsTop = activeTop <= eps + + val topR = if (hitsTop) outerR else innerR + val bottomR = outerR + + val path = Path().apply { + addRoundRect( + RoundRect( + rect = Rect(0f, activeTop, w, activeBottom), + topLeft = CornerRadius(topR, topR), + bottomLeft = CornerRadius(bottomR, bottomR), + topRight = CornerRadius(topR, topR), + bottomRight = CornerRadius(bottomR, bottomR), + ) + ) + } + + val brush = + if (gradientColors != null) { + Brush.verticalGradient( + colors = gradientColors, + startY = activeBottom, + endY = activeTop + ) + } else { + Brush.linearGradient(listOf(activeSolidColor, activeSolidColor)) + } + + drawPath(path = path, brush = brush) + } + } + ) +} + @Composable private fun TrackIcon( icon: (@Composable BoxScope.(sliderIconsState: SliderIconsState) -> Unit)?, From ab04f1f351e47a5b53e2798ebf668cb735e8ae50 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Fri, 27 Feb 2026 00:57:28 +0530 Subject: [PATCH 136/190] SystemUI: Add custom gradient start/end color support Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 18 +++ .../qs/panels/ui/compose/infinitegrid/Tile.kt | 101 +++++++++++++++- .../ui/VolumeDialogSliderViewBinder.kt | 19 ++- .../ui/compose/VolumeDialogSliderTrack.kt | 111 ++++++++++++++++-- 4 files changed, 236 insertions(+), 13 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 03a1e6a0bd29..188c657910d5 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7840,6 +7840,24 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String VOLUME_SLIDER_GRADIENT = "volume_slider_gradient"; + /** + * Gradient color mode + * @hide + */ + public static final String CUSTOM_GRADIENT_COLOR_MODE = "custom_gradient_color_mode"; + + /** + * Gradient start color + * @hide + */ + public static final String CUSTOM_GRADIENT_START_COLOR = "custom_gradient_start_color"; + + /** + * Gradient end color + * @hide + */ + public static final String CUSTOM_GRADIENT_END_COLOR = "custom_gradient_end_color"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index e7144d066ad3..3fa7f753d211 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -804,6 +804,93 @@ fun rememberQsGradient(): Boolean { return enabled } +@Composable +private fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +private fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + private object TileDefaults { val ActiveIconCornerRadius = 16.dp @@ -1007,11 +1094,19 @@ private object TileDefaults { fun qsTileBackgroundBrush(enabled: Boolean): Brush? { if (!enabled) return null - return Brush.linearGradient( - colors = listOf( + val mode = rememberGradientColorMode() + val colors = if (mode == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary - ), + ) + } + + return Brush.linearGradient( + colors = colors, start = Offset(0f, 0f), end = Offset.Infinite ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt index bb6b59c08e97..7bdffaee7302 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.LaunchedEffect 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.input.pointer.pointerInput import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext @@ -46,6 +47,9 @@ import com.android.systemui.res.R import com.android.systemui.volume.dialog.domain.interactor.DesktopAudioTileDetailsFeatureInteractor import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderScope import com.android.systemui.volume.dialog.sliders.ui.compose.SliderTrack +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientColorMode +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberGradientCustomColors +import com.android.systemui.volume.dialog.sliders.ui.compose.rememberVolumeGradientEnabled import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogOverscrollViewModel import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel import com.android.systemui.volume.haptics.ui.VolumeHapticsConfigsProvider @@ -128,6 +132,16 @@ private fun VolumeDialogSlider( } } + val thumbColorOverride: Color? = + if (!rememberVolumeGradientEnabled()) { + null + } else if (rememberGradientColorMode() == 1) { + val (customStart, _) = rememberGradientCustomColors() + customStart + } else { + MaterialTheme.colorScheme.primary + } + Slider( value = sliderStateModel.value, valueRange = sliderStateModel.valueRange, @@ -191,6 +205,7 @@ private fun VolumeDialogSlider( isVisible = iconsState.isInactiveTrackEndIconVisible, ) }, + ignoreGradient = false, ) }, thumb = { sliderState, interactions -> @@ -198,7 +213,9 @@ private fun VolumeDialogSlider( sliderState = sliderState, interactionSource = interactions, enabled = !sliderStateModel.isDisabled, - colors = colors, + colors = SliderDefaults.colors( + thumbColor = thumbColorOverride ?: SliderDefaults.colors().thumbColor + ), thumbSize = if (isVolumeDialogVertical) { DpSize(52.dp, 4.dp) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt index 54b11f04a1a1..c8234357c2eb 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/compose/VolumeDialogSliderTrack.kt @@ -39,6 +39,7 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -81,6 +82,7 @@ fun SliderTrack( activeTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackStartIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, inactiveTrackEndIcon: (@Composable BoxScope.(iconsState: SliderIconsState) -> Unit)? = null, + ignoreGradient: Boolean = true, ) { val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val measurePolicy = @@ -96,17 +98,21 @@ fun SliderTrack( measurePolicy = measurePolicy, content = { - val gradientEnabled = rememberVolumeGradientEnabled() - - val gStart = MaterialTheme.colorScheme.primary - val gEnd = MaterialTheme.colorScheme.secondary + val gradientColors = if (rememberGradientColorMode() == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } val activeBrush = - if (gradientEnabled && gStart != Color(0) && gEnd != Color(0)) { - listOf(gStart, gEnd) - } else { + if (!ignoreGradient && rememberVolumeGradientEnabled()) + gradientColors + else null - } GradientSliderTrack( sliderState = sliderState, @@ -167,7 +173,7 @@ private data class TrackGradient( ) @Composable -private fun rememberVolumeGradientEnabled(): Boolean { +fun rememberVolumeGradientEnabled(): Boolean { val context = LocalContext.current val contentResolver = context.contentResolver @@ -204,6 +210,93 @@ private fun rememberVolumeGradientEnabled(): Boolean { return enabled } +@Composable +fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + mode = readMode() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + return mode +} + +@Composable +fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { + 0 + } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + + onDispose { + contentResolver.unregisterContentObserver(observer) + } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + @Composable @OptIn(ExperimentalMaterial3Api::class) private fun GradientSliderTrack( From 1b2979f2be6a5f0df3a8cc41c694f41dc210f1eb Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sat, 28 Feb 2026 06:36:10 +0000 Subject: [PATCH 137/190] SystemUI: Add QS brightness slider gradient customization - Adapt for both default and minimal style Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 6 + .../brightness/ui/compose/BrightnessSlider.kt | 185 ++++++++++++++++-- 2 files changed, 180 insertions(+), 11 deletions(-) diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 188c657910d5..14662b2bde48 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7834,6 +7834,12 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String QS_TILE_GRADIENT = "qs_tile_gradient"; + /** + * Gradient on QS brightness slider + * @hide + */ + public static final String QS_BRIGHTNESS_SLIDER_GRADIENT = "qs_brightness_slider_gradient"; + /** * Gradient on Volume slider * @hide diff --git a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt index cf5d355252a2..df60421bd068 100644 --- a/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt +++ b/packages/SystemUI/src/com/android/systemui/brightness/ui/compose/BrightnessSlider.kt @@ -74,11 +74,15 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.painter.BitmapPainter @@ -171,6 +175,8 @@ fun BrightnessSlider( else -> Dimensions.SliderTrackRoundedCorner } + val brightnessGradient = brightnessSliderGradient() + var value by remember(gammaValue) { mutableIntStateOf(gammaValue) } val animatedValue by animateFloatAsState(targetValue = value.toFloat(), label = "BrightnessSliderAnimatedValue") @@ -244,8 +250,15 @@ fun BrightnessSlider( val axIconRes = if (autoMode) R.drawable.ic_qs_brightness_auto_on else iconRes val axIconSize = 56.dp val iconTapScope = rememberCoroutineScope() + val axGradientTrackColor: Color = if (brightnessGradient != null) { + val mode = rememberGradientColorMode() + if (mode == 1) rememberGradientCustomColors().first + else MaterialTheme.colorScheme.primary + } else { + CustomColorScheme.current.qsTileColor + } val sliderColors = PlatformSliderDefaults.defaultPlatformSliderColors().copy( - trackColor = CustomColorScheme.current.qsTileColor, + trackColor = axGradientTrackColor, ) Box(modifier = modifier) { @@ -318,7 +331,7 @@ fun BrightnessSlider( return } - val colors = colors() + val colors = colors(brightnessGradient) // The value state is recreated every time gammaValue changes, so we recreate this derivedState // We have to use value as that's the value that changes when the user is dragging (gammaValue @@ -469,12 +482,34 @@ fun BrightnessSlider( ) if (activeTrackEnd > 0f) { - drawRoundRect( - color = colors.activeTrackColor, - topLeft = Offset(0f, 0f), - size = Size(activeTrackEnd, trackHeight), - cornerRadius = cornerRadius - ) + val gradient = brightnessGradient + if (gradient != null) { + val clipPath = Path().apply { + addRoundRect( + RoundRect( + left = 0f, + top = 0f, + right = activeTrackEnd.coerceAtMost(trackWidth), + bottom = trackHeight, + cornerRadius = cornerRadius, + ) + ) + } + clipPath(clipPath) { + drawRect( + brush = gradient.brush, + topLeft = Offset.Zero, + size = Size(trackWidth, trackHeight), + ) + } + } else { + drawRoundRect( + color = colors.activeTrackColor, + topLeft = Offset(0f, 0f), + size = Size(activeTrackEnd, trackHeight), + cornerRadius = cornerRadius + ) + } } val yOffset = trackHeight / 2 - IconSize.toSize().height / 2 @@ -511,6 +546,7 @@ fun BrightnessSlider( drawAutoBrightnessButton( autoMode = autoMode, hapticsEnabled = hapticsEnabled, + brightnessGradient = brightnessGradient, onIconClick = onIconClick ) } @@ -568,6 +604,127 @@ fun rememberSliderShapeMode(): Int { return shapeMode } +private data class BrightnessGradient(val brush: Brush) + +@Composable +private fun rememberSliderGradient(): Boolean { + val context = LocalContext.current + val contentResolver = context.contentResolver + + fun readEnabled(): Boolean = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT, 0, + UserHandle.USER_CURRENT + ) != 0 + } catch (_: Throwable) { false } + + var enabled by remember { mutableStateOf(readEnabled()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + enabled = readEnabled() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.QS_BRIGHTNESS_SLIDER_GRADIENT), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return enabled +} + +@Composable +private fun rememberGradientColorMode(): Int { + val contentResolver = LocalContext.current.contentResolver + + fun readMode(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_COLOR_MODE, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + var mode by remember { mutableIntStateOf(readMode()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { mode = readMode() } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_COLOR_MODE), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + return mode +} + +@Composable +private fun rememberGradientCustomColors(): Pair { + val contentResolver = LocalContext.current.contentResolver + + fun readStart(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_START_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + fun readEnd(): Int = try { + Settings.System.getIntForUser( + contentResolver, Settings.System.CUSTOM_GRADIENT_END_COLOR, 0, + UserHandle.USER_CURRENT + ) + } catch (_: Throwable) { 0 } + + var startInt by remember { mutableIntStateOf(readStart()) } + var endInt by remember { mutableIntStateOf(readEnd()) } + + DisposableEffect(contentResolver) { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + startInt = readStart() + endInt = readEnd() + } + } + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_START_COLOR), + false, observer, UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(Settings.System.CUSTOM_GRADIENT_END_COLOR), + false, observer, UserHandle.USER_ALL + ) + onDispose { contentResolver.unregisterContentObserver(observer) } + } + + val start = if (startInt != 0) Color(startInt) else MaterialTheme.colorScheme.primary + val end = if (endInt != 0) Color(endInt) else MaterialTheme.colorScheme.secondary + return start to end +} + +@Composable +private fun brightnessSliderGradient(): BrightnessGradient? { + if (!rememberSliderGradient()) return null + + val mode = rememberGradientColorMode() + val colors = if (mode == 1) { + val (start, end) = rememberGradientCustomColors() + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary, + ) + } + + return BrightnessGradient(brush = Brush.horizontalGradient(colors)) +} + private fun Modifier.sliderBackground(color: Color, corner: Dp) = drawWithCache { val offsetAround = SliderBackgroundFrameSize.toSize() val newSize = Size(size.width + 2 * offsetAround.width, size.height + 2 * offsetAround.height) @@ -612,6 +769,7 @@ private fun readUseAxStyle(cr: ContentResolver): Boolean = private fun drawAutoBrightnessButton( autoMode: Boolean, hapticsEnabled: Boolean, + brightnessGradient: BrightnessGradient?, onIconClick: suspend () -> Unit, ) { val view = LocalView.current @@ -630,9 +788,10 @@ private fun drawAutoBrightnessButton( 3 -> RoundedCornerShape(0.dp) else -> RoundedCornerShape(animatedCornerRadius) } + val autoIconBrush: Brush? = if (autoMode) brightnessGradient?.brush else null val backgroundColor by animateColorAsState( targetValue = if (autoMode) { - MaterialTheme.colorScheme.primary + if (autoIconBrush == null) MaterialTheme.colorScheme.primary else Color.Unspecified } else { CustomColorScheme.current.qsTileColor } @@ -659,7 +818,10 @@ private fun drawAutoBrightnessButton( modifier = Modifier .size(TrackHeight) .clip(autoIconShape) - .background(backgroundColor) + .then( + if (autoIconBrush != null) Modifier.background(autoIconBrush) + else Modifier.background(backgroundColor) + ) .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, // Disable ripple effect @@ -833,9 +995,10 @@ object BrightnessSliderMotionTestKeys { } @Composable -private fun colors(): SliderColors { +private fun colors(brightnessGradient: BrightnessGradient?): SliderColors { return SliderDefaults.colors() .copy( + activeTrackColor = if (brightnessGradient != null) Color.Transparent else SliderDefaults.colors().activeTrackColor, inactiveTrackColor = CustomColorScheme.current.qsTileColor, activeTickColor = MaterialTheme.colorScheme.onPrimary, inactiveTickColor = MaterialTheme.colorScheme.onSurface, From c7819b3a70b3cb492a0694e3eaa28c09a906df95 Mon Sep 17 00:00:00 2001 From: Tommy Webb Date: Sun, 8 Feb 2026 10:37:20 -0500 Subject: [PATCH 138/190] Fix external storage access with system user locked Make adaptations that allow secondary users to access (their own) external storage even if the system user is locked, so that when the system is configured to allow this, it works properly. * Do not require the system user to be unlocked in order to initialize external storage; allow any full user (i.e. non-profile) to do so. Under typical circumstances, the system user will be unlocked first. AOSP appeared to rely on that idea - along with the idea that the system user would only be unlocked once - as a way to assure that system-wide access to external storage was initialized just once and at an appropriate time. However, if settings allow switching users when the system user is locked, then a different full user may unlock first, and that's a fine enough time to make external storage available, considering that such users need external storage, too. And we ensure it only (successfully) happens once by checking that the component name assigned during initialization has yet to be set. * Don't falsely report that volumes are unmounted just because the system user (user 0) is locked. Instead, check the calling user itself, or rather its parent if it is a profile. This AOSP behavior that we're altering was added as part of a legacy obb data migration, as a way to prevent listing volumes when that migration is still in progress. The volumes would be reported as unmounted during that time. That migration is specific to user 0 and has no effect on other users; so, if user 0 is locked, it's of no consequence to users outside of its profile group, since they cannot access its data by design. Test: Manual: Create a new user. `adb shell settings put global \ allow_user_switching_when_system_user_locked 1`. Ensure your main user has a PIN/password/pattern. Reboot. Switch to the new user without unlocking the main user. Open Files. With this change, you'll see folders. Without it, you'll see an error. Change-Id: Ie832a65758d763a12f61ccbb46331a3d98de7268 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../com/android/server/StorageManagerService.java | 12 ++++++++---- .../server/storage/StorageSessionController.java | 12 ++++++++---- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/services/core/java/com/android/server/StorageManagerService.java b/services/core/java/com/android/server/StorageManagerService.java index 7c005db76e79..aa9d658c7980 100644 --- a/services/core/java/com/android/server/StorageManagerService.java +++ b/services/core/java/com/android/server/StorageManagerService.java @@ -3905,18 +3905,22 @@ public StorageVolume[] getVolumeList(int userId, String callingPackage, int flag } } - // Report all volumes as unmounted until we've recorded that user 0 has unlocked. There + // Report all volumes as unmounted until we've recorded that the parent has unlocked. There // are no guarantees that callers will see a consistent view of the volume before that // point - final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM); + final boolean systemUserUnlocked; final boolean userIsDemo; final boolean storagePermission; final boolean ceStorageUnlocked; final long token = Binder.clearCallingIdentity(); try { - userIsDemo = LocalServices.getService(UserManagerInternal.class) - .getUserInfo(userId).isDemo(); + final UserInfo userInfo = LocalServices.getService(UserManagerInternal.class) + .getUserInfo(userId); + final int parentUserId = userInfo.profileGroupId != UserInfo.NO_PROFILE_GROUP_ID + ? userInfo.profileGroupId : userId; + systemUserUnlocked = isSystemUnlocked(parentUserId); + userIsDemo = userInfo.isDemo(); storagePermission = mStorageManagerInternal.hasExternalStorage(callingUid, callingPackage); ceStorageUnlocked = isCeStorageUnlocked(userId); diff --git a/services/core/java/com/android/server/storage/StorageSessionController.java b/services/core/java/com/android/server/storage/StorageSessionController.java index 281aeb68f224..f1551fc68445 100644 --- a/services/core/java/com/android/server/storage/StorageSessionController.java +++ b/services/core/java/com/android/server/storage/StorageSessionController.java @@ -272,8 +272,11 @@ public void onVolumeUnmount(ImmutableVolumeInfo vol) { */ public void onUnlockUser(int userId) throws ExternalStorageServiceException { Slog.i(TAG, "On user unlock " + userId); - if (userId == 0) { - initExternalStorageServiceComponent(); + if (mExternalStorageServiceComponent == null) { + final UserInfo info = mUserManager.getUserInfo(userId); + if (info != null && info.isFull()) { + initExternalStorageServiceComponent(userId); + } } } @@ -358,7 +361,8 @@ public void onReset(IVold vold, Runnable resetHandlerRunnable) { } } - private void initExternalStorageServiceComponent() throws ExternalStorageServiceException { + private void initExternalStorageServiceComponent(int userId) + throws ExternalStorageServiceException { Slog.i(TAG, "Initialialising..."); ProviderInfo provider = mContext.getPackageManager().resolveContentProvider( MediaStore.AUTHORITY, PackageManager.MATCH_DIRECT_BOOT_AWARE @@ -371,7 +375,7 @@ private void initExternalStorageServiceComponent() throws ExternalStorageService mExternalStorageServicePackageName = provider.applicationInfo.packageName; mExternalStorageServiceAppId = UserHandle.getAppId(provider.applicationInfo.uid); - ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(UserHandle.USER_SYSTEM); + ServiceInfo serviceInfo = resolveExternalStorageServiceAsUser(userId); if (serviceInfo == null) { throw new ExternalStorageServiceException( "No valid ExternalStorageService component found"); From 22ac9d578ac87b4cbf05b17c9833c22be764d579 Mon Sep 17 00:00:00 2001 From: Tommy Webb Date: Wed, 25 Jan 2023 13:25:04 -0500 Subject: [PATCH 139/190] Fix secondary user crash with system user locked Resolve an incorrect exception causing a crash to boot animation when unlocking secondary users, if Backup Manager is enabled for the user and the system user has yet to be unlocked: `java.lang.IllegalStateException: SharedPreferences in credential encrypted storage are not available until after user is unlocked` Provide the user's context for creating UserBackupPreferences, and use this context's userId when determining whether credential encrypted storage is available or not, when calling Context#getSharedPreferences. Test: Manual: Set a screen lock for system user. Turn on Settings > Multiple Users and create a secondary user. Run: `adb shell settings put global allow_user_switching_when_system_user_locked 1`. Enable Backup Manager for the user, e.g. `adb shell bmgr --user 10 enable true` (if the user is id 10; see `adb shell pm list users`). Reboot. Switch to the secondary user before unlocking system. Device no longer crashes to boot animation. Issue: calyxos#1352 Co-authored-by: Oliver Scott Change-Id: I8354307d10cafe12f556ac13a38122d53399c1d8 Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/app/ContextImpl.java | 2 +- .../com/android/server/backup/UserBackupManagerService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index a5aff241237d..37953653117c 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -652,7 +652,7 @@ public SharedPreferences getSharedPreferences(File file, int mode) { + "if UserManager is not available. " + "(e.g. from inside an isolated process)"); } - if (!um.isUserUnlockingOrUnlocked(UserHandle.myUserId())) { + if (!um.isUserUnlockingOrUnlocked(getUserId())) { throw new IllegalStateException("SharedPreferences in " + "credential encrypted storage are not available until after " + "user (id " + UserHandle.myUserId() + ") is unlocked"); diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index dcae8edd80ad..a91b4cb1cc7f 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -605,7 +605,7 @@ private UserBackupManagerService( backupManagerMonitorDumpsysUtils::deleteExpiredBMMEvents, INITIALIZATION_DELAY_MILLIS); - mBackupPreferences = new UserBackupPreferences(mContext, mBaseStateDir); + mBackupPreferences = new UserBackupPreferences(userContext, mBaseStateDir); // Power management mWakelock = From 7a75ebbbf2a4f6d1874528fe745e241482b7bae3 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 08:42:53 +0000 Subject: [PATCH 140/190] SystemUI: Use stroke-only widget backgrounds for aod Signed-off-by: Ghosuto --- ...ockscreen_widget_background_circle_aod.xml | 9 ++ ...ockscreen_widget_background_square_aod.xml | 9 ++ .../LockScreenWidgetsController.java | 97 ++++++++++++++++--- 3 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml create mode 100644 packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml new file mode 100644 index 000000000000..99d14ff6bde4 --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_circle_aod.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml b/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml new file mode 100644 index 000000000000..4262b73eeff7 --- /dev/null +++ b/packages/SystemUI/res/drawable/lockscreen_widget_background_square_aod.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java index 8f5e45198acb..13e03ec9e2fe 100644 --- a/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java +++ b/packages/SystemUI/src/com/android/systemui/lockscreen/LockScreenWidgetsController.java @@ -251,6 +251,7 @@ public void onDozingChanged(boolean dozing) { return; } mDozing = dozing; + updateWidgetViews(); updateContainerVisibility(); } }; @@ -422,8 +423,41 @@ public void updateWidgetViews() { private void updateMainWidgetResources(LaunchableFAB efab, boolean active) { if (efab == null) return; efab.setElevation(0); + + if (mDozing) { + int bgRes; + switch (mThemeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square_aod; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle_aod; + break; + } + efab.setBackgroundTintList(null); + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } else { + int bgRes; + switch (mThemeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + } + efab.setBackgroundDrawable(mContext.getDrawable(bgRes)); + } + setButtonActiveState(null, efab, false); - long visibleWidgetCount = mMainWidgetsList.stream().filter(widget -> !"none".equals(widget)).count(); + long visibleWidgetCount = mMainWidgetsList.stream() + .filter(widget -> !"none".equals(widget)).count(); ViewGroup.LayoutParams params = efab.getLayoutParams(); if (params instanceof LinearLayout.LayoutParams) { LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) params; @@ -466,7 +500,7 @@ private void updateContainerVisibility() { if (secondaryWidgetsContainer != null) { secondaryWidgetsContainer.setVisibility(isSecondaryWidgetsEmpty ? View.GONE : View.VISIBLE); } - final boolean shouldHideContainer = isEmpty || mDozing || !mLockscreenWidgetsEnabled; + final boolean shouldHideContainer = isEmpty || !mLockscreenWidgetsEnabled; mView.setVisibility(shouldHideContainer ? View.GONE : View.VISIBLE); } @@ -474,16 +508,30 @@ private void updateWidgetsResources(LaunchableImageView iv) { if (iv == null) return; final int themeStyle = mThemeStyle; int bgRes; - switch (themeStyle) { - case 0: - case 3: - default: - bgRes = R.drawable.lockscreen_widget_background_circle; - break; - case 1: - case 2: - bgRes = R.drawable.lockscreen_widget_background_square; - break; + if (mDozing) { + switch (themeStyle) { + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square_aod; + break; + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle_aod; + break; + } + } else { + switch (themeStyle) { + case 0: + case 3: + default: + bgRes = R.drawable.lockscreen_widget_background_circle; + break; + case 1: + case 2: + bgRes = R.drawable.lockscreen_widget_background_square; + break; + } } iv.setBackgroundResource(bgRes); setButtonActiveState(iv, null, false); @@ -661,6 +709,23 @@ public void onLongPress(MotionEvent e) { } private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, boolean active) { + if (mDozing) { + if (iv != null) { + iv.setBackgroundTintList(null); + iv.setImageTintList(ColorStateList.valueOf(Color.WHITE)); + } + if (efab != null) { + efab.setBackgroundTintList(null); + if (efab != weatherButtonFab) { + efab.setIconTint(ColorStateList.valueOf(Color.WHITE)); + } else { + efab.setIconTint(null); + } + efab.setTextColor(Color.WHITE); + } + return; + } + int bgTint; int tintColor; if (mThemeStyle == 2 || mThemeStyle == 3) { @@ -683,17 +748,17 @@ private void setButtonActiveState(LaunchableImageView iv, LaunchableFAB efab, bo if (iv != null) { iv.setBackgroundTintList(ColorStateList.valueOf(bgTint)); if (iv != weatherButton) { - iv.setImageTintList(ColorStateList.valueOf(tintColor)); + iv.setImageTintList(ColorStateList.valueOf(tintColor)); } else { - iv.setImageTintList(null); + iv.setImageTintList(null); } } if (efab != null) { efab.setBackgroundTintList(ColorStateList.valueOf(bgTint)); if (efab != weatherButtonFab) { - efab.setIconTint(ColorStateList.valueOf(tintColor)); + efab.setIconTint(ColorStateList.valueOf(tintColor)); } else { - efab.setIconTint(null); + efab.setIconTint(null); } efab.setTextColor(tintColor); } From f5e7d554cc7133b23d6df0a28636266fa413a48f Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 16:30:48 +0000 Subject: [PATCH 141/190] SystemUI: Adapt gradient support to tiles style and ringer slider Signed-off-by: Ghosuto --- .../common/ringer/RingerSliderInterfaces.kt | 7 ++ .../common/ringer/RingerSliderWidget.kt | 22 +++++-- .../qs/panels/ui/compose/infinitegrid/Tile.kt | 66 ++++++++++--------- .../tiles/impl/ringer/QSTileRingerDefaults.kt | 8 +++ 4 files changed, 64 insertions(+), 39 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt index ab25fa25115e..83c2c10295a0 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderInterfaces.kt @@ -16,6 +16,7 @@ package com.android.systemui.common.ringer import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.Dp @@ -39,6 +40,12 @@ interface RingerSliderTheme { val dndIcon: Color val dozeStroke: Dp + + val activeBgBrush: Brush? + @Composable get() = null + + val dndBgBrush: Brush? + @Composable get() = null } interface RingerSliderDimens { diff --git a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt index 109d040fdc76..3555efe68a96 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ringer/RingerSliderWidget.kt @@ -50,7 +50,10 @@ fun RingerSliderWidget( ) { val mode by interactor.ringerMode.collectAsState(initial = interactor.getCurrentMode()) val isDndEnabled by interactor.dndMode.collectAsState(initial = interactor.isDndEnabled()) - + + val activeBrush = theme.activeBgBrush + val dndBrush = theme.dndBgBrush + val targetPosition = when (mode) { AudioManager.RINGER_MODE_NORMAL -> 0f AudioManager.RINGER_MODE_VIBRATE -> 1f @@ -189,13 +192,18 @@ fun RingerSliderWidget( .offset(x = thumbOffset) .size(dimens.thumbSize) .padding(dimens.thumbPadding) - .background( + .then( when { - isDozing -> Color.Transparent - isDndEnabled -> theme.dndBg - else -> theme.activeBg - }, - thumbShape + isDozing -> Modifier.background(Color.Transparent, thumbShape) + isDndEnabled -> if (dndBrush != null) + Modifier.background(dndBrush, thumbShape) + else + Modifier.background(theme.dndBg, thumbShape) + else -> if (activeBrush != null) + Modifier.background(activeBrush, thumbShape) + else + Modifier.background(theme.activeBg, thumbShape) + } ) .then( when { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index 3fa7f753d211..9631fea47720 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -502,7 +502,7 @@ fun TileContainer( interactionSource = interactionSource, ) .tileTestTag(iconOnly) - .thenIf(!isDualTarget || iconOnly) { + .thenIf(!isDualTarget || iconOnly || (colors.iconBackgroundGradient != null && colors.background == MaterialTheme.colorScheme.primary)) { Modifier .drawBehind { val brush = colors.iconBackgroundGradient @@ -805,7 +805,31 @@ fun rememberQsGradient(): Boolean { } @Composable -private fun rememberGradientColorMode(): Int { +fun rememberQsTileBackgroundBrush(): Brush? { + val enabled = rememberQsGradient() + val mode = rememberGradientColorMode() + val (start, end) = rememberGradientCustomColors() + + if (!enabled) return null + + val colors = if (mode == 1) { + listOf(start, end) + } else { + listOf( + MaterialTheme.colorScheme.primary, + MaterialTheme.colorScheme.secondary + ) + } + + return Brush.linearGradient( + colors = colors, + start = Offset(0f, 0f), + end = Offset.Infinite + ) +} + +@Composable +internal fun rememberGradientColorMode(): Int { val contentResolver = LocalContext.current.contentResolver fun readMode(): Int = try { @@ -840,7 +864,7 @@ private fun rememberGradientColorMode(): Int { } @Composable -private fun rememberGradientCustomColors(): Pair { +internal fun rememberGradientCustomColors(): Pair { val contentResolver = LocalContext.current.contentResolver fun readStart(): Int = try { @@ -897,8 +921,7 @@ private object TileDefaults { /** An active tile uses the active color as background */ @Composable fun activeTileColors(): TileColors { - val gradientEnabled = rememberQsGradient() - val gradient = qsTileBackgroundBrush(gradientEnabled) + val gradient = rememberQsTileBackgroundBrush() return TileColors( background = MaterialTheme.colorScheme.primary, @@ -915,8 +938,7 @@ private object TileDefaults { fun activeDualTargetTileColors(): TileColors { val context = LocalContext.current val isSingleToneStyle = DualTargetTileStyleProvider.isSingleToneStyle(context) - val gradientEnabled = rememberQsGradient() - val gradient = qsTileBackgroundBrush(gradientEnabled) + val gradient = rememberQsTileBackgroundBrush() return if (isSingleToneStyle) { TileColors( @@ -990,15 +1012,17 @@ private object TileDefaults { } @Composable - @ReadOnlyComposable - fun activeDualTargetMonochromeTileColors(): TileColors = - TileColors( + fun activeDualTargetMonochromeTileColors(): TileColors { + val gradient = rememberQsTileBackgroundBrush() + return TileColors( background = MaterialTheme.colorScheme.primary, iconBackground = Color.Transparent, label = MaterialTheme.colorScheme.onPrimary, secondaryLabel = MaterialTheme.colorScheme.onPrimary, icon = MaterialTheme.colorScheme.onPrimary, + iconBackgroundGradient = gradient, ) + } @Composable @ReadOnlyComposable @@ -1089,28 +1113,6 @@ private object TileDefaults { mutableStateOf(RoundedCornerShape(corner)) } } - - @Composable - fun qsTileBackgroundBrush(enabled: Boolean): Brush? { - if (!enabled) return null - - val mode = rememberGradientColorMode() - val colors = if (mode == 1) { - val (start, end) = rememberGradientCustomColors() - listOf(start, end) - } else { - listOf( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.secondary - ) - } - - return Brush.linearGradient( - colors = colors, - start = Offset(0f, 0f), - end = Offset.Infinite - ) - } } /** diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt index 15f1baa7a8c8..7a3ff849d6f7 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/ringer/QSTileRingerDefaults.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.tiles.impl.ringer import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.material3.MaterialTheme import androidx.compose.ui.unit.Dp @@ -24,6 +25,7 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.common.ringer.RingerSliderDimens import com.android.systemui.common.ringer.RingerSliderTheme import com.android.systemui.qs.panels.ui.compose.infinitegrid.CustomColorScheme +import com.android.systemui.qs.panels.ui.compose.infinitegrid.rememberQsTileBackgroundBrush class QSTileRingerTheme( ) : RingerSliderTheme { @@ -46,6 +48,12 @@ class QSTileRingerTheme( @Composable get() = MaterialTheme.colorScheme.onPrimary override val dozeStroke: Dp = 2.dp + + override val activeBgBrush: Brush? + @Composable get() = rememberQsTileBackgroundBrush() + + override val dndBgBrush: Brush? + @Composable get() = rememberQsTileBackgroundBrush() } class QSTileRingerDimens( From c6a07f1c442702fa4c967681643bcea416aabdc7 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 19:27:46 +0000 Subject: [PATCH 142/190] SystemUI: Fix opacity handling for all clock style components Signed-off-by: Ghosuto --- .../android/systemui/clocks/ClockStyle.java | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index 143ebd79013c..c7f4bedd4291 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -258,17 +258,23 @@ private int resolveClockColor() { } } + private void updateClockAppearance() { + if (currentClockView == null) return; + float alpha = mClockOpacity / 100f; + currentClockView.setAlpha(alpha); + applyTextClockColor(currentClockView); + } + private void updateClockTextColor() { - if (currentClockView != null) { - updateTextClockColor(currentClockView); - } + if (currentClockView == null) return; + applyTextClockColor(currentClockView); } - private void updateTextClockColor(View view) { + private void applyTextClockColor(View view) { if (view instanceof ViewGroup) { ViewGroup viewGroup = (ViewGroup) view; for (int i = 0; i < viewGroup.getChildCount(); i++) { - updateTextClockColor(viewGroup.getChildAt(i)); + applyTextClockColor(viewGroup.getChildAt(i)); } } @@ -284,10 +290,7 @@ private void updateTextClockColor(View view) { if ((originalColor & 0x00FFFFFF) != (whiteColor & 0x00FFFFFF)) return; - int color = resolveClockColor(); - int alpha = Math.round((mClockOpacity / 100f) * 255); - color = (color & 0x00FFFFFF) | (alpha << 24); - textClock.setTextColor(color); + textClock.setTextColor(resolveClockColor()); } private void updateClockFrameMargin() { @@ -326,7 +329,7 @@ private void updateClockView() { if (currentClockView instanceof LinearLayout) { ((LinearLayout) currentClockView).setGravity(gravity); } - updateClockTextColor(); + updateClockAppearance(); updateClockFrameMargin(); } } @@ -358,9 +361,10 @@ public void onTuningChanged(String key, String newValue) { break; case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); - // Keep opacity within valid range (0-100) mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); - updateClockTextColor(); + if (currentClockView != null) { + currentClockView.setAlpha(mClockOpacity / 100f); + } break; case CLOCK_FRAME_MARGIN_TOP_KEY: mClockFrameMarginTop = TunerService.parseInteger(newValue, DEFAULT_MARGIN_TOP); @@ -378,4 +382,4 @@ private boolean isCenterClock(int clockStyle) { } return false; } -} \ No newline at end of file +} From 1bb9e7539fe2808232a754d445a0f8d6e299c6b5 Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sat, 28 Feb 2026 19:29:26 +0000 Subject: [PATCH 143/190] SystemUI: Apply clock opacity at 70% during AOD/dozing Signed-off-by: Ghosuto --- .../com/android/systemui/clocks/ClockStyle.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java index c7f4bedd4291..f79b265be119 100644 --- a/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java +++ b/packages/SystemUI/src/com/android/systemui/clocks/ClockStyle.java @@ -82,6 +82,7 @@ public class ClockStyle extends RelativeLayout implements TunerService.Tunable { private static final int DEFAULT_OPACITY = 100; private static final int DEFAULT_MARGIN_TOP = 15; private static final int DEFAULT_CUSTOM_COLOR = Color.WHITE; + private static final int AOD_OPACITY_CAP = 70; private final Context mContext; private final KeyguardManager mKeyguardManager; @@ -150,6 +151,7 @@ public void onDozingChanged(boolean dozing) { return; } mDozing = dozing; + applyClockAlpha(); if (mDozing) { startBurnInProtection(); } else { @@ -258,10 +260,15 @@ private int resolveClockColor() { } } + private void applyClockAlpha() { + if (currentClockView == null) return; + int effective = (mDozing && mClockOpacity > AOD_OPACITY_CAP) ? AOD_OPACITY_CAP : mClockOpacity; + currentClockView.setAlpha(effective / 100f); + } + private void updateClockAppearance() { if (currentClockView == null) return; - float alpha = mClockOpacity / 100f; - currentClockView.setAlpha(alpha); + applyClockAlpha(); applyTextClockColor(currentClockView); } @@ -362,9 +369,7 @@ public void onTuningChanged(String key, String newValue) { case CLOCK_TEXT_OPACITY_KEY: mClockOpacity = TunerService.parseInteger(newValue, DEFAULT_OPACITY); mClockOpacity = Math.max(0, Math.min(100, mClockOpacity)); - if (currentClockView != null) { - currentClockView.setAlpha(mClockOpacity / 100f); - } + applyClockAlpha(); break; case CLOCK_FRAME_MARGIN_TOP_KEY: mClockFrameMarginTop = TunerService.parseInteger(newValue, DEFAULT_MARGIN_TOP); From a1ecc146edc0fca59ce6bd147421ebdeccba110b Mon Sep 17 00:00:00 2001 From: Riddle Hsu Date: Fri, 29 Aug 2025 14:40:07 +0800 Subject: [PATCH 144/190] Reduce blocking operation on display thread For example, thread A acquires wm lock 20ms and it posts a message to another thread B that also needs to enter wm lock. Then B will need to wait for the 20ms operation to finish. Currently the common cases are the message of task-layer-rank and idle-check. For rank, it can be done with deferred scope without posting a message. For idle-check, it can post a regular 10ms timeout rather than an immediate message if the activity is in a transition because when transition is finished, scheduleProcessStoppingAndFinishingActivitiesIfNeeded will also check idle. Bug: 422656135 Test: Return to home from an app by pressing home key. Check trace about the timestamp of setProcessGroup. Change-Id: I9f455f1063ef04c049c5743eda9f6c175ec3a0ba Signed-off-by: Ghosuto --- .../core/java/com/android/server/wm/ActivityRecord.java | 4 ++-- .../com/android/server/wm/ActivityTaskManagerService.java | 5 +++++ .../java/com/android/server/wm/RootWindowContainer.java | 6 ++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 139a5d939adb..6e5d18d15d28 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -6048,8 +6048,8 @@ void makeInvisible() { case PAUSING: case PAUSED: case STARTED: - addToStopping(true /* scheduleIdle */, - canEnterPictureInPicture /* idleDelayed */, "makeInvisible"); + final boolean idleDelayed = canEnterPictureInPicture || inTransition(); + addToStopping(true /* scheduleIdle */, idleDelayed, "makeInvisible"); break; default: diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index f7aad2633592..01fb333c5e7c 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -5516,6 +5516,11 @@ void continueWindowLayout() { // ClientTransactions is queued during #deferWindowLayout() for performance. // Notify to continue. mLifecycleManager.onLayoutContinued(); + + if (mRootWindowContainer.mTaskLayersChanged + && !mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mRootWindowContainer.rankTaskLayers(); + } } /** diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index d4d46be9fa80..930e1d173963 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -253,7 +253,7 @@ class RootWindowContainer extends WindowContainer DeviceStateAutoRotateSettingController mDeviceStateAutoRotateSettingController; // Whether tasks have moved and we need to rank the tasks before next OOM scoring - private boolean mTaskLayersChanged = true; + boolean mTaskLayersChanged = true; private int mTmpTaskLayerRank; private final RankTaskLayersRunnable mRankTaskLayersRunnable = new RankTaskLayersRunnable(); private Region mTmpOccludingRegion; @@ -2991,7 +2991,9 @@ void invalidateTaskLayersAndUpdateOomAdjIfNeeded() { void invalidateTaskLayers() { if (!mTaskLayersChanged) { mTaskLayersChanged = true; - mService.mH.post(mRankTaskLayersRunnable); + if (!mWindowManager.mWindowPlacerLocked.isLayoutDeferred()) { + mService.mH.post(mRankTaskLayersRunnable); + } } } From 672cc480a7358a6c26a804ad7c01bc4d0f6c0e37 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Mon, 1 Sep 2025 08:16:20 +0800 Subject: [PATCH 145/190] services: Optimizing home to desktop transition Change-Id: I2e5ace507691b2cefdd702e8f246c269afd91cd0 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../android/server/wm/WindowProcessController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 74cfc773ae1d..c9981c2bbcc2 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -91,6 +91,7 @@ import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.function.pooled.PooledLambda; +import com.android.server.DisplayThread; import com.android.server.Watchdog; import com.android.server.am.Flags; import com.android.server.am.psc.AsyncBatchSession; @@ -567,7 +568,7 @@ void postPendingUiCleanMsg(boolean pendingUiClean) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiClean, mListener, pendingUiClean); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } long getInteractionEventTime() { @@ -1443,7 +1444,7 @@ public long getInputDispatchingTimeoutMillis() { void clearProfilerIfNeeded() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::clearProfilerIfNeeded, mListener)); } @@ -1490,7 +1491,7 @@ void addToPendingTop() { void updateServiceConnectionActivities() { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.sendMessage(PooledLambda.obtainMessage( + DisplayThread.getHandler().sendMessage(PooledLambda.obtainMessage( WindowProcessListener::updateServiceConnectionActivities, mListener)); } @@ -1499,7 +1500,7 @@ void setPendingUiCleanAndForceProcessStateUpTo(int newState) { final Message m = PooledLambda.obtainMessage( WindowProcessListener::setPendingUiCleanAndForceProcessStateUpTo, mListener, newState); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } boolean isRemoved() { @@ -1568,7 +1569,7 @@ void appDied(String reason) { // Posting on handler so WM lock isn't held when we call into AM. final Message m = PooledLambda.obtainMessage( WindowProcessListener::appDied, mListener, reason); - mAtm.mH.sendMessage(m); + DisplayThread.getHandler().sendMessage(m); } /** @@ -2211,7 +2212,7 @@ void removeAnimatingReason(@AnimatingReason int reason) { /** Applies the animating state to activity manager for updating process priority. */ private void setAnimating(boolean animating) { // Posting on handler so WM lock isn't held when we call into AM. - mAtm.mH.post(() -> mListener.setRunningRemoteAnimation(animating)); + DisplayThread.getHandler().post(() -> mListener.setRunningRemoteAnimation(animating)); } boolean isRunningRemoteTransition() { From 4649f19df1c9a7fe1f9e8f558e1a70b98b6fa3e0 Mon Sep 17 00:00:00 2001 From: wenbo wang Date: Wed, 20 Aug 2025 06:08:23 -0700 Subject: [PATCH 146/190] Optimize the response speed of recents animations In the recent animation scene, we don't need to wait for the launcher to draw, we just need the wallpaper to finish drawing and start the animation. Bug: 422656135 Test:Three key navigation and gesture swipe up to recent to test for any issues. Change-Id: I9d9433434e334ee68cb5b2dd2cc182a584207c7f Signed-off-by: Ghosuto --- .../android/server/wm/BLASTSyncEngine.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 70f075658df4..25adf74fd207 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -20,6 +20,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_SYNC_ENGINE; import static com.android.server.wm.WindowState.BLAST_TIMEOUT_DURATION; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import android.annotation.NonNull; import android.annotation.Nullable; @@ -207,9 +208,14 @@ private boolean tryFinish() { for (int i = mRootMembers.size() - 1; i >= 0; --i) { final WindowContainer wc = mRootMembers.valueAt(i); if (!wc.isSyncFinished(this)) { - ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", - mSyncId, wc); - return false; + if(canIgnoreFromRecents(wc)){ + //if wallpaper has drawn, ignore unfinished container when window is animating by recents. + Slog.w(TAG, "Sync group " + mSyncId + " ignoring " + wc + "when wallpaper has drawn"); + } else { + ProtoLog.v(WM_DEBUG_SYNC_ENGINE, "SyncGroup %d: Unfinished container: %s", + mSyncId, wc); + return false; + } } } finishNow(); @@ -656,4 +662,17 @@ boolean hasPendingSyncSets() { void addOnIdleListener(Runnable onIdleListener) { mOnIdleListeners.add(onIdleListener); } + + /** @return {@code true} if wallpaper has drawn when window is animating by recents.*/ + private boolean canIgnoreFromRecents(WindowContainer wc){ + + if(wc.asActivityRecord() != null && wc.getDisplayContent() != null + && wc.asActivityRecord().isActivityTypeHomeOrRecents() + && mWm.mAtmService.getTransitionController().getCollectingTransitionType() == TRANSIT_TO_FRONT + && wc.getDisplayContent().mWallpaperController.wallpaperTransitionReady() + && wc.getDisplayContent().mWallpaperController.getTopVisibleWallpaper() != null){ + return true; + } + return false; + } } From e5cbe8a2247d6a0e8a5e8973f3a3f76252633fe4 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Tue, 19 Aug 2025 07:52:54 +0800 Subject: [PATCH 147/190] services: Launcher blast sync timeout opt Change-Id: I5fa3d9d66aff7a94e84fe3f666eaeb5be8a8f8e9 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../com/android/server/wm/ActivityRecord.java | 21 +++++++++++++++++++ .../android/server/wm/BLASTSyncEngine.java | 4 ++++ 2 files changed, 25 insertions(+) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 6e5d18d15d28..163cfb35b284 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -672,6 +672,8 @@ enum State { * @see WindowContainer#providesOrientation() */ final boolean mStyleFillsParent; + + private int mSyncTimeoutCounter = 0; // The input dispatching timeout for this application token in milliseconds. long mInputDispatchingTimeoutMillis = DEFAULT_DISPATCHING_TIMEOUT_MILLIS; @@ -9395,6 +9397,25 @@ void setShouldDockBigOverlays(boolean shouldDockBigOverlays) { getTask().getRootTask().onShouldDockBigOverlaysChanged(); } + void checkSyncTimeout(BLASTSyncEngine.SyncGroup group) { + if (attachedToProcess() && "com.android.launcher3".equals(packageName)) { + mAtmService.mH.post(() -> { + synchronized (mAtmService.mGlobalLock) { + if (!hasProcess()) { + return; + } + WindowProcessController wpc = app; + mSyncTimeoutCounter++; + if (mSyncTimeoutCounter < 3) { + return; + } + Slog.d(TAG, "Sync timeout, try kill process"); + mAtmService.mAmInternal.killProcess(wpc.mName, wpc.mUid, "syncTimeout"); + } + }); + } + } + @Override boolean isSyncFinished(BLASTSyncEngine.SyncGroup group) { if (task != null && task.mSharedStartingData != null) { diff --git a/services/core/java/com/android/server/wm/BLASTSyncEngine.java b/services/core/java/com/android/server/wm/BLASTSyncEngine.java index 25adf74fd207..6828d3be3c45 100644 --- a/services/core/java/com/android/server/wm/BLASTSyncEngine.java +++ b/services/core/java/com/android/server/wm/BLASTSyncEngine.java @@ -433,6 +433,10 @@ private void onTimeout() { .mUnknownAppVisibilityController.getDebugMessage()); } }); + ActivityRecord r = wc.asActivityRecord(); + if (r != null) { + r.checkSyncTimeout(this); + } } } From 225001ad5f31ec5fe0b9f0dce049bd288c920b5a Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Thu, 13 Nov 2025 20:32:07 +0800 Subject: [PATCH 148/190] services: Fixing powerhal soft reboot Change-Id: I32df30b4eeba4f56786e568f706eea23cfe23de8 Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../com/android/server/power/hint/HintManagerService.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/core/java/com/android/server/power/hint/HintManagerService.java b/services/core/java/com/android/server/power/hint/HintManagerService.java index 9174855b6466..0a505dbc48b6 100644 --- a/services/core/java/com/android/server/power/hint/HintManagerService.java +++ b/services/core/java/com/android/server/power/hint/HintManagerService.java @@ -1028,10 +1028,11 @@ public void closeChannel() { if (mConfig != null) { try { mPowerHal.closeSessionChannel(mTgid, mUid); - } catch (RemoteException e) { - throw new IllegalStateException("Failed to close session channel!", e); + } catch (Exception e) { + Slog.w(TAG, "Session channel already dead for uid " + mUid); + } finally { + mConfig = null; } - mConfig = null; } } From eff7024d8bf93290f203f8e1186d641e4abe8e99 Mon Sep 17 00:00:00 2001 From: rmp22 <195054967+rmp22@users.noreply.github.com> Date: Fri, 1 Aug 2025 20:56:13 +0800 Subject: [PATCH 149/190] services: Threads priority enhance Change-Id: Id4a46262e60c73b529a4b33e6261176158eff7bc Signed-off-by: rmp22 <195054967+rmp22@users.noreply.github.com> Signed-off-by: Ghosuto --- .../android/wm/shell/dagger/WMShellConcurrencyModule.java | 6 +++--- libs/hwui/platform/android/thread/CommonPoolBase.h | 2 +- .../systemui/util/concurrency/SysUIConcurrencyModule.kt | 2 +- services/core/java/com/android/server/AnimationThread.java | 4 ++-- services/core/java/com/android/server/DisplayThread.java | 2 +- services/core/java/com/android/server/UiThread.java | 2 +- services/core/java/com/android/server/am/OomAdjuster.java | 4 ++-- .../java/com/android/server/wm/SurfaceAnimationThread.java | 4 ++-- .../server/wm/WindowManagerThreadPriorityBooster.java | 6 +++--- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index bd23a058e76d..5e369529fbd8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -17,7 +17,7 @@ package com.android.wm.shell.dagger; import static android.os.Process.THREAD_PRIORITY_BACKGROUND; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_FOREGROUND; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; @@ -96,7 +96,7 @@ public static ShellExecutor provideSysUIMainExecutor( * See {@link com.android.systemui.SystemUIFactory#init(Context, boolean)}. */ public static HandlerThread createShellMainThread() { - HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_DISPLAY); + HandlerThread mainThread = new HandlerThread("wmshell.main", THREAD_PRIORITY_URGENT_DISPLAY); return mainThread; } @@ -167,7 +167,7 @@ public static Choreographer provideShellMainChoreographer( @Provides @ShellAnimationThread public static Handler provideShellAnimationHandler() { - HandlerThread animThread = new HandlerThread("wmshell.anim", THREAD_PRIORITY_DISPLAY); + HandlerThread animThread = new HandlerThread("wmshell.anim", THREAD_PRIORITY_URGENT_DISPLAY); animThread.start(); if (Build.IS_DEBUGGABLE) { animThread.getLooper().setTraceTag(Trace.TRACE_TAG_WINDOW_MANAGER); diff --git a/libs/hwui/platform/android/thread/CommonPoolBase.h b/libs/hwui/platform/android/thread/CommonPoolBase.h index 8f836b612440..42d0d113ff17 100644 --- a/libs/hwui/platform/android/thread/CommonPoolBase.h +++ b/libs/hwui/platform/android/thread/CommonPoolBase.h @@ -41,7 +41,7 @@ class CommonPoolBase { tids[i] = pthread_gettid_np(self); tidConditionVars[i].notify_one(); } - setpriority(PRIO_PROCESS, 0, PRIORITY_FOREGROUND); + setpriority(PRIO_PROCESS, 0, PRIORITY_URGENT_DISPLAY); auto startHook = renderthread::RenderThread::getOnStartHook(); if (startHook) { startHook(name.data()); diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt index 6dfca92226ab..69ad69ef3310 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -129,7 +129,7 @@ object SysUIConcurrencyModule { ): UiThreadContext { return if (Flags.edgeBackGestureHandlerThread()) { val thread = - HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_DISPLAY).apply { + HandlerThread("BackPanelUiThread", Process.THREAD_PRIORITY_URGENT_DISPLAY).apply { start() looper.setSlowLogThresholdMs( LONG_SLOW_DISPATCH_THRESHOLD, diff --git a/services/core/java/com/android/server/AnimationThread.java b/services/core/java/com/android/server/AnimationThread.java index 826e7b52a9df..8f7643a13f1d 100644 --- a/services/core/java/com/android/server/AnimationThread.java +++ b/services/core/java/com/android/server/AnimationThread.java @@ -16,7 +16,7 @@ package com.android.server; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import android.os.Handler; import android.os.Trace; @@ -32,7 +32,7 @@ public final class AnimationThread extends ServiceThread { private static Handler sHandler; private AnimationThread() { - super("android.anim", THREAD_PRIORITY_DISPLAY, false /*allowIo*/); + super("android.anim", THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/DisplayThread.java b/services/core/java/com/android/server/DisplayThread.java index 13e9550a7bae..8d1c976a17d8 100644 --- a/services/core/java/com/android/server/DisplayThread.java +++ b/services/core/java/com/android/server/DisplayThread.java @@ -35,7 +35,7 @@ public final class DisplayThread extends ServiceThread { private DisplayThread() { // DisplayThread runs important stuff, but these are not as important as things running in // AnimationThread. Thus, set the priority to one lower. - super("android.display", Process.THREAD_PRIORITY_DISPLAY + 1, false /*allowIo*/); + super("android.display", Process.THREAD_PRIORITY_URGENT_DISPLAY + 1, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/UiThread.java b/services/core/java/com/android/server/UiThread.java index 88004bd5f619..722e47dd58b7 100644 --- a/services/core/java/com/android/server/UiThread.java +++ b/services/core/java/com/android/server/UiThread.java @@ -35,7 +35,7 @@ public final class UiThread extends ServiceThread { private static Handler sHandler; private UiThread() { - super("android.ui", Process.THREAD_PRIORITY_FOREGROUND, false /*allowIo*/); + super("android.ui", Process.THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } @Override diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 1ae9c6b72bdc..fd2cd51be5bb 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -68,7 +68,7 @@ import static android.os.Process.THREAD_GROUP_FOREGROUND_WINDOW; import static android.os.Process.THREAD_GROUP_RESTRICTED; import static android.os.Process.THREAD_GROUP_TOP_APP; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static com.android.server.am.ActivityManagerDebugConfig.DEBUG_ALL; @@ -2182,7 +2182,7 @@ protected boolean applyOomAdjLSP(ProcessRecordInternal state, boolean doingAll, } if (renderThreadTid != 0) { - mInjector.setThreadPriority(renderThreadTid, THREAD_PRIORITY_DISPLAY); + mInjector.setThreadPriority(renderThreadTid, THREAD_PRIORITY_URGENT_DISPLAY); } } } catch (Exception e) { diff --git a/services/core/java/com/android/server/wm/SurfaceAnimationThread.java b/services/core/java/com/android/server/wm/SurfaceAnimationThread.java index 8ea715c4084e..2806ff9bc912 100644 --- a/services/core/java/com/android/server/wm/SurfaceAnimationThread.java +++ b/services/core/java/com/android/server/wm/SurfaceAnimationThread.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import android.os.Handler; import android.os.Trace; @@ -32,7 +32,7 @@ public final class SurfaceAnimationThread extends ServiceThread { private static Handler sHandler; private SurfaceAnimationThread() { - super("android.anim.lf", THREAD_PRIORITY_DISPLAY, false /*allowIo*/); + super("android.anim.lf", THREAD_PRIORITY_URGENT_DISPLAY, false /*allowIo*/); } private static void ensureThreadLocked() { diff --git a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java index 1b70d1d4a8b6..057abe81dcf8 100644 --- a/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java +++ b/services/core/java/com/android/server/wm/WindowManagerThreadPriorityBooster.java @@ -16,7 +16,7 @@ package com.android.server.wm; -import static android.os.Process.THREAD_PRIORITY_DISPLAY; +import static android.os.Process.THREAD_PRIORITY_URGENT_DISPLAY; import static android.os.Process.THREAD_PRIORITY_TOP_APP_BOOST; import static android.os.Process.myTid; import static android.os.Process.setThreadPriority; @@ -44,7 +44,7 @@ class WindowManagerThreadPriorityBooster extends ThreadPriorityBooster { private boolean mBoundsAnimationRunning; WindowManagerThreadPriorityBooster() { - super(THREAD_PRIORITY_DISPLAY, INDEX_WINDOW); + super(THREAD_PRIORITY_URGENT_DISPLAY, INDEX_WINDOW); mAnimationThreadId = AnimationThread.get().getThreadId(); mSurfaceAnimationThreadId = SurfaceAnimationThread.get().getThreadId(); } @@ -93,7 +93,7 @@ void setBoundsAnimationRunning(boolean running) { @GuardedBy("mLock") private void updatePriorityLocked() { int priority = (mAppTransitionRunning || mBoundsAnimationRunning) - ? THREAD_PRIORITY_TOP_APP_BOOST : THREAD_PRIORITY_DISPLAY; + ? THREAD_PRIORITY_TOP_APP_BOOST : THREAD_PRIORITY_URGENT_DISPLAY; setBoostToPriority(priority); setThreadPriority(mAnimationThreadId, priority); setThreadPriority(mSurfaceAnimationThreadId, priority); From 08bfa7059f820f56205a66502c92c8a898d0de7a Mon Sep 17 00:00:00 2001 From: Ghosuto Date: Sun, 1 Mar 2026 17:58:57 +0000 Subject: [PATCH 150/190] SystemUI: Reduce media metadata bitmap size Signed-off-by: Ghosuto --- core/res/res/values/config.xml | 2 +- media/java/android/media/session/MediaSession.java | 2 +- .../systemui/media/controls/domain/pipeline/MediaDataLoader.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 00e01e0b1eba..825314201648 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4224,7 +4224,7 @@ - 320dp + 260dp false diff --git a/media/java/android/media/session/MediaSession.java b/media/java/android/media/session/MediaSession.java index a0bf7faa3151..161da450fa4d 100644 --- a/media/java/android/media/session/MediaSession.java +++ b/media/java/android/media/session/MediaSession.java @@ -204,7 +204,7 @@ public MediaSession(@NonNull Context context, @NonNull String tag, int bitmapSize = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize); - mMaxBitmapSize = Math.min(bitmapSize, 500); + mMaxBitmapSize = Math.min(bitmapSize, 460); mCbStub = new CallbackStub(this); MediaSessionManager manager = (MediaSessionManager) context diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt index b2e7fe0012d9..da65471afae4 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/domain/pipeline/MediaDataLoader.kt @@ -94,7 +94,7 @@ constructor( context.resources.getDimensionPixelSize( com.android.internal.R.dimen.config_mediaMetadataBitmapMaxSize ), - 500 + 460 ) private val artworkHeight: Int = context.resources.getDimensionPixelSize(R.dimen.qs_media_session_height_expanded) From c46a7357024950abf1ebb898c955208159d592ec Mon Sep 17 00:00:00 2001 From: Anna-Sophie Kasierocka Date: Mon, 6 Jan 2025 21:40:25 +0100 Subject: [PATCH 151/190] SystemUI: Ongoing action progressbar chip implementation This commit provides an implementation of a progressbar chip in statusbar which features ongoing action progress from a notification The chip is featured to the right of default statusbar clock position, but before the icon container. The contents of chip are miniature app icon and a progress bar The implementation provided is basic and lacks customizations which are planned to be added further Idea is taken from this reddit post: https://www.reddit.com/r/NOTHING/comments/1dyuak6/nothing_os_30_images_fan_made/ neobuddy89: * Removed opacity customization * Switch from VibrationUtils to VibratorHelper. * Renamed settings key for media progress and compact mode. * Disabled chip by default. Squashed: From: NurKeinNeid Date: Sat, 22 Feb 2025 16:14:01 +0100 Subject: SystemUI: Add ongoing action chip toggle setting [1/2] Change-Id: I2083b5184d9a8233722661e42d7d5f33876fc276 Signed-off-by: NurKeinNeid Signed-off-by: Pranav Vashi From: f104a Date: Thu, 16 Jan 2025 15:57:43 +0100 Subject: SystemUI: Don't tint progressbar chip icon Change-Id: I5de1ff070e8ced513db5e4867728fcd727e5f511 Signed-off-by: Pranav Vashi From: f104a Date: Thu, 27 Feb 2025 16:17:47 +0100 Subject: SystemUI: Hide ongoing action progress chip on lockscreen Change-Id: Iace5817e4c3111d23a1c38c0707e4822ce8fd93c Signed-off-by: Pranav Vashi From: Ghosuto Date: Fri, 21 Mar 2025 10:14:55 +0000 Subject: SystemUI: Add click action to status bar ongoing progress chip Signed-off-by: Ghosuto Signed-off-by: Pranav Vashi From: Ghosuto Date: Fri, 21 Mar 2025 16:25:28 +0000 Subject: SystemUI: Introduce media playback progress bar in action chip - Add support for displaying media playback progress in the status bar. - Integrate MediaSessionManagerHelper (thanks to RisingOS and AxionAOSP) to track media playback state. - Show media progress only when music/video is playing; hide during downloads. - Add gesture controls: - Single tap: stop media. - Double tap: Skip to the next track. - Long press: Open the media app. Signed-off-by: Ghosuto Signed-off-by: Pranav Vashi From: Ghosuto Date: Sat, 22 Mar 2025 08:25:06 +0000 Subject: SystemUI: Enhance media playback gestures and controls - Added swipe gestures to change tracks. - Implemented double-tap to play/pause. Signed-off-by: Ghosuto Signed-off-by: Pranav Vashi From: Ghosuto Date: Mon, 24 Mar 2025 18:54:59 +0000 Subject: [PATCH 15/31] SystemUI: Remove unused foreground drawable of ongoing chip Signed-off-by: Pranav Vashi From: Ghosuto Date: Wed, 26 Mar 2025 07:58:27 +0000 Subject: [PATCH 16/31] SystemUI: Optimize code and improve Ongoingchip handling Signed-off-by: Ghosuto Signed-off-by: Pranav Vashi From: Ghosuto Date: Sun, 30 Mar 2025 19:41:23 +0000 Subject: [PATCH 17/31] SystemUI: Minor improvements in ongonig chip Signed-off-by: Pranav Vashi From: Ghosuto Date: Wed, 2 Apr 2025 03:58:56 +0000 Subject: SystemUI: Increase Progress chip size Signed-off-by: Ghosuto Signed-off-by: Pranav Vashi From: drkphnx Date: Sun, 27 Apr 2025 18:13:03 +0000 Subject: OnGoingActionProgressController: Disable media playback progress by default Signed-off-by: drkphnx Signed-off-by: Pranav Vashi From: Dmitrii Date: Thu, 8 May 2025 21:00:10 +0000 Subject: Optimize OnGoingActionProgressController - Implemented UI update debouncing (150ms) to eliminate rapid redraws - Offloaded intensive operations to background threads - Added icon caching to prevent redundant drawable creation - Split UI updates into partial refreshes - only updating what changed - Added proper memory management and resource cleanup - Improved state tracking and null safety Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: Dmitrii Date: Sat, 10 May 2025 08:39:48 +0000 Subject: SystemUI: Introduce compact progress indicator style [1/2] Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: Dmitrii Date: Sat, 10 May 2025 15:36:13 +0000 Subject: Progress indicator: dont track notifications when feature off Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: Dmitrii Date: Sat, 24 May 2025 10:21:22 +0000 Subject: fix: add null checks for PlaybackState in media progress methods Prevents NPE during fast forward/rewind operations Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: NurKeinNeid Date: Mon, 26 May 2025 19:12:49 +0200 Subject: OnGoingActionProgressController: Improve thread safety and error handling - Add proper synchronization for shared state - Improve icon loading with error handling - Add executor cleanup and cache size limits Signed-off-by: NurKeinNeid Signed-off-by: Pranav Vashi From: Dmitrii Date: Tue, 15 Jul 2025 20:35:54 +0000 Subject: SystemUI: Hide progress chip during heads-up notifications Hide the progress chip when a heads-up notification is displayed to prevent UI overlap. The controller now listens to the HeadsUpManager to determine when a HUN is pinned. Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: Dmitrii Date: Mon, 22 Sep 2025 16:43:01 +0000 Subject: OnGoingActionProgressController: Fix stuck progress bar after upload/download completion Add stale progress detection and fix race conditions in notification handling Change-Id: Ia0edd27b153a7a2d46fee4560b4e8e66275f5deb Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: NurKeinNeid Date: Sat, 8 Nov 2025 22:50:02 +0100 Subject: SystemUI: Improve media progress tracking with timestamp updates Signed-off-by: NurKeinNeid Signed-off-by: Pranav Vashi From: Dmitrii Date: Sat, 17 Jan 2026 19:57:55 +0100 Subject: ProgressIndicator: compose refactor Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi From: Dmitrii Date: Sat, 17 Jan 2026 20:02:44 +0100 Subject: fix ongoing indicator again Signed-off-by: Dmitrii Signed-off-by: Pranav Vashi Change-Id: Ide3ff14927b314d9b988e2c24eba5345fad168aa Co-authored-by: Ghosuto Co-authored-by: NurKeinNeid Co-authored-by: Dmitrii Co-authored-by: Pranav Vashi Signed-off-by: Ghosuto --- core/java/android/provider/Settings.java | 15 + .../res/drawable-night/popup_background.xml | 7 + .../action_chip_compact_background.xml | 5 + .../drawable/circular_progress_drawable.xml | 19 + .../res/drawable/ic_default_music_icon.xml | 11 + .../res/drawable/ic_media_output_next.xml | 15 + .../res/drawable/ic_media_output_pause.xml | 21 + .../res/drawable/ic_media_output_play.xml | 15 + .../res/drawable/ic_media_output_prev.xml | 15 + .../res/drawable/popup_background.xml | 7 + .../res/layout/media_control_popup.xml | 28 + packages/SystemUI/res/layout/status_bar.xml | 6 + .../layout/status_bar_ongoing_action_chip.xml | 60 + ...status_bar_ongoing_action_chip_compact.xml | 53 + .../SystemUI/res/values/lunaris_colors.xml | 5 + .../SystemUI/res/values/lunaris_dimens.xml | 12 + .../OnGoingActionProgressController.java | 1129 +++++++++++++++++ .../statusbar/OnGoingActionProgressGroup.java | 48 + .../statusbar/OngoingActionProgressCompose.kt | 377 ++++++ .../statusbar/phone/CentralSurfacesImpl.java | 19 +- .../phone/PhoneStatusBarViewController.kt | 15 + .../shared/ui/composable/StatusBarRoot.kt | 44 +- .../util/MediaSessionManagerHelper.kt | 263 ++++ .../android/systemui/util/IconFetcher.java | 92 ++ 24 files changed, 2277 insertions(+), 4 deletions(-) create mode 100644 packages/SystemUI/res/drawable-night/popup_background.xml create mode 100644 packages/SystemUI/res/drawable/action_chip_compact_background.xml create mode 100644 packages/SystemUI/res/drawable/circular_progress_drawable.xml create mode 100644 packages/SystemUI/res/drawable/ic_default_music_icon.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_output_next.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_output_pause.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_output_play.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_output_prev.xml create mode 100644 packages/SystemUI/res/drawable/popup_background.xml create mode 100644 packages/SystemUI/res/layout/media_control_popup.xml create mode 100644 packages/SystemUI/res/layout/status_bar_ongoing_action_chip.xml create mode 100644 packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt create mode 100644 packages/SystemUI/src/com/android/systemui/util/IconFetcher.java diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 14662b2bde48..0075e898779e 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -7864,6 +7864,21 @@ public static void setShowGTalkServiceStatusForUser(ContentResolver cr, boolean */ public static final String CUSTOM_GRADIENT_END_COLOR = "custom_gradient_end_color"; + /** + * @hide + */ + public static final String ONGOING_ACTION_CHIP = "ongoing_action_chip"; + + /** + * @hide + */ + public static final String ONGOING_MEDIA_PROGRESS = "ongoing_media_progress"; + + /** + * @hide + */ + public static final String ONGOING_COMPACT_MODE = "ongoing_compact_mode"; + /** * Keys we no longer back up under the current schema, but want to continue to * process when restoring historical backup datasets. diff --git a/packages/SystemUI/res/drawable-night/popup_background.xml b/packages/SystemUI/res/drawable-night/popup_background.xml new file mode 100644 index 000000000000..1ff16466e5ac --- /dev/null +++ b/packages/SystemUI/res/drawable-night/popup_background.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/action_chip_compact_background.xml b/packages/SystemUI/res/drawable/action_chip_compact_background.xml new file mode 100644 index 000000000000..48f0abe63f5f --- /dev/null +++ b/packages/SystemUI/res/drawable/action_chip_compact_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/packages/SystemUI/res/drawable/circular_progress_drawable.xml b/packages/SystemUI/res/drawable/circular_progress_drawable.xml new file mode 100644 index 000000000000..aaa99554ce69 --- /dev/null +++ b/packages/SystemUI/res/drawable/circular_progress_drawable.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_default_music_icon.xml b/packages/SystemUI/res/drawable/ic_default_music_icon.xml new file mode 100644 index 000000000000..6df40cf3bab4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_default_music_icon.xml @@ -0,0 +1,11 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_output_next.xml b/packages/SystemUI/res/drawable/ic_media_output_next.xml new file mode 100644 index 000000000000..fe97f868ca1e --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_output_next.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_output_pause.xml b/packages/SystemUI/res/drawable/ic_media_output_pause.xml new file mode 100644 index 000000000000..9d784ddbf662 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_output_pause.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_output_play.xml b/packages/SystemUI/res/drawable/ic_media_output_play.xml new file mode 100644 index 000000000000..3f53bc76a210 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_output_play.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_output_prev.xml b/packages/SystemUI/res/drawable/ic_media_output_prev.xml new file mode 100644 index 000000000000..188ae7e1f88d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_output_prev.xml @@ -0,0 +1,15 @@ + + + + diff --git a/packages/SystemUI/res/drawable/popup_background.xml b/packages/SystemUI/res/drawable/popup_background.xml new file mode 100644 index 000000000000..e1f64332990e --- /dev/null +++ b/packages/SystemUI/res/drawable/popup_background.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/packages/SystemUI/res/layout/media_control_popup.xml b/packages/SystemUI/res/layout/media_control_popup.xml new file mode 100644 index 000000000000..7e8a1edf6ecf --- /dev/null +++ b/packages/SystemUI/res/layout/media_control_popup.xml @@ -0,0 +1,28 @@ + + + + + + + + diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index dd73e6cdbce2..9611ed077b06 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -120,6 +120,12 @@ android:id="@+id/ongoing_activity_chip_secondary" android:visibility="gone"/> + + + + + + + + + + + + + + diff --git a/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml b/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml new file mode 100644 index 000000000000..06875f7362eb --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml @@ -0,0 +1,53 @@ + + + + + + + + + + diff --git a/packages/SystemUI/res/values/lunaris_colors.xml b/packages/SystemUI/res/values/lunaris_colors.xml index 424d31585bdc..af99caad8fde 100644 --- a/packages/SystemUI/res/values/lunaris_colors.xml +++ b/packages/SystemUI/res/values/lunaris_colors.xml @@ -13,4 +13,9 @@ #89000000 @android:color/system_accent2_200 @android:color/system_accent2_700 + + + @*android:color/system_accent1_400 + #33FFFFFF + #33000000 diff --git a/packages/SystemUI/res/values/lunaris_dimens.xml b/packages/SystemUI/res/values/lunaris_dimens.xml index e87f7eec8c4b..7f095106957e 100644 --- a/packages/SystemUI/res/values/lunaris_dimens.xml +++ b/packages/SystemUI/res/values/lunaris_dimens.xml @@ -127,4 +127,16 @@ 180dp 0dp 0dp + + + 70dp + 18sp + 10sp + 10sp + 4sp + 5dp + + + 28dp + 16dp diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java new file mode 100644 index 000000000000..e7bc4f6f7457 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java @@ -0,0 +1,1129 @@ +/** + * Copyright (c) 2025 VoltageOS + * + * 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.statusbar; + +import android.app.Notification; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.database.ContentObserver; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.os.VibrationEffect; +import android.provider.Settings; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; +import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; +import com.android.systemui.res.R; +import com.android.systemui.util.IconFetcher; +import com.android.systemui.statusbar.OnGoingActionProgressGroup; +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.policy.KeyguardStateController; +import com.android.systemui.statusbar.util.MediaSessionManagerHelper; + +import java.util.HashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ExecutorService; + +public class OnGoingActionProgressController implements NotificationListener.NotificationHandler, + KeyguardStateController.Callback, OnHeadsUpChangedListener { + private static final String TAG = "OngoingActionProgressController"; + private static final String ONGOING_ACTION_CHIP_ENABLED = "ongoing_action_chip"; + private static final String ONGOING_MEDIA_PROGRESS = "ongoing_media_progress"; + private static final String ONGOING_COMPACT_MODE_ENABLED = "ongoing_compact_mode"; + private static final int SWIPE_THRESHOLD = 100; + private static final int SWIPE_VELOCITY_THRESHOLD = 100; + private static final int MEDIA_UPDATE_INTERVAL_MS = 1000; + private static final int DEBOUNCE_DELAY_MS = 150; + private static final int MAX_ICON_CACHE_SIZE = 20; + private static final int STALE_PROGRESS_CHECK_INTERVAL_MS = 5000; + private static final int PROGRESS_TIMEOUT_MS = 30000; + + private static final VibrationEffect VIBRATION_EFFECT = + VibrationEffect.get(VibrationEffect.EFFECT_CLICK); + + public interface StateCallback { + void onStateChanged(boolean isVisible, int progress, int maxProgress, + Drawable icon, boolean isIconAdaptive, String packageName, + boolean isCompactMode, boolean showMediaControls); + } + + private final Context mContext; + private final ContentResolver mContentResolver; + private final Handler mHandler; + private final SettingsObserver mSettingsObserver; + private final KeyguardStateController mKeyguardStateController; + private final NotificationListener mNotificationListener; + private final HeadsUpManager mHeadsUpManager; + private final VibratorHelper mVibrator; + private final IconFetcher mIconFetcher; + private final MediaSessionManagerHelper mMediaSessionHelper; + private final Executor mBackgroundExecutor; + private StateCallback mStateCallback = null; + private final boolean mIsComposeMode; + private final Object mLock = new Object(); + private final Runnable mUiUpdateRunnable = new Runnable() { + @Override + public void run() { + synchronized (mLock) { + mUpdatePending = false; + mLastUpdateTime = System.currentTimeMillis(); + updateViews(); + } + } + }; + + private final ProgressBar mProgressBar; + private final ProgressBar mCircularProgressBar; + private final View mProgressRootView; + private final View mCompactRootView; + private final ImageView mIconView; + private final ImageView mCompactIconView; + + private final HashMap mIconCache = new HashMap<>(); + + private boolean mShowMediaProgress = true; + private boolean mIsTrackingProgress = false; + private boolean mIsForceHidden = false; + private boolean mHeadsUpPinned = false; + private long mLastProgressUpdateTime = 0; + private boolean mIsEnabled; + private boolean mIsCompactModeEnabled = false; + private int mCurrentProgress = 0; + private int mCurrentProgressMax = 0; + private Drawable mCurrentIcon = null; + private boolean mCurrentIconIsAdaptive = false; + private boolean mIsMenuVisible = false; + private boolean mIsSystemChipVisible = false; + + private String mTrackedNotificationKey; + private String mTrackedPackageName; + private PopupWindow mMediaPopup; + private boolean mIsPopupActive = false; + private boolean mNeedsFullUiUpdate = true; + private boolean mIsViewAttached = false; + private boolean mIsExpanded = false; + + private boolean mUpdatePending = false; + private long mLastUpdateTime = 0; + + private final GestureDetector mGestureDetector; + private final Handler mMediaProgressHandler = new Handler(Looper.getMainLooper()); + private final Runnable mMediaProgressRunnable = new Runnable() { + @Override + public void run() { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + updateMediaProgressOnly(); + mMediaProgressHandler.postDelayed(this, MEDIA_UPDATE_INTERVAL_MS); + } + } + }; + + private final Runnable mStaleProgressChecker = new Runnable() { + @Override + public void run() { + synchronized (OnGoingActionProgressController.this) { + checkForStaleProgress(); + } + if (mIsViewAttached) { + mHandler.postDelayed(this, STALE_PROGRESS_CHECK_INTERVAL_MS); + } + } + }; + + private final Runnable mCompactCollapseRunnable = () -> { + if (mIsCompactModeEnabled && mIsExpanded) { + mIsExpanded = false; + requestUiUpdate(); + } + }; + + private final Runnable mMenuCollapseRunnable = () -> { + mIsMenuVisible = false; + notifyStateCallback(); + }; + + private final MediaSessionManagerHelper.MediaMetadataListener mMediaMetadataListener = + new MediaSessionManagerHelper.MediaMetadataListener() { + @Override + public void onMediaMetadataChanged() { + mNeedsFullUiUpdate = true; + requestUiUpdate(); + } + + @Override + public void onPlaybackStateChanged() { + mNeedsFullUiUpdate = true; + requestUiUpdate(); + } + }; + + public OnGoingActionProgressController( + Context context, OnGoingActionProgressGroup progressGroup, + NotificationListener notificationListener, KeyguardStateController keyguardStateController, + HeadsUpManager headsUpManager, VibratorHelper vibrator) { + + mIsComposeMode = (progressGroup.rootView == null && progressGroup.compactRootView == null); + + if (progressGroup == null) { + Log.wtf(TAG, "progressGroup is null"); + throw new IllegalArgumentException("progressGroup cannot be null"); + } + + mNotificationListener = notificationListener; + if (mNotificationListener == null) { + Log.wtf(TAG, "mNotificationListener is null"); + throw new IllegalArgumentException("notificationListener cannot be null"); + } + + mKeyguardStateController = keyguardStateController; + mHeadsUpManager = headsUpManager; + mContext = context; + mContentResolver = context.getContentResolver(); + mHandler = new Handler(Looper.getMainLooper()); + mSettingsObserver = new SettingsObserver(mHandler); + mBackgroundExecutor = Executors.newSingleThreadExecutor(); + mVibrator = vibrator; + + mProgressBar = progressGroup.progressBarView; + mCircularProgressBar = progressGroup.circularProgressBarView; + mProgressRootView = progressGroup.rootView; + mCompactRootView = progressGroup.compactRootView; + mIconView = progressGroup.iconView; + mCompactIconView = progressGroup.compactIconView; + + mIconFetcher = new IconFetcher(context); + mMediaSessionHelper = MediaSessionManagerHelper.Companion.getInstance(context); + + mGestureDetector = mIsComposeMode ? null : new GestureDetector(mContext, new MediaGestureListener()); + mKeyguardStateController.addCallback(this); + mHeadsUpManager.addListener(this); + mNotificationListener.addNotificationHandler(this); + mSettingsObserver.register(); + + if (!mIsComposeMode) { + if (mProgressRootView != null && mGestureDetector != null) { + mProgressRootView.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event)); + } + + if (mCompactRootView != null && mGestureDetector != null) { + mCompactRootView.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event)); + + mCompactRootView.setOnClickListener(v -> { + onInteraction(); + }); + } + } + + mMediaSessionHelper.addMediaMetadataListener(mMediaMetadataListener); + + mIsViewAttached = true; + updateSettings(); + + mHandler.postDelayed(mStaleProgressChecker, STALE_PROGRESS_CHECK_INTERVAL_MS); + } + + /** + * Sets a callback for Compose to receive state updates + * @param callback Callback to be notified of state changes, or null to unregister + */ + public void setStateCallback(StateCallback callback) { + mStateCallback = callback; + notifyStateCallback(); + } + + public void expandCompactView() { + mIsExpanded = true; + + // Reset collapse timer + mHandler.removeCallbacks(mCompactCollapseRunnable); + mHandler.postDelayed(mCompactCollapseRunnable, 5000); + + if (mIsComposeMode) { + notifyStateCallback(); + return; + } + + if (mCompactRootView != null) mCompactRootView.setVisibility(View.GONE); + if (mProgressRootView != null) mProgressRootView.setVisibility(View.VISIBLE); + + requestUiUpdate(); + } + + private class MediaGestureListener extends GestureDetector.SimpleOnGestureListener { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + onInteraction(); + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + toggleMediaPlaybackState(); + } + mVibrator.vibrate(VIBRATION_EFFECT); + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + openMediaApp(); + } + mVibrator.vibrate(VIBRATION_EFFECT); + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (!(mShowMediaProgress && mMediaSessionHelper.isMediaPlaying())) { + return false; + } + float diffX = e2.getX() - e1.getX(); + if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && + Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { + if (diffX > 0) { + skipToNextTrack(); + } else { + skipToPreviousTrack(); + } + return true; + } + return false; + } + } + + private void requestUiUpdate() { + long currentTime = System.currentTimeMillis(); + synchronized (mLock) { + if (!mUpdatePending && (currentTime - mLastUpdateTime > DEBOUNCE_DELAY_MS)) { + mUpdatePending = false; + mLastUpdateTime = currentTime; + updateViews(); + } else if (!mUpdatePending) { + mUpdatePending = true; + mHandler.postDelayed(mUiUpdateRunnable, DEBOUNCE_DELAY_MS); + } + } + } + + /** + * Notifies the Compose callback of current state + */ + private void notifyStateCallback() { + if (mStateCallback == null) { + return; + } + + boolean isVisible = !mIsForceHidden && !mHeadsUpPinned && !mIsSystemChipVisible; + + boolean isMediaPlaying = mShowMediaProgress && mMediaSessionHelper.isMediaPlaying(); + boolean hasNotificationProgress = mIsEnabled && mIsTrackingProgress; + + isVisible = isVisible && (isMediaPlaying || hasNotificationProgress); + + if (isVisible) { + boolean isCompact = mIsCompactModeEnabled && !mIsExpanded; + mStateCallback.onStateChanged( + true, mCurrentProgress, mCurrentProgressMax, + mCurrentIcon, mCurrentIconIsAdaptive, mTrackedPackageName, + isCompact, mIsMenuVisible + ); + } else { + mStateCallback.onStateChanged(false, 0, 0, null, false, null, false, false); + } + } + + private void updateViews() { + if (!mIsViewAttached) { + if (mIsComposeMode) { + notifyStateCallback(); + } + return; + } + + if (mIsForceHidden || mHeadsUpPinned) { + if (!mIsComposeMode) { + if (mProgressRootView != null) mProgressRootView.setVisibility(View.GONE); + if (mCompactRootView != null) mCompactRootView.setVisibility(View.GONE); + } + notifyStateCallback(); + return; + } + + boolean isMediaPlaying = mShowMediaProgress && mMediaSessionHelper.isMediaPlaying(); + + if (mIsCompactModeEnabled && !mIsExpanded) { + if (!mIsComposeMode && mProgressRootView != null) { + mProgressRootView.setVisibility(View.GONE); + } + + if (!mIsEnabled && !isMediaPlaying) { + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.GONE); + } + notifyStateCallback(); + return; + } + + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.VISIBLE); + } + + if (isMediaPlaying) { + updateMediaProgressCompact(); + } else { + updateNotificationProgressCompact(); + } + } else { + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.GONE); + } + + if (isMediaPlaying) { + if (!mIsComposeMode && mProgressRootView != null) { + mProgressRootView.setVisibility(View.VISIBLE); + } + + if (mNeedsFullUiUpdate) { + updateMediaProgressFull(); + mNeedsFullUiUpdate = false; + } else { + updateMediaProgressOnly(); + } + } else { + updateNotificationProgress(); + } + } + notifyStateCallback(); + } + + private void updateMediaProgressOnly() { + if (!mIsViewAttached && !mIsComposeMode) { + return; + } + + long totalDuration = mMediaSessionHelper.getTotalDuration(); + + android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); + long currentProgress = 0; + + if (playbackState != null) { + currentProgress = playbackState.getPosition(); + } + + mCurrentProgress = (int) currentProgress; + mCurrentProgressMax = (int) totalDuration; + if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; + + if (!mIsComposeMode && mProgressRootView != null && + mProgressRootView.getVisibility() == View.VISIBLE && mProgressBar != null && totalDuration > 0) { + mProgressBar.setMax((int) totalDuration); + mProgressBar.setProgress((int) currentProgress); + } + + if (!mIsComposeMode && mCompactRootView != null && + mCompactRootView.getVisibility() == View.VISIBLE && mCircularProgressBar != null && totalDuration > 0) { + mCircularProgressBar.setMax((int) totalDuration); + mCircularProgressBar.setProgress((int) currentProgress); + } + + if (mIsComposeMode) { + notifyStateCallback(); + } + } + + private void updateMediaProgressFull() { + if (!mIsViewAttached && !mIsComposeMode) return; + + if (!mIsComposeMode && mProgressRootView != null) { + mProgressRootView.setVisibility(View.VISIBLE); + } + + mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); + mMediaProgressHandler.post(mMediaProgressRunnable); + + Drawable mediaAppIcon = mMediaSessionHelper.getMediaAppIcon(); + + if (mediaAppIcon != null) { + mCurrentIcon = mediaAppIcon; + mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; + if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(mediaAppIcon); + } else { + String packageName = null; + + android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); + if (playbackState != null && playbackState.getExtras() != null) { + packageName = playbackState.getExtras().getString("package"); + } + if (packageName != null) { + loadIconInBackground(packageName, result -> { + Drawable drawable = result != null ? result.drawable : null; + boolean isAdaptive = result != null ? result.isAdaptive : false; + + if (drawable != null) { + mCurrentIcon = drawable; + mCurrentIconIsAdaptive = isAdaptive; + if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(drawable); + } else { + setDefaultMediaIcon(); + } + if (mIsComposeMode) notifyStateCallback(); + }); + } else { + setDefaultMediaIcon(); + } + } + + updateMediaProgressOnly(); + } + + private void setDefaultMediaIcon() { + mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); + mCurrentIconIsAdaptive = false; + if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(mCurrentIcon); + } + + private void updateMediaProgressCompact() { + if (!mIsViewAttached && !mIsComposeMode) return; + + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.VISIBLE); + } + + mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); + mMediaProgressHandler.post(mMediaProgressRunnable); + + long totalDuration = mMediaSessionHelper.getTotalDuration(); + + android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); + long currentProgress = 0; + + if (playbackState != null) { + currentProgress = playbackState.getPosition(); + } + + mCurrentProgress = (int) currentProgress; + mCurrentProgressMax = (int) totalDuration; + if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; + + if (!mIsComposeMode && totalDuration > 0 && mCircularProgressBar != null) { + mCircularProgressBar.setMax((int) totalDuration); + mCircularProgressBar.setProgress((int) currentProgress); + } + + Drawable mediaAppIcon = mMediaSessionHelper.getMediaAppIcon(); + + if (mediaAppIcon != null) { + mCurrentIcon = mediaAppIcon; + mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; + if (!mIsComposeMode && mCompactIconView != null) { + mCompactIconView.setImageDrawable(mediaAppIcon); + } + } else { + String packageName = null; + if (playbackState != null && playbackState.getExtras() != null) { + packageName = playbackState.getExtras().getString("package"); + } + + if (packageName != null) { + loadIconInBackground(packageName, result -> { + Drawable drawable = result != null ? result.drawable : null; + boolean isAdaptive = result != null ? result.isAdaptive : false; + + if (drawable != null) { + mCurrentIcon = drawable; + mCurrentIconIsAdaptive = isAdaptive; + if (!mIsComposeMode && mCompactIconView != null) mCompactIconView.setImageDrawable(drawable); + } else { + setDefaultMediaIconCompact(); + } + if (mIsComposeMode) notifyStateCallback(); + }); + } else { + setDefaultMediaIconCompact(); + } + } + } + + private void setDefaultMediaIconCompact() { + mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); + mCurrentIconIsAdaptive = false; + if (!mIsComposeMode && mCompactIconView != null) mCompactIconView.setImageDrawable(mCurrentIcon); + } + + private void updateNotificationProgress() { + if (!mIsViewAttached && !mIsComposeMode) return; + + if (!mIsEnabled || !mIsTrackingProgress) { + if (!mIsComposeMode && mProgressRootView != null) { + mProgressRootView.setVisibility(View.GONE); + } + mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); + return; + } + + if (!mIsComposeMode && mProgressRootView != null) { + mProgressRootView.setVisibility(View.VISIBLE); + } + if (mCurrentProgressMax <= 0) { + Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); + mCurrentProgressMax = 100; + } + + if (!mIsComposeMode && mProgressBar != null) { + mProgressBar.setMax(mCurrentProgressMax); + mProgressBar.setProgress(mCurrentProgress); + } + + if (mTrackedPackageName != null) { + loadIconInBackground(mTrackedPackageName, result -> { + Drawable drawable = result != null ? result.drawable : null; + boolean isAdaptive = result != null ? result.isAdaptive : false; + + mCurrentIcon = drawable; + mCurrentIconIsAdaptive = isAdaptive; + if (!mIsComposeMode && mIconView != null && drawable != null) { + mIconView.setImageDrawable(drawable); + } + if (mIsComposeMode) notifyStateCallback(); + }); + } + } + + private void updateNotificationProgressCompact() { + if (!mIsViewAttached && !mIsComposeMode) return; + + if (!mIsEnabled || !mIsTrackingProgress) { + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.GONE); + } + mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); + return; + } + + if (!mIsComposeMode && mCompactRootView != null) { + mCompactRootView.setVisibility(View.VISIBLE); + } + if (mCurrentProgressMax <= 0) { + Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); + mCurrentProgressMax = 100; + } + + if (!mIsComposeMode && mCircularProgressBar != null) { + mCircularProgressBar.setMax(mCurrentProgressMax); + mCircularProgressBar.setProgress(mCurrentProgress); + } + + if (mTrackedPackageName != null) { + loadIconInBackground(mTrackedPackageName, result -> { + Drawable drawable = result != null ? result.drawable : null; + boolean isAdaptive = result != null ? result.isAdaptive : false; + + mCurrentIcon = drawable; + mCurrentIconIsAdaptive = isAdaptive; + if (!mIsComposeMode && mCompactIconView != null && drawable != null) { + mCompactIconView.setImageDrawable(drawable); + } + if (mIsComposeMode) notifyStateCallback(); + }); + } + } + + private void loadIconInBackground(String packageName, IconCallback callback) { + if (packageName == null) return; + + if (mIconCache.containsKey(packageName)) { + IconFetcher.AdaptiveDrawableResult cachedResult = mIconCache.get(packageName); + if (cachedResult != null) { + callback.onIconLoaded(cachedResult); + return; + } + } + + mBackgroundExecutor.execute(() -> { + final IconFetcher.AdaptiveDrawableResult iconResult = + mIconFetcher.getMonotonicPackageIcon(packageName); + + if (iconResult != null && iconResult.drawable != null) { + if (mIsComposeMode) { + int sizePx = (int) (24 * mContext.getResources().getDisplayMetrics().density); + iconResult.drawable.setBounds(0, 0, sizePx, sizePx); + + if (iconResult.isAdaptive && iconResult.drawable instanceof AdaptiveIconDrawable) { + } + } + + mIconCache.put(packageName, iconResult); + + mHandler.post(() -> { + callback.onIconLoaded(iconResult); + }); + } + }); + } + + private interface IconCallback { + void onIconLoaded(@Nullable IconFetcher.AdaptiveDrawableResult result); + } + + private void extractProgress(Notification notification) { + Bundle extras = notification.extras; + mCurrentProgressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 100); + mCurrentProgress = extras.getInt(Notification.EXTRA_PROGRESS, 0); + } + + private void trackProgress(final StatusBarNotification sbn) { + mIsTrackingProgress = true; + mTrackedNotificationKey = sbn.getKey(); + mTrackedPackageName = sbn.getPackageName(); + mLastProgressUpdateTime = System.currentTimeMillis(); + extractProgress(sbn.getNotification()); + requestUiUpdate(); + } + + private void clearProgressTracking() { + mIsTrackingProgress = false; + mTrackedNotificationKey = null; + mTrackedPackageName = null; + mCurrentProgress = 0; + mCurrentProgressMax = 0; + mLastProgressUpdateTime = 0; + requestUiUpdate(); + } + + private void checkForStaleProgress() { + if (!mIsTrackingProgress || mTrackedNotificationKey == null) return; + + StatusBarNotification sbn = findNotificationByKey(mTrackedNotificationKey); + if (sbn == null) { + clearProgressTracking(); + return; + } + + if (!hasProgress(sbn.getNotification())) { + clearProgressTracking(); + return; + } + + if (mLastProgressUpdateTime == 0) { + mLastProgressUpdateTime = System.currentTimeMillis(); + return; + } + + if (System.currentTimeMillis() - mLastProgressUpdateTime > PROGRESS_TIMEOUT_MS + && mCurrentProgressMax > 0 + && mCurrentProgress >= mCurrentProgressMax) { + clearProgressTracking(); + } + } + + private void updateProgressIfNeeded(final StatusBarNotification sbn) { + if (!mIsTrackingProgress) return; + + if (sbn.getKey().equals(mTrackedNotificationKey)) { + if (!hasProgress(sbn.getNotification())) { + clearProgressTracking(); + return; + } + + mLastProgressUpdateTime = System.currentTimeMillis(); + extractProgress(sbn.getNotification()); + requestUiUpdate(); + } + } + + @Nullable + private StatusBarNotification findNotificationByKey(String key) { + if (key == null || mNotificationListener == null) return null; + + for (StatusBarNotification notification : mNotificationListener.getActiveNotifications()) { + if (notification.getKey().equals(key)) { + return notification; + } + } + return null; + } + + private static boolean hasProgress(@NonNull final Notification notification) { + Bundle extras = notification.extras; + if (extras == null) return false; + + boolean indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false); + boolean maxProgressValid = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0; + return extras.containsKey(Notification.EXTRA_PROGRESS) && + extras.containsKey(Notification.EXTRA_PROGRESS_MAX) && + !indeterminate && maxProgressValid; + } + + public void onInteraction() { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + if (mIsComposeMode) { + mIsMenuVisible = !mIsMenuVisible; + notifyStateCallback(); + if (mIsMenuVisible) { + mHandler.removeCallbacks(mMenuCollapseRunnable); + mHandler.postDelayed(mMenuCollapseRunnable, 5000); + } + } else { + showMediaPopup(mProgressRootView); + } + } else { + openTrackedApp(); + } + mVibrator.vibrate(VIBRATION_EFFECT); + } + + public void onLongPress() { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + openMediaApp(); + } else { + openTrackedApp(); + } + mVibrator.vibrate(VIBRATION_EFFECT); + } + + public void onDoubleTap() { + if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { + toggleMediaPlaybackState(); + mVibrator.vibrate(VIBRATION_EFFECT); + } + } + + public void onSwipe(boolean isNext) { + if (isNext) skipToNextTrack(); + else skipToPreviousTrack(); + } + + public void onMediaAction(int action) { + if (action == 0) skipToPreviousTrack(); + else if (action == 1) toggleMediaPlaybackState(); + else if (action == 2) skipToNextTrack(); + mHandler.removeCallbacks(mMenuCollapseRunnable); + mHandler.postDelayed(mMenuCollapseRunnable, 5000); + } + + public void onMediaMenuDismiss() { + mIsMenuVisible = false; + notifyStateCallback(); + } + + public void setSystemChipVisible(boolean visible) { + if (mIsSystemChipVisible != visible) { + mIsSystemChipVisible = visible; + notifyStateCallback(); + requestUiUpdate(); + } + } + + private void showMediaPopup(View anchorView) { + if (mIsComposeMode || anchorView == null) { + return; + } + if (mIsPopupActive) { + if (mMediaPopup != null) { + mMediaPopup.dismiss(); + } + mIsPopupActive = false; + return; + } + + Context context = anchorView.getContext(); + View popupView = LayoutInflater.from(context).inflate(R.layout.media_control_popup, null); + + if (mMediaPopup != null && mMediaPopup.isShowing()) { + mMediaPopup.dismiss(); + } + + mMediaPopup = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, true); + mMediaPopup.setOutsideTouchable(true); + mMediaPopup.setFocusable(true); + mMediaPopup.setOnDismissListener(() -> mIsPopupActive = false); + + ImageButton btnPrevious = popupView.findViewById(R.id.btn_previous); + ImageButton btnNext = popupView.findViewById(R.id.btn_next); + + if (btnPrevious != null) { + btnPrevious.setOnClickListener(v -> { + skipToPreviousTrack(); + mMediaPopup.dismiss(); + }); + } + + if (btnNext != null) { + btnNext.setOnClickListener(v -> { + skipToNextTrack(); + mMediaPopup.dismiss(); + }); + } + + anchorView.post(() -> { + if (!mIsViewAttached) return; + + int offsetX = -popupView.getWidth() / 3; + int offsetY = -anchorView.getHeight(); + mMediaPopup.showAsDropDown(anchorView, offsetX, offsetY); + mIsPopupActive = true; + }); + } + + private void openTrackedApp() { + if (mTrackedPackageName == null) { + Log.w(TAG, "No tracked package available"); + return; + } + + Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(mTrackedPackageName); + if (launchIntent != null) { + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mContext.startActivity(launchIntent); + } else { + Log.w(TAG, "No launch intent for package: " + mTrackedPackageName); + } + } + + private void onNotificationPosted(final StatusBarNotification sbn) { + if (sbn == null || !mIsEnabled) return; + + Notification notification = sbn.getNotification(); + if (notification == null) return; + + synchronized (this) { + boolean hasValidProgress = hasProgress(notification); + String currentKey = mTrackedNotificationKey; + + if (!hasValidProgress) { + if (currentKey != null && currentKey.equals(sbn.getKey())) { + clearProgressTracking(); + } + return; + } + + if (!mIsTrackingProgress) { + trackProgress(sbn); + } else if (sbn.getKey().equals(currentKey)) { + updateProgressIfNeeded(sbn); + } + } + } + + private void onNotificationRemoved(final StatusBarNotification sbn) { + if (sbn == null) return; + + synchronized (this) { + if (!mIsTrackingProgress) return; + + if (sbn.getKey().equals(mTrackedNotificationKey)) { + clearProgressTracking(); + return; + } + + if (sbn.getPackageName().equals(mTrackedPackageName)) { + StatusBarNotification currentSbn = findNotificationByKey(mTrackedNotificationKey); + if (currentSbn == null || !hasProgress(currentSbn.getNotification())) { + clearProgressTracking(); + } + } + } + } + + public void setForceHidden(final boolean forceHidden) { + if (mIsForceHidden != forceHidden) { + Log.d(TAG, "setForceHidden " + forceHidden); + mIsForceHidden = forceHidden; + notifyStateCallback(); + requestUiUpdate(); + } + } + + private void toggleMediaPlaybackState() { + if (mMediaSessionHelper != null) { + mMediaSessionHelper.toggleMediaPlaybackState(); + } + } + + private void skipToNextTrack() { + if (mMediaSessionHelper != null) { + mMediaSessionHelper.nextSong(); + } + } + + private void skipToPreviousTrack() { + if (mMediaSessionHelper != null) { + mMediaSessionHelper.prevSong(); + } + } + + private void openMediaApp() { + if (mMediaSessionHelper != null) { + mMediaSessionHelper.launchMediaApp(); + } + } + + @Override + public void onNotificationPosted(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap) { + onNotificationPosted(sbn); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap) { + onNotificationRemoved(sbn); + } + + @Override + public void onNotificationRemoved(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap, int _reason) { + onNotificationRemoved(sbn); + } + + @Override + public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { + mHeadsUpPinned = inPinnedMode; + notifyStateCallback(); + requestUiUpdate(); + } + + @Override + public void onNotificationRankingUpdate(NotificationListenerService.RankingMap _rankingMap) { + } + + @Override + public void onNotificationsInitialized() { + } + + @Override + public void onKeyguardShowingChanged() { + setForceHidden(mKeyguardStateController.isShowing()); + } + + private class SettingsObserver extends ContentObserver { + SettingsObserver(Handler handler) { super(handler); } + + @Override + public void onChange(boolean selfChange, Uri uri) { + super.onChange(selfChange, uri); + if (uri.equals(Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED)) || + uri.equals(Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS)) || + uri.equals(Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED))) { + updateSettings(); + } + } + + public void register() { + mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED), + false, this, UserHandle.USER_ALL); + mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS), + false, this, UserHandle.USER_ALL); + mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED), + false, this, UserHandle.USER_ALL); + updateSettings(); + } + + public void unregister() { + mContentResolver.unregisterContentObserver(this); + } + } + + private void updateSettings() { + boolean wasEnabled = mIsEnabled; + boolean wasShowingMedia = mShowMediaProgress; + boolean wasCompactMode = mIsCompactModeEnabled; + + mIsEnabled = Settings.System.getIntForUser(mContentResolver, + ONGOING_ACTION_CHIP_ENABLED, 0, UserHandle.USER_CURRENT) == 1; + mShowMediaProgress = Settings.System.getIntForUser(mContentResolver, + ONGOING_MEDIA_PROGRESS, 0, UserHandle.USER_CURRENT) == 1; + mIsCompactModeEnabled = Settings.System.getIntForUser(mContentResolver, + ONGOING_COMPACT_MODE_ENABLED, 0, UserHandle.USER_CURRENT) == 1; + + if (wasEnabled != mIsEnabled || wasShowingMedia != mShowMediaProgress || wasCompactMode != mIsCompactModeEnabled) { + mNeedsFullUiUpdate = true; + mIsExpanded = false; + } + + requestUiUpdate(); + } + + public void destroy() { + mIsViewAttached = false; + + mHandler.removeCallbacks(mStaleProgressChecker); + + mSettingsObserver.unregister(); + mKeyguardStateController.removeCallback(this); + mHeadsUpManager.removeListener(this); + mMediaSessionHelper.removeMediaMetadataListener(mMediaMetadataListener); + + mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); + mHandler.removeCallbacksAndMessages(null); + + if (mMediaPopup != null && mMediaPopup.isShowing()) { + mMediaPopup.dismiss(); + } + + synchronized (mLock) { + mIsTrackingProgress = false; + mTrackedNotificationKey = null; + mTrackedPackageName = null; + mIconCache.clear(); + } + + if (!mIsComposeMode && mIconView != null) { + mIconView.setImageDrawable(null); + } + + if (!mIsComposeMode && mCompactIconView != null) { + mCompactIconView.setImageDrawable(null); + } + mCurrentIcon = null; + + // Shutdown the background executor + if (mBackgroundExecutor instanceof ExecutorService) { + ((ExecutorService) mBackgroundExecutor).shutdown(); + } + } + + private static int getThemeColor(Context context, int attrResId) { + TypedValue typedValue = new TypedValue(); + context.getTheme().resolveAttribute(attrResId, typedValue, true); + return typedValue.data; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java new file mode 100644 index 000000000000..e385e4ab5b90 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java @@ -0,0 +1,48 @@ +/** + * Copyright (c) 2025, The LineageOS 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.statusbar; + +import androidx.annotation.Nullable; + +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; + +/** On-going action progress chip view group stores all elements of chip */ +public class OnGoingActionProgressGroup { + public final View rootView; + public final View compactRootView; + public final ImageView iconView; + public final ImageView compactIconView; + public final ProgressBar progressBarView; + public final ProgressBar circularProgressBarView; + + public OnGoingActionProgressGroup( + @Nullable View rootView, + @Nullable ImageView iconView, + @Nullable ProgressBar progressBarView, + @Nullable View compactRootView, + @Nullable ImageView compactIconView, + @Nullable ProgressBar circularProgressBarView) { + this.rootView = rootView; + this.iconView = iconView; + this.progressBarView = progressBarView; + this.compactRootView = compactRootView; + this.compactIconView = compactIconView; + this.circularProgressBarView = circularProgressBarView; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt new file mode 100644 index 000000000000..283229746b22 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt @@ -0,0 +1,377 @@ +/* + * Copyright (C) 2025 VoltageOS + * + * 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.statusbar + +import android.content.Context +import android.graphics.Bitmap +import android.util.Log +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.statusbar.VibratorHelper; +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager +import com.android.systemui.statusbar.policy.KeyguardStateController +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +private const val TAG = "OngoingActionProgressCompose" + +/** + * Composable that displays an ongoing action progress indicator in the status bar. + * Shows app icon and progress bar for notifications with progress information. + */ +@Composable +fun OngoingActionProgress( + controller: OnGoingActionProgressComposeController, + modifier: Modifier = Modifier +) { + val state by controller.state.collectAsState() + + val accentColor = MaterialTheme.colorScheme.primary + + AnimatedVisibility( + visible = state.isVisible, + enter = fadeIn(), + exit = fadeOut(), + modifier = modifier + ) { + Box( + contentAlignment = Alignment.Center + ) { + val progressValue = if (state.maxProgress > 0) { + (state.progress.toFloat() / state.maxProgress.toFloat()).coerceIn(0f, 1f) + } else { + 0f + } + + var dragOffset = 0f + val gestureModifier = Modifier.pointerInput(Unit) { + detectHorizontalDragGestures( + onDragStart = { dragOffset = 0f }, + onDragEnd = { + if (dragOffset < -50) controller.onSwipe(true) + else if (dragOffset > 50) controller.onSwipe(false) + } + ) { _, dragAmount -> + dragOffset += dragAmount + } + }.pointerInput(Unit) { + detectTapGestures( + onDoubleTap = { controller.onDoubleTap() }, + onLongPress = { controller.onLongPress() }, + onTap = { controller.onInteraction() } + ) + } + + if (state.isCompactMode) { + Box( + modifier = Modifier + .size(26.dp) + .then(gestureModifier), + contentAlignment = Alignment.Center + ) { + Canvas(modifier = Modifier.fillMaxSize()) { + val strokeWidthPx = 3.dp.toPx() + val diameter = size.minDimension - strokeWidthPx + val radius = diameter / 2 + val topLeftOffset = center - Offset(radius, radius) + val arcSize = Size(diameter, diameter) + + drawArc( + color = Color(0x33FFFFFF), + startAngle = 0f, + sweepAngle = 360f, + useCenter = false, + topLeft = topLeftOffset, + size = arcSize, + style = Stroke(width = strokeWidthPx) + ) + + drawArc( + color = accentColor, + startAngle = -90f, + sweepAngle = 360f * progressValue, + useCenter = false, + topLeft = topLeftOffset, + size = arcSize, + style = Stroke(width = strokeWidthPx, cap = StrokeCap.Round) + ) + } + + state.icon?.let { iconBitmap -> + Image( + bitmap = iconBitmap, + contentDescription = "App icon", + modifier = Modifier.size(14.dp) + .clip(RoundedCornerShape(14.dp)), + colorFilter = null + ) + } + } + } else { + Row( + modifier = Modifier + .width(86.dp) + .height(26.dp) + .padding(horizontal = 6.dp, vertical = 4.dp) + .then(gestureModifier), + verticalAlignment = Alignment.CenterVertically + ) { + state.icon?.let { iconBitmap -> + Image( + bitmap = iconBitmap, + contentDescription = "App icon", + modifier = Modifier + .size(16.dp) + .clip(RoundedCornerShape(16.dp)) // Clip to prevent rendering artifacts + .padding(start = 1.dp), + colorFilter = null + ) + + Spacer(modifier = Modifier.width(5.dp)) + } + + Box( + modifier = Modifier + .weight(1f) + .height(6.dp) + .padding(end = 3.dp) + .clip(RoundedCornerShape(3.dp)) + .background(Color(0x33FFFFFF)) + ) { + Box( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth(progressValue) + .background(accentColor) + ) + } + } + } + + if (state.showMediaControls) { + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = { controller.onMediaMenuDismiss() } + ) { + Row( + modifier = Modifier + .padding(top = 8.dp) + .width(140.dp) + .height(48.dp) + .shadow(8.dp, RoundedCornerShape(24.dp)) + .background(Color(0xFF202020), RoundedCornerShape(24.dp)) + .padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(0) }, contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(12.dp)) { + val path = Path().apply { + moveTo(size.width, 0f) + lineTo(0f, size.height / 2) + lineTo(size.width, size.height) + close() + } + drawPath(path, Color.White, style = Fill) + drawRect(Color.White, topLeft = Offset(0f, 0f), size = Size(2.dp.toPx(), size.height)) + } + } + + Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(1) }, contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(14.dp)) { + drawCircle(Color.White) + } + } + + Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(2) }, contentAlignment = Alignment.Center) { + Canvas(modifier = Modifier.size(12.dp)) { + val path = Path().apply { + moveTo(0f, 0f) + lineTo(size.width, size.height / 2) + lineTo(0f, size.height) + close() + } + drawPath(path, Color.White, style = Fill) + drawRect(Color.White, topLeft = Offset(size.width - 2.dp.toPx(), 0f), size = Size(2.dp.toPx(), size.height)) + } + } + } + } + } + } + } +} + +/** + * State data for the progress indicator + */ +data class ProgressState( + val isVisible: Boolean = false, + val progress: Int = 0, + val maxProgress: Int = 100, + val icon: androidx.compose.ui.graphics.ImageBitmap? = null, + val packageName: String? = null, + val isIconAdaptive: Boolean = false, + val isCompactMode: Boolean = false, + val showMediaControls: Boolean = false +) + +/** + * Compose-friendly controller that bridges the Java OnGoingActionProgressController + * to Compose state. + */ +class OnGoingActionProgressComposeController( + context: Context, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper +) { + private val _state = MutableStateFlow(ProgressState()) + val state: StateFlow = _state + + private val javaController: OnGoingActionProgressController + + init { + Log.d(TAG, "Initializing OnGoingActionProgressComposeController") + + val dummyGroup = OnGoingActionProgressGroup(null, null, null, null, null, null) + + try { + javaController = OnGoingActionProgressController( + context, + dummyGroup, + notificationListener, + keyguardStateController, + headsUpManager, + vibrator + ) + + javaController.setStateCallback { isVisible, progress, maxProgress, icon, isAdaptive, packageName, isCompact, showMenu -> + Log.d(TAG, "State callback: isVisible=$isVisible, compact=$isCompact, showMenu=$showMenu") + + val iconSizePx = if (isCompact) { + (14 * context.resources.displayMetrics.density).toInt() * 2 + } else { + (16 * context.resources.displayMetrics.density).toInt() * 2 + } + + val iconBitmap = try { + icon?.let { drawable -> + drawable.toBitmap( + width = iconSizePx, + height = iconSizePx, + config = Bitmap.Config.ARGB_8888 + ).asImageBitmap() + } + } catch (e: Exception) { + Log.e(TAG, "Failed to convert icon to bitmap", e) + null + } + + _state.value = ProgressState( + isVisible = isVisible, + progress = progress, + maxProgress = maxProgress, + icon = iconBitmap, + packageName = packageName, + isIconAdaptive = isAdaptive, + isCompactMode = isCompact, + showMediaControls = showMenu + ) + } + + Log.d(TAG, "OnGoingActionProgressComposeController initialized successfully") + } catch (e: Exception) { + Log.e(TAG, "Failed to initialize OnGoingActionProgressController", e) + throw e + } + } + + fun destroy() { + javaController.destroy() + } + + fun onInteraction() { + javaController.onInteraction() + } + + fun onMediaAction(action: Int) { + javaController.onMediaAction(action) + } + + fun onMediaMenuDismiss() { + javaController.onMediaMenuDismiss() + } + + fun onDoubleTap() { + javaController.onDoubleTap() + } + + fun onSwipe(isNext: Boolean) { + javaController.onSwipe(isNext) + } + + fun onLongPress() { + javaController.onLongPress() + } + + fun setSystemChipVisible(visible: Boolean) { + javaController.setSystemChipVisible(visible) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index c5aeb3cc1c7d..e2f6550df1e0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -203,15 +203,18 @@ import com.android.systemui.statusbar.LiftReveal; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.LockscreenShadeTransitionController; +import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.OnGoingActionProgressController; import com.android.systemui.statusbar.PowerButtonReveal; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.core.StatusBarInitializer; import com.android.systemui.statusbar.core.StatusBarRootModernization; @@ -417,6 +420,11 @@ public QSPanelController getQSPanelController() { private final LightRevealScrim mLightRevealScrim; private PowerButtonReveal mPowerButtonReveal; + private OnGoingActionProgressController mOnGoingActionProgressController = null; + private final VibratorHelper mVibratorHelper; + + @Inject public NotificationListener mNotificationListener; + /** * Whether we should delay the AOD->Lockscreen animation. * If false, the animation will start in onStartedWakingUp(). @@ -761,7 +769,8 @@ public CentralSurfacesImpl( EdgeLightViewController edgeLightViewController, NowPlayingViewController nowPlayingViewController, ChargingAnimationViewController chargingAnimationViewController, - BurnInProtectionController burnInProtectionController + BurnInProtectionController burnInProtectionController, + VibratorHelper vibrator ) { mContext = context; mNotificationsController = notificationsController; @@ -915,6 +924,7 @@ public CentralSurfacesImpl( mEdgeLightViewController = edgeLightViewController; mNowPlayingViewController = nowPlayingViewController; mChargingAnimationViewController = chargingAnimationViewController; + mVibratorHelper = vibrator; } private void initBubbles(Bubbles bubbles) { @@ -1348,6 +1358,13 @@ protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) { setBouncerShowingForStatusBarComponents(mBouncerShowing); checkBarModes(); mBurnInProtectionController.setPhoneStatusBarView(mPhoneStatusBarViewController.getPhoneStatusBarView()); + if (!StatusBarRootModernization.isEnabled()) { + mOnGoingActionProgressController = + new OnGoingActionProgressController( + mContext, + statusBarViewController.getOngoingActionProgressGroup(), mNotificationListener, + mKeyguardStateController, mHeadsUpManager, mVibratorHelper); + } }); } if (!StatusBarRootModernization.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 63be7d4d88e7..b964e5bf8515 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -24,6 +24,8 @@ import android.view.InputDevice import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration +import android.widget.ImageView +import android.widget.ProgressBar import androidx.annotation.VisibleForTesting import com.android.systemui.Gefingerpoken import com.android.systemui.battery.BatteryMeterView @@ -45,6 +47,7 @@ import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.OnGoingActionProgressGroup import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore @@ -350,6 +353,18 @@ private constructor( darkIconDispatcher.removeDarkReceiver(clockRight) } + fun getOngoingActionProgressGroup(): OnGoingActionProgressGroup { + return OnGoingActionProgressGroup( + mView.findViewById(R.id.status_bar_ongoing_action_chip), + mView.findViewById(R.id.ongoing_action_app_icon) as ImageView, + mView.findViewById(R.id.app_action_progress) as ProgressBar, + + mView.findViewById(R.id.status_bar_ongoing_action_chip_compact), + mView.findViewById(R.id.ongoing_action_app_icon_compact) as ImageView, + mView.findViewById(R.id.circular_progress) as ProgressBar + ) + } + fun getPhoneStatusBarView(): PhoneStatusBarView { return mView } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index 805692bb5f3c..9e8612983c5f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -76,11 +76,15 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ui.composable.VariableDayDate +import com.android.systemui.statusbar.NotificationListener +import com.android.systemui.statusbar.OngoingActionProgress import com.android.systemui.statusbar.StatusBarAlwaysUseRegionSampling +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.core.RudimentaryBattery import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.core.StatusBarForDesktop import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips @@ -96,6 +100,7 @@ import com.android.systemui.statusbar.phone.StatusIconContainer import com.android.systemui.statusbar.phone.domain.interactor.IsAreaDark import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization +import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.statusbar.phone.ui.DarkIconManager import com.android.systemui.statusbar.phone.ui.StatusBarIconController import com.android.systemui.statusbar.phone.ui.TintedIconManager @@ -143,6 +148,10 @@ constructor( @DisplayAware private val homeStatusBarViewBinder: HomeStatusBarViewBinder, @DisplayAware private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, + private val notificationListener: NotificationListener, + private val keyguardStateController: KeyguardStateController, + private val headsUpManager: HeadsUpManager, + private val vibrator: VibratorHelper, ) { fun create(root: ViewGroup, andThen: (ViewGroup) -> Unit): ComposeView { val composeView = ComposeView(root.context) @@ -168,6 +177,10 @@ constructor( statusBarRegionSamplingViewModelFactory = statusBarRegionSamplingViewModelFactory, onViewCreated = andThen, + notificationListener = notificationListener, + keyguardStateController = keyguardStateController, + headsUpManager = headsUpManager, + vibrator = vibrator, modifier = Modifier.sysUiResTagContainer(), ) } @@ -207,6 +220,10 @@ fun StatusBarRoot( mediaViewModelFactory: MediaViewModel.Factory, statusBarRegionSamplingViewModelFactory: StatusBarRegionSamplingViewModel.Factory, onViewCreated: (ViewGroup) -> Unit, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper, modifier: Modifier = Modifier, ) { val displayId = parent.context.displayId @@ -266,6 +283,10 @@ fun StatusBarRoot( statusBarViewModel = statusBarViewModel, iconViewStore = iconViewStore, appHandlesViewModel = appHandlesViewModel, + notificationListener = notificationListener, + keyguardStateController = keyguardStateController, + headsUpManager = headsUpManager, + vibrator = vibrator, context = context, ) } @@ -407,6 +428,10 @@ private fun addStartSideComposable( statusBarViewModel: HomeStatusBarViewModel, iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, appHandlesViewModel: AppHandlesViewModel, + notificationListener: NotificationListener, + keyguardStateController: KeyguardStateController, + headsUpManager: HeadsUpManager, + vibrator: VibratorHelper, context: Context, ) { val startSideExceptHeadsUp = @@ -425,9 +450,7 @@ private fun addStartSideComposable( LinearLayout.LayoutParams.WRAP_CONTENT, ) .apply { - if (showDate) { - gravity = android.view.Gravity.CENTER_VERTICAL - } + gravity = android.view.Gravity.CENTER_VERTICAL } setContent { @@ -488,7 +511,22 @@ private fun addStartSideComposable( ) } + val progressController = remember { + com.android.systemui.statusbar.OnGoingActionProgressComposeController( + context, + notificationListener, + keyguardStateController, + headsUpManager, + vibrator + ) + } + val chipsVisibilityModel = statusBarViewModel.ongoingActivityChips + val hasSystemChips = chipsVisibilityModel.chips.active.isNotEmpty() + progressController.setSystemChipVisible(hasSystemChips) + + OngoingActionProgress(controller = progressController) + if (chipsVisibilityModel.areChipsAllowed) { OngoingActivityChips( chips = chipsVisibilityModel.chips, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt new file mode 100644 index 000000000000..380155c5e173 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/util/MediaSessionManagerHelper.kt @@ -0,0 +1,263 @@ +/* +* Copyright (C) 2023-2024 The risingOS Android Project +* 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.statusbar.util + +import android.content.Context +import android.content.res.Configuration +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.drawable.Drawable +import android.media.MediaMetadata +import android.media.session.MediaController +import android.media.session.MediaSessionLegacyHelper +import android.media.session.MediaSessionManager +import android.media.session.PlaybackState +import android.os.SystemClock +import android.provider.Settings +import android.text.TextUtils +import android.view.KeyEvent +import android.view.View + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* + +class MediaSessionManagerHelper private constructor(private val context: Context) { + + interface MediaMetadataListener { + fun onMediaMetadataChanged() {} + fun onPlaybackStateChanged() {} + } + + private val _mediaMetadata = MutableStateFlow(null) + val mediaMetadata: StateFlow = _mediaMetadata + + private val _playbackState = MutableStateFlow(null) + val playbackState: StateFlow = _playbackState + + private val scope = CoroutineScope(Dispatchers.Main) + private var collectJob: Job? = null + + private var lastSavedPackageName: String? = null + private val mediaSessionManager: MediaSessionManager = context.getSystemService(MediaSessionManager::class.java)!! + private var activeController: MediaController? = null + private val listeners = mutableSetOf() + + private val mediaControllerCallback = object : MediaController.Callback() { + override fun onMetadataChanged(metadata: MediaMetadata?) { + _mediaMetadata.value = metadata + } + + override fun onPlaybackStateChanged(state: PlaybackState?) { + _playbackState.value = state + } + } + + private val tickerFlow = flow { + while (true) { + emit(Unit) + delay(1000) + } + }.flowOn(Dispatchers.Default) + + init { + lastSavedPackageName = Settings.System.getString( + context.contentResolver, + "media_session_last_package_name" + ) + + scope.launch { + tickerFlow + .map { fetchActiveController() } + .distinctUntilChanged { old, new -> sameSessions(old, new) } + .collect { controller -> + activeController?.unregisterCallback(mediaControllerCallback) + activeController = controller + controller?.registerCallback(mediaControllerCallback) + _mediaMetadata.value = controller?.metadata + _playbackState.value = controller?.playbackState + saveLastNonNullPackageName() + } + } + } + + private suspend fun fetchActiveController(): MediaController? = withContext(Dispatchers.IO) { + var localController: MediaController? = null + val remoteSessions = mutableSetOf() + + mediaSessionManager.getActiveSessions(null) + .filter { controller -> + controller.playbackState?.state == PlaybackState.STATE_PLAYING && + controller.playbackInfo != null + } + .forEach { controller -> + when (controller.playbackInfo?.playbackType) { + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> { + remoteSessions.add(controller.packageName) + if (localController?.packageName == controller.packageName) { + localController = null + } + } + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { + if (!remoteSessions.contains(controller.packageName)) { + localController = localController ?: controller + } + } + } + } + localController + } + + fun addMediaMetadataListener(listener: MediaMetadataListener) { + listeners.add(listener) + if (listeners.size == 1) { + startCollecting() + } + listener.onMediaMetadataChanged() + listener.onPlaybackStateChanged() + } + + fun removeMediaMetadataListener(listener: MediaMetadataListener) { + listeners.remove(listener) + if (listeners.isEmpty()) { + stopCollecting() + } + } + + private fun startCollecting() { + collectJob = scope.launch { + launch { mediaMetadata.collect { notifyListeners { onMediaMetadataChanged() } } } + launch { playbackState.collect { notifyListeners { onPlaybackStateChanged() } } } + } + } + + private fun stopCollecting() { + collectJob?.cancel() + collectJob = null + } + + private fun notifyListeners(action: MediaMetadataListener.() -> Unit) { + listeners.forEach { it.action() } + } + + fun seekTo(time: Long) { + activeController?.transportControls?.seekTo(time) + } + + fun getTotalDuration() = mediaMetadata.value?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L + + private fun saveLastNonNullPackageName() { + activeController?.packageName?.takeIf { it.isNotEmpty() }?.let { pkg -> + if (pkg != lastSavedPackageName) { + Settings.System.putString( + context.contentResolver, + "media_session_last_package_name", + pkg + ) + lastSavedPackageName = pkg + } + } + } + + fun getMediaBitmap(): Bitmap? = mediaMetadata.value?.let { + it.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_ART) ?: + it.getBitmap(MediaMetadata.METADATA_KEY_DISPLAY_ICON) + } + + fun getCurrentMediaMetadata(): MediaMetadata? { + return mediaMetadata.value + } + + fun getMediaAppIcon(): Drawable? { + val packageName = activeController?.packageName ?: return null + return try { + val pm = context.packageManager + pm.getApplicationIcon(packageName) + } catch (e: PackageManager.NameNotFoundException) { + null + } + } + + fun isMediaControllerAvailable() = activeController?.packageName?.isNotEmpty() ?: false + + fun isMediaPlaying() = playbackState.value?.state == PlaybackState.STATE_PLAYING + + fun getMediaControllerPlaybackState(): PlaybackState? { + return activeController?.playbackState ?: null + } + + private fun sameSessions(a: MediaController?, b: MediaController?): Boolean { + if (a == b) return true + if (a == null) return false + return a.controlsSameSession(b) + } + + private fun dispatchMediaKeyWithWakeLockToMediaSession(keycode: Int) { + val helper = MediaSessionLegacyHelper.getHelper(context) ?: return + var event = KeyEvent( + SystemClock.uptimeMillis(), + SystemClock.uptimeMillis(), + KeyEvent.ACTION_DOWN, + keycode, + 0 + ) + helper.sendMediaButtonEvent(event, true) + event = KeyEvent.changeAction(event, KeyEvent.ACTION_UP) + helper.sendMediaButtonEvent(event, true) + } + + fun prevSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PREVIOUS) + } + + fun nextSong() { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_NEXT) + } + + fun toggleMediaPlaybackState() { + if (isMediaPlaying()) { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PAUSE) + } else { + dispatchMediaKeyWithWakeLockToMediaSession(KeyEvent.KEYCODE_MEDIA_PLAY) + } + } + + fun launchMediaApp() { + lastSavedPackageName?.takeIf { it.isNotEmpty() }?.let { + launchMediaPlayerApp(it) + } + } + + fun launchMediaPlayerApp(packageName: String) { + if (packageName.isNotEmpty()) { + val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName) + launchIntent?.let { intent -> + context.startActivity(intent) + } + } + } + + companion object { + @Volatile + private var instance: MediaSessionManagerHelper? = null + + fun getInstance(context: Context): MediaSessionManagerHelper = + instance ?: synchronized(this) { + instance ?: MediaSessionManagerHelper(context).also { instance = it } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java b/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java new file mode 100644 index 000000000000..f99d1ca1724e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2025, The LineageOS 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.util; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; + +/** A class helping to fetch different versions of icons @LineageExtension */ +public class IconFetcher { + + /** A class which stores wether icon is adaptive and icon itself. */ + public class AdaptiveDrawableResult { + public boolean isAdaptive; + public Drawable drawable; + + public AdaptiveDrawableResult(boolean isAdaptive, Drawable drawable) { + this.isAdaptive = isAdaptive; + this.drawable = drawable; + } + } + + private final Context mContext; + + public IconFetcher(Context context) { + mContext = context; + } + + /** + * Gets a standard package icon + * + * @param packageName name of package for which icon would be fetched + */ + public Drawable getPackageIcon(String packageName) { + PackageManager packageManager = mContext.getPackageManager(); + try { + return packageManager.getApplicationIcon(packageName); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return mContext.getDrawable(android.R.drawable.sym_def_app_icon); + } + } + + /** + * Returns a monotonic version of the app icon as a Drawable. The foreground of adaptive icons + * is extracted and tinted, while non-adaptive icons are directly tinted. + * + * @param packageName The package name of the app whose icon is to be fetched. + * @param tintColor The color to use for the monotonic tint. + * @return A monotonic Drawable of the app icon or standard app icon within + * AdaptiveDrawableResult + */ + public AdaptiveDrawableResult getMonotonicPackageIcon(String packageName) { + try { + PackageManager packageManager = mContext.getPackageManager(); + Drawable icon = packageManager.getApplicationIcon(packageName); + + if (icon instanceof AdaptiveIconDrawable) { + AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon; + + Drawable foreground = adaptiveIcon.getForeground(); + + return new AdaptiveDrawableResult(true, icon); + } else { + return new AdaptiveDrawableResult(false, icon); + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + Drawable defaultIcon = mContext.getDrawable(android.R.drawable.sym_def_app_icon); + // The icon is not adaptive by default + return new AdaptiveDrawableResult(false, defaultIcon); + } + } +} From 009590bc7af75f2bf766a28671b404b39e780066 Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Sun, 1 Mar 2026 22:20:07 +0530 Subject: [PATCH 152/190] SystemUI: Clean up legacy code in ongoing action progress chip Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../res/drawable-night/popup_background.xml | 7 - .../action_chip_compact_background.xml | 5 - .../drawable/circular_progress_drawable.xml | 19 -- .../res/drawable/ic_media_output_next.xml | 15 - .../res/drawable/ic_media_output_pause.xml | 21 -- .../res/drawable/ic_media_output_play.xml | 15 - .../res/drawable/ic_media_output_prev.xml | 15 - .../res/drawable/popup_background.xml | 7 - .../res/layout/media_control_popup.xml | 28 -- packages/SystemUI/res/layout/status_bar.xml | 6 - .../layout/status_bar_ongoing_action_chip.xml | 60 ---- ...status_bar_ongoing_action_chip_compact.xml | 53 --- .../SystemUI/res/values/lunaris_colors.xml | 5 - .../OnGoingActionProgressController.java | 316 +----------------- .../statusbar/OnGoingActionProgressGroup.java | 48 --- .../statusbar/OngoingActionProgressCompose.kt | 3 - .../statusbar/phone/CentralSurfacesImpl.java | 19 +- .../phone/PhoneStatusBarViewController.kt | 15 - 18 files changed, 17 insertions(+), 640 deletions(-) delete mode 100644 packages/SystemUI/res/drawable-night/popup_background.xml delete mode 100644 packages/SystemUI/res/drawable/action_chip_compact_background.xml delete mode 100644 packages/SystemUI/res/drawable/circular_progress_drawable.xml delete mode 100644 packages/SystemUI/res/drawable/ic_media_output_next.xml delete mode 100644 packages/SystemUI/res/drawable/ic_media_output_pause.xml delete mode 100644 packages/SystemUI/res/drawable/ic_media_output_play.xml delete mode 100644 packages/SystemUI/res/drawable/ic_media_output_prev.xml delete mode 100644 packages/SystemUI/res/drawable/popup_background.xml delete mode 100644 packages/SystemUI/res/layout/media_control_popup.xml delete mode 100644 packages/SystemUI/res/layout/status_bar_ongoing_action_chip.xml delete mode 100644 packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java diff --git a/packages/SystemUI/res/drawable-night/popup_background.xml b/packages/SystemUI/res/drawable-night/popup_background.xml deleted file mode 100644 index 1ff16466e5ac..000000000000 --- a/packages/SystemUI/res/drawable-night/popup_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/packages/SystemUI/res/drawable/action_chip_compact_background.xml b/packages/SystemUI/res/drawable/action_chip_compact_background.xml deleted file mode 100644 index 48f0abe63f5f..000000000000 --- a/packages/SystemUI/res/drawable/action_chip_compact_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/drawable/circular_progress_drawable.xml b/packages/SystemUI/res/drawable/circular_progress_drawable.xml deleted file mode 100644 index aaa99554ce69..000000000000 --- a/packages/SystemUI/res/drawable/circular_progress_drawable.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/SystemUI/res/drawable/ic_media_output_next.xml b/packages/SystemUI/res/drawable/ic_media_output_next.xml deleted file mode 100644 index fe97f868ca1e..000000000000 --- a/packages/SystemUI/res/drawable/ic_media_output_next.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/drawable/ic_media_output_pause.xml b/packages/SystemUI/res/drawable/ic_media_output_pause.xml deleted file mode 100644 index 9d784ddbf662..000000000000 --- a/packages/SystemUI/res/drawable/ic_media_output_pause.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - diff --git a/packages/SystemUI/res/drawable/ic_media_output_play.xml b/packages/SystemUI/res/drawable/ic_media_output_play.xml deleted file mode 100644 index 3f53bc76a210..000000000000 --- a/packages/SystemUI/res/drawable/ic_media_output_play.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/drawable/ic_media_output_prev.xml b/packages/SystemUI/res/drawable/ic_media_output_prev.xml deleted file mode 100644 index 188ae7e1f88d..000000000000 --- a/packages/SystemUI/res/drawable/ic_media_output_prev.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - diff --git a/packages/SystemUI/res/drawable/popup_background.xml b/packages/SystemUI/res/drawable/popup_background.xml deleted file mode 100644 index e1f64332990e..000000000000 --- a/packages/SystemUI/res/drawable/popup_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/packages/SystemUI/res/layout/media_control_popup.xml b/packages/SystemUI/res/layout/media_control_popup.xml deleted file mode 100644 index 7e8a1edf6ecf..000000000000 --- a/packages/SystemUI/res/layout/media_control_popup.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - diff --git a/packages/SystemUI/res/layout/status_bar.xml b/packages/SystemUI/res/layout/status_bar.xml index 9611ed077b06..dd73e6cdbce2 100644 --- a/packages/SystemUI/res/layout/status_bar.xml +++ b/packages/SystemUI/res/layout/status_bar.xml @@ -120,12 +120,6 @@ android:id="@+id/ongoing_activity_chip_secondary" android:visibility="gone"/> - - - - - - - - - - - - - - diff --git a/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml b/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml deleted file mode 100644 index 06875f7362eb..000000000000 --- a/packages/SystemUI/res/layout/status_bar_ongoing_action_chip_compact.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - diff --git a/packages/SystemUI/res/values/lunaris_colors.xml b/packages/SystemUI/res/values/lunaris_colors.xml index af99caad8fde..424d31585bdc 100644 --- a/packages/SystemUI/res/values/lunaris_colors.xml +++ b/packages/SystemUI/res/values/lunaris_colors.xml @@ -13,9 +13,4 @@ #89000000 @android:color/system_accent2_200 @android:color/system_accent2_700 - - - @*android:color/system_accent1_400 - #33FFFFFF - #33000000 diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java index e7bc4f6f7457..e00c7fe70901 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java @@ -20,11 +20,9 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; -import android.content.res.ColorStateList; import android.database.ContentObserver; import android.graphics.drawable.Drawable; import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.BitmapDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -35,16 +33,6 @@ import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.util.Log; -import android.util.TypedValue; -import android.view.GestureDetector; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.PopupWindow; -import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -53,7 +41,6 @@ import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; import com.android.systemui.res.R; import com.android.systemui.util.IconFetcher; -import com.android.systemui.statusbar.OnGoingActionProgressGroup; import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.util.MediaSessionManagerHelper; @@ -69,11 +56,8 @@ public class OnGoingActionProgressController implements NotificationListener.Not private static final String ONGOING_ACTION_CHIP_ENABLED = "ongoing_action_chip"; private static final String ONGOING_MEDIA_PROGRESS = "ongoing_media_progress"; private static final String ONGOING_COMPACT_MODE_ENABLED = "ongoing_compact_mode"; - private static final int SWIPE_THRESHOLD = 100; - private static final int SWIPE_VELOCITY_THRESHOLD = 100; private static final int MEDIA_UPDATE_INTERVAL_MS = 1000; private static final int DEBOUNCE_DELAY_MS = 150; - private static final int MAX_ICON_CACHE_SIZE = 20; private static final int STALE_PROGRESS_CHECK_INTERVAL_MS = 5000; private static final int PROGRESS_TIMEOUT_MS = 30000; @@ -98,7 +82,6 @@ void onStateChanged(boolean isVisible, int progress, int maxProgress, private final MediaSessionManagerHelper mMediaSessionHelper; private final Executor mBackgroundExecutor; private StateCallback mStateCallback = null; - private final boolean mIsComposeMode; private final Object mLock = new Object(); private final Runnable mUiUpdateRunnable = new Runnable() { @Override @@ -111,13 +94,6 @@ public void run() { } }; - private final ProgressBar mProgressBar; - private final ProgressBar mCircularProgressBar; - private final View mProgressRootView; - private final View mCompactRootView; - private final ImageView mIconView; - private final ImageView mCompactIconView; - private final HashMap mIconCache = new HashMap<>(); private boolean mShowMediaProgress = true; @@ -136,8 +112,6 @@ public void run() { private String mTrackedNotificationKey; private String mTrackedPackageName; - private PopupWindow mMediaPopup; - private boolean mIsPopupActive = false; private boolean mNeedsFullUiUpdate = true; private boolean mIsViewAttached = false; private boolean mIsExpanded = false; @@ -145,7 +119,6 @@ public void run() { private boolean mUpdatePending = false; private long mLastUpdateTime = 0; - private final GestureDetector mGestureDetector; private final Handler mMediaProgressHandler = new Handler(Looper.getMainLooper()); private final Runnable mMediaProgressRunnable = new Runnable() { @Override @@ -197,17 +170,10 @@ public void onPlaybackStateChanged() { }; public OnGoingActionProgressController( - Context context, OnGoingActionProgressGroup progressGroup, + Context context, NotificationListener notificationListener, KeyguardStateController keyguardStateController, HeadsUpManager headsUpManager, VibratorHelper vibrator) { - mIsComposeMode = (progressGroup.rootView == null && progressGroup.compactRootView == null); - - if (progressGroup == null) { - Log.wtf(TAG, "progressGroup is null"); - throw new IllegalArgumentException("progressGroup cannot be null"); - } - mNotificationListener = notificationListener; if (mNotificationListener == null) { Log.wtf(TAG, "mNotificationListener is null"); @@ -223,36 +189,14 @@ public OnGoingActionProgressController( mBackgroundExecutor = Executors.newSingleThreadExecutor(); mVibrator = vibrator; - mProgressBar = progressGroup.progressBarView; - mCircularProgressBar = progressGroup.circularProgressBarView; - mProgressRootView = progressGroup.rootView; - mCompactRootView = progressGroup.compactRootView; - mIconView = progressGroup.iconView; - mCompactIconView = progressGroup.compactIconView; - mIconFetcher = new IconFetcher(context); mMediaSessionHelper = MediaSessionManagerHelper.Companion.getInstance(context); - mGestureDetector = mIsComposeMode ? null : new GestureDetector(mContext, new MediaGestureListener()); mKeyguardStateController.addCallback(this); mHeadsUpManager.addListener(this); mNotificationListener.addNotificationHandler(this); mSettingsObserver.register(); - if (!mIsComposeMode) { - if (mProgressRootView != null && mGestureDetector != null) { - mProgressRootView.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event)); - } - - if (mCompactRootView != null && mGestureDetector != null) { - mCompactRootView.setOnTouchListener((v, event) -> mGestureDetector.onTouchEvent(event)); - - mCompactRootView.setOnClickListener(v -> { - onInteraction(); - }); - } - } - mMediaSessionHelper.addMediaMetadataListener(mMediaMetadataListener); mIsViewAttached = true; @@ -277,58 +221,8 @@ public void expandCompactView() { mHandler.removeCallbacks(mCompactCollapseRunnable); mHandler.postDelayed(mCompactCollapseRunnable, 5000); - if (mIsComposeMode) { - notifyStateCallback(); - return; - } - - if (mCompactRootView != null) mCompactRootView.setVisibility(View.GONE); - if (mProgressRootView != null) mProgressRootView.setVisibility(View.VISIBLE); - - requestUiUpdate(); - } - - private class MediaGestureListener extends GestureDetector.SimpleOnGestureListener { - @Override - public boolean onSingleTapConfirmed(MotionEvent e) { - onInteraction(); - return true; - } - - @Override - public boolean onDoubleTap(MotionEvent e) { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - toggleMediaPlaybackState(); - } - mVibrator.vibrate(VIBRATION_EFFECT); - return true; - } - - @Override - public void onLongPress(MotionEvent e) { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - openMediaApp(); - } - mVibrator.vibrate(VIBRATION_EFFECT); - } - - @Override - public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { - if (!(mShowMediaProgress && mMediaSessionHelper.isMediaPlaying())) { - return false; - } - float diffX = e2.getX() - e1.getX(); - if (Math.abs(diffX) > Math.abs(e2.getY() - e1.getY()) && - Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) { - if (diffX > 0) { - skipToNextTrack(); - } else { - skipToPreviousTrack(); - } - return true; - } - return false; - } + notifyStateCallback(); + return; } private void requestUiUpdate() { @@ -374,17 +268,11 @@ private void notifyStateCallback() { private void updateViews() { if (!mIsViewAttached) { - if (mIsComposeMode) { - notifyStateCallback(); - } + notifyStateCallback(); return; } if (mIsForceHidden || mHeadsUpPinned) { - if (!mIsComposeMode) { - if (mProgressRootView != null) mProgressRootView.setVisibility(View.GONE); - if (mCompactRootView != null) mCompactRootView.setVisibility(View.GONE); - } notifyStateCallback(); return; } @@ -392,37 +280,18 @@ private void updateViews() { boolean isMediaPlaying = mShowMediaProgress && mMediaSessionHelper.isMediaPlaying(); if (mIsCompactModeEnabled && !mIsExpanded) { - if (!mIsComposeMode && mProgressRootView != null) { - mProgressRootView.setVisibility(View.GONE); - } - if (!mIsEnabled && !isMediaPlaying) { - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.GONE); - } notifyStateCallback(); return; } - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.VISIBLE); - } - if (isMediaPlaying) { updateMediaProgressCompact(); } else { updateNotificationProgressCompact(); } } else { - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.GONE); - } - if (isMediaPlaying) { - if (!mIsComposeMode && mProgressRootView != null) { - mProgressRootView.setVisibility(View.VISIBLE); - } - if (mNeedsFullUiUpdate) { updateMediaProgressFull(); mNeedsFullUiUpdate = false; @@ -437,10 +306,6 @@ private void updateViews() { } private void updateMediaProgressOnly() { - if (!mIsViewAttached && !mIsComposeMode) { - return; - } - long totalDuration = mMediaSessionHelper.getTotalDuration(); android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); @@ -454,30 +319,10 @@ private void updateMediaProgressOnly() { mCurrentProgressMax = (int) totalDuration; if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; - if (!mIsComposeMode && mProgressRootView != null && - mProgressRootView.getVisibility() == View.VISIBLE && mProgressBar != null && totalDuration > 0) { - mProgressBar.setMax((int) totalDuration); - mProgressBar.setProgress((int) currentProgress); - } - - if (!mIsComposeMode && mCompactRootView != null && - mCompactRootView.getVisibility() == View.VISIBLE && mCircularProgressBar != null && totalDuration > 0) { - mCircularProgressBar.setMax((int) totalDuration); - mCircularProgressBar.setProgress((int) currentProgress); - } - - if (mIsComposeMode) { - notifyStateCallback(); - } + notifyStateCallback(); } private void updateMediaProgressFull() { - if (!mIsViewAttached && !mIsComposeMode) return; - - if (!mIsComposeMode && mProgressRootView != null) { - mProgressRootView.setVisibility(View.VISIBLE); - } - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); mMediaProgressHandler.post(mMediaProgressRunnable); @@ -486,7 +331,6 @@ private void updateMediaProgressFull() { if (mediaAppIcon != null) { mCurrentIcon = mediaAppIcon; mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; - if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(mediaAppIcon); } else { String packageName = null; @@ -502,11 +346,10 @@ private void updateMediaProgressFull() { if (drawable != null) { mCurrentIcon = drawable; mCurrentIconIsAdaptive = isAdaptive; - if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(drawable); } else { setDefaultMediaIcon(); } - if (mIsComposeMode) notifyStateCallback(); + notifyStateCallback(); }); } else { setDefaultMediaIcon(); @@ -519,16 +362,9 @@ private void updateMediaProgressFull() { private void setDefaultMediaIcon() { mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); mCurrentIconIsAdaptive = false; - if (!mIsComposeMode && mIconView != null) mIconView.setImageDrawable(mCurrentIcon); } private void updateMediaProgressCompact() { - if (!mIsViewAttached && !mIsComposeMode) return; - - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.VISIBLE); - } - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); mMediaProgressHandler.post(mMediaProgressRunnable); @@ -545,19 +381,11 @@ private void updateMediaProgressCompact() { mCurrentProgressMax = (int) totalDuration; if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; - if (!mIsComposeMode && totalDuration > 0 && mCircularProgressBar != null) { - mCircularProgressBar.setMax((int) totalDuration); - mCircularProgressBar.setProgress((int) currentProgress); - } - Drawable mediaAppIcon = mMediaSessionHelper.getMediaAppIcon(); if (mediaAppIcon != null) { mCurrentIcon = mediaAppIcon; mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; - if (!mIsComposeMode && mCompactIconView != null) { - mCompactIconView.setImageDrawable(mediaAppIcon); - } } else { String packageName = null; if (playbackState != null && playbackState.getExtras() != null) { @@ -572,11 +400,10 @@ private void updateMediaProgressCompact() { if (drawable != null) { mCurrentIcon = drawable; mCurrentIconIsAdaptive = isAdaptive; - if (!mIsComposeMode && mCompactIconView != null) mCompactIconView.setImageDrawable(drawable); } else { setDefaultMediaIconCompact(); } - if (mIsComposeMode) notifyStateCallback(); + notifyStateCallback(); }); } else { setDefaultMediaIconCompact(); @@ -587,33 +414,19 @@ private void updateMediaProgressCompact() { private void setDefaultMediaIconCompact() { mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); mCurrentIconIsAdaptive = false; - if (!mIsComposeMode && mCompactIconView != null) mCompactIconView.setImageDrawable(mCurrentIcon); } private void updateNotificationProgress() { - if (!mIsViewAttached && !mIsComposeMode) return; - if (!mIsEnabled || !mIsTrackingProgress) { - if (!mIsComposeMode && mProgressRootView != null) { - mProgressRootView.setVisibility(View.GONE); - } mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); return; } - if (!mIsComposeMode && mProgressRootView != null) { - mProgressRootView.setVisibility(View.VISIBLE); - } if (mCurrentProgressMax <= 0) { Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); mCurrentProgressMax = 100; } - if (!mIsComposeMode && mProgressBar != null) { - mProgressBar.setMax(mCurrentProgressMax); - mProgressBar.setProgress(mCurrentProgress); - } - if (mTrackedPackageName != null) { loadIconInBackground(mTrackedPackageName, result -> { Drawable drawable = result != null ? result.drawable : null; @@ -621,38 +434,22 @@ private void updateNotificationProgress() { mCurrentIcon = drawable; mCurrentIconIsAdaptive = isAdaptive; - if (!mIsComposeMode && mIconView != null && drawable != null) { - mIconView.setImageDrawable(drawable); - } - if (mIsComposeMode) notifyStateCallback(); + notifyStateCallback(); }); } } private void updateNotificationProgressCompact() { - if (!mIsViewAttached && !mIsComposeMode) return; - if (!mIsEnabled || !mIsTrackingProgress) { - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.GONE); - } mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); return; } - if (!mIsComposeMode && mCompactRootView != null) { - mCompactRootView.setVisibility(View.VISIBLE); - } if (mCurrentProgressMax <= 0) { Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); mCurrentProgressMax = 100; } - if (!mIsComposeMode && mCircularProgressBar != null) { - mCircularProgressBar.setMax(mCurrentProgressMax); - mCircularProgressBar.setProgress(mCurrentProgress); - } - if (mTrackedPackageName != null) { loadIconInBackground(mTrackedPackageName, result -> { Drawable drawable = result != null ? result.drawable : null; @@ -660,10 +457,7 @@ private void updateNotificationProgressCompact() { mCurrentIcon = drawable; mCurrentIconIsAdaptive = isAdaptive; - if (!mIsComposeMode && mCompactIconView != null && drawable != null) { - mCompactIconView.setImageDrawable(drawable); - } - if (mIsComposeMode) notifyStateCallback(); + notifyStateCallback(); }); } } @@ -684,13 +478,8 @@ private void loadIconInBackground(String packageName, IconCallback callback) { mIconFetcher.getMonotonicPackageIcon(packageName); if (iconResult != null && iconResult.drawable != null) { - if (mIsComposeMode) { - int sizePx = (int) (24 * mContext.getResources().getDisplayMetrics().density); - iconResult.drawable.setBounds(0, 0, sizePx, sizePx); - - if (iconResult.isAdaptive && iconResult.drawable instanceof AdaptiveIconDrawable) { - } - } + int sizePx = (int) (24 * mContext.getResources().getDisplayMetrics().density); + iconResult.drawable.setBounds(0, 0, sizePx, sizePx); mIconCache.put(packageName, iconResult); @@ -796,15 +585,11 @@ private static boolean hasProgress(@NonNull final Notification notification) { public void onInteraction() { if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - if (mIsComposeMode) { - mIsMenuVisible = !mIsMenuVisible; - notifyStateCallback(); - if (mIsMenuVisible) { - mHandler.removeCallbacks(mMenuCollapseRunnable); - mHandler.postDelayed(mMenuCollapseRunnable, 5000); - } - } else { - showMediaPopup(mProgressRootView); + mIsMenuVisible = !mIsMenuVisible; + notifyStateCallback(); + if (mIsMenuVisible) { + mHandler.removeCallbacks(mMenuCollapseRunnable); + mHandler.postDelayed(mMenuCollapseRunnable, 5000); } } else { openTrackedApp(); @@ -854,58 +639,6 @@ public void setSystemChipVisible(boolean visible) { } } - private void showMediaPopup(View anchorView) { - if (mIsComposeMode || anchorView == null) { - return; - } - if (mIsPopupActive) { - if (mMediaPopup != null) { - mMediaPopup.dismiss(); - } - mIsPopupActive = false; - return; - } - - Context context = anchorView.getContext(); - View popupView = LayoutInflater.from(context).inflate(R.layout.media_control_popup, null); - - if (mMediaPopup != null && mMediaPopup.isShowing()) { - mMediaPopup.dismiss(); - } - - mMediaPopup = new PopupWindow(popupView, ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, true); - mMediaPopup.setOutsideTouchable(true); - mMediaPopup.setFocusable(true); - mMediaPopup.setOnDismissListener(() -> mIsPopupActive = false); - - ImageButton btnPrevious = popupView.findViewById(R.id.btn_previous); - ImageButton btnNext = popupView.findViewById(R.id.btn_next); - - if (btnPrevious != null) { - btnPrevious.setOnClickListener(v -> { - skipToPreviousTrack(); - mMediaPopup.dismiss(); - }); - } - - if (btnNext != null) { - btnNext.setOnClickListener(v -> { - skipToNextTrack(); - mMediaPopup.dismiss(); - }); - } - - anchorView.post(() -> { - if (!mIsViewAttached) return; - - int offsetX = -popupView.getWidth() / 3; - int offsetY = -anchorView.getHeight(); - mMediaPopup.showAsDropDown(anchorView, offsetX, offsetY); - mIsPopupActive = true; - }); - } - private void openTrackedApp() { if (mTrackedPackageName == null) { Log.w(TAG, "No tracked package available"); @@ -1095,10 +828,6 @@ public void destroy() { mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); mHandler.removeCallbacksAndMessages(null); - if (mMediaPopup != null && mMediaPopup.isShowing()) { - mMediaPopup.dismiss(); - } - synchronized (mLock) { mIsTrackingProgress = false; mTrackedNotificationKey = null; @@ -1106,13 +835,6 @@ public void destroy() { mIconCache.clear(); } - if (!mIsComposeMode && mIconView != null) { - mIconView.setImageDrawable(null); - } - - if (!mIsComposeMode && mCompactIconView != null) { - mCompactIconView.setImageDrawable(null); - } mCurrentIcon = null; // Shutdown the background executor @@ -1120,10 +842,4 @@ public void destroy() { ((ExecutorService) mBackgroundExecutor).shutdown(); } } - - private static int getThemeColor(Context context, int attrResId) { - TypedValue typedValue = new TypedValue(); - context.getTheme().resolveAttribute(attrResId, typedValue, true); - return typedValue.data; - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java deleted file mode 100644 index e385e4ab5b90..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressGroup.java +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Copyright (c) 2025, The LineageOS 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.statusbar; - -import androidx.annotation.Nullable; - -import android.view.View; -import android.widget.ImageView; -import android.widget.ProgressBar; - -/** On-going action progress chip view group stores all elements of chip */ -public class OnGoingActionProgressGroup { - public final View rootView; - public final View compactRootView; - public final ImageView iconView; - public final ImageView compactIconView; - public final ProgressBar progressBarView; - public final ProgressBar circularProgressBarView; - - public OnGoingActionProgressGroup( - @Nullable View rootView, - @Nullable ImageView iconView, - @Nullable ProgressBar progressBarView, - @Nullable View compactRootView, - @Nullable ImageView compactIconView, - @Nullable ProgressBar circularProgressBarView) { - this.rootView = rootView; - this.iconView = iconView; - this.progressBarView = progressBarView; - this.compactRootView = compactRootView; - this.compactIconView = compactIconView; - this.circularProgressBarView = circularProgressBarView; - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt index 283229746b22..1402ce821cc6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt @@ -290,12 +290,9 @@ class OnGoingActionProgressComposeController( init { Log.d(TAG, "Initializing OnGoingActionProgressComposeController") - val dummyGroup = OnGoingActionProgressGroup(null, null, null, null, null, null) - try { javaController = OnGoingActionProgressController( context, - dummyGroup, notificationListener, keyguardStateController, headsUpManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index e2f6550df1e0..c5aeb3cc1c7d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -203,18 +203,15 @@ import com.android.systemui.statusbar.LiftReveal; import com.android.systemui.statusbar.LightRevealScrim; import com.android.systemui.statusbar.LockscreenShadeTransitionController; -import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationShadeDepthController; import com.android.systemui.statusbar.NotificationShadeWindowController; -import com.android.systemui.statusbar.OnGoingActionProgressController; import com.android.systemui.statusbar.PowerButtonReveal; import com.android.systemui.statusbar.PulseExpansionHandler; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.SysuiStatusBarStateController; -import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.core.StatusBarConnectedDisplays; import com.android.systemui.statusbar.core.StatusBarInitializer; import com.android.systemui.statusbar.core.StatusBarRootModernization; @@ -420,11 +417,6 @@ public QSPanelController getQSPanelController() { private final LightRevealScrim mLightRevealScrim; private PowerButtonReveal mPowerButtonReveal; - private OnGoingActionProgressController mOnGoingActionProgressController = null; - private final VibratorHelper mVibratorHelper; - - @Inject public NotificationListener mNotificationListener; - /** * Whether we should delay the AOD->Lockscreen animation. * If false, the animation will start in onStartedWakingUp(). @@ -769,8 +761,7 @@ public CentralSurfacesImpl( EdgeLightViewController edgeLightViewController, NowPlayingViewController nowPlayingViewController, ChargingAnimationViewController chargingAnimationViewController, - BurnInProtectionController burnInProtectionController, - VibratorHelper vibrator + BurnInProtectionController burnInProtectionController ) { mContext = context; mNotificationsController = notificationsController; @@ -924,7 +915,6 @@ public CentralSurfacesImpl( mEdgeLightViewController = edgeLightViewController; mNowPlayingViewController = nowPlayingViewController; mChargingAnimationViewController = chargingAnimationViewController; - mVibratorHelper = vibrator; } private void initBubbles(Bubbles bubbles) { @@ -1358,13 +1348,6 @@ protected void makeStatusBarView(@Nullable RegisterStatusBarResult result) { setBouncerShowingForStatusBarComponents(mBouncerShowing); checkBarModes(); mBurnInProtectionController.setPhoneStatusBarView(mPhoneStatusBarViewController.getPhoneStatusBarView()); - if (!StatusBarRootModernization.isEnabled()) { - mOnGoingActionProgressController = - new OnGoingActionProgressController( - mContext, - statusBarViewController.getOngoingActionProgressGroup(), mNotificationListener, - mKeyguardStateController, mHeadsUpManager, mVibratorHelper); - } }); } if (!StatusBarRootModernization.isEnabled() && !StatusBarConnectedDisplays.isEnabled()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index b964e5bf8515..63be7d4d88e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -24,8 +24,6 @@ import android.view.InputDevice import android.view.MotionEvent import android.view.View import android.view.ViewConfiguration -import android.widget.ImageView -import android.widget.ProgressBar import androidx.annotation.VisibleForTesting import com.android.systemui.Gefingerpoken import com.android.systemui.battery.BatteryMeterView @@ -47,7 +45,6 @@ import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarConfigurationController import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore -import com.android.systemui.statusbar.OnGoingActionProgressGroup import com.android.systemui.statusbar.policy.Clock import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.window.StatusBarWindowControllerStore @@ -353,18 +350,6 @@ private constructor( darkIconDispatcher.removeDarkReceiver(clockRight) } - fun getOngoingActionProgressGroup(): OnGoingActionProgressGroup { - return OnGoingActionProgressGroup( - mView.findViewById(R.id.status_bar_ongoing_action_chip), - mView.findViewById(R.id.ongoing_action_app_icon) as ImageView, - mView.findViewById(R.id.app_action_progress) as ProgressBar, - - mView.findViewById(R.id.status_bar_ongoing_action_chip_compact), - mView.findViewById(R.id.ongoing_action_app_icon_compact) as ImageView, - mView.findViewById(R.id.circular_progress) as ProgressBar - ) - } - fun getPhoneStatusBarView(): PhoneStatusBarView { return mView } From eb26c8dc8aaf7157368eb3b04ca949190b665a5f Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 2 Mar 2026 06:20:45 +0530 Subject: [PATCH 153/190] SystemUI: Refactor ongoing action progress chip * No ExecutorService, no Handler-based repeaters. * Coroutines handle debounce, media ticker, stale checks, and delayed collapses. * State runs on Dispatchers.Main.immediate, so you can drop most locking. * Icon loads run on a background dispatcher, then return to main for cache and UI updates. * Duplicate icon fetches for the same package get suppressed via inFlightIconLoads. * destroy() cancels all jobs and scope, so you avoid leaks and stray callbacks. * Clean up biggest duplication in StateCallback entirely. * Kill IconFetcher separate utility. Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../OnGoingActionProgressController.java | 845 ------------------ .../OnGoingActionProgressController.kt | 790 ++++++++++++++++ .../statusbar/OngoingActionProgressCompose.kt | 150 +--- .../android/systemui/util/IconFetcher.java | 92 -- 4 files changed, 833 insertions(+), 1044 deletions(-) delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/util/IconFetcher.java diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java deleted file mode 100644 index e00c7fe70901..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.java +++ /dev/null @@ -1,845 +0,0 @@ -/** - * Copyright (c) 2025 VoltageOS - * - * 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.statusbar; - -import android.app.Notification; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.database.ContentObserver; -import android.graphics.drawable.Drawable; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.net.Uri; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.os.UserHandle; -import android.os.VibrationEffect; -import android.provider.Settings; -import android.service.notification.NotificationListenerService; -import android.service.notification.StatusBarNotification; -import android.util.Log; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.android.systemui.statusbar.notification.headsup.HeadsUpManager; -import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener; -import com.android.systemui.res.R; -import com.android.systemui.util.IconFetcher; -import com.android.systemui.statusbar.VibratorHelper; -import com.android.systemui.statusbar.policy.KeyguardStateController; -import com.android.systemui.statusbar.util.MediaSessionManagerHelper; - -import java.util.HashMap; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ExecutorService; - -public class OnGoingActionProgressController implements NotificationListener.NotificationHandler, - KeyguardStateController.Callback, OnHeadsUpChangedListener { - private static final String TAG = "OngoingActionProgressController"; - private static final String ONGOING_ACTION_CHIP_ENABLED = "ongoing_action_chip"; - private static final String ONGOING_MEDIA_PROGRESS = "ongoing_media_progress"; - private static final String ONGOING_COMPACT_MODE_ENABLED = "ongoing_compact_mode"; - private static final int MEDIA_UPDATE_INTERVAL_MS = 1000; - private static final int DEBOUNCE_DELAY_MS = 150; - private static final int STALE_PROGRESS_CHECK_INTERVAL_MS = 5000; - private static final int PROGRESS_TIMEOUT_MS = 30000; - - private static final VibrationEffect VIBRATION_EFFECT = - VibrationEffect.get(VibrationEffect.EFFECT_CLICK); - - public interface StateCallback { - void onStateChanged(boolean isVisible, int progress, int maxProgress, - Drawable icon, boolean isIconAdaptive, String packageName, - boolean isCompactMode, boolean showMediaControls); - } - - private final Context mContext; - private final ContentResolver mContentResolver; - private final Handler mHandler; - private final SettingsObserver mSettingsObserver; - private final KeyguardStateController mKeyguardStateController; - private final NotificationListener mNotificationListener; - private final HeadsUpManager mHeadsUpManager; - private final VibratorHelper mVibrator; - private final IconFetcher mIconFetcher; - private final MediaSessionManagerHelper mMediaSessionHelper; - private final Executor mBackgroundExecutor; - private StateCallback mStateCallback = null; - private final Object mLock = new Object(); - private final Runnable mUiUpdateRunnable = new Runnable() { - @Override - public void run() { - synchronized (mLock) { - mUpdatePending = false; - mLastUpdateTime = System.currentTimeMillis(); - updateViews(); - } - } - }; - - private final HashMap mIconCache = new HashMap<>(); - - private boolean mShowMediaProgress = true; - private boolean mIsTrackingProgress = false; - private boolean mIsForceHidden = false; - private boolean mHeadsUpPinned = false; - private long mLastProgressUpdateTime = 0; - private boolean mIsEnabled; - private boolean mIsCompactModeEnabled = false; - private int mCurrentProgress = 0; - private int mCurrentProgressMax = 0; - private Drawable mCurrentIcon = null; - private boolean mCurrentIconIsAdaptive = false; - private boolean mIsMenuVisible = false; - private boolean mIsSystemChipVisible = false; - - private String mTrackedNotificationKey; - private String mTrackedPackageName; - private boolean mNeedsFullUiUpdate = true; - private boolean mIsViewAttached = false; - private boolean mIsExpanded = false; - - private boolean mUpdatePending = false; - private long mLastUpdateTime = 0; - - private final Handler mMediaProgressHandler = new Handler(Looper.getMainLooper()); - private final Runnable mMediaProgressRunnable = new Runnable() { - @Override - public void run() { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - updateMediaProgressOnly(); - mMediaProgressHandler.postDelayed(this, MEDIA_UPDATE_INTERVAL_MS); - } - } - }; - - private final Runnable mStaleProgressChecker = new Runnable() { - @Override - public void run() { - synchronized (OnGoingActionProgressController.this) { - checkForStaleProgress(); - } - if (mIsViewAttached) { - mHandler.postDelayed(this, STALE_PROGRESS_CHECK_INTERVAL_MS); - } - } - }; - - private final Runnable mCompactCollapseRunnable = () -> { - if (mIsCompactModeEnabled && mIsExpanded) { - mIsExpanded = false; - requestUiUpdate(); - } - }; - - private final Runnable mMenuCollapseRunnable = () -> { - mIsMenuVisible = false; - notifyStateCallback(); - }; - - private final MediaSessionManagerHelper.MediaMetadataListener mMediaMetadataListener = - new MediaSessionManagerHelper.MediaMetadataListener() { - @Override - public void onMediaMetadataChanged() { - mNeedsFullUiUpdate = true; - requestUiUpdate(); - } - - @Override - public void onPlaybackStateChanged() { - mNeedsFullUiUpdate = true; - requestUiUpdate(); - } - }; - - public OnGoingActionProgressController( - Context context, - NotificationListener notificationListener, KeyguardStateController keyguardStateController, - HeadsUpManager headsUpManager, VibratorHelper vibrator) { - - mNotificationListener = notificationListener; - if (mNotificationListener == null) { - Log.wtf(TAG, "mNotificationListener is null"); - throw new IllegalArgumentException("notificationListener cannot be null"); - } - - mKeyguardStateController = keyguardStateController; - mHeadsUpManager = headsUpManager; - mContext = context; - mContentResolver = context.getContentResolver(); - mHandler = new Handler(Looper.getMainLooper()); - mSettingsObserver = new SettingsObserver(mHandler); - mBackgroundExecutor = Executors.newSingleThreadExecutor(); - mVibrator = vibrator; - - mIconFetcher = new IconFetcher(context); - mMediaSessionHelper = MediaSessionManagerHelper.Companion.getInstance(context); - - mKeyguardStateController.addCallback(this); - mHeadsUpManager.addListener(this); - mNotificationListener.addNotificationHandler(this); - mSettingsObserver.register(); - - mMediaSessionHelper.addMediaMetadataListener(mMediaMetadataListener); - - mIsViewAttached = true; - updateSettings(); - - mHandler.postDelayed(mStaleProgressChecker, STALE_PROGRESS_CHECK_INTERVAL_MS); - } - - /** - * Sets a callback for Compose to receive state updates - * @param callback Callback to be notified of state changes, or null to unregister - */ - public void setStateCallback(StateCallback callback) { - mStateCallback = callback; - notifyStateCallback(); - } - - public void expandCompactView() { - mIsExpanded = true; - - // Reset collapse timer - mHandler.removeCallbacks(mCompactCollapseRunnable); - mHandler.postDelayed(mCompactCollapseRunnable, 5000); - - notifyStateCallback(); - return; - } - - private void requestUiUpdate() { - long currentTime = System.currentTimeMillis(); - synchronized (mLock) { - if (!mUpdatePending && (currentTime - mLastUpdateTime > DEBOUNCE_DELAY_MS)) { - mUpdatePending = false; - mLastUpdateTime = currentTime; - updateViews(); - } else if (!mUpdatePending) { - mUpdatePending = true; - mHandler.postDelayed(mUiUpdateRunnable, DEBOUNCE_DELAY_MS); - } - } - } - - /** - * Notifies the Compose callback of current state - */ - private void notifyStateCallback() { - if (mStateCallback == null) { - return; - } - - boolean isVisible = !mIsForceHidden && !mHeadsUpPinned && !mIsSystemChipVisible; - - boolean isMediaPlaying = mShowMediaProgress && mMediaSessionHelper.isMediaPlaying(); - boolean hasNotificationProgress = mIsEnabled && mIsTrackingProgress; - - isVisible = isVisible && (isMediaPlaying || hasNotificationProgress); - - if (isVisible) { - boolean isCompact = mIsCompactModeEnabled && !mIsExpanded; - mStateCallback.onStateChanged( - true, mCurrentProgress, mCurrentProgressMax, - mCurrentIcon, mCurrentIconIsAdaptive, mTrackedPackageName, - isCompact, mIsMenuVisible - ); - } else { - mStateCallback.onStateChanged(false, 0, 0, null, false, null, false, false); - } - } - - private void updateViews() { - if (!mIsViewAttached) { - notifyStateCallback(); - return; - } - - if (mIsForceHidden || mHeadsUpPinned) { - notifyStateCallback(); - return; - } - - boolean isMediaPlaying = mShowMediaProgress && mMediaSessionHelper.isMediaPlaying(); - - if (mIsCompactModeEnabled && !mIsExpanded) { - if (!mIsEnabled && !isMediaPlaying) { - notifyStateCallback(); - return; - } - - if (isMediaPlaying) { - updateMediaProgressCompact(); - } else { - updateNotificationProgressCompact(); - } - } else { - if (isMediaPlaying) { - if (mNeedsFullUiUpdate) { - updateMediaProgressFull(); - mNeedsFullUiUpdate = false; - } else { - updateMediaProgressOnly(); - } - } else { - updateNotificationProgress(); - } - } - notifyStateCallback(); - } - - private void updateMediaProgressOnly() { - long totalDuration = mMediaSessionHelper.getTotalDuration(); - - android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); - long currentProgress = 0; - - if (playbackState != null) { - currentProgress = playbackState.getPosition(); - } - - mCurrentProgress = (int) currentProgress; - mCurrentProgressMax = (int) totalDuration; - if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; - - notifyStateCallback(); - } - - private void updateMediaProgressFull() { - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); - mMediaProgressHandler.post(mMediaProgressRunnable); - - Drawable mediaAppIcon = mMediaSessionHelper.getMediaAppIcon(); - - if (mediaAppIcon != null) { - mCurrentIcon = mediaAppIcon; - mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; - } else { - String packageName = null; - - android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); - if (playbackState != null && playbackState.getExtras() != null) { - packageName = playbackState.getExtras().getString("package"); - } - if (packageName != null) { - loadIconInBackground(packageName, result -> { - Drawable drawable = result != null ? result.drawable : null; - boolean isAdaptive = result != null ? result.isAdaptive : false; - - if (drawable != null) { - mCurrentIcon = drawable; - mCurrentIconIsAdaptive = isAdaptive; - } else { - setDefaultMediaIcon(); - } - notifyStateCallback(); - }); - } else { - setDefaultMediaIcon(); - } - } - - updateMediaProgressOnly(); - } - - private void setDefaultMediaIcon() { - mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); - mCurrentIconIsAdaptive = false; - } - - private void updateMediaProgressCompact() { - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); - mMediaProgressHandler.post(mMediaProgressRunnable); - - long totalDuration = mMediaSessionHelper.getTotalDuration(); - - android.media.session.PlaybackState playbackState = mMediaSessionHelper.getMediaControllerPlaybackState(); - long currentProgress = 0; - - if (playbackState != null) { - currentProgress = playbackState.getPosition(); - } - - mCurrentProgress = (int) currentProgress; - mCurrentProgressMax = (int) totalDuration; - if (mCurrentProgressMax <= 0) mCurrentProgressMax = 100; - - Drawable mediaAppIcon = mMediaSessionHelper.getMediaAppIcon(); - - if (mediaAppIcon != null) { - mCurrentIcon = mediaAppIcon; - mCurrentIconIsAdaptive = mediaAppIcon instanceof AdaptiveIconDrawable; - } else { - String packageName = null; - if (playbackState != null && playbackState.getExtras() != null) { - packageName = playbackState.getExtras().getString("package"); - } - - if (packageName != null) { - loadIconInBackground(packageName, result -> { - Drawable drawable = result != null ? result.drawable : null; - boolean isAdaptive = result != null ? result.isAdaptive : false; - - if (drawable != null) { - mCurrentIcon = drawable; - mCurrentIconIsAdaptive = isAdaptive; - } else { - setDefaultMediaIconCompact(); - } - notifyStateCallback(); - }); - } else { - setDefaultMediaIconCompact(); - } - } - } - - private void setDefaultMediaIconCompact() { - mCurrentIcon = mContext.getResources().getDrawable(R.drawable.ic_default_music_icon); - mCurrentIconIsAdaptive = false; - } - - private void updateNotificationProgress() { - if (!mIsEnabled || !mIsTrackingProgress) { - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); - return; - } - - if (mCurrentProgressMax <= 0) { - Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); - mCurrentProgressMax = 100; - } - - if (mTrackedPackageName != null) { - loadIconInBackground(mTrackedPackageName, result -> { - Drawable drawable = result != null ? result.drawable : null; - boolean isAdaptive = result != null ? result.isAdaptive : false; - - mCurrentIcon = drawable; - mCurrentIconIsAdaptive = isAdaptive; - notifyStateCallback(); - }); - } - } - - private void updateNotificationProgressCompact() { - if (!mIsEnabled || !mIsTrackingProgress) { - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); - return; - } - - if (mCurrentProgressMax <= 0) { - Log.w(TAG, "updateViews: invalid max progress " + mCurrentProgressMax + ", using 100"); - mCurrentProgressMax = 100; - } - - if (mTrackedPackageName != null) { - loadIconInBackground(mTrackedPackageName, result -> { - Drawable drawable = result != null ? result.drawable : null; - boolean isAdaptive = result != null ? result.isAdaptive : false; - - mCurrentIcon = drawable; - mCurrentIconIsAdaptive = isAdaptive; - notifyStateCallback(); - }); - } - } - - private void loadIconInBackground(String packageName, IconCallback callback) { - if (packageName == null) return; - - if (mIconCache.containsKey(packageName)) { - IconFetcher.AdaptiveDrawableResult cachedResult = mIconCache.get(packageName); - if (cachedResult != null) { - callback.onIconLoaded(cachedResult); - return; - } - } - - mBackgroundExecutor.execute(() -> { - final IconFetcher.AdaptiveDrawableResult iconResult = - mIconFetcher.getMonotonicPackageIcon(packageName); - - if (iconResult != null && iconResult.drawable != null) { - int sizePx = (int) (24 * mContext.getResources().getDisplayMetrics().density); - iconResult.drawable.setBounds(0, 0, sizePx, sizePx); - - mIconCache.put(packageName, iconResult); - - mHandler.post(() -> { - callback.onIconLoaded(iconResult); - }); - } - }); - } - - private interface IconCallback { - void onIconLoaded(@Nullable IconFetcher.AdaptiveDrawableResult result); - } - - private void extractProgress(Notification notification) { - Bundle extras = notification.extras; - mCurrentProgressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 100); - mCurrentProgress = extras.getInt(Notification.EXTRA_PROGRESS, 0); - } - - private void trackProgress(final StatusBarNotification sbn) { - mIsTrackingProgress = true; - mTrackedNotificationKey = sbn.getKey(); - mTrackedPackageName = sbn.getPackageName(); - mLastProgressUpdateTime = System.currentTimeMillis(); - extractProgress(sbn.getNotification()); - requestUiUpdate(); - } - - private void clearProgressTracking() { - mIsTrackingProgress = false; - mTrackedNotificationKey = null; - mTrackedPackageName = null; - mCurrentProgress = 0; - mCurrentProgressMax = 0; - mLastProgressUpdateTime = 0; - requestUiUpdate(); - } - - private void checkForStaleProgress() { - if (!mIsTrackingProgress || mTrackedNotificationKey == null) return; - - StatusBarNotification sbn = findNotificationByKey(mTrackedNotificationKey); - if (sbn == null) { - clearProgressTracking(); - return; - } - - if (!hasProgress(sbn.getNotification())) { - clearProgressTracking(); - return; - } - - if (mLastProgressUpdateTime == 0) { - mLastProgressUpdateTime = System.currentTimeMillis(); - return; - } - - if (System.currentTimeMillis() - mLastProgressUpdateTime > PROGRESS_TIMEOUT_MS - && mCurrentProgressMax > 0 - && mCurrentProgress >= mCurrentProgressMax) { - clearProgressTracking(); - } - } - - private void updateProgressIfNeeded(final StatusBarNotification sbn) { - if (!mIsTrackingProgress) return; - - if (sbn.getKey().equals(mTrackedNotificationKey)) { - if (!hasProgress(sbn.getNotification())) { - clearProgressTracking(); - return; - } - - mLastProgressUpdateTime = System.currentTimeMillis(); - extractProgress(sbn.getNotification()); - requestUiUpdate(); - } - } - - @Nullable - private StatusBarNotification findNotificationByKey(String key) { - if (key == null || mNotificationListener == null) return null; - - for (StatusBarNotification notification : mNotificationListener.getActiveNotifications()) { - if (notification.getKey().equals(key)) { - return notification; - } - } - return null; - } - - private static boolean hasProgress(@NonNull final Notification notification) { - Bundle extras = notification.extras; - if (extras == null) return false; - - boolean indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false); - boolean maxProgressValid = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0; - return extras.containsKey(Notification.EXTRA_PROGRESS) && - extras.containsKey(Notification.EXTRA_PROGRESS_MAX) && - !indeterminate && maxProgressValid; - } - - public void onInteraction() { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - mIsMenuVisible = !mIsMenuVisible; - notifyStateCallback(); - if (mIsMenuVisible) { - mHandler.removeCallbacks(mMenuCollapseRunnable); - mHandler.postDelayed(mMenuCollapseRunnable, 5000); - } - } else { - openTrackedApp(); - } - mVibrator.vibrate(VIBRATION_EFFECT); - } - - public void onLongPress() { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - openMediaApp(); - } else { - openTrackedApp(); - } - mVibrator.vibrate(VIBRATION_EFFECT); - } - - public void onDoubleTap() { - if (mShowMediaProgress && mMediaSessionHelper.isMediaPlaying()) { - toggleMediaPlaybackState(); - mVibrator.vibrate(VIBRATION_EFFECT); - } - } - - public void onSwipe(boolean isNext) { - if (isNext) skipToNextTrack(); - else skipToPreviousTrack(); - } - - public void onMediaAction(int action) { - if (action == 0) skipToPreviousTrack(); - else if (action == 1) toggleMediaPlaybackState(); - else if (action == 2) skipToNextTrack(); - mHandler.removeCallbacks(mMenuCollapseRunnable); - mHandler.postDelayed(mMenuCollapseRunnable, 5000); - } - - public void onMediaMenuDismiss() { - mIsMenuVisible = false; - notifyStateCallback(); - } - - public void setSystemChipVisible(boolean visible) { - if (mIsSystemChipVisible != visible) { - mIsSystemChipVisible = visible; - notifyStateCallback(); - requestUiUpdate(); - } - } - - private void openTrackedApp() { - if (mTrackedPackageName == null) { - Log.w(TAG, "No tracked package available"); - return; - } - - Intent launchIntent = mContext.getPackageManager().getLaunchIntentForPackage(mTrackedPackageName); - if (launchIntent != null) { - launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - mContext.startActivity(launchIntent); - } else { - Log.w(TAG, "No launch intent for package: " + mTrackedPackageName); - } - } - - private void onNotificationPosted(final StatusBarNotification sbn) { - if (sbn == null || !mIsEnabled) return; - - Notification notification = sbn.getNotification(); - if (notification == null) return; - - synchronized (this) { - boolean hasValidProgress = hasProgress(notification); - String currentKey = mTrackedNotificationKey; - - if (!hasValidProgress) { - if (currentKey != null && currentKey.equals(sbn.getKey())) { - clearProgressTracking(); - } - return; - } - - if (!mIsTrackingProgress) { - trackProgress(sbn); - } else if (sbn.getKey().equals(currentKey)) { - updateProgressIfNeeded(sbn); - } - } - } - - private void onNotificationRemoved(final StatusBarNotification sbn) { - if (sbn == null) return; - - synchronized (this) { - if (!mIsTrackingProgress) return; - - if (sbn.getKey().equals(mTrackedNotificationKey)) { - clearProgressTracking(); - return; - } - - if (sbn.getPackageName().equals(mTrackedPackageName)) { - StatusBarNotification currentSbn = findNotificationByKey(mTrackedNotificationKey); - if (currentSbn == null || !hasProgress(currentSbn.getNotification())) { - clearProgressTracking(); - } - } - } - } - - public void setForceHidden(final boolean forceHidden) { - if (mIsForceHidden != forceHidden) { - Log.d(TAG, "setForceHidden " + forceHidden); - mIsForceHidden = forceHidden; - notifyStateCallback(); - requestUiUpdate(); - } - } - - private void toggleMediaPlaybackState() { - if (mMediaSessionHelper != null) { - mMediaSessionHelper.toggleMediaPlaybackState(); - } - } - - private void skipToNextTrack() { - if (mMediaSessionHelper != null) { - mMediaSessionHelper.nextSong(); - } - } - - private void skipToPreviousTrack() { - if (mMediaSessionHelper != null) { - mMediaSessionHelper.prevSong(); - } - } - - private void openMediaApp() { - if (mMediaSessionHelper != null) { - mMediaSessionHelper.launchMediaApp(); - } - } - - @Override - public void onNotificationPosted(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap) { - onNotificationPosted(sbn); - } - - @Override - public void onNotificationRemoved(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap) { - onNotificationRemoved(sbn); - } - - @Override - public void onNotificationRemoved(StatusBarNotification sbn, NotificationListenerService.RankingMap _rankingMap, int _reason) { - onNotificationRemoved(sbn); - } - - @Override - public void onHeadsUpPinnedModeChanged(boolean inPinnedMode) { - mHeadsUpPinned = inPinnedMode; - notifyStateCallback(); - requestUiUpdate(); - } - - @Override - public void onNotificationRankingUpdate(NotificationListenerService.RankingMap _rankingMap) { - } - - @Override - public void onNotificationsInitialized() { - } - - @Override - public void onKeyguardShowingChanged() { - setForceHidden(mKeyguardStateController.isShowing()); - } - - private class SettingsObserver extends ContentObserver { - SettingsObserver(Handler handler) { super(handler); } - - @Override - public void onChange(boolean selfChange, Uri uri) { - super.onChange(selfChange, uri); - if (uri.equals(Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED)) || - uri.equals(Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS)) || - uri.equals(Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED))) { - updateSettings(); - } - } - - public void register() { - mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED), - false, this, UserHandle.USER_ALL); - mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS), - false, this, UserHandle.USER_ALL); - mContentResolver.registerContentObserver(Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED), - false, this, UserHandle.USER_ALL); - updateSettings(); - } - - public void unregister() { - mContentResolver.unregisterContentObserver(this); - } - } - - private void updateSettings() { - boolean wasEnabled = mIsEnabled; - boolean wasShowingMedia = mShowMediaProgress; - boolean wasCompactMode = mIsCompactModeEnabled; - - mIsEnabled = Settings.System.getIntForUser(mContentResolver, - ONGOING_ACTION_CHIP_ENABLED, 0, UserHandle.USER_CURRENT) == 1; - mShowMediaProgress = Settings.System.getIntForUser(mContentResolver, - ONGOING_MEDIA_PROGRESS, 0, UserHandle.USER_CURRENT) == 1; - mIsCompactModeEnabled = Settings.System.getIntForUser(mContentResolver, - ONGOING_COMPACT_MODE_ENABLED, 0, UserHandle.USER_CURRENT) == 1; - - if (wasEnabled != mIsEnabled || wasShowingMedia != mShowMediaProgress || wasCompactMode != mIsCompactModeEnabled) { - mNeedsFullUiUpdate = true; - mIsExpanded = false; - } - - requestUiUpdate(); - } - - public void destroy() { - mIsViewAttached = false; - - mHandler.removeCallbacks(mStaleProgressChecker); - - mSettingsObserver.unregister(); - mKeyguardStateController.removeCallback(this); - mHeadsUpManager.removeListener(this); - mMediaSessionHelper.removeMediaMetadataListener(mMediaMetadataListener); - - mMediaProgressHandler.removeCallbacks(mMediaProgressRunnable); - mHandler.removeCallbacksAndMessages(null); - - synchronized (mLock) { - mIsTrackingProgress = false; - mTrackedNotificationKey = null; - mTrackedPackageName = null; - mIconCache.clear(); - } - - mCurrentIcon = null; - - // Shutdown the background executor - if (mBackgroundExecutor instanceof ExecutorService) { - ((ExecutorService) mBackgroundExecutor).shutdown(); - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt new file mode 100644 index 000000000000..27d74c848047 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OnGoingActionProgressController.kt @@ -0,0 +1,790 @@ +/* + * SPDX-FileCopyrightText: VoltageOS + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.systemui.statusbar + +import android.app.Notification +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.database.ContentObserver +import android.graphics.Bitmap +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.os.UserHandle +import android.os.VibrationEffect +import android.provider.Settings +import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log +import androidx.annotation.VisibleForTesting +import android.graphics.drawable.Drawable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.graphics.drawable.toBitmap +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.headsup.HeadsUpManager +import com.android.systemui.statusbar.notification.headsup.OnHeadsUpChangedListener +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.util.MediaSessionManagerHelper +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.async +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap + +class OnGoingActionProgressController( + private val context: Context, + private val notificationListener: NotificationListener, + private val keyguardStateController: KeyguardStateController, + private val headsUpManager: HeadsUpManager, + private val vibrator: VibratorHelper, + @VisibleForTesting + private val bgDispatcher: CoroutineDispatcher = Dispatchers.Default, +) : NotificationListener.NotificationHandler, + KeyguardStateController.Callback, + OnHeadsUpChangedListener { + + private val contentResolver: ContentResolver = context.contentResolver + private val mainScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + private val mediaSessionHelper = MediaSessionManagerHelper.getInstance(context) + + private val iconCache = HashMap() + private val inFlightIconLoads = ConcurrentHashMap() + + private var showMediaProgress = true + private var isTrackingProgress = false + private var isForceHidden = false + private var headsUpPinned = false + private var lastProgressUpdateTime = 0L + private var isEnabled = false + private var isCompactModeEnabled = false + + private var currentProgress = 0 + private var currentProgressMax = 0 + private var currentIcon: Drawable? = null + + private var isMenuVisible = false + private var isSystemChipVisible = false + + private var trackedNotificationKey: String? = null + private var trackedPackageName: String? = null + + private var needsFullUiUpdate = true + private var isViewAttached = false + private var isExpanded = false + + private var lastUpdateTime = 0L + private var uiUpdateJob: Job? = null + + private var mediaProgressJob: Job? = null + private var staleCheckerJob: Job? = null + private var compactCollapseJob: Job? = null + private var menuCollapseJob: Job? = null + + private val _state = MutableStateFlow(ProgressState()) + val state: StateFlow = _state.asStateFlow() + + private val settingsObserver = + object : ContentObserver(Handler(Looper.getMainLooper())) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + if (uri == null) return + val enabledUri = Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED) + val mediaUri = Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS) + val compactUri = Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED) + + if (uri == enabledUri || uri == mediaUri || uri == compactUri) { + updateSettings() + } + } + + fun register() { + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_ACTION_CHIP_ENABLED), + false, + this, + UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_MEDIA_PROGRESS), + false, + this, + UserHandle.USER_ALL + ) + contentResolver.registerContentObserver( + Settings.System.getUriFor(ONGOING_COMPACT_MODE_ENABLED), + false, + this, + UserHandle.USER_ALL + ) + updateSettings() + } + + fun unregister() { + contentResolver.unregisterContentObserver(this) + } + } + + private val mediaMetadataListener = object : MediaSessionManagerHelper.MediaMetadataListener { + override fun onMediaMetadataChanged() { + needsFullUiUpdate = true + requestUiUpdate() + } + + override fun onPlaybackStateChanged() { + needsFullUiUpdate = true + requestUiUpdate() + } + } + + init { + requireNotNull(notificationListener) { "notificationListener cannot be null" } + + keyguardStateController.addCallback(this) + headsUpManager.addListener(this) + notificationListener.addNotificationHandler(this) + + settingsObserver.register() + mediaSessionHelper.addMediaMetadataListener(mediaMetadataListener) + + isViewAttached = true + updateSettings() + + staleCheckerJob = mainScope.launch { + while (isActive && isViewAttached) { + delay(STALE_PROGRESS_CHECK_INTERVAL_MS) + checkForStaleProgress() + } + } + } + + private fun publish(state: ProgressState) { + _state.value = state + } + + fun expandCompactView() { + isExpanded = true + compactCollapseJob?.cancel() + compactCollapseJob = mainScope.launch { + delay(5000L) + if (isCompactModeEnabled && isExpanded) { + isExpanded = false + requestUiUpdate() + } + } + updateProgressState() + } + + private fun requestUiUpdate() { + val now = System.currentTimeMillis() + + uiUpdateJob?.cancel() + uiUpdateJob = mainScope.launch { + val elapsed = now - lastUpdateTime + if (elapsed <= DEBOUNCE_DELAY_MS) { + delay(DEBOUNCE_DELAY_MS) + } + lastUpdateTime = System.currentTimeMillis() + updateViews() + } + } + + private fun updateProgressState() { + var isVisible = !isForceHidden && !headsUpPinned && !isSystemChipVisible + val isMediaPlaying = showMediaProgress && mediaSessionHelper.isMediaPlaying() + val hasNotificationProgress = isEnabled && isTrackingProgress + + isVisible = isVisible && (isMediaPlaying || hasNotificationProgress) + + if (!isVisible) { + val update = ProgressState( + isVisible = false, + progress = 0, + maxProgress = 0, + iconBitmap = null, + packageName = null, + isCompactMode = false, + showMediaControls = false + ) + publish(update) + return + } + + val isCompact = isCompactModeEnabled && !isExpanded + + val iconSizePx = if (isCompact) { + (14f * context.resources.displayMetrics.density).toInt() * 2 + } else { + (16f * context.resources.displayMetrics.density).toInt() * 2 + } + + val currentIconBitmap = try { + currentIcon?.let { drawable -> + drawable.toBitmap( + width = iconSizePx, + height = iconSizePx, + config = Bitmap.Config.ARGB_8888 + ).asImageBitmap() + } + } catch (e: Exception) { + Log.e(TAG, "Failed to convert icon to bitmap", e) + null + } + + val update = ProgressState( + isVisible = true, + progress = currentProgress, + maxProgress = currentProgressMax, + iconBitmap = currentIconBitmap, + packageName = trackedPackageName, + isCompactMode = isCompact, + showMediaControls = isMenuVisible + ) + publish(update) + } + + private fun updateViews() { + if (!isViewAttached) { + updateProgressState() + return + } + + if (isForceHidden || headsUpPinned) { + updateProgressState() + return + } + + val isMediaPlaying = showMediaProgress && mediaSessionHelper.isMediaPlaying() + + if (isCompactModeEnabled && !isExpanded) { + if (!isEnabled && !isMediaPlaying) { + stopMediaLoop() + updateProgressState() + return + } + if (isMediaPlaying) updateMediaProgressCompact() else updateNotificationProgressCompact() + } else { + if (isMediaPlaying) { + if (needsFullUiUpdate) { + updateMediaProgressFull() + needsFullUiUpdate = false + } else { + updateMediaProgressOnly() + } + } else { + stopMediaLoop() + updateNotificationProgress() + } + } + + updateProgressState() + } + + private fun ensureMediaLoopRunning() { + if (mediaProgressJob?.isActive == true) return + mediaProgressJob = mainScope.launch { + while (isActive && showMediaProgress && mediaSessionHelper.isMediaPlaying()) { + updateMediaProgressOnly() + delay(MEDIA_UPDATE_INTERVAL_MS) + } + } + } + + private fun stopMediaLoop() { + mediaProgressJob?.cancel() + mediaProgressJob = null + } + + private fun updateMediaProgressOnly() { + val totalDuration = mediaSessionHelper.getTotalDuration() + val playbackState = mediaSessionHelper.getMediaControllerPlaybackState() + val pos = playbackState?.position ?: 0L + + currentProgress = pos.toInt() + currentProgressMax = totalDuration.toInt().takeIf { it > 0 } ?: 100 + + updateProgressState() + } + + private fun updateMediaProgressFull() { + ensureMediaLoopRunning() + + val mediaAppIcon = mediaSessionHelper.getMediaAppIcon() + if (mediaAppIcon != null) { + currentIcon = mediaAppIcon + updateMediaProgressOnly() + return + } + + val playbackState = mediaSessionHelper.getMediaControllerPlaybackState() + val pkg = playbackState?.extras?.getString("package") + + if (pkg.isNullOrEmpty()) { + setDefaultMediaIcon() + updateMediaProgressOnly() + return + } + + loadIcon(pkg) { drawable -> + if (drawable != null) { + currentIcon = drawable + } else { + setDefaultMediaIcon() + } + updateProgressState() + } + + updateMediaProgressOnly() + } + + private fun updateMediaProgressCompact() { + ensureMediaLoopRunning() + + val totalDuration = mediaSessionHelper.getTotalDuration() + val playbackState = mediaSessionHelper.getMediaControllerPlaybackState() + val pos = playbackState?.position ?: 0L + + currentProgress = pos.toInt() + currentProgressMax = totalDuration.toInt().takeIf { it > 0 } ?: 100 + + val mediaAppIcon = mediaSessionHelper.getMediaAppIcon() + if (mediaAppIcon != null) { + currentIcon = mediaAppIcon + return + } + + val pkg = playbackState?.extras?.getString("package") + if (pkg.isNullOrEmpty()) { + setDefaultMediaIconCompact() + return + } + + loadIcon(pkg) { drawable -> + if (drawable != null) { + currentIcon = drawable + } else { + setDefaultMediaIconCompact() + } + updateProgressState() + } + } + + private fun setDefaultMediaIcon() { + currentIcon = context.resources.getDrawable(R.drawable.ic_default_music_icon, context.theme) + } + + private fun setDefaultMediaIconCompact() { + currentIcon = context.resources.getDrawable(R.drawable.ic_default_music_icon, context.theme) + } + + private fun updateNotificationProgress() { + if (!isEnabled || !isTrackingProgress) { + stopMediaLoop() + return + } + + if (currentProgressMax <= 0) { + Log.w(TAG, "Invalid max progress $currentProgressMax, using 100") + currentProgressMax = 100 + } + + val pkg = trackedPackageName ?: return + loadIcon(pkg) { drawable -> + currentIcon = drawable + updateProgressState() + } + } + + private fun updateNotificationProgressCompact() { + updateNotificationProgress() + } + + private fun fetchPackageIcon(packageName: String): Drawable { + val pm = context.packageManager + return try { + pm.getApplicationIcon(packageName) + } catch (t: Throwable) { + Log.w(TAG, "Failed to load icon for $packageName", t) + pm.defaultActivityIcon + } + } + + private fun loadIcon(packageName: String, onLoaded: (Drawable?) -> Unit) { + iconCache[packageName]?.let { + onLoaded(it) + return + } + + if (inFlightIconLoads.containsKey(packageName)) return + + val job = mainScope.launch { + val drawable = withContext(bgDispatcher) { + fetchPackageIcon(packageName) + } + + val sizePx = (24f * context.resources.displayMetrics.density).toInt() + drawable.setBounds(0, 0, sizePx, sizePx) + + iconCache[packageName] = drawable + onLoaded(drawable) + } + + inFlightIconLoads[packageName] = job + job.invokeOnCompletion { inFlightIconLoads.remove(packageName) } + } + + private fun extractProgress(notification: Notification) { + val extras = notification.extras + currentProgressMax = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 100) + currentProgress = extras.getInt(Notification.EXTRA_PROGRESS, 0) + } + + private fun trackProgress(sbn: StatusBarNotification) { + isTrackingProgress = true + trackedNotificationKey = sbn.key + trackedPackageName = sbn.packageName + lastProgressUpdateTime = System.currentTimeMillis() + extractProgress(sbn.notification) + requestUiUpdate() + } + + private fun clearProgressTracking() { + isTrackingProgress = false + trackedNotificationKey = null + trackedPackageName = null + currentProgress = 0 + currentProgressMax = 0 + lastProgressUpdateTime = 0L + requestUiUpdate() + } + + private fun checkForStaleProgress() { + if (!isTrackingProgress) return + val key = trackedNotificationKey ?: return + + val sbn = findNotificationByKey(key) + if (sbn == null || !hasProgress(sbn.notification)) { + clearProgressTracking() + return + } + + if (lastProgressUpdateTime == 0L) { + lastProgressUpdateTime = System.currentTimeMillis() + return + } + + val timedOut = System.currentTimeMillis() - lastProgressUpdateTime > PROGRESS_TIMEOUT_MS + val finished = currentProgressMax > 0 && currentProgress >= currentProgressMax + + if (timedOut && finished) { + clearProgressTracking() + } + } + + private fun updateProgressIfNeeded(sbn: StatusBarNotification) { + if (!isTrackingProgress) return + if (sbn.key != trackedNotificationKey) return + + if (!hasProgress(sbn.notification)) { + clearProgressTracking() + return + } + + lastProgressUpdateTime = System.currentTimeMillis() + extractProgress(sbn.notification) + requestUiUpdate() + } + + private fun findNotificationByKey(key: String): StatusBarNotification? { + val actives = notificationListener.activeNotifications ?: return null + for (n in actives) { + if (n.key == key) return n + } + return null + } + + private fun hasProgress(notification: Notification): Boolean { + val extras = notification.extras ?: return false + val indeterminate = extras.getBoolean(Notification.EXTRA_PROGRESS_INDETERMINATE, false) + val maxValid = extras.getInt(Notification.EXTRA_PROGRESS_MAX, 0) > 0 + return extras.containsKey(Notification.EXTRA_PROGRESS) && + extras.containsKey(Notification.EXTRA_PROGRESS_MAX) && + !indeterminate && maxValid + } + + fun onInteraction() { + if (showMediaProgress && mediaSessionHelper.isMediaPlaying()) { + isMenuVisible = !isMenuVisible + updateProgressState() + if (isMenuVisible) { + menuCollapseJob?.cancel() + menuCollapseJob = mainScope.launch { + delay(5000L) + isMenuVisible = false + updateProgressState() + } + } + } else { + openTrackedApp() + } + vibrator.vibrate(VIBRATION_EFFECT) + } + + fun onLongPress() { + if (showMediaProgress && mediaSessionHelper.isMediaPlaying()) openMediaApp() else openTrackedApp() + vibrator.vibrate(VIBRATION_EFFECT) + } + + fun onDoubleTap() { + if (showMediaProgress && mediaSessionHelper.isMediaPlaying()) { + toggleMediaPlaybackState() + vibrator.vibrate(VIBRATION_EFFECT) + } + } + + fun onSwipe(isNext: Boolean) { + if (isNext) skipToNextTrack() else skipToPreviousTrack() + } + + fun onMediaAction(action: Int) { + when (action) { + 0 -> skipToPreviousTrack() + 1 -> toggleMediaPlaybackState() + 2 -> skipToNextTrack() + } + + menuCollapseJob?.cancel() + menuCollapseJob = mainScope.launch { + delay(5000L) + isMenuVisible = false + updateProgressState() + } + } + + fun onMediaMenuDismiss() { + isMenuVisible = false + updateProgressState() + } + + fun setSystemChipVisible(visible: Boolean) { + if (isSystemChipVisible == visible) return + isSystemChipVisible = visible + updateProgressState() + requestUiUpdate() + } + + private fun openTrackedApp() { + val pkg = trackedPackageName + if (pkg.isNullOrEmpty()) { + Log.w(TAG, "No tracked package available") + return + } + + val launchIntent = context.packageManager.getLaunchIntentForPackage(pkg) + if (launchIntent == null) { + Log.w(TAG, "No launch intent for package: $pkg") + return + } + + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(launchIntent) + } + + private fun toggleMediaPlaybackState() { + mediaSessionHelper.toggleMediaPlaybackState() + } + + private fun skipToNextTrack() { + mediaSessionHelper.nextSong() + } + + private fun skipToPreviousTrack() { + mediaSessionHelper.prevSong() + } + + private fun openMediaApp() { + mediaSessionHelper.launchMediaApp() + } + + override fun onNotificationPosted( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap? + ) { + if (sbn == null) return + mainScope.launch { handleNotificationPosted(sbn) } + } + + override fun onNotificationRemoved( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap? + ) { + if (sbn == null) return + mainScope.launch { handleNotificationRemoved(sbn) } + } + + override fun onNotificationRemoved( + sbn: StatusBarNotification?, + rankingMap: NotificationListenerService.RankingMap?, + reason: Int + ) { + if (sbn == null) return + mainScope.launch { handleNotificationRemoved(sbn) } + } + + override fun onNotificationRankingUpdate(rankingMap: NotificationListenerService.RankingMap?) = Unit + override fun onNotificationsInitialized() = Unit + + private fun handleNotificationPosted(sbn: StatusBarNotification) { + if (!isEnabled) return + val notification = sbn.notification ?: return + + val hasValidProgress = hasProgress(notification) + val currentKey = trackedNotificationKey + + if (!hasValidProgress) { + if (currentKey != null && currentKey == sbn.key) clearProgressTracking() + return + } + + if (!isTrackingProgress) { + trackProgress(sbn) + } else if (sbn.key == currentKey) { + updateProgressIfNeeded(sbn) + } + } + + private fun handleNotificationRemoved(sbn: StatusBarNotification) { + if (!isTrackingProgress) return + + if (sbn.key == trackedNotificationKey) { + clearProgressTracking() + return + } + + if (sbn.packageName == trackedPackageName) { + val current = trackedNotificationKey?.let { findNotificationByKey(it) } + if (current == null || !hasProgress(current.notification)) { + clearProgressTracking() + } + } + } + + override fun onHeadsUpPinnedModeChanged(inPinnedMode: Boolean) { + headsUpPinned = inPinnedMode + updateProgressState() + requestUiUpdate() + } + + override fun onKeyguardShowingChanged() { + setForceHidden(keyguardStateController.isShowing) + } + + fun setForceHidden(forceHidden: Boolean) { + if (isForceHidden == forceHidden) return + Log.d(TAG, "setForceHidden $forceHidden") + isForceHidden = forceHidden + updateProgressState() + requestUiUpdate() + } + + private fun updateSettings() { + val wasEnabled = isEnabled + val wasShowingMedia = showMediaProgress + val wasCompactMode = isCompactModeEnabled + + isEnabled = Settings.System.getIntForUser( + contentResolver, + ONGOING_ACTION_CHIP_ENABLED, + 0, + UserHandle.USER_CURRENT + ) == 1 + + showMediaProgress = Settings.System.getIntForUser( + contentResolver, + ONGOING_MEDIA_PROGRESS, + 0, + UserHandle.USER_CURRENT + ) == 1 + + isCompactModeEnabled = Settings.System.getIntForUser( + contentResolver, + ONGOING_COMPACT_MODE_ENABLED, + 0, + UserHandle.USER_CURRENT + ) == 1 + + if (wasEnabled != isEnabled || wasShowingMedia != showMediaProgress || wasCompactMode != isCompactModeEnabled) { + needsFullUiUpdate = true + isExpanded = false + } + + requestUiUpdate() + } + + fun destroy() { + isViewAttached = false + + settingsObserver.unregister() + keyguardStateController.removeCallback(this) + headsUpManager.removeListener(this) + mediaSessionHelper.removeMediaMetadataListener(mediaMetadataListener) + notificationListener.removeNotificationHandler(this) + + uiUpdateJob?.cancel() + mediaProgressJob?.cancel() + staleCheckerJob?.cancel() + compactCollapseJob?.cancel() + menuCollapseJob?.cancel() + + iconCache.clear() + inFlightIconLoads.values.forEach { it.cancel() } + inFlightIconLoads.clear() + + currentIcon = null + mainScope.cancel() + } + + companion object { + private const val TAG = "OngoingActionProgressController" + + private const val ONGOING_ACTION_CHIP_ENABLED = Settings.System.ONGOING_ACTION_CHIP + private const val ONGOING_MEDIA_PROGRESS = Settings.System.ONGOING_MEDIA_PROGRESS + private const val ONGOING_COMPACT_MODE_ENABLED = Settings.System.ONGOING_COMPACT_MODE + + private const val MEDIA_UPDATE_INTERVAL_MS = 1000L + private const val DEBOUNCE_DELAY_MS = 150L + private const val STALE_PROGRESS_CHECK_INTERVAL_MS = 5000L + private const val PROGRESS_TIMEOUT_MS = 30000L + + private val VIBRATION_EFFECT: VibrationEffect = + VibrationEffect.get(VibrationEffect.EFFECT_CLICK) + } +} + +@Immutable +data class ProgressState( + val isVisible: Boolean = false, + val progress: Int = 0, + val maxProgress: Int = 0, + val iconBitmap: ImageBitmap? = null, + val packageName: String? = null, + val isCompactMode: Boolean = false, + val showMediaControls: Boolean = false, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt index 1402ce821cc6..21b56ec2f229 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt @@ -1,23 +1,12 @@ /* - * Copyright (C) 2025 VoltageOS - * - * 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. + * SPDX-FileCopyrightText: VoltageOS + * SPDX-FileCopyrightText: crDroid Android Project + * SPDX-License-Identifier: Apache-2.0 */ package com.android.systemui.statusbar import android.content.Context -import android.graphics.Bitmap import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn @@ -55,18 +44,21 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup -import androidx.core.graphics.drawable.toBitmap import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.policy.KeyguardStateController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch private const val TAG = "OngoingActionProgressCompose" @@ -152,7 +144,7 @@ fun OngoingActionProgress( ) } - state.icon?.let { iconBitmap -> + state.iconBitmap?.let { iconBitmap -> Image( bitmap = iconBitmap, contentDescription = "App icon", @@ -171,7 +163,7 @@ fun OngoingActionProgress( .then(gestureModifier), verticalAlignment = Alignment.CenterVertically ) { - state.icon?.let { iconBitmap -> + state.iconBitmap?.let { iconBitmap -> Image( bitmap = iconBitmap, contentDescription = "App icon", @@ -258,117 +250,61 @@ fun OngoingActionProgress( } /** - * State data for the progress indicator - */ -data class ProgressState( - val isVisible: Boolean = false, - val progress: Int = 0, - val maxProgress: Int = 100, - val icon: androidx.compose.ui.graphics.ImageBitmap? = null, - val packageName: String? = null, - val isIconAdaptive: Boolean = false, - val isCompactMode: Boolean = false, - val showMediaControls: Boolean = false -) - -/** - * Compose-friendly controller that bridges the Java OnGoingActionProgressController - * to Compose state. + * Compose-facing controller that adapts OnGoingActionProgressController state + * into Compose-friendly ProgressState. */ class OnGoingActionProgressComposeController( - context: Context, + private val context: Context, notificationListener: NotificationListener, keyguardStateController: KeyguardStateController, headsUpManager: HeadsUpManager, vibrator: VibratorHelper ) { + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private val _state = MutableStateFlow(ProgressState()) val state: StateFlow = _state - private val javaController: OnGoingActionProgressController + private val controller: OnGoingActionProgressController init { Log.d(TAG, "Initializing OnGoingActionProgressComposeController") - try { - javaController = OnGoingActionProgressController( - context, - notificationListener, - keyguardStateController, - headsUpManager, - vibrator - ) - - javaController.setStateCallback { isVisible, progress, maxProgress, icon, isAdaptive, packageName, isCompact, showMenu -> - Log.d(TAG, "State callback: isVisible=$isVisible, compact=$isCompact, showMenu=$showMenu") - - val iconSizePx = if (isCompact) { - (14 * context.resources.displayMetrics.density).toInt() * 2 - } else { - (16 * context.resources.displayMetrics.density).toInt() * 2 - } - - val iconBitmap = try { - icon?.let { drawable -> - drawable.toBitmap( - width = iconSizePx, - height = iconSizePx, - config = Bitmap.Config.ARGB_8888 - ).asImageBitmap() - } - } catch (e: Exception) { - Log.e(TAG, "Failed to convert icon to bitmap", e) - null - } + controller = OnGoingActionProgressController( + context, + notificationListener, + keyguardStateController, + headsUpManager, + vibrator + ) + scope.launch { + controller.state.collect { state -> _state.value = ProgressState( - isVisible = isVisible, - progress = progress, - maxProgress = maxProgress, - icon = iconBitmap, - packageName = packageName, - isIconAdaptive = isAdaptive, - isCompactMode = isCompact, - showMediaControls = showMenu + isVisible = state.isVisible, + progress = state.progress, + maxProgress = state.maxProgress, + iconBitmap = state.iconBitmap, + packageName = state.packageName, + isCompactMode = state.isCompactMode, + showMediaControls = state.showMediaControls ) } - - Log.d(TAG, "OnGoingActionProgressComposeController initialized successfully") - } catch (e: Exception) { - Log.e(TAG, "Failed to initialize OnGoingActionProgressController", e) - throw e } - } - fun destroy() { - javaController.destroy() + Log.d(TAG, "OnGoingActionProgressComposeController initialized successfully") } - fun onInteraction() { - javaController.onInteraction() - } - - fun onMediaAction(action: Int) { - javaController.onMediaAction(action) - } - - fun onMediaMenuDismiss() { - javaController.onMediaMenuDismiss() - } - - fun onDoubleTap() { - javaController.onDoubleTap() - } - - fun onSwipe(isNext: Boolean) { - javaController.onSwipe(isNext) - } - - fun onLongPress() { - javaController.onLongPress() + fun destroy() { + scope.cancel() + controller.destroy() } - fun setSystemChipVisible(visible: Boolean) { - javaController.setSystemChipVisible(visible) - } + fun onInteraction() = controller.onInteraction() + fun onMediaAction(action: Int) = controller.onMediaAction(action) + fun onMediaMenuDismiss() = controller.onMediaMenuDismiss() + fun onDoubleTap() = controller.onDoubleTap() + fun onSwipe(isNext: Boolean) = controller.onSwipe(isNext) + fun onLongPress() = controller.onLongPress() + fun setSystemChipVisible(visible: Boolean) = controller.setSystemChipVisible(visible) } diff --git a/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java b/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java deleted file mode 100644 index f99d1ca1724e..000000000000 --- a/packages/SystemUI/src/com/android/systemui/util/IconFetcher.java +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright (c) 2025, The LineageOS 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.util; - -import android.content.Context; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.graphics.PorterDuff; -import android.graphics.PorterDuffColorFilter; -import android.graphics.drawable.AdaptiveIconDrawable; -import android.graphics.drawable.Drawable; - -/** A class helping to fetch different versions of icons @LineageExtension */ -public class IconFetcher { - - /** A class which stores wether icon is adaptive and icon itself. */ - public class AdaptiveDrawableResult { - public boolean isAdaptive; - public Drawable drawable; - - public AdaptiveDrawableResult(boolean isAdaptive, Drawable drawable) { - this.isAdaptive = isAdaptive; - this.drawable = drawable; - } - } - - private final Context mContext; - - public IconFetcher(Context context) { - mContext = context; - } - - /** - * Gets a standard package icon - * - * @param packageName name of package for which icon would be fetched - */ - public Drawable getPackageIcon(String packageName) { - PackageManager packageManager = mContext.getPackageManager(); - try { - return packageManager.getApplicationIcon(packageName); - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - return mContext.getDrawable(android.R.drawable.sym_def_app_icon); - } - } - - /** - * Returns a monotonic version of the app icon as a Drawable. The foreground of adaptive icons - * is extracted and tinted, while non-adaptive icons are directly tinted. - * - * @param packageName The package name of the app whose icon is to be fetched. - * @param tintColor The color to use for the monotonic tint. - * @return A monotonic Drawable of the app icon or standard app icon within - * AdaptiveDrawableResult - */ - public AdaptiveDrawableResult getMonotonicPackageIcon(String packageName) { - try { - PackageManager packageManager = mContext.getPackageManager(); - Drawable icon = packageManager.getApplicationIcon(packageName); - - if (icon instanceof AdaptiveIconDrawable) { - AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon; - - Drawable foreground = adaptiveIcon.getForeground(); - - return new AdaptiveDrawableResult(true, icon); - } else { - return new AdaptiveDrawableResult(false, icon); - } - } catch (PackageManager.NameNotFoundException e) { - e.printStackTrace(); - Drawable defaultIcon = mContext.getDrawable(android.R.drawable.sym_def_app_icon); - // The icon is not adaptive by default - return new AdaptiveDrawableResult(false, defaultIcon); - } - } -} From b0af5241d8b872166d2d944c994796432b07780b Mon Sep 17 00:00:00 2001 From: Pranav Vashi Date: Mon, 2 Mar 2026 08:43:56 +0530 Subject: [PATCH 154/190] SystemUI: Use proper media buttons for ongoing action chip and theme it Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- .../res/drawable/ic_media_control_pause.xml | 15 ++++ .../drawable/ic_media_control_skip_next.xml | 15 ++++ .../ic_media_control_skip_previous.xml | 15 ++++ .../statusbar/OngoingActionProgressCompose.kt | 71 +++++++++++-------- 4 files changed, 86 insertions(+), 30 deletions(-) create mode 100644 packages/SystemUI/res/drawable/ic_media_control_pause.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_control_skip_next.xml create mode 100644 packages/SystemUI/res/drawable/ic_media_control_skip_previous.xml diff --git a/packages/SystemUI/res/drawable/ic_media_control_pause.xml b/packages/SystemUI/res/drawable/ic_media_control_pause.xml new file mode 100644 index 000000000000..82da053b48c4 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_control_pause.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_control_skip_next.xml b/packages/SystemUI/res/drawable/ic_media_control_skip_next.xml new file mode 100644 index 000000000000..cc2e671619e0 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_control_skip_next.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/SystemUI/res/drawable/ic_media_control_skip_previous.xml b/packages/SystemUI/res/drawable/ic_media_control_skip_previous.xml new file mode 100644 index 000000000000..8e68a60e8fea --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_media_control_skip_previous.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt index 21b56ec2f229..5ddff19b2ebf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/OngoingActionProgressCompose.kt @@ -47,8 +47,10 @@ import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Popup +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.notification.headsup.HeadsUpManager import com.android.systemui.statusbar.policy.KeyguardStateController @@ -74,6 +76,7 @@ fun OngoingActionProgress( val state by controller.state.collectAsState() val accentColor = MaterialTheme.colorScheme.primary + val surfaceColor = MaterialTheme.colorScheme.surface AnimatedVisibility( visible = state.isVisible, @@ -206,42 +209,28 @@ fun OngoingActionProgress( .width(140.dp) .height(48.dp) .shadow(8.dp, RoundedCornerShape(24.dp)) - .background(Color(0xFF202020), RoundedCornerShape(24.dp)) + .background(surfaceColor, RoundedCornerShape(24.dp)) .padding(horizontal = 16.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(0) }, contentAlignment = Alignment.Center) { - Canvas(modifier = Modifier.size(12.dp)) { - val path = Path().apply { - moveTo(size.width, 0f) - lineTo(0f, size.height / 2) - lineTo(size.width, size.height) - close() - } - drawPath(path, Color.White, style = Fill) - drawRect(Color.White, topLeft = Offset(0f, 0f), size = Size(2.dp.toPx(), size.height)) - } - } + MediaControlButton( + iconRes = R.drawable.ic_media_control_skip_previous, + contentDescription = "Previous", + onClick = { controller.onMediaAction(0) } + ) - Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(1) }, contentAlignment = Alignment.Center) { - Canvas(modifier = Modifier.size(14.dp)) { - drawCircle(Color.White) - } - } + MediaControlButton( + iconRes = R.drawable.ic_media_control_pause, + contentDescription = "Pause", + onClick = { controller.onMediaAction(1) } + ) - Box(modifier = Modifier.size(32.dp).clickable { controller.onMediaAction(2) }, contentAlignment = Alignment.Center) { - Canvas(modifier = Modifier.size(12.dp)) { - val path = Path().apply { - moveTo(0f, 0f) - lineTo(size.width, size.height / 2) - lineTo(0f, size.height) - close() - } - drawPath(path, Color.White, style = Fill) - drawRect(Color.White, topLeft = Offset(size.width - 2.dp.toPx(), 0f), size = Size(2.dp.toPx(), size.height)) - } - } + MediaControlButton( + iconRes = R.drawable.ic_media_control_skip_next, + contentDescription = "Next", + onClick = { controller.onMediaAction(2) } + ) } } } @@ -249,6 +238,28 @@ fun OngoingActionProgress( } } +@Composable +private fun MediaControlButton( + iconRes: Int, + contentDescription: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(32.dp) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = contentDescription, + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface) + ) + } +} + /** * Compose-facing controller that adapts OnGoingActionProgressController state * into Compose-friendly ProgressState. From 072e744801cc3dfd8ca7cf728d7beb72497d653d Mon Sep 17 00:00:00 2001 From: someone5678 <59456192+someone5678@users.noreply.github.com> Date: Wed, 1 Oct 2025 15:29:58 +0900 Subject: [PATCH 155/190] SettingsTheme: Correctly theming AlertDialog with M3 colors Change-Id: I25755bded0e38fb5f43f49e70126ee5d73a2856d Signed-off-by: Pranav Vashi Signed-off-by: Ghosuto --- packages/SettingsLib/SettingsTheme/res/values/themes.xml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/SettingsLib/SettingsTheme/res/values/themes.xml b/packages/SettingsLib/SettingsTheme/res/values/themes.xml index 2d881d1a8a7b..0cbab1439550 100644 --- a/packages/SettingsLib/SettingsTheme/res/values/themes.xml +++ b/packages/SettingsLib/SettingsTheme/res/values/themes.xml @@ -32,11 +32,9 @@