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