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