1 /*
2  * Copyright (C) 2017 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 com.google.android.setupcompat.internal.Preconditions.ensureOnMainThread;
20 
21 import android.annotation.SuppressLint;
22 import android.annotation.TargetApi;
23 import android.content.Context;
24 import android.content.res.ColorStateList;
25 import android.content.res.TypedArray;
26 import android.graphics.Color;
27 import android.os.Build;
28 import android.os.Build.VERSION_CODES;
29 import android.os.PersistableBundle;
30 import android.util.AttributeSet;
31 import android.view.ContextThemeWrapper;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 import android.view.ViewStub;
35 import android.widget.Button;
36 import android.widget.LinearLayout;
37 import android.widget.LinearLayout.LayoutParams;
38 import androidx.annotation.AttrRes;
39 import androidx.annotation.CallSuper;
40 import androidx.annotation.ColorInt;
41 import androidx.annotation.IdRes;
42 import androidx.annotation.LayoutRes;
43 import androidx.annotation.MainThread;
44 import androidx.annotation.NonNull;
45 import androidx.annotation.Nullable;
46 import androidx.annotation.StyleRes;
47 import androidx.annotation.VisibleForTesting;
48 import com.google.android.setupcompat.PartnerCustomizationLayout;
49 import com.google.android.setupcompat.R;
50 import com.google.android.setupcompat.internal.FooterButtonPartnerConfig;
51 import com.google.android.setupcompat.internal.TemplateLayout;
52 import com.google.android.setupcompat.logging.internal.FooterBarMixinMetrics;
53 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
54 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
55 import com.google.android.setupcompat.template.FooterButton.ButtonType;
56 import java.util.Locale;
57 import java.util.concurrent.atomic.AtomicInteger;
58 
59 /**
60  * A {@link Mixin} for managing buttons. By default, the button bar expects that buttons on the
61  * start (left for LTR) are "secondary" borderless buttons, while buttons on the end (right for LTR)
62  * are "primary" accent-colored buttons.
63  */
64 public class FooterBarMixin implements Mixin {
65 
66   private final Context context;
67 
68   @Nullable private final ViewStub footerStub;
69 
70   @VisibleForTesting final boolean applyPartnerResources;
71   @VisibleForTesting final boolean applyDynamicColor;
72   @VisibleForTesting final boolean useFullDynamicColor;
73 
74   private LinearLayout buttonContainer;
75   private FooterButton primaryButton;
76   private FooterButton secondaryButton;
77   @IdRes private int primaryButtonId;
78   @IdRes private int secondaryButtonId;
79   ColorStateList primaryDefaultTextColor = null;
80   ColorStateList secondaryDefaultTextColor = null;
81   @VisibleForTesting public FooterButtonPartnerConfig primaryButtonPartnerConfigForTesting;
82   @VisibleForTesting public FooterButtonPartnerConfig secondaryButtonPartnerConfigForTesting;
83 
84   private int footerBarPaddingTop;
85   private int footerBarPaddingBottom;
86   @VisibleForTesting int defaultPadding;
87   @ColorInt private final int footerBarPrimaryBackgroundColor;
88   @ColorInt private final int footerBarSecondaryBackgroundColor;
89   private boolean removeFooterBarWhenEmpty = true;
90   private boolean isSecondaryButtonInPrimaryStyle = false;
91 
92   private static final AtomicInteger nextGeneratedId = new AtomicInteger(1);
93 
94   @VisibleForTesting public final FooterBarMixinMetrics metrics = new FooterBarMixinMetrics();
95 
createButtonEventListener(@dRes int id)96   private FooterButton.OnButtonEventListener createButtonEventListener(@IdRes int id) {
97 
98     return new FooterButton.OnButtonEventListener() {
99 
100       @Override
101       public void onEnabledChanged(boolean enabled) {
102         if (buttonContainer != null) {
103           Button button = buttonContainer.findViewById(id);
104           if (button != null) {
105             button.setEnabled(enabled);
106             if (applyPartnerResources && !applyDynamicColor) {
107               updateButtonTextColorWithEnabledState(
108                   button,
109                   (id == primaryButtonId || isSecondaryButtonInPrimaryStyle)
110                       ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
111                       : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR);
112             }
113           }
114         }
115       }
116 
117       @Override
118       public void onVisibilityChanged(int visibility) {
119         if (buttonContainer != null) {
120           Button button = buttonContainer.findViewById(id);
121           if (button != null) {
122             button.setVisibility(visibility);
123             autoSetButtonBarVisibility();
124           }
125         }
126       }
127 
128       @Override
129       public void onTextChanged(CharSequence text) {
130         if (buttonContainer != null) {
131           Button button = buttonContainer.findViewById(id);
132           if (button != null) {
133             button.setText(text);
134           }
135         }
136       }
137 
138       @Override
139       @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
140       public void onLocaleChanged(Locale locale) {
141         if (buttonContainer != null) {
142           Button button = buttonContainer.findViewById(id);
143           if (button != null && locale != null) {
144             button.setTextLocale(locale);
145           }
146         }
147       }
148 
149       @Override
150       @TargetApi(VERSION_CODES.JELLY_BEAN_MR1)
151       public void onDirectionChanged(int direction) {
152         if (buttonContainer != null && direction != -1) {
153           buttonContainer.setLayoutDirection(direction);
154         }
155       }
156     };
157   }
158 
159   /**
160    * Creates a mixin for managing buttons on the footer.
161    *
162    * @param layout The {@link TemplateLayout} containing this mixin.
163    * @param attrs XML attributes given to the layout.
164    * @param defStyleAttr The default style attribute as given to the constructor of the layout.
165    */
166   public FooterBarMixin(
167       TemplateLayout layout, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
168     context = layout.getContext();
169     footerStub = layout.findManagedViewById(R.id.suc_layout_footer);
170     this.applyPartnerResources =
171         layout instanceof PartnerCustomizationLayout
172             && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource();
173 
174     applyDynamicColor =
175         layout instanceof PartnerCustomizationLayout
176             && ((PartnerCustomizationLayout) layout).shouldApplyDynamicColor();
177 
178     useFullDynamicColor =
179         layout instanceof PartnerCustomizationLayout
180             && ((PartnerCustomizationLayout) layout).useFullDynamicColor();
181 
182     TypedArray a =
183         context.obtainStyledAttributes(attrs, R.styleable.SucFooterBarMixin, defStyleAttr, 0);
184     defaultPadding =
185         a.getDimensionPixelSize(R.styleable.SucFooterBarMixin_sucFooterBarPaddingVertical, 0);
186     footerBarPaddingTop =
187         a.getDimensionPixelSize(
188             R.styleable.SucFooterBarMixin_sucFooterBarPaddingTop, defaultPadding);
189     footerBarPaddingBottom =
190         a.getDimensionPixelSize(
191             R.styleable.SucFooterBarMixin_sucFooterBarPaddingBottom, defaultPadding);
192     footerBarPrimaryBackgroundColor =
193         a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterBackground, 0);
194     footerBarSecondaryBackgroundColor =
195         a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterBackground, 0);
196 
197     int primaryBtn =
198         a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterButton, 0);
199     int secondaryBtn =
200         a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterButton, 0);
201     a.recycle();
202 
203     FooterButtonInflater inflater = new FooterButtonInflater(context);
204 
205     if (secondaryBtn != 0) {
206       setSecondaryButton(inflater.inflate(secondaryBtn));
207       metrics.logPrimaryButtonInitialStateVisibility(/* isVisible= */ true, /* isUsingXml= */ true);
208     }
209 
210     if (primaryBtn != 0) {
211       setPrimaryButton(inflater.inflate(primaryBtn));
212       metrics.logSecondaryButtonInitialStateVisibility(
213           /* isVisible= */ true, /* isUsingXml= */ true);
214     }
215   }
216 
217   private View addSpace() {
218     LinearLayout buttonContainer = ensureFooterInflated();
219     View space = new View(buttonContainer.getContext());
220     space.setLayoutParams(new LayoutParams(0, 0, 1.0f));
221     space.setVisibility(View.INVISIBLE);
222     buttonContainer.addView(space);
223     return space;
224   }
225 
226   @NonNull
227   private LinearLayout ensureFooterInflated() {
228     if (buttonContainer == null) {
229       if (footerStub == null) {
230         throw new IllegalStateException("Footer stub is not found in this template");
231       }
232       buttonContainer = (LinearLayout) inflateFooter(R.layout.suc_footer_button_bar);
233       onFooterBarInflated(buttonContainer);
234       onFooterBarApplyPartnerResource(buttonContainer);
235     }
236     return buttonContainer;
237   }
238 
239   /**
240    * Notifies that the footer bar has been inflated to the view hierarchy. Calling super is
241    * necessary while subclass implement it.
242    */
243   @CallSuper
244   protected void onFooterBarInflated(LinearLayout buttonContainer) {
245     if (buttonContainer == null) {
246       // Ignore action since buttonContainer is null
247       return;
248     }
249     if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
250       buttonContainer.setId(View.generateViewId());
251     } else {
252       buttonContainer.setId(generateViewId());
253     }
254     updateFooterBarPadding(
255         buttonContainer,
256         buttonContainer.getPaddingLeft(),
257         footerBarPaddingTop,
258         buttonContainer.getPaddingRight(),
259         footerBarPaddingBottom);
260   }
261 
262   /**
263    * Notifies while the footer bar apply Partner Resource. Calling super is necessary while subclass
264    * implement it.
265    */
266   @CallSuper
267   protected void onFooterBarApplyPartnerResource(LinearLayout buttonContainer) {
268     if (buttonContainer == null) {
269       // Ignore action since buttonContainer is null
270       return;
271     }
272     if (!applyPartnerResources) {
273       return;
274     }
275 
276     // skip apply partner resources on footerbar background if dynamic color enabled
277     if (!useFullDynamicColor) {
278       @ColorInt
279       int color =
280           PartnerConfigHelper.get(context)
281               .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR);
282       buttonContainer.setBackgroundColor(color);
283     }
284 
285     footerBarPaddingTop =
286         (int)
287             PartnerConfigHelper.get(context)
288                 .getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_TOP);
289     footerBarPaddingBottom =
290         (int)
291             PartnerConfigHelper.get(context)
292                 .getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_BOTTOM);
293     updateFooterBarPadding(
294         buttonContainer,
295         buttonContainer.getPaddingLeft(),
296         footerBarPaddingTop,
297         buttonContainer.getPaddingRight(),
298         footerBarPaddingBottom);
299 
300     if (PartnerConfigHelper.get(context)
301         .isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT)) {
302       int minHeight =
303           (int)
304               PartnerConfigHelper.get(context)
305                   .getDimension(context, PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT);
306       if (minHeight > 0) {
307         buttonContainer.setMinimumHeight(minHeight);
308       }
309     }
310   }
311 
312   /**
313    * Inflate FooterActionButton with layout "suc_button". Subclasses can implement this method to
314    * modify the footer button layout as necessary.
315    */
316   @SuppressLint("InflateParams")
317   protected FooterActionButton createThemedButton(Context context, @StyleRes int theme) {
318     // Inflate a single button from XML, which when using support lib, will take advantage of
319     // the injected layout inflater and give us AppCompatButton instead.
320     LayoutInflater inflater = LayoutInflater.from(new ContextThemeWrapper(context, theme));
321     return (FooterActionButton) inflater.inflate(R.layout.suc_button, null, false);
322   }
323 
324   /** Sets primary button for footer. */
325   @MainThread
326   public void setPrimaryButton(FooterButton footerButton) {
327     ensureOnMainThread("setPrimaryButton");
328     ensureFooterInflated();
329 
330     // Setup button partner config
331     FooterButtonPartnerConfig footerButtonPartnerConfig =
332         new FooterButtonPartnerConfig.Builder(footerButton)
333             .setPartnerTheme(
334                 getPartnerTheme(
335                     footerButton,
336                     /* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Primary,
337                     /* buttonBackgroundColorConfig= */ PartnerConfig
338                         .CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR))
339             .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
340             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
341             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
342             .setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
343             .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
344             .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
345             .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR)
346             .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
347             .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
348             .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
349             .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
350             .build();
351 
352     FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
353     // update information for primary button. Need to update as long as the button inflated.
354     primaryButtonId = button.getId();
355     primaryDefaultTextColor = button.getTextColors();
356     primaryButton = footerButton;
357     primaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
358 
359     onFooterButtonInflated(button, footerBarPrimaryBackgroundColor);
360     onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
361 
362     // Make sure the position of buttons are correctly and prevent primary button create twice or
363     // more.
364     repopulateButtons();
365   }
366 
367   /** Returns the {@link FooterButton} of primary button. */
368   public FooterButton getPrimaryButton() {
369     return primaryButton;
370   }
371 
372   @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
373   public Button getPrimaryButtonView() {
374     return buttonContainer == null ? null : buttonContainer.findViewById(primaryButtonId);
375   }
376 
377   @VisibleForTesting
378   boolean isPrimaryButtonVisible() {
379     return getPrimaryButtonView() != null && getPrimaryButtonView().getVisibility() == View.VISIBLE;
380   }
381 
382   /** Sets secondary button for footer. */
383   @MainThread
384   public void setSecondaryButton(FooterButton footerButton) {
385     setSecondaryButton(footerButton, /*usePrimaryStyle= */ false);
386   }
387 
388   /** Sets secondary button for footer. Allow to use the primary button style. */
389   @MainThread
390   public void setSecondaryButton(FooterButton footerButton, boolean usePrimaryStyle) {
391     ensureOnMainThread("setSecondaryButton");
392     isSecondaryButtonInPrimaryStyle = usePrimaryStyle;
393     ensureFooterInflated();
394 
395     // Setup button partner config
396     FooterButtonPartnerConfig footerButtonPartnerConfig =
397         new FooterButtonPartnerConfig.Builder(footerButton)
398             .setPartnerTheme(
399                 getPartnerTheme(
400                     footerButton,
401                     /* defaultPartnerTheme= */ usePrimaryStyle
402                         ? R.style.SucPartnerCustomizationButton_Primary
403                         : R.style.SucPartnerCustomizationButton_Secondary,
404                     /* buttonBackgroundColorConfig= */ usePrimaryStyle
405                         ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR
406                         : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR))
407             .setButtonBackgroundConfig(
408                 usePrimaryStyle
409                     ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR
410                     : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
411             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
412             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
413             .setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
414             .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
415             .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
416             .setTextColorConfig(
417                 usePrimaryStyle
418                     ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
419                     : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
420             .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
421             .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
422             .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
423             .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
424             .build();
425 
426     FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
427     // update information for secondary button. Need to update as long as the button inflated.
428     secondaryButtonId = button.getId();
429     secondaryDefaultTextColor = button.getTextColors();
430     secondaryButton = footerButton;
431     secondaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
432 
433     onFooterButtonInflated(button, footerBarSecondaryBackgroundColor);
434     onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
435 
436     // Make sure the position of buttons are correctly and prevent secondary button create twice or
437     // more.
438     repopulateButtons();
439   }
440 
441   /**
442    * Corrects the order of footer buttons after the button has been inflated to the view hierarchy.
443    * Subclasses can implement this method to modify the order of footer buttons as necessary.
444    */
445   protected void repopulateButtons() {
446     LinearLayout buttonContainer = ensureFooterInflated();
447     Button tempPrimaryButton = getPrimaryButtonView();
448     Button tempSecondaryButton = getSecondaryButtonView();
449     buttonContainer.removeAllViews();
450 
451     if (tempSecondaryButton != null) {
452       if (isSecondaryButtonInPrimaryStyle) {
453         // Since the secondary button has the same style (with background) as the primary button,
454         // we need to have the left padding equal to the right padding.
455         updateFooterBarPadding(
456             buttonContainer,
457             buttonContainer.getPaddingRight(),
458             buttonContainer.getPaddingTop(),
459             buttonContainer.getPaddingRight(),
460             buttonContainer.getPaddingBottom());
461       }
462       buttonContainer.addView(tempSecondaryButton);
463     }
464     addSpace();
465     if (tempPrimaryButton != null) {
466       buttonContainer.addView(tempPrimaryButton);
467     }
468   }
469 
470   /**
471    * Notifies that the footer button has been inInflated and add to the view hierarchy. Calling
472    * super is necessary while subclass implement it.
473    */
474   @CallSuper
475   protected void onFooterButtonInflated(Button button, @ColorInt int defaultButtonBackgroundColor) {
476     // Try to set default background
477     if (defaultButtonBackgroundColor != 0) {
478       FooterButtonStyleUtils.updateButtonBackground(button, defaultButtonBackgroundColor);
479     } else {
480       // TODO: get button background color from activity theme
481     }
482     buttonContainer.addView(button);
483     autoSetButtonBarVisibility();
484   }
485 
486   private int getPartnerTheme(
487       FooterButton footerButton,
488       int defaultPartnerTheme,
489       PartnerConfig buttonBackgroundColorConfig) {
490     int overrideTheme = footerButton.getTheme();
491 
492     // Set the default theme if theme is not set, or when running in setup flow.
493     if (footerButton.getTheme() == 0 || applyPartnerResources) {
494       overrideTheme = defaultPartnerTheme;
495     }
496     // TODO: Make sure customize attributes in theme can be applied during setup flow.
497     // If sets background color to full transparent, the button changes to colored borderless ink
498     // button style.
499     if (applyPartnerResources) {
500       int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundColorConfig);
501       if (color == Color.TRANSPARENT) {
502         overrideTheme = R.style.SucPartnerCustomizationButton_Secondary;
503       } else if (color != Color.TRANSPARENT) {
504         // TODO: remove the constrain (color != Color.WHITE), need to check all pages
505         // go well without customization. It should be fine since the default value of secondary bg
506         // color is set as transparent.
507         overrideTheme = R.style.SucPartnerCustomizationButton_Primary;
508       }
509     }
510     return overrideTheme;
511   }
512 
513   @VisibleForTesting
514   public LinearLayout getButtonContainer() {
515     return buttonContainer;
516   }
517 
518   /** Returns the {@link FooterButton} of secondary button. */
519   public FooterButton getSecondaryButton() {
520     return secondaryButton;
521   }
522 
523   /**
524    * Sets whether the footer bar should be removed when there are no footer buttons in the bar.
525    *
526    * @param value True if footer bar is gone, false otherwise.
527    */
528   public void setRemoveFooterBarWhenEmpty(boolean value) {
529     removeFooterBarWhenEmpty = value;
530     autoSetButtonBarVisibility();
531   }
532 
533   /**
534    * Checks the visibility state of footer buttons to set the visibility state of this footer bar
535    * automatically.
536    */
537   private void autoSetButtonBarVisibility() {
538     Button primaryButton = getPrimaryButtonView();
539     Button secondaryButton = getSecondaryButtonView();
540     boolean primaryVisible = primaryButton != null && primaryButton.getVisibility() == View.VISIBLE;
541     boolean secondaryVisible =
542         secondaryButton != null && secondaryButton.getVisibility() == View.VISIBLE;
543 
544     if (buttonContainer != null) {
545       buttonContainer.setVisibility(
546           primaryVisible || secondaryVisible
547               ? View.VISIBLE
548               : removeFooterBarWhenEmpty ? View.GONE : View.INVISIBLE);
549     }
550   }
551 
552   /** Returns the visibility status for this footer bar. */
553   @VisibleForTesting
554   public int getVisibility() {
555     return buttonContainer.getVisibility();
556   }
557 
558   @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
559   public Button getSecondaryButtonView() {
560     return buttonContainer == null ? null : buttonContainer.findViewById(secondaryButtonId);
561   }
562 
563   @VisibleForTesting
564   boolean isSecondaryButtonVisible() {
565     return getSecondaryButtonView() != null
566         && getSecondaryButtonView().getVisibility() == View.VISIBLE;
567   }
568 
569   private static int generateViewId() {
570     for (; ; ) {
571       final int result = nextGeneratedId.get();
572       // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
573       int newValue = result + 1;
574       if (newValue > 0x00FFFFFF) {
575         newValue = 1; // Roll over to 1, not 0.
576       }
577       if (nextGeneratedId.compareAndSet(result, newValue)) {
578         return result;
579       }
580     }
581   }
582 
583   private FooterActionButton inflateButton(
584       FooterButton footerButton, FooterButtonPartnerConfig footerButtonPartnerConfig) {
585     FooterActionButton button =
586         createThemedButton(context, footerButtonPartnerConfig.getPartnerTheme());
587     if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
588       button.setId(View.generateViewId());
589     } else {
590       button.setId(generateViewId());
591     }
592 
593     // apply initial configuration into button view.
594     button.setText(footerButton.getText());
595     button.setOnClickListener(footerButton);
596     button.setVisibility(footerButton.getVisibility());
597     button.setEnabled(footerButton.isEnabled());
598     button.setFooterButton(footerButton);
599 
600     footerButton.setOnButtonEventListener(createButtonEventListener(button.getId()));
601     return button;
602   }
603 
604   // TODO: Make sure customize attributes in theme can be applied during setup flow.
605   @TargetApi(VERSION_CODES.Q)
606   private void onFooterButtonApplyPartnerResource(
607       Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
608     if (!applyPartnerResources) {
609       return;
610     }
611     FooterButtonStyleUtils.applyButtonPartnerResources(
612         context,
613         button,
614         applyDynamicColor,
615         /* isButtonIconAtEnd= */ (button.getId() == primaryButtonId),
616         footerButtonPartnerConfig);
617     if (!applyDynamicColor) {
618       // adjust text color based on enabled state
619       updateButtonTextColorWithEnabledState(
620           button, footerButtonPartnerConfig.getButtonTextColorConfig());
621     }
622   }
623 
624   private void updateButtonTextColorWithEnabledState(
625       Button button, PartnerConfig buttonTextColorConfig) {
626     if (button.isEnabled()) {
627       FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig(
628           context, button, buttonTextColorConfig);
629     } else {
630       FooterButtonStyleUtils.updateButtonTextDisableColor(
631           button,
632           /* is Primary= */ (primaryButtonId == button.getId() || isSecondaryButtonInPrimaryStyle)
633               ? primaryDefaultTextColor
634               : secondaryDefaultTextColor);
635     }
636   }
637 
638   private static PartnerConfig getDrawablePartnerConfig(@ButtonType int buttonType) {
639     PartnerConfig result;
640     switch (buttonType) {
641       case ButtonType.ADD_ANOTHER:
642         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_ADD_ANOTHER;
643         break;
644       case ButtonType.CANCEL:
645         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CANCEL;
646         break;
647       case ButtonType.CLEAR:
648         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CLEAR;
649         break;
650       case ButtonType.DONE:
651         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_DONE;
652         break;
653       case ButtonType.NEXT:
654         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_NEXT;
655         break;
656       case ButtonType.OPT_IN:
657         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_OPT_IN;
658         break;
659       case ButtonType.SKIP:
660         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_SKIP;
661         break;
662       case ButtonType.STOP:
663         result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_STOP;
664         break;
665       case ButtonType.OTHER:
666       default:
667         result = null;
668         break;
669     }
670     return result;
671   }
672 
673   protected View inflateFooter(@LayoutRes int footer) {
674     if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
675       LayoutInflater inflater =
676           LayoutInflater.from(
677               new ContextThemeWrapper(context, R.style.SucPartnerCustomizationButtonBar_Stackable));
678       footerStub.setLayoutInflater(inflater);
679     }
680     footerStub.setLayoutResource(footer);
681     return footerStub.inflate();
682   }
683 
684   private void updateFooterBarPadding(
685       LinearLayout buttonContainer, int left, int top, int right, int bottom) {
686     if (buttonContainer == null) {
687       // Ignore action since buttonContainer is null
688       return;
689     }
690     buttonContainer.setPadding(left, top, right, bottom);
691   }
692 
693   /** Returns the paddingTop of footer bar. */
694   @VisibleForTesting
695   int getPaddingTop() {
696     return (buttonContainer != null) ? buttonContainer.getPaddingTop() : footerStub.getPaddingTop();
697   }
698 
699   /** Returns the paddingBottom of footer bar. */
700   @VisibleForTesting
701   int getPaddingBottom() {
702     return (buttonContainer != null)
703         ? buttonContainer.getPaddingBottom()
704         : footerStub.getPaddingBottom();
705   }
706 
707   /** Uses for notify mixin the view already attached to window. */
708   public void onAttachedToWindow() {
709     metrics.logPrimaryButtonInitialStateVisibility(
710         /* isVisible= */ isPrimaryButtonVisible(), /* isUsingXml= */ false);
711     metrics.logSecondaryButtonInitialStateVisibility(
712         /* isVisible= */ isSecondaryButtonVisible(), /* isUsingXml= */ false);
713   }
714 
715   /** Uses for notify mixin the view already detached from window. */
716   public void onDetachedFromWindow() {
717     metrics.updateButtonVisibility(isPrimaryButtonVisible(), isSecondaryButtonVisible());
718   }
719 
720   /**
721    * Assigns logging metrics to bundle for PartnerCustomizationLayout to log metrics to SetupWizard.
722    */
723   @TargetApi(VERSION_CODES.Q)
724   public PersistableBundle getLoggingMetrics() {
725     return metrics.getMetrics();
726   }
727 }
728