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 if (IS_AT_LEAST_S) { 156 final Resources.Theme theme = context.getTheme(); 157 @ColorInt final int accentColor = 158 context.getResources().getColor(mAttentionLevel.getAccentColorResId(), theme); 159 @ColorInt final int backgroundColor = 160 context.getResources().getColor( 161 mAttentionLevel.getBackgroundColorResId(), theme); 162 163 holder.setDividerAllowedAbove(false); 164 holder.setDividerAllowedBelow(false); 165 holder.itemView.getBackground().setTint(backgroundColor); 166 167 mPositiveButtonInfo.mColor = accentColor; 168 mNegativeButtonInfo.mColor = accentColor; 169 170 mDismissButtonInfo.mButton = (ImageButton) holder.findViewById(R.id.banner_dismiss_btn); 171 mDismissButtonInfo.setUpButton(); 172 173 final TextView subtitleView = (TextView) holder.findViewById(R.id.banner_subtitle); 174 subtitleView.setText(mSubtitle); 175 subtitleView.setVisibility(mSubtitle == null ? View.GONE : View.VISIBLE); 176 177 final ImageView iconView = (ImageView) holder.findViewById(R.id.banner_icon); 178 if (iconView != null) { 179 Drawable icon = getIcon(); 180 iconView.setImageDrawable( 181 icon == null 182 ? getContext().getDrawable(R.drawable.ic_warning) 183 : icon); 184 iconView.setColorFilter( 185 new PorterDuffColorFilter(accentColor, PorterDuff.Mode.SRC_IN)); 186 } 187 } else { 188 holder.setDividerAllowedAbove(true); 189 holder.setDividerAllowedBelow(true); 190 } 191 192 mPositiveButtonInfo.setUpButton(); 193 mNegativeButtonInfo.setUpButton(); 194 } 195 196 /** 197 * Set the visibility state of positive button. 198 */ setPositiveButtonVisible(boolean isVisible)199 public BannerMessagePreference setPositiveButtonVisible(boolean isVisible) { 200 if (isVisible != mPositiveButtonInfo.mIsVisible) { 201 mPositiveButtonInfo.mIsVisible = isVisible; 202 notifyChanged(); 203 } 204 return this; 205 } 206 207 /** 208 * Set the visibility state of negative button. 209 */ setNegativeButtonVisible(boolean isVisible)210 public BannerMessagePreference setNegativeButtonVisible(boolean isVisible) { 211 if (isVisible != mNegativeButtonInfo.mIsVisible) { 212 mNegativeButtonInfo.mIsVisible = isVisible; 213 notifyChanged(); 214 } 215 return this; 216 } 217 218 /** 219 * Set the visibility state of dismiss button. 220 */ 221 @RequiresApi(Build.VERSION_CODES.S) setDismissButtonVisible(boolean isVisible)222 public BannerMessagePreference setDismissButtonVisible(boolean isVisible) { 223 if (isVisible != mDismissButtonInfo.mIsVisible) { 224 mDismissButtonInfo.mIsVisible = isVisible; 225 notifyChanged(); 226 } 227 return this; 228 } 229 230 /** 231 * Register a callback to be invoked when positive button is clicked. 232 */ setPositiveButtonOnClickListener( View.OnClickListener listener)233 public BannerMessagePreference setPositiveButtonOnClickListener( 234 View.OnClickListener listener) { 235 if (listener != mPositiveButtonInfo.mListener) { 236 mPositiveButtonInfo.mListener = listener; 237 notifyChanged(); 238 } 239 return this; 240 } 241 242 /** 243 * Register a callback to be invoked when negative button is clicked. 244 */ setNegativeButtonOnClickListener( View.OnClickListener listener)245 public BannerMessagePreference setNegativeButtonOnClickListener( 246 View.OnClickListener listener) { 247 if (listener != mNegativeButtonInfo.mListener) { 248 mNegativeButtonInfo.mListener = listener; 249 notifyChanged(); 250 } 251 return this; 252 } 253 254 /** 255 * Register a callback to be invoked when the dismiss button is clicked. 256 */ 257 @RequiresApi(Build.VERSION_CODES.S) setDismissButtonOnClickListener( View.OnClickListener listener)258 public BannerMessagePreference setDismissButtonOnClickListener( 259 View.OnClickListener listener) { 260 if (listener != mDismissButtonInfo.mListener) { 261 mDismissButtonInfo.mListener = listener; 262 notifyChanged(); 263 } 264 return this; 265 } 266 267 /** 268 * Sets the text to be displayed in positive button. 269 */ setPositiveButtonText(@tringRes int textResId)270 public BannerMessagePreference setPositiveButtonText(@StringRes int textResId) { 271 return setPositiveButtonText(getContext().getString(textResId)); 272 } 273 274 /** 275 * Sets the text to be displayed in positive button. 276 */ setPositiveButtonText(String positiveButtonText)277 public BannerMessagePreference setPositiveButtonText(String positiveButtonText) { 278 if (!TextUtils.equals(positiveButtonText, mPositiveButtonInfo.mText)) { 279 mPositiveButtonInfo.mText = positiveButtonText; 280 notifyChanged(); 281 } 282 return this; 283 } 284 285 /** 286 * Sets the text to be displayed in negative button. 287 */ setNegativeButtonText(@tringRes int textResId)288 public BannerMessagePreference setNegativeButtonText(@StringRes int textResId) { 289 return setNegativeButtonText(getContext().getString(textResId)); 290 } 291 292 /** 293 * Sets the text to be displayed in negative button. 294 */ setNegativeButtonText(String negativeButtonText)295 public BannerMessagePreference setNegativeButtonText(String negativeButtonText) { 296 if (!TextUtils.equals(negativeButtonText, mNegativeButtonInfo.mText)) { 297 mNegativeButtonInfo.mText = negativeButtonText; 298 notifyChanged(); 299 } 300 return this; 301 } 302 303 /** 304 * Sets the subtitle. 305 */ 306 @RequiresApi(Build.VERSION_CODES.S) setSubtitle(@tringRes int textResId)307 public BannerMessagePreference setSubtitle(@StringRes int textResId) { 308 return setSubtitle(getContext().getString(textResId)); 309 } 310 311 /** 312 * Sets the subtitle. 313 */ 314 @RequiresApi(Build.VERSION_CODES.S) setSubtitle(String subtitle)315 public BannerMessagePreference setSubtitle(String subtitle) { 316 if (!TextUtils.equals(subtitle, mSubtitle)) { 317 mSubtitle = subtitle; 318 notifyChanged(); 319 } 320 return this; 321 } 322 323 /** 324 * Sets the attention level. This will update the color theme of the preference. 325 */ 326 @RequiresApi(Build.VERSION_CODES.S) setAttentionLevel(AttentionLevel attentionLevel)327 public BannerMessagePreference setAttentionLevel(AttentionLevel attentionLevel) { 328 if (attentionLevel == mAttentionLevel) { 329 return this; 330 } 331 332 if (attentionLevel != null) { 333 mAttentionLevel = attentionLevel; 334 notifyChanged(); 335 } 336 return this; 337 } 338 339 static class ButtonInfo { 340 private Button mButton; 341 private CharSequence mText; 342 private View.OnClickListener mListener; 343 private boolean mIsVisible = true; 344 @ColorInt private int mColor; 345 setUpButton()346 void setUpButton() { 347 mButton.setText(mText); 348 mButton.setOnClickListener(mListener); 349 350 if (IS_AT_LEAST_S) { 351 mButton.setTextColor(mColor); 352 } 353 354 if (shouldBeVisible()) { 355 mButton.setVisibility(View.VISIBLE); 356 } else { 357 mButton.setVisibility(View.GONE); 358 } 359 } 360 361 /** 362 * By default, two buttons are visible. 363 * If user didn't set a text for a button, then it should not be shown. 364 */ shouldBeVisible()365 private boolean shouldBeVisible() { 366 return mIsVisible && (!TextUtils.isEmpty(mText)); 367 } 368 } 369 370 static class DismissButtonInfo { 371 private ImageButton mButton; 372 private View.OnClickListener mListener; 373 private boolean mIsVisible = true; 374 setUpButton()375 void setUpButton() { 376 mButton.setOnClickListener(mListener); 377 if (shouldBeVisible()) { 378 mButton.setVisibility(View.VISIBLE); 379 } else { 380 mButton.setVisibility(View.GONE); 381 } 382 } 383 384 /** 385 * By default, dismiss button is visible if it has a click listener. 386 */ shouldBeVisible()387 private boolean shouldBeVisible() { 388 return mIsVisible && (mListener != null); 389 } 390 } 391 } 392