1 /*
2  * Copyright 2018 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.google.android.setupcompat.template;
18 
19 import static java.lang.annotation.RetentionPolicy.SOURCE;
20 
21 import android.annotation.TargetApi;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.os.Build.VERSION_CODES;
25 import android.os.PersistableBundle;
26 import android.util.AttributeSet;
27 import android.view.View;
28 import android.view.View.OnClickListener;
29 import androidx.annotation.IntDef;
30 import androidx.annotation.NonNull;
31 import androidx.annotation.Nullable;
32 import androidx.annotation.StringRes;
33 import androidx.annotation.StyleRes;
34 import com.google.android.setupcompat.R;
35 import com.google.android.setupcompat.logging.CustomEvent;
36 import java.lang.annotation.Retention;
37 import java.util.Locale;
38 
39 /**
40  * Definition of a footer button. Clients can use this class to customize attributes like text,
41  * button type and click listener, and FooterBarMixin will inflate a corresponding Button view.
42  */
43 public final class FooterButton implements OnClickListener {
44   private static final String KEY_BUTTON_ON_CLICK_COUNT = "_onClickCount";
45   private static final String KEY_BUTTON_TEXT = "_text";
46   private static final String KEY_BUTTON_TYPE = "_type";
47 
48   @ButtonType private final int buttonType;
49   private CharSequence text;
50   private boolean enabled = true;
51   private int visibility = View.VISIBLE;
52   private int theme;
53   private OnClickListener onClickListener;
54   private OnClickListener onClickListenerWhenDisabled;
55   private OnButtonEventListener buttonListener;
56   private int clickCount = 0;
57   private Locale locale;
58   private int direction;
59 
FooterButton(Context context, AttributeSet attrs)60   public FooterButton(Context context, AttributeSet attrs) {
61     TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterButton);
62     text = a.getString(R.styleable.SucFooterButton_android_text);
63     onClickListener = null;
64     buttonType =
65         getButtonTypeValue(
66             a.getInt(R.styleable.SucFooterButton_sucButtonType, /* defValue= */ ButtonType.OTHER));
67     theme = a.getResourceId(R.styleable.SucFooterButton_android_theme, /* defValue= */ 0);
68     a.recycle();
69   }
70 
71   /**
72    * Allows client customize text, click listener and theme for footer button before Button has been
73    * created. The {@link FooterBarMixin} will inflate a corresponding Button view.
74    *
75    * @param text The text for button.
76    * @param listener The listener for button.
77    * @param buttonType The type of button.
78    * @param theme The theme for button.
79    */
FooterButton( CharSequence text, @Nullable OnClickListener listener, @ButtonType int buttonType, @StyleRes int theme, Locale locale, int direction)80   private FooterButton(
81       CharSequence text,
82       @Nullable OnClickListener listener,
83       @ButtonType int buttonType,
84       @StyleRes int theme,
85       Locale locale,
86       int direction) {
87     this.text = text;
88     onClickListener = listener;
89     this.buttonType = buttonType;
90     this.theme = theme;
91     this.locale = locale;
92     this.direction = direction;
93   }
94 
95   /** Returns the text that this footer button is displaying. */
getText()96   public CharSequence getText() {
97     return text;
98   }
99 
100   /**
101    * Registers a callback to be invoked when this view of footer button is clicked.
102    *
103    * @param listener The callback that will run
104    */
setOnClickListener(@ullable OnClickListener listener)105   public void setOnClickListener(@Nullable OnClickListener listener) {
106     onClickListener = listener;
107   }
108 
109   /** Returns an {@link OnClickListener} of this footer button. */
getOnClickListenerWhenDisabled()110   public OnClickListener getOnClickListenerWhenDisabled() {
111     return onClickListenerWhenDisabled;
112   }
113 
114   /**
115    * Registers a callback to be invoked when footer button disabled and touch event has reacted.
116    *
117    * @param listener The callback that will run
118    */
setOnClickListenerWhenDisabled(@ullable OnClickListener listener)119   public void setOnClickListenerWhenDisabled(@Nullable OnClickListener listener) {
120     onClickListenerWhenDisabled = listener;
121   }
122 
123   /** Returns the type of this footer button icon. */
124   @ButtonType
getButtonType()125   public int getButtonType() {
126     return buttonType;
127   }
128 
129   /** Returns the theme of this footer button. */
130   @StyleRes
getTheme()131   public int getTheme() {
132     return theme;
133   }
134 
135   /**
136    * Sets the enabled state of this footer button.
137    *
138    * @param enabled True if this view is enabled, false otherwise.
139    */
setEnabled(boolean enabled)140   public void setEnabled(boolean enabled) {
141     this.enabled = enabled;
142     if (buttonListener != null) {
143       buttonListener.onEnabledChanged(enabled);
144     }
145   }
146 
147   /** Returns the enabled status for this footer button. */
isEnabled()148   public boolean isEnabled() {
149     return enabled;
150   }
151 
152   /** Returns the layout direction for this footer button. */
getLayoutDirection()153   public int getLayoutDirection() {
154     return direction;
155   }
156 
157   /** Returns the text locale for this footer button. */
getTextLocale()158   public Locale getTextLocale() {
159     return locale;
160   }
161 
162   /**
163    * Sets the visibility state of this footer button.
164    *
165    * @param visibility one of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
166    */
setVisibility(int visibility)167   public void setVisibility(int visibility) {
168     this.visibility = visibility;
169     if (buttonListener != null) {
170       buttonListener.onVisibilityChanged(visibility);
171     }
172   }
173 
174   /** Returns the visibility status for this footer button. */
getVisibility()175   public int getVisibility() {
176     return visibility;
177   }
178 
179   /** Sets the text to be displayed using a string resource identifier. */
setText(Context context, @StringRes int resId)180   public void setText(Context context, @StringRes int resId) {
181     setText(context.getText(resId));
182   }
183 
184   /** Sets the text to be displayed on footer button. */
setText(CharSequence text)185   public void setText(CharSequence text) {
186     this.text = text;
187     if (buttonListener != null) {
188       buttonListener.onTextChanged(text);
189     }
190   }
191 
192   /** Sets the text locale to be displayed on footer button. */
setTextLocale(Locale locale)193   public void setTextLocale(Locale locale) {
194     this.locale = locale;
195     if (buttonListener != null) {
196       buttonListener.onLocaleChanged(locale);
197     }
198   }
199 
200   /** Sets the layout direction to be displayed on footer button. */
setLayoutDirection(int direction)201   public void setLayoutDirection(int direction) {
202     this.direction = direction;
203     if (buttonListener != null) {
204       buttonListener.onDirectionChanged(direction);
205     }
206   }
207 
208   /**
209    * Registers a callback to be invoked when footer button API has set.
210    *
211    * @param listener The callback that will run
212    */
setOnButtonEventListener(@ullable OnButtonEventListener listener)213   void setOnButtonEventListener(@Nullable OnButtonEventListener listener) {
214     if (listener != null) {
215       buttonListener = listener;
216     } else {
217       throw new NullPointerException("Event listener of footer button may not be null.");
218     }
219   }
220 
221   @Override
onClick(View v)222   public void onClick(View v) {
223     if (onClickListener != null) {
224       clickCount++;
225       onClickListener.onClick(v);
226     }
227   }
228 
229   /** Interface definition for a callback to be invoked when footer button API has set. */
230   interface OnButtonEventListener {
231 
onEnabledChanged(boolean enabled)232     void onEnabledChanged(boolean enabled);
233 
onVisibilityChanged(int visibility)234     void onVisibilityChanged(int visibility);
235 
onTextChanged(CharSequence text)236     void onTextChanged(CharSequence text);
237 
onLocaleChanged(Locale locale)238     void onLocaleChanged(Locale locale);
239 
onDirectionChanged(int direction)240     void onDirectionChanged(int direction);
241   }
242 
243   /** Maximum valid value of ButtonType */
244   private static final int MAX_BUTTON_TYPE = 8;
245 
246   @Retention(SOURCE)
247   @IntDef({
248     ButtonType.OTHER,
249     ButtonType.ADD_ANOTHER,
250     ButtonType.CANCEL,
251     ButtonType.CLEAR,
252     ButtonType.DONE,
253     ButtonType.NEXT,
254     ButtonType.OPT_IN,
255     ButtonType.SKIP,
256     ButtonType.STOP
257   })
258   /**
259    * Types for footer button. The button appearance and behavior may change based on its type. In
260    * order to be backward compatible with application built with old version of setupcompat; each
261    * ButtonType should not be changed.
262    */
263   public @interface ButtonType {
264     /** A type of button that doesn't fit into any other categories. */
265     int OTHER = 0;
266     /**
267      * A type of button that will set up additional elements of the ongoing setup step(s) when
268      * clicked.
269      */
270     int ADD_ANOTHER = 1;
271     /** A type of button that will cancel the ongoing setup step(s) and exit setup when clicked. */
272     int CANCEL = 2;
273     /** A type of button that will clear the progress when clicked. (eg: clear PIN code) */
274     int CLEAR = 3;
275     /** A type of button that will exit the setup flow when clicked. */
276     int DONE = 4;
277     /** A type of button that will go to the next screen, or next step in the flow when clicked. */
278     int NEXT = 5;
279     /** A type of button to opt-in or agree to the features described in the current screen. */
280     int OPT_IN = 6;
281     /** A type of button that will skip the current step when clicked. */
282     int SKIP = 7;
283     /** A type of button that will stop the ongoing setup step(s) and skip forward when clicked. */
284     int STOP = 8;
285   }
286 
getButtonTypeValue(int value)287   private int getButtonTypeValue(int value) {
288     if (value >= 0 && value <= MAX_BUTTON_TYPE) {
289       return value;
290     } else {
291       throw new IllegalArgumentException("Not a ButtonType");
292     }
293   }
294 
getButtonTypeName()295   private String getButtonTypeName() {
296     switch (buttonType) {
297       case ButtonType.ADD_ANOTHER:
298         return "ADD_ANOTHER";
299       case ButtonType.CANCEL:
300         return "CANCEL";
301       case ButtonType.CLEAR:
302         return "CLEAR";
303       case ButtonType.DONE:
304         return "DONE";
305       case ButtonType.NEXT:
306         return "NEXT";
307       case ButtonType.OPT_IN:
308         return "OPT_IN";
309       case ButtonType.SKIP:
310         return "SKIP";
311       case ButtonType.STOP:
312         return "STOP";
313       case ButtonType.OTHER:
314       default:
315         return "OTHER";
316     }
317   }
318 
319   /**
320    * Returns footer button related metrics bundle for PartnerCustomizationLayout to log to
321    * SetupWizard.
322    */
323   @TargetApi(VERSION_CODES.Q)
getMetrics(String buttonName)324   public PersistableBundle getMetrics(String buttonName) {
325     PersistableBundle bundle = new PersistableBundle();
326     bundle.putString(
327         buttonName + KEY_BUTTON_TEXT, CustomEvent.trimsStringOverMaxLength(getText().toString()));
328     bundle.putString(buttonName + KEY_BUTTON_TYPE, getButtonTypeName());
329     bundle.putInt(buttonName + KEY_BUTTON_ON_CLICK_COUNT, clickCount);
330     return bundle;
331   }
332 
333   /**
334    * Builder class for constructing {@code FooterButton} objects.
335    *
336    * <p>Allows client customize text, click listener and theme for footer button before Button has
337    * been created. The {@link FooterBarMixin} will inflate a corresponding Button view.
338    *
339    * <p>Example:
340    *
341    * <pre class="prettyprint">
342    * FooterButton primaryButton =
343    *     new FooterButton.Builder(mContext)
344    *         .setText(R.string.primary_button_label)
345    *         .setListener(primaryButton)
346    *         .setButtonType(ButtonType.NEXT)
347    *         .setTheme(R.style.SuwGlifButton_Primary)
348    *         .setTextLocale(Locale.CANADA)
349    *         .setLayoutDirection(View.LAYOUT_DIRECTION_LTR)
350    *         .build();
351    * </pre>
352    */
353   public static class Builder {
354     private final Context context;
355     private String text = "";
356     private Locale locale = null;
357     private int direction = -1;
358     private OnClickListener onClickListener = null;
359     @ButtonType private int buttonType = ButtonType.OTHER;
360     private int theme = 0;
361 
Builder(@onNull Context context)362     public Builder(@NonNull Context context) {
363       this.context = context;
364     }
365 
366     /** Sets the {@code text} of FooterButton. */
setText(String text)367     public Builder setText(String text) {
368       this.text = text;
369       return this;
370     }
371 
372     /** Sets the {@code text} of FooterButton by resource. */
setText(@tringRes int text)373     public Builder setText(@StringRes int text) {
374       this.text = context.getString(text);
375       return this;
376     }
377 
378     /** Sets the {@code locale} of FooterButton. */
setTextLocale(Locale locale)379     public Builder setTextLocale(Locale locale) {
380       this.locale = locale;
381       return this;
382     }
383 
384     /** Sets the {@code direction} of FooterButton. */
setLayoutDirection(int direction)385     public Builder setLayoutDirection(int direction) {
386       this.direction = direction;
387       return this;
388     }
389 
390     /** Sets the {@code listener} of FooterButton. */
setListener(@ullable OnClickListener listener)391     public Builder setListener(@Nullable OnClickListener listener) {
392       onClickListener = listener;
393       return this;
394     }
395 
396     /** Sets the {@code buttonType} of FooterButton. */
setButtonType(@uttonType int buttonType)397     public Builder setButtonType(@ButtonType int buttonType) {
398       this.buttonType = buttonType;
399       return this;
400     }
401 
402     /** Sets the {@code theme} for applying FooterButton. */
setTheme(@tyleRes int theme)403     public Builder setTheme(@StyleRes int theme) {
404       this.theme = theme;
405       return this;
406     }
407 
build()408     public FooterButton build() {
409       return new FooterButton(text, onClickListener, buttonType, theme, locale, direction);
410     }
411   }
412 }
413