diff options
8 files changed, 626 insertions, 112 deletions
diff --git a/packages/SettingsLib/BannerMessagePreference/Android.bp b/packages/SettingsLib/BannerMessagePreference/Android.bp index 182daeb45d7c..203a3bf64910 100644 --- a/packages/SettingsLib/BannerMessagePreference/Android.bp +++ b/packages/SettingsLib/BannerMessagePreference/Android.bp @@ -31,5 +31,6 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.healthfitness", + "com.android.permission", ], } diff --git a/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml b/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml new file mode 100644 index 000000000000..c999de7d99ea --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/res/drawable-v35/settingslib_resolved_banner_avd.xml @@ -0,0 +1,157 @@ +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<animated-vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt"> + <aapt:attr name="android:drawable"> + <vector + android:width="112dp" + android:height="112dp" + android:viewportHeight="112" + android:viewportWidth="112"> + <group android:name="_R_G"> + <group + android:name="_R_G_L_1_G" + android:translateX="56.5" + android:translateY="56.625"> + <path + android:name="_R_G_L_1_G_D_0_P_0" + android:fillAlpha="0" + android:fillColor="?android:attr/textColorPrimary" + android:fillType="nonZero" + android:pathData=" M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:trimPathEnd="1" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> + <group + android:name="_R_G_L_0_G" + android:rotation="-90" + android:translateX="56" + android:translateY="56"> + <path + android:name="_R_G_L_0_G_D_0_P_0" + android:pathData=" M53.5 0 C53.5,-29.53 29.53,-53.5 0,-53.5 C0,-53.5 0,-53.5 0,-53.5 C-29.53,-53.5 -53.5,-29.53 -53.5,0 C-53.5,0 -53.5,0 -53.5,0 C-53.5,29.53 -29.53,53.5 0,53.5 C0,53.5 0,53.5 0,53.5 C29.53,53.5 53.5,29.53 53.5,0 C53.5,0 53.5,0 53.5,0c " + android:strokeAlpha="1" + android:strokeColor="?android:attr/textColorTertiary" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="5" + android:trimPathEnd="0" + android:trimPathOffset="0" + android:trimPathStart="0" /> + </group> + </group> + <group android:name="time_group" /> + </vector> + </aapt:attr> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="400" + android:propertyName="fillAlpha" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="0" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="17" + android:propertyName="fillAlpha" + android:startOffset="400" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_1_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="400" + android:propertyName="pathData" + android:startOffset="0" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="83" + android:propertyName="pathData" + android:startOffset="400" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -15.32,-1.16 -15.32,-1.16 C-15.32,-1.16 -14.67,-0.73 -14.67,-0.73 C-14.67,-0.73 -14.39,-0.44 -14.39,-0.44 C-14.39,-0.44 -17.32,2.5 -17.32,2.5 C-17.32,2.5 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 -6.3,7.65 -6.3,7.65 C-6.3,7.65 -3.48,10.47 -3.48,10.47 C-3.48,10.47 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + <objectAnimator + android:duration="167" + android:propertyName="pathData" + android:startOffset="483" + android:valueFrom="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 -6.3,7.65 -6.3,7.65 C-6.3,7.65 -3.48,10.47 -3.48,10.47 C-3.48,10.47 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueTo="M-14.75 -0.59 C-14.75,-0.59 -6.41,7.75 -6.41,7.75 C-6.41,7.75 14.77,-13.41 14.77,-13.41 C14.77,-13.41 17.59,-10.59 17.59,-10.59 C17.59,-10.59 -6.41,13.41 -6.41,13.41 C-6.41,13.41 -17.59,2.23 -17.59,2.23 C-17.59,2.23 -14.75,-0.59 -14.75,-0.59c " + android:valueType="pathType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.667,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="_R_G_L_0_G_D_0_P_0"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="500" + android:propertyName="trimPathEnd" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType"> + <aapt:attr name="android:interpolator"> + <pathInterpolator android:pathData="M 0.0,0.0 c0.2,0 0,1 1.0,1.0" /> + </aapt:attr> + </objectAnimator> + </set> + </aapt:attr> + </target> + <target android:name="time_group"> + <aapt:attr name="android:animation"> + <set android:ordering="together"> + <objectAnimator + android:duration="667" + android:propertyName="translateX" + android:startOffset="0" + android:valueFrom="0" + android:valueTo="1" + android:valueType="floatType" /> + </set> + </aapt:attr> + </target> +</animated-vector>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml b/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml index b10ef6e77ec7..c448a2d434f8 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/layout-v35/settingslib_expressive_banner_message.xml @@ -28,81 +28,104 @@ android:orientation="vertical" style="@style/Banner.Preference.SettingsLib.Expressive"> - <RelativeLayout - android:id="@+id/top_row" + <FrameLayout android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal"> + android:layout_height="wrap_content"> <LinearLayout + android:id="@+id/banner_content" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_alignParentStart="true" - android:layout_marginEnd="@dimen/settingslib_expressive_space_medium4" android:orientation="vertical"> - <TextView - android:id="@+id/banner_header" + + <RelativeLayout + android:id="@+id/top_row" android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Header.SettingsLib.Expressive"/> + android:orientation="horizontal"> - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <ImageView - android:id="@+id/banner_icon" - android:layout_width="@dimen/settingslib_expressive_space_small3" - android:layout_height="@dimen/settingslib_expressive_space_small3" - android:layout_gravity="center_vertical" - android:importantForAccessibility="no" - android:scaleType="fitCenter" /> - - <TextView - android:id="@+id/banner_title" + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Title.SettingsLib.Expressive" /> - </LinearLayout> + android:layout_alignParentStart="true" + android:layout_marginEnd="@dimen/settingslib_expressive_space_medium4" + android:orientation="vertical"> + <TextView + android:id="@+id/banner_header" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Header.SettingsLib.Expressive"/> + + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/banner_icon" + android:layout_width="@dimen/settingslib_expressive_space_small3" + android:layout_height="@dimen/settingslib_expressive_space_small3" + android:layout_gravity="center_vertical" + android:layout_marginEnd="@dimen/settingslib_expressive_space_extrasmall4" + android:importantForAccessibility="no" + android:scaleType="fitCenter" /> + + <TextView + android:id="@+id/banner_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Title.SettingsLib.Expressive" /> + </LinearLayout> + + <TextView + android:id="@+id/banner_subtitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/Banner.Subtitle.SettingsLib.Expressive"/> + </LinearLayout> + + <ImageButton + android:id="@+id/banner_dismiss_btn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:contentDescription="@string/accessibility_banner_message_dismiss" + style="@style/Banner.Dismiss.SettingsLib.Expressive" /> + </RelativeLayout> <TextView - android:id="@+id/banner_subtitle" + android:id="@+id/banner_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/Banner.Summary.SettingsLib.Expressive"/> + + <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - style="@style/Banner.Subtitle.SettingsLib.Expressive"/> + android:id="@+id/banner_buttons_frame" + android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6" + android:orientation="horizontal"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/banner_negative_btn" + android:layout_weight="1" + style="@style/Banner.NegativeButton.SettingsLib.Expressive"/> + <Space + android:id="@+id/banner_button_space" + android:layout_width="@dimen/settingslib_expressive_space_extrasmall4" + android:layout_height="@dimen/settingslib_expressive_space_small1"/> + <com.google.android.material.button.MaterialButton + android:id="@+id/banner_positive_btn" + android:layout_weight="1" + style="@style/Banner.PositiveButton.SettingsLib.Expressive"/> + </LinearLayout> </LinearLayout> - <ImageButton - android:id="@+id/banner_dismiss_btn" + <TextView + android:id="@+id/resolved_banner_text" android:layout_width="wrap_content" android:layout_height="wrap_content" - style="@style/Banner.Dismiss.SettingsLib.Expressive" /> - </RelativeLayout> - - <TextView - android:id="@+id/banner_summary" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - style="@style/Banner.Summary.SettingsLib.Expressive"/> - - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:id="@+id/banner_buttons_frame" - android:paddingTop="@dimen/settingslib_expressive_space_extrasmall6" - android:orientation="horizontal"> - - <com.google.android.material.button.MaterialButton - android:id="@+id/banner_negative_btn" - android:layout_weight="1" - style="@style/Banner.NegativeButton.SettingsLib.Expressive"/> - <Space - android:layout_width="@dimen/settingslib_expressive_space_extrasmall4" - android:layout_height="@dimen/settingslib_expressive_space_small1"/> - <com.google.android.material.button.MaterialButton - android:id="@+id/banner_positive_btn" - android:layout_weight="1" - style="@style/Banner.PositiveButton.SettingsLib.Expressive"/> - </LinearLayout> + android:drawableTop="@drawable/settingslib_resolved_banner_avd" + android:visibility="gone" + style="@style/Banner.ResolvedText.SettingsLib.Expressive"/> + </FrameLayout> </com.android.settingslib.widget.BannerMessageView> </LinearLayout>
\ No newline at end of file diff --git a/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml b/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml index b864311bf9b7..09e07ccef683 100644 --- a/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml +++ b/packages/SettingsLib/BannerMessagePreference/res/values-v35/styles_expressive.xml @@ -33,7 +33,6 @@ <style name="Banner.Title.SettingsLib.Expressive" parent=""> <item name="android:layout_gravity">start</item> - <item name="android:layout_marginLeft">@dimen/settingslib_expressive_space_extrasmall4</item> <item name="android:textAlignment">viewStart</item> <item name="android:textAppearance">@style/TextAppearance.SettingsLib.TitleLarge.Emphasized</item> <item name="android:textColor">?android:attr/textColorPrimary</item> @@ -73,4 +72,11 @@ parent="@style/SettingsLibButtonStyle.Expressive.Outline.Extra"> <item name="materialSizeOverlay">@style/SizeOverlay.Material3Expressive.Button.Small</item> </style> + + <style name="Banner.ResolvedText.SettingsLib.Expressive" parent=""> + <item name="android:layout_gravity">center</item> + <item name="android:drawablePadding">@dimen/settingslib_expressive_space_small1</item> + <item name="android:textAppearance">@style/TextAppearance.SettingsLib.BodyMedium</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> </resources> diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java index c82829d6ccea..c90a76a39510 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessagePreference.java @@ -35,6 +35,8 @@ import android.widget.TextView; import androidx.annotation.ColorInt; import androidx.annotation.ColorRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.StringRes; import androidx.preference.Preference; @@ -43,6 +45,9 @@ import androidx.preference.PreferenceViewHolder; import com.android.settingslib.widget.preference.banner.R; import com.google.android.material.button.MaterialButton; + +import java.util.Objects; + /** * Banner message is a banner displaying important information (permission request, page error etc), * and provide actions for user to address. It requires a user action to be dismissed. @@ -67,13 +72,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD R.color.banner_accent_attention_normal, R.color.settingslib_banner_button_background_normal); - // Corresponds to the enum valye of R.attr.attentionLevel + // Corresponds to the enum value of R.attr.attentionLevel private final int mAttrValue; @ColorRes private final int mBackgroundColorResId; @ColorRes private final int mAccentColorResId; @ColorRes private final int mButtonBackgroundColorResId; - AttentionLevel(int attrValue, @ColorRes int backgroundColorResId, + AttentionLevel( + int attrValue, + @ColorRes int backgroundColorResId, @ColorRes int accentColorResId, @ColorRes int buttonBackgroundColorResId) { mAttrValue = attrValue; @@ -115,33 +122,38 @@ public class BannerMessagePreference extends Preference implements GroupSectionD new BannerMessagePreference.DismissButtonInfo(); // Default attention level is High. - private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; - private CharSequence mSubtitle; - private CharSequence mHeader; + @NonNull private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; + @Nullable private CharSequence mSubtitle; + @Nullable private CharSequence mHeader; private int mButtonOrientation; + @Nullable private ResolutionAnimator.Data mResolutionData; - public BannerMessagePreference(Context context) { + public BannerMessagePreference(@NonNull Context context) { super(context); init(context, null /* attrs */); } - public BannerMessagePreference(Context context, AttributeSet attrs) { + public BannerMessagePreference(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(context, attrs); } - public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr) { + public BannerMessagePreference( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } - public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr, + public BannerMessagePreference( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } - private void init(Context context, AttributeSet attrs) { + private void init(@NonNull Context context, @Nullable AttributeSet attrs) { setSelectable(false); int resId = SettingsThemeHelper.isExpressiveTheme(context) ? R.layout.settingslib_expressive_banner_message @@ -166,7 +178,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } @Override - public void onBindViewHolder(PreferenceViewHolder holder) { + public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { super.onBindViewHolder(holder); final Context context = getContext(); @@ -193,14 +205,20 @@ public class BannerMessagePreference extends Preference implements GroupSectionD final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); if (iconView != null) { Drawable icon = getIcon(); - iconView.setImageDrawable( - icon == null - ? getContext().getDrawable(R.drawable.ic_warning) - : icon); - if (mAttentionLevel != AttentionLevel.NORMAL - && !SettingsThemeHelper.isExpressiveTheme(context)) { - iconView.setColorFilter( - new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); + + if (icon == null && SettingsThemeHelper.isExpressiveTheme(context)) { + iconView.setVisibility(View.GONE); + } else { + iconView.setVisibility(View.VISIBLE); + iconView.setImageDrawable( + icon == null + ? getContext().getDrawable(R.drawable.ic_warning) + : icon); + if (mAttentionLevel != AttentionLevel.NORMAL + && !SettingsThemeHelper.isExpressiveTheme(context)) { + iconView.setColorFilter( + new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); + } } } @@ -233,8 +251,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD mDismissButtonInfo.setUpButton(); final TextView subtitleView = (TextView) holder.findViewById(R.id.banner_subtitle); - subtitleView.setText(mSubtitle); - subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); + if (subtitleView != null) { + subtitleView.setText(mSubtitle); + subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); + } TextView headerView = (TextView) holder.findViewById(R.id.banner_header); if (headerView != null) { @@ -268,11 +288,25 @@ public class BannerMessagePreference extends Preference implements GroupSectionD linearLayout.setOrientation(mButtonOrientation); } } + + View buttonSpace = holder.findViewById(R.id.banner_button_space); + if (buttonSpace != null) { + if (mPositiveButtonInfo.shouldBeVisible() && mNegativeButtonInfo.shouldBeVisible()) { + buttonSpace.setVisibility(View.VISIBLE); + } else { + buttonSpace.setVisibility(View.GONE); + } + } + + if (mResolutionData != null) { + new ResolutionAnimator(mResolutionData, holder).startResolutionAnimation(); + } } /** - * Set the visibility state of positive button. + * Sets the visibility state of the positive button. */ + @NonNull public BannerMessagePreference setPositiveButtonVisible(boolean isVisible) { if (isVisible != mPositiveButtonInfo.mIsVisible) { mPositiveButtonInfo.mIsVisible = isVisible; @@ -282,8 +316,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Set the visibility state of negative button. + * Sets the visibility state of the negative button. */ + @NonNull public BannerMessagePreference setNegativeButtonVisible(boolean isVisible) { if (isVisible != mNegativeButtonInfo.mIsVisible) { mNegativeButtonInfo.mIsVisible = isVisible; @@ -293,9 +328,10 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Set the visibility state of dismiss button. + * Sets the visibility state of the dismiss button. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setDismissButtonVisible(boolean isVisible) { if (isVisible != mDismissButtonInfo.mIsVisible) { mDismissButtonInfo.mIsVisible = isVisible; @@ -305,10 +341,35 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when positive button is clicked. + * Sets the enabled state of the positive button. + */ + @NonNull + public BannerMessagePreference setPositiveButtonEnabled(boolean isEnabled) { + if (isEnabled != mPositiveButtonInfo.mIsEnabled) { + mPositiveButtonInfo.mIsEnabled = isEnabled; + notifyChanged(); + } + return this; + } + + /** + * Sets the enabled state of the negative button. + */ + @NonNull + public BannerMessagePreference setNegativeButtonEnabled(boolean isEnabled) { + if (isEnabled != mNegativeButtonInfo.mIsEnabled) { + mNegativeButtonInfo.mIsEnabled = isEnabled; + notifyChanged(); + } + return this; + } + + /** + * Registers a callback to be invoked when positive button is clicked. */ + @NonNull public BannerMessagePreference setPositiveButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mPositiveButtonInfo.mListener) { mPositiveButtonInfo.mListener = listener; notifyChanged(); @@ -317,10 +378,11 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when negative button is clicked. + * Registers a callback to be invoked when negative button is clicked. */ + @NonNull public BannerMessagePreference setNegativeButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mNegativeButtonInfo.mListener) { mNegativeButtonInfo.mListener = listener; notifyChanged(); @@ -329,11 +391,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Register a callback to be invoked when the dismiss button is clicked. + * Registers a callback to be invoked when the dismiss button is clicked. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setDismissButtonOnClickListener( - View.OnClickListener listener) { + @Nullable View.OnClickListener listener) { if (listener != mDismissButtonInfo.mListener) { mDismissButtonInfo.mListener = listener; notifyChanged(); @@ -344,6 +407,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in positive button. */ + @NonNull public BannerMessagePreference setPositiveButtonText(@StringRes int textResId) { return setPositiveButtonText(getContext().getString(textResId)); } @@ -351,7 +415,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in positive button. */ - public BannerMessagePreference setPositiveButtonText(CharSequence positiveButtonText) { + @NonNull + public BannerMessagePreference setPositiveButtonText( + @Nullable CharSequence positiveButtonText) { if (!TextUtils.equals(positiveButtonText, mPositiveButtonInfo.mText)) { mPositiveButtonInfo.mText = positiveButtonText; notifyChanged(); @@ -362,6 +428,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in negative button. */ + @NonNull public BannerMessagePreference setNegativeButtonText(@StringRes int textResId) { return setNegativeButtonText(getContext().getString(textResId)); } @@ -369,7 +436,9 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the text to be displayed in negative button. */ - public BannerMessagePreference setNegativeButtonText(CharSequence negativeButtonText) { + @NonNull + public BannerMessagePreference setNegativeButtonText( + @Nullable CharSequence negativeButtonText) { if (!TextUtils.equals(negativeButtonText, mNegativeButtonInfo.mText)) { mNegativeButtonInfo.mText = negativeButtonText; notifyChanged(); @@ -380,8 +449,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets button orientation. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull public BannerMessagePreference setButtonOrientation(int orientation) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (mButtonOrientation != orientation) { mButtonOrientation = orientation; notifyChanged(); @@ -393,6 +466,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the subtitle. */ @RequiresApi(Build.VERSION_CODES.S) + @NonNull public BannerMessagePreference setSubtitle(@StringRes int textResId) { return setSubtitle(getContext().getString(textResId)); } @@ -401,7 +475,8 @@ public class BannerMessagePreference extends Preference implements GroupSectionD * Sets the subtitle. */ @RequiresApi(Build.VERSION_CODES.S) - public BannerMessagePreference setSubtitle(CharSequence subtitle) { + @NonNull + public BannerMessagePreference setSubtitle(@Nullable CharSequence subtitle) { if (!TextUtils.equals(subtitle, mSubtitle)) { mSubtitle = subtitle; notifyChanged(); @@ -412,7 +487,7 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the header. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) + @NonNull public BannerMessagePreference setHeader(@StringRes int textResId) { return setHeader(getContext().getString(textResId)); } @@ -420,8 +495,12 @@ public class BannerMessagePreference extends Preference implements GroupSectionD /** * Sets the header. */ - @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM) - public BannerMessagePreference setHeader(CharSequence header) { + @NonNull + public BannerMessagePreference setHeader(@Nullable CharSequence header) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (!TextUtils.equals(header, mHeader)) { mHeader = header; notifyChanged(); @@ -430,32 +509,75 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } /** - * Sets the attention level. This will update the color theme of the preference. + * Plays a resolution animation, showing the given message. */ - public BannerMessagePreference setAttentionLevel(AttentionLevel attentionLevel) { - if (attentionLevel == mAttentionLevel) { + @NonNull + public BannerMessagePreference showResolutionAnimation( + @NonNull CharSequence resolutionMessage, + @NonNull ResolutionCompletedCallback onCompletionCallback) { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { return this; } + ResolutionAnimator.Data resolutionData = + new ResolutionAnimator.Data(resolutionMessage, onCompletionCallback); + if (!Objects.equals(mResolutionData, resolutionData)) { + mResolutionData = resolutionData; + notifyChanged(); + } + return this; + } - if (attentionLevel != null) { - mAttentionLevel = attentionLevel; + /** + * Removes the resolution animation from this preference. + * + * <p>Should be called after the resolution animation completes if this preference will be + * reused. Otherwise the resolution animation will be played everytime this preference is + * displayed. + */ + @NonNull + public BannerMessagePreference clearResolutionAnimation() { + if (!SettingsThemeHelper.isExpressiveTheme(getContext())) { + return this; + } + if (mResolutionData != null) { + mResolutionData = null; notifyChanged(); } return this; } + /** + * Sets the attention level. This will update the color theme of the preference. + */ + @NonNull + public BannerMessagePreference setAttentionLevel(@NonNull AttentionLevel attentionLevel) { + if (attentionLevel == mAttentionLevel) { + return this; + } + + mAttentionLevel = attentionLevel; + notifyChanged(); + return this; + } + static class ButtonInfo { - private Button mButton; - private CharSequence mText; - private View.OnClickListener mListener; + @Nullable private Button mButton; + @Nullable private CharSequence mText; + @Nullable private View.OnClickListener mListener; private boolean mIsVisible = true; + private boolean mIsEnabled = true; @ColorInt private int mColor; @ColorInt private int mBackgroundColor; - private ColorStateList mStrokeColor; + @Nullable private ColorStateList mStrokeColor; void setUpButton() { + if (mButton == null) { + return; + } + mButton.setText(mText); mButton.setOnClickListener(mListener); + mButton.setEnabled(mIsEnabled); MaterialButton btn = null; if (mButton instanceof MaterialButton) { @@ -492,11 +614,15 @@ public class BannerMessagePreference extends Preference implements GroupSectionD } static class DismissButtonInfo { - private ImageButton mButton; - private View.OnClickListener mListener; + @Nullable private ImageButton mButton; + @Nullable private View.OnClickListener mListener; private boolean mIsVisible = true; void setUpButton() { + if (mButton == null) { + return; + } + mButton.setOnClickListener(mListener); if (shouldBeVisible()) { mButton.setVisibility(View.VISIBLE); diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java index ff4e79ddaaa1..eabb6341c3dd 100644 --- a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/BannerMessageView.java @@ -23,6 +23,7 @@ import android.view.TouchDelegate; import android.view.View; import android.widget.LinearLayout; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.settingslib.widget.preference.banner.R; @@ -34,22 +35,25 @@ import com.android.settingslib.widget.preference.banner.R; * {@link BannerMessagePreference} to a {@code PreferenceScreen}. */ public class BannerMessageView extends LinearLayout { - private Rect mTouchTargetForDismissButton; + @Nullable private Rect mTouchTargetForDismissButton; - public BannerMessageView(Context context) { + public BannerMessageView(@NonNull Context context) { super(context); } - public BannerMessageView(Context context, - @Nullable AttributeSet attrs) { + public BannerMessageView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); } - public BannerMessageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + public BannerMessageView( + @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } - public BannerMessageView(Context context, AttributeSet attrs, int defStyleAttr, + public BannerMessageView( + @NonNull Context context, + @Nullable AttributeSet attrs, + int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } diff --git a/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt new file mode 100644 index 000000000000..fbf910a42423 --- /dev/null +++ b/packages/SettingsLib/BannerMessagePreference/src/com/android/settingslib/widget/ResolutionAnimator.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.widget + +import android.graphics.drawable.Animatable2 +import android.graphics.drawable.AnimatedVectorDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.provider.DeviceConfig +import android.transition.Fade +import android.transition.Transition +import android.transition.TransitionListenerAdapter +import android.transition.TransitionManager +import android.transition.TransitionSet +import android.view.View +import android.view.ViewGroup +import android.view.animation.LinearInterpolator +import android.widget.TextView +import androidx.preference.PreferenceViewHolder +import com.android.settingslib.widget.preference.banner.R +import java.time.Duration + +/** Callback to communicate when a banner message resolution animation is completed. */ +fun interface ResolutionCompletedCallback { + fun onCompleted() +} + +internal class ResolutionAnimator( + private val data: Data, + private val preferenceViewHolder: PreferenceViewHolder, +) { + + data class Data( + val resolutionMessage: CharSequence, + val resolutionCompletedCallback: ResolutionCompletedCallback, + ) + + private val defaultBannerContent: View? + get() = preferenceViewHolder.findView(R.id.banner_content) + private val resolvedTextView: TextView? + get() = preferenceViewHolder.findView(R.id.resolved_banner_text) + + fun startResolutionAnimation() { + resolvedTextView?.text = data.resolutionMessage + resolvedTextView?.resolutionDrawable?.reset() + + val transitionSet = + TransitionSet() + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL) + .setInterpolator(linearInterpolator) + .addTransition(hideIssueContentTransition) + .addTransition( + showResolvedContentTransition + .clone() + .addListener( + object : TransitionListenerAdapter() { + override fun onTransitionEnd(transition: Transition) { + super.onTransitionEnd(transition) + startIssueResolvedAnimation() + } + } + ) + ) + + preferenceViewHolder.itemView.post { + TransitionManager.beginDelayedTransition( + preferenceViewHolder.itemView as ViewGroup, + transitionSet, + ) + + defaultBannerContent?.visibility = View.INVISIBLE + resolvedTextView?.visibility = View.VISIBLE + } + + preferenceViewHolder.itemView.addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + + override fun onViewDetachedFromWindow(v: View) { + v.removeOnAttachStateChangeListener(this) + cancelAnimationsAndFinish() + } + } + ) + } + + private fun startIssueResolvedAnimation() { + val animatedDrawable = resolvedTextView?.resolutionDrawable + + if (animatedDrawable == null) { + hideResolvedUiAndFinish() + return + } + + animatedDrawable.apply { + clearAnimationCallbacks() + registerAnimationCallback( + object : Animatable2.AnimationCallback() { + override fun onAnimationEnd(drawable: Drawable) { + super.onAnimationEnd(drawable) + hideResolvedUiAndFinish() + } + } + ) + start() + } + } + + private fun hideResolvedUiAndFinish() { + val hideTransition = + hideResolvedContentTransition + .clone() + .setInterpolator(linearInterpolator) + .addListener( + object : TransitionListenerAdapter() { + override fun onTransitionEnd(transition: Transition) { + super.onTransitionEnd(transition) + data.resolutionCompletedCallback.onCompleted() + } + } + ) + TransitionManager.beginDelayedTransition( + preferenceViewHolder.itemView as ViewGroup, + hideTransition, + ) + resolvedTextView?.visibility = View.GONE + } + + private fun cancelAnimationsAndFinish() { + TransitionManager.endTransitions(preferenceViewHolder.itemView as ViewGroup) + + resolvedTextView?.visibility = View.GONE + + val animatedDrawable = resolvedTextView?.resolutionDrawable + animatedDrawable?.clearAnimationCallbacks() + animatedDrawable?.stop() + + data.resolutionCompletedCallback.onCompleted() + } + + private companion object { + private val linearInterpolator = LinearInterpolator() + + private val HIDE_ISSUE_CONTENT_TRANSITION_DURATION = Duration.ofMillis(333) + private val hideIssueContentTransition = + Fade(Fade.OUT).setDuration(HIDE_ISSUE_CONTENT_TRANSITION_DURATION.toMillis()) + + private val SHOW_RESOLVED_CONTENT_TRANSITION_DELAY = Duration.ofMillis(133) + private val SHOW_RESOLVED_CONTENT_TRANSITION_DURATION = Duration.ofMillis(250) + private val showResolvedContentTransition = + Fade(Fade.IN) + .setStartDelay(SHOW_RESOLVED_CONTENT_TRANSITION_DELAY.toMillis()) + .setDuration(SHOW_RESOLVED_CONTENT_TRANSITION_DURATION.toMillis()) + + private val hideResolvedContentTransitionDelay + get() = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + Duration.ofMillis( + DeviceConfig.getLong( + "settings_ui", + "banner_message_pref_hide_resolved_content_delay_millis", + 400, + ) + ) + } else { + Duration.ofMillis(400) + } + + private val HIDE_RESOLVED_UI_TRANSITION_DURATION = Duration.ofMillis(167) + private val hideResolvedContentTransition + get() = + Fade(Fade.OUT) + .setStartDelay(hideResolvedContentTransitionDelay.toMillis()) + .setDuration(HIDE_RESOLVED_UI_TRANSITION_DURATION.toMillis()) + + inline fun <reified T : View> PreferenceViewHolder.findView(id: Int): T? = + findViewById(id) as? T + + val TextView.resolutionDrawable: AnimatedVectorDrawable? + get() = compoundDrawables.find { it != null } as? AnimatedVectorDrawable + } +} diff --git a/packages/SettingsLib/ButtonPreference/Android.bp b/packages/SettingsLib/ButtonPreference/Android.bp index a377f312ffbf..c8375a992d30 100644 --- a/packages/SettingsLib/ButtonPreference/Android.bp +++ b/packages/SettingsLib/ButtonPreference/Android.bp @@ -30,5 +30,6 @@ android_library { apex_available: [ "//apex_available:platform", "com.android.healthfitness", + "com.android.permission", ], } |