1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.widget; 18 19 import android.content.Context; 20 import android.content.res.Resources; 21 import android.content.res.TypedArray; 22 import android.graphics.PorterDuff; 23 import android.graphics.PorterDuffColorFilter; 24 import android.graphics.drawable.Drawable; 25 import android.os.Build; 26 import android.text.TextUtils; 27 import android.util.AttributeSet; 28 import android.view.View; 29 import android.widget.Button; 30 import android.widget.ImageButton; 31 import android.widget.ImageView; 32 import android.widget.TextView; 33 34 import androidx.annotation.ColorInt; 35 import androidx.annotation.ColorRes; 36 import androidx.annotation.RequiresApi; 37 import androidx.annotation.StringRes; 38 import androidx.preference.Preference; 39 import androidx.preference.PreferenceViewHolder; 40 41 import com.android.settingslib.utils.BuildCompatUtils; 42 43 /** 44 * Banner message is a banner displaying important information (permission request, page error etc), 45 * and provide actions for user to address. It requires a user action to be dismissed. 46 */ 47 public class BannerMessagePreference extends Preference { 48 49 public enum AttentionLevel { 50 HIGH(0, R.color.banner_background_attention_high, R.color.banner_accent_attention_high), 51 MEDIUM(1, 52 R.color.banner_background_attention_medium, 53 R.color.banner_accent_attention_medium), 54 LOW(2, R.color.banner_background_attention_low, R.color.banner_accent_attention_low); 55 56 // Corresponds to the enum valye of R.attr.attentionLevel 57 private final int mAttrValue; 58 @ColorRes private final int mBackgroundColorResId; 59 @ColorRes private final int mAccentColorResId; 60 AttentionLevel(int attrValue, @ColorRes int backgroundColorResId, @ColorRes int accentColorResId)61 AttentionLevel(int attrValue, @ColorRes int backgroundColorResId, 62 @ColorRes int accentColorResId) { 63 mAttrValue = attrValue; 64 mBackgroundColorResId = backgroundColorResId; 65 mAccentColorResId = accentColorResId; 66 } 67 fromAttr(int attrValue)68 static AttentionLevel fromAttr(int attrValue) { 69 for (AttentionLevel level : values()) { 70 if (level.mAttrValue == attrValue) { 71 return level; 72 } 73 } 74 throw new IllegalArgumentException(); 75 } 76 getAccentColorResId()77 public @ColorRes int getAccentColorResId() { 78 return mAccentColorResId; 79 } 80 getBackgroundColorResId()81 public @ColorRes int getBackgroundColorResId() { 82 return mBackgroundColorResId; 83 } 84 } 85 86 private static final String TAG = "BannerPreference"; 87 private static final boolean IS_AT_LEAST_S = BuildCompatUtils.isAtLeastS(); 88 89 private final BannerMessagePreference.ButtonInfo mPositiveButtonInfo = 90 new BannerMessagePreference.ButtonInfo(); 91 private final BannerMessagePreference.ButtonInfo mNegativeButtonInfo = 92 new BannerMessagePreference.ButtonInfo(); 93 private final BannerMessagePreference.DismissButtonInfo mDismissButtonInfo = 94 new BannerMessagePreference.DismissButtonInfo(); 95 96 // Default attention level is High. 97 private AttentionLevel mAttentionLevel = AttentionLevel.HIGH; 98 private String mSubtitle; 99 BannerMessagePreference(Context context)100 public BannerMessagePreference(Context context) { 101 super(context); 102 init(context, null /* attrs */); 103 } 104 BannerMessagePreference(Context context, AttributeSet attrs)105 public BannerMessagePreference(Context context, AttributeSet attrs) { 106 super(context, attrs); 107 init(context, attrs); 108 } 109 BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr)110 public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr) { 111 super(context, attrs, defStyleAttr); 112 init(context, attrs); 113 } 114 BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)115 public BannerMessagePreference(Context context, AttributeSet attrs, int defStyleAttr, 116 int defStyleRes) { 117 super(context, attrs, defStyleAttr, defStyleRes); 118 init(context, attrs); 119 } 120 init(Context context, AttributeSet attrs)121 private void init(Context context, AttributeSet attrs) { 122 setSelectable(false); 123 setLayoutResource(R.layout.settingslib_banner_message); 124 125 if (IS_AT_LEAST_S) { 126 if (attrs != null) { 127 // Get attention level and subtitle from layout XML 128 TypedArray a = 129 context.obtainStyledAttributes(attrs, R.styleable.BannerMessagePreference); 130 int mAttentionLevelValue = 131 a.getInt(R.styleable.BannerMessagePreference_attentionLevel, 0); 132 mAttentionLevel = AttentionLevel.fromAttr(mAttentionLevelValue); 133 mSubtitle = a.getString(R.styleable.BannerMessagePreference_subtitle); 134 a.recycle(); 135 } 136 } 137 } 138 139 @Override onBindViewHolder(PreferenceViewHolder holder)140 public void onBindViewHolder(PreferenceViewHolder holder) { 141 super.onBindViewHolder(holder); 142 final Context context = getContext(); 143 144 final TextView titleView = (TextView) holder.findViewById(R.id.banner_title); 145 CharSequence title = getTitle(); 146 titleView.setText(title); 147 titleView.setVisibility(title == null ? View.GONE : View.VISIBLE); 148 149 final TextView summaryView = (TextView) holder.findViewById(R.id.banner_summary); 150 summaryView.setText(getSummary()); 151 152 mPositiveButtonInfo.mButton = (Button) holder.findViewById(R.id.banner_positive_btn); 153 mNegativeButtonInfo.mButton = (Button) holder.findViewById(R.id.banner_negative_btn); 154 155 final Resources.Theme theme = context.getTheme(); 156 @ColorInt final int accentColor = 157 context.getResources().getColor(mAttentionLevel.getAccentColorResId(), theme); 158 159 final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); 160 if (iconView != null) { 161 Drawable icon = getIcon(); 162 iconView.setImageDrawable( 163 icon == null 164 ? getContext().getDrawable(R.drawable.ic_warning) 165 : icon); 166 iconView.setColorFilter( 167 new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); 168 } 169 170 if (IS_AT_LEAST_S) { 171 @ColorInt final int backgroundColor = 172 context.getResources().getColor( 173 mAttentionLevel.getBackgroundColorResId(), theme); 174 175 holder.setDividerAllowedAbove(false); 176 holder.setDividerAllowedBelow(false); 177 holder.itemView.getBackground().setTint(backgroundColor); 178 179 mPositiveButtonInfo.mColor = accentColor; 180 mNegativeButtonInfo.mColor = accentColor; 181 182 mDismissButtonInfo.mButton = (ImageButton) holder.findViewById(R.id.banner_dismiss_btn); 183 mDismissButtonInfo.setUpButton(); 184 185 final TextView subtitleView = (TextView) holder.findViewById(R.id.banner_subtitle); 186 subtitleView.setText(mSubtitle); 187 subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); 188 189 } else { 190 holder.setDividerAllowedAbove(true); 191 holder.setDividerAllowedBelow(true); 192 } 193 194 mPositiveButtonInfo.setUpButton(); 195 mNegativeButtonInfo.setUpButton(); 196 } 197 198 /** 199 * Set the visibility state of positive button. 200 */ setPositiveButtonVisible(boolean isVisible)201 public BannerMessagePreference setPositiveButtonVisible(boolean isVisible) { 202 if (isVisible != mPositiveButtonInfo.mIsVisible) { 203 mPositiveButtonInfo.mIsVisible = isVisible; 204 notifyChanged(); 205 } 206 return this; 207 } 208 209 /** 210 * Set the visibility state of negative button. 211 */ setNegativeButtonVisible(boolean isVisible)212 public BannerMessagePreference setNegativeButtonVisible(boolean isVisible) { 213 if (isVisible != mNegativeButtonInfo.mIsVisible) { 214 mNegativeButtonInfo.mIsVisible = isVisible; 215 notifyChanged(); 216 } 217 return this; 218 } 219 220 /** 221 * Set the visibility state of dismiss button. 222 */ 223 @RequiresApi(Build.VERSION_CODES.S) setDismissButtonVisible(boolean isVisible)224 public BannerMessagePreference setDismissButtonVisible(boolean isVisible) { 225 if (isVisible != mDismissButtonInfo.mIsVisible) { 226 mDismissButtonInfo.mIsVisible = isVisible; 227 notifyChanged(); 228 } 229 return this; 230 } 231 232 /** 233 * Register a callback to be invoked when positive button is clicked. 234 */ setPositiveButtonOnClickListener( View.OnClickListener listener)235 public BannerMessagePreference setPositiveButtonOnClickListener( 236 View.OnClickListener listener) { 237 if (listener != mPositiveButtonInfo.mListener) { 238 mPositiveButtonInfo.mListener = listener; 239 notifyChanged(); 240 } 241 return this; 242 } 243 244 /** 245 * Register a callback to be invoked when negative button is clicked. 246 */ setNegativeButtonOnClickListener( View.OnClickListener listener)247 public BannerMessagePreference setNegativeButtonOnClickListener( 248 View.OnClickListener listener) { 249 if (listener != mNegativeButtonInfo.mListener) { 250 mNegativeButtonInfo.mListener = listener; 251 notifyChanged(); 252 } 253 return this; 254 } 255 256 /** 257 * Register a callback to be invoked when the dismiss button is clicked. 258 */ 259 @RequiresApi(Build.VERSION_CODES.S) setDismissButtonOnClickListener( View.OnClickListener listener)260 public BannerMessagePreference setDismissButtonOnClickListener( 261 View.OnClickListener listener) { 262 if (listener != mDismissButtonInfo.mListener) { 263 mDismissButtonInfo.mListener = listener; 264 notifyChanged(); 265 } 266 return this; 267 } 268 269 /** 270 * Sets the text to be displayed in positive button. 271 */ setPositiveButtonText(@tringRes int textResId)272 public BannerMessagePreference setPositiveButtonText(@StringRes int textResId) { 273 return setPositiveButtonText(getContext().getString(textResId)); 274 } 275 276 /** 277 * Sets the text to be displayed in positive button. 278 */ setPositiveButtonText(String positiveButtonText)279 public BannerMessagePreference setPositiveButtonText(String positiveButtonText) { 280 if (!TextUtils.equals(positiveButtonText, mPositiveButtonInfo.mText)) { 281 mPositiveButtonInfo.mText = positiveButtonText; 282 notifyChanged(); 283 } 284 return this; 285 } 286 287 /** 288 * Sets the text to be displayed in negative button. 289 */ setNegativeButtonText(@tringRes int textResId)290 public BannerMessagePreference setNegativeButtonText(@StringRes int textResId) { 291 return setNegativeButtonText(getContext().getString(textResId)); 292 } 293 294 /** 295 * Sets the text to be displayed in negative button. 296 */ setNegativeButtonText(String negativeButtonText)297 public BannerMessagePreference setNegativeButtonText(String negativeButtonText) { 298 if (!TextUtils.equals(negativeButtonText, mNegativeButtonInfo.mText)) { 299 mNegativeButtonInfo.mText = negativeButtonText; 300 notifyChanged(); 301 } 302 return this; 303 } 304 305 /** 306 * Sets the subtitle. 307 */ 308 @RequiresApi(Build.VERSION_CODES.S) setSubtitle(@tringRes int textResId)309 public BannerMessagePreference setSubtitle(@StringRes int textResId) { 310 return setSubtitle(getContext().getString(textResId)); 311 } 312 313 /** 314 * Sets the subtitle. 315 */ 316 @RequiresApi(Build.VERSION_CODES.S) setSubtitle(String subtitle)317 public BannerMessagePreference setSubtitle(String subtitle) { 318 if (!TextUtils.equals(subtitle, mSubtitle)) { 319 mSubtitle = subtitle; 320 notifyChanged(); 321 } 322 return this; 323 } 324 325 /** 326 * Sets the attention level. This will update the color theme of the preference. 327 */ setAttentionLevel(AttentionLevel attentionLevel)328 public BannerMessagePreference setAttentionLevel(AttentionLevel attentionLevel) { 329 if (attentionLevel == mAttentionLevel) { 330 return this; 331 } 332 333 if (attentionLevel != null) { 334 mAttentionLevel = attentionLevel; 335 notifyChanged(); 336 } 337 return this; 338 } 339 340 static class ButtonInfo { 341 private Button mButton; 342 private CharSequence mText; 343 private View.OnClickListener mListener; 344 private boolean mIsVisible = true; 345 @ColorInt private int mColor; 346 setUpButton()347 void setUpButton() { 348 mButton.setText(mText); 349 mButton.setOnClickListener(mListener); 350 351 if (IS_AT_LEAST_S) { 352 mButton.setTextColor(mColor); 353 } 354 355 if (shouldBeVisible()) { 356 mButton.setVisibility(View.VISIBLE); 357 } else { 358 mButton.setVisibility(View.GONE); 359 } 360 } 361 362 /** 363 * By default, two buttons are visible. 364 * If user didn't set a text for a button, then it should not be shown. 365 */ shouldBeVisible()366 private boolean shouldBeVisible() { 367 return mIsVisible && (!TextUtils.isEmpty(mText)); 368 } 369 } 370 371 static class DismissButtonInfo { 372 private ImageButton mButton; 373 private View.OnClickListener mListener; 374 private boolean mIsVisible = true; 375 setUpButton()376 void setUpButton() { 377 mButton.setOnClickListener(mListener); 378 if (shouldBeVisible()) { 379 mButton.setVisibility(View.VISIBLE); 380 } else { 381 mButton.setVisibility(View.GONE); 382 } 383 } 384 385 /** 386 * By default, dismiss button is visible if it has a click listener. 387 */ shouldBeVisible()388 private boolean shouldBeVisible() { 389 return mIsVisible && (mListener != null); 390 } 391 } 392 } 393