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