blob: bc9e5c19d0b854e40d275ac34b5a4ae2b4474b17 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.template;
import static com.google.android.setupcompat.internal.Preconditions.ensureOnMainThread;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff.Mode;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.PersistableBundle;
import androidx.annotation.AttrRes;
import androidx.annotation.CallSuper;
import androidx.annotation.ColorInt;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.StateSet;
import android.util.TypedValue;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewStub;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.LinearLayout.LayoutParams;
import com.google.android.setupcompat.PartnerCustomizationLayout;
import com.google.android.setupcompat.R;
import com.google.android.setupcompat.internal.FooterButtonPartnerConfig;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.internal.TemplateLayout;
import com.google.android.setupcompat.logging.internal.FooterBarMixinMetrics;
import com.google.android.setupcompat.partnerconfig.PartnerConfig;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
import com.google.android.setupcompat.template.FooterButton.ButtonType;
import java.util.concurrent.atomic.AtomicInteger;
/**
* A {@link Mixin} for managing buttons. By default, the button bar expects that buttons on the
* start (left for LTR) are "secondary" borderless buttons, while buttons on the end (right for LTR)
* are "primary" accent-colored buttons.
*/
public class FooterBarMixin implements Mixin {
private final Context context;
@Nullable private final ViewStub footerStub;
@VisibleForTesting final boolean applyPartnerResources;
private LinearLayout buttonContainer;
private FooterButton primaryButton;
private FooterButton secondaryButton;
@IdRes private int primaryButtonId;
@IdRes private int secondaryButtonId;
ColorStateList primaryDefaultTextColor = null;
ColorStateList secondaryDefaultTextColor = null;
@VisibleForTesting public FooterButtonPartnerConfig primaryButtonPartnerConfigForTesting;
@VisibleForTesting public FooterButtonPartnerConfig secondaryButtonPartnerConfigForTesting;
private int footerBarPaddingTop;
private int footerBarPaddingBottom;
@VisibleForTesting int defaultPadding;
@ColorInt private final int footerBarPrimaryBackgroundColor;
@ColorInt private final int footerBarSecondaryBackgroundColor;
private boolean removeFooterBarWhenEmpty = true;
private static final float DEFAULT_DISABLED_ALPHA = 0.26f;
private static final AtomicInteger nextGeneratedId = new AtomicInteger(1);
@VisibleForTesting public final FooterBarMixinMetrics metrics = new FooterBarMixinMetrics();
private FooterButton.OnButtonEventListener createButtonEventListener(@IdRes int id) {
return new FooterButton.OnButtonEventListener() {
@Override
public void onEnabledChanged(boolean enabled) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setEnabled(enabled);
if (applyPartnerResources) {
updateButtonTextColorWithPartnerConfig(
button,
(id == primaryButtonId)
? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
: PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR);
}
}
}
}
@Override
public void onVisibilityChanged(int visibility) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setVisibility(visibility);
autoSetButtonBarVisibility();
}
}
}
@Override
public void onTextChanged(CharSequence text) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setText(text);
}
}
}
};
}
/**
* Creates a mixin for managing buttons on the footer.
*
* @param layout The {@link TemplateLayout} containing this mixin.
* @param attrs XML attributes given to the layout.
* @param defStyleAttr The default style attribute as given to the constructor of the layout.
*/
public FooterBarMixin(
TemplateLayout layout, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
context = layout.getContext();
footerStub = layout.findManagedViewById(R.id.suc_layout_footer);
this.applyPartnerResources =
layout instanceof PartnerCustomizationLayout
&& ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource();
TypedArray a =
context.obtainStyledAttributes(attrs, R.styleable.SucFooterBarMixin, defStyleAttr, 0);
defaultPadding =
a.getDimensionPixelSize(R.styleable.SucFooterBarMixin_sucFooterBarPaddingVertical, 0);
footerBarPaddingTop =
a.getDimensionPixelSize(
R.styleable.SucFooterBarMixin_sucFooterBarPaddingTop, defaultPadding);
footerBarPaddingBottom =
a.getDimensionPixelSize(
R.styleable.SucFooterBarMixin_sucFooterBarPaddingBottom, defaultPadding);
footerBarPrimaryBackgroundColor =
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterBackground, 0);
footerBarSecondaryBackgroundColor =
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterBackground, 0);
int primaryBtn =
a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterButton, 0);
int secondaryBtn =
a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterButton, 0);
a.recycle();
FooterButtonInflater inflater = new FooterButtonInflater(context);
if (secondaryBtn != 0) {
setSecondaryButton(inflater.inflate(secondaryBtn));
metrics.logPrimaryButtonInitialStateVisibility(/* isVisible= */ true, /* isUsingXml= */ true);
}
if (primaryBtn != 0) {
setPrimaryButton(inflater.inflate(primaryBtn));
metrics.logSecondaryButtonInitialStateVisibility(
/* isVisible= */ true, /* isUsingXml= */ true);
}
}
private View addSpace() {
LinearLayout buttonContainer = ensureFooterInflated();
View space = new View(buttonContainer.getContext());
space.setLayoutParams(new LayoutParams(0, 0, 1.0f));
space.setVisibility(View.INVISIBLE);
buttonContainer.addView(space);
return space;
}
@NonNull
private LinearLayout ensureFooterInflated() {
if (buttonContainer == null) {
if (footerStub == null) {
throw new IllegalStateException("Footer stub is not found in this template");
}
buttonContainer = (LinearLayout) inflateFooter(R.layout.suc_footer_button_bar);
onFooterBarInflated(buttonContainer);
onFooterBarApplyPartnerResource(buttonContainer);
}
return buttonContainer;
}
/**
* Notifies that the footer bar has been inflated to the view hierarchy. Calling super is
* necessary while subclass implement it.
*/
@CallSuper
protected void onFooterBarInflated(LinearLayout buttonContainer) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
buttonContainer.setId(View.generateViewId());
} else {
buttonContainer.setId(generateViewId());
}
updateFooterBarPadding(
buttonContainer,
buttonContainer.getPaddingLeft(),
footerBarPaddingTop,
buttonContainer.getPaddingRight(),
footerBarPaddingBottom);
}
/**
* Notifies while the footer bar apply Partner Resource. Calling super is necessary while subclass
* implement it.
*/
@CallSuper
protected void onFooterBarApplyPartnerResource(LinearLayout buttonContainer) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
if (!applyPartnerResources) {
return;
}
@ColorInt
int color =
PartnerConfigHelper.get(context)
.getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR);
buttonContainer.setBackgroundColor(color);
footerBarPaddingTop =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_TOP);
footerBarPaddingBottom =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_BOTTOM);
updateFooterBarPadding(
buttonContainer,
buttonContainer.getPaddingLeft(),
footerBarPaddingTop,
buttonContainer.getPaddingRight(),
footerBarPaddingBottom);
}
/**
* Inflate FooterActionButton with layout "suc_button". Subclasses can implement this method to
* modify the footer button layout as necessary.
*/
@SuppressLint("InflateParams")
protected FooterActionButton createThemedButton(Context context, @StyleRes int theme) {
// Inflate a single button from XML, which when using support lib, will take advantage of
// the injected layout inflater and give us AppCompatButton instead.
LayoutInflater inflater = LayoutInflater.from(new ContextThemeWrapper(context, theme));
return (FooterActionButton) inflater.inflate(R.layout.suc_button, null, false);
}
/** Sets primary button for footer. */
@MainThread
public void setPrimaryButton(FooterButton footerButton) {
ensureOnMainThread("setPrimaryButton");
ensureFooterInflated();
// Setup button partner config
FooterButtonPartnerConfig footerButtonPartnerConfig =
new FooterButtonPartnerConfig.Builder(footerButton)
.setPartnerTheme(
getPartnerTheme(
footerButton,
/* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Primary,
/* buttonBackgroundColorConfig= */ PartnerConfig
.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR))
.setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
.setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
.setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
.setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.build();
FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
// update information for primary button. Need to update as long as the button inflated.
primaryButtonId = button.getId();
primaryDefaultTextColor = button.getTextColors();
primaryButton = footerButton;
primaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
onFooterButtonInflated(button, footerBarPrimaryBackgroundColor);
onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
// Make sure the position of buttons are correctly and prevent primary button create twice or
// more.
repopulateButtons();
}
/** Returns the {@link FooterButton} of primary button. */
public FooterButton getPrimaryButton() {
return primaryButton;
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public Button getPrimaryButtonView() {
return buttonContainer == null ? null : buttonContainer.findViewById(primaryButtonId);
}
@VisibleForTesting
boolean isPrimaryButtonVisible() {
return getPrimaryButtonView() != null && getPrimaryButtonView().getVisibility() == View.VISIBLE;
}
/** Sets secondary button for footer. */
@MainThread
public void setSecondaryButton(FooterButton footerButton) {
ensureOnMainThread("setSecondaryButton");
ensureFooterInflated();
// Setup button partner config
FooterButtonPartnerConfig footerButtonPartnerConfig =
new FooterButtonPartnerConfig.Builder(footerButton)
.setPartnerTheme(
getPartnerTheme(
footerButton,
/* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Secondary,
/* buttonBackgroundColorConfig= */ PartnerConfig
.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR))
.setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
.setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
.setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
.setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.build();
FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
// update information for secondary button. Need to update as long as the button inflated.
secondaryButtonId = button.getId();
secondaryDefaultTextColor = button.getTextColors();
secondaryButton = footerButton;
secondaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
onFooterButtonInflated(button, footerBarSecondaryBackgroundColor);
onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
// Make sure the position of buttons are correctly and prevent secondary button create twice or
// more.
repopulateButtons();
}
/**
* Corrects the order of footer buttons after the button has been inflated to the view hierarchy.
* Subclasses can implement this method to modify the order of footer buttons as necessary.
*/
protected void repopulateButtons() {
LinearLayout buttonContainer = ensureFooterInflated();
Button tempPrimaryButton = getPrimaryButtonView();
Button tempSecondaryButton = getSecondaryButtonView();
buttonContainer.removeAllViews();
if (tempSecondaryButton != null) {
buttonContainer.addView(tempSecondaryButton);
}
addSpace();
if (tempPrimaryButton != null) {
buttonContainer.addView(tempPrimaryButton);
}
}
/**
* Notifies that the footer button has been inInflated and add to the view hierarchy. Calling
* super is necessary while subclass implement it.
*/
@CallSuper
protected void onFooterButtonInflated(Button button, @ColorInt int defaultButtonBackgroundColor) {
// Try to set default background
if (defaultButtonBackgroundColor != 0) {
updateButtonBackground(button, defaultButtonBackgroundColor);
} else {
// TODO: get button background color from activity theme
}
buttonContainer.addView(button);
autoSetButtonBarVisibility();
}
private int getPartnerTheme(
FooterButton footerButton,
int defaultPartnerTheme,
PartnerConfig buttonBackgroundColorConfig) {
int overrideTheme = footerButton.getTheme();
// Set the default theme if theme is not set, or when running in setup flow.
if (footerButton.getTheme() == 0 || applyPartnerResources) {
overrideTheme = defaultPartnerTheme;
}
// TODO: Make sure customize attributes in theme can be applied during setup flow.
// If sets background color to full transparent, the button changes to colored borderless ink
// button style.
if (applyPartnerResources) {
int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundColorConfig);
if (color == Color.TRANSPARENT) {
overrideTheme = R.style.SucPartnerCustomizationButton_Secondary;
} else if (color != Color.TRANSPARENT) {
// TODO: remove the constrain (color != Color.WHITE), need to check all pages
// go well without customization. It should be fine since the default value of secondary bg
// color is set as transparent.
overrideTheme = R.style.SucPartnerCustomizationButton_Primary;
}
}
return overrideTheme;
}
@VisibleForTesting
public LinearLayout getButtonContainer() {
return buttonContainer;
}
/** Returns the {@link FooterButton} of secondary button. */
public FooterButton getSecondaryButton() {
return secondaryButton;
}
/**
* Sets whether the footer bar should be removed when there are no footer buttons in the bar.
*
* @param value True if footer bar is gone, false otherwise.
*/
public void setRemoveFooterBarWhenEmpty(boolean value) {
removeFooterBarWhenEmpty = value;
autoSetButtonBarVisibility();
}
/**
* Checks the visibility state of footer buttons to set the visibility state of this footer bar
* automatically.
*/
private void autoSetButtonBarVisibility() {
Button primaryButton = getPrimaryButtonView();
Button secondaryButton = getSecondaryButtonView();
boolean primaryVisible = primaryButton != null && primaryButton.getVisibility() == View.VISIBLE;
boolean secondaryVisible =
secondaryButton != null && secondaryButton.getVisibility() == View.VISIBLE;
if (buttonContainer != null) {
buttonContainer.setVisibility(
primaryVisible || secondaryVisible
? View.VISIBLE
: removeFooterBarWhenEmpty ? View.GONE : View.INVISIBLE);
}
}
/** Returns the visibility status for this footer bar. */
@VisibleForTesting
public int getVisibility() {
return buttonContainer.getVisibility();
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public Button getSecondaryButtonView() {
return buttonContainer == null ? null : buttonContainer.findViewById(secondaryButtonId);
}
@VisibleForTesting
boolean isSecondaryButtonVisible() {
return getSecondaryButtonView() != null
&& getSecondaryButtonView().getVisibility() == View.VISIBLE;
}
private static int generateViewId() {
for (; ; ) {
final int result = nextGeneratedId.get();
// aapt-generated IDs have the high byte nonzero; clamp to the range under that.
int newValue = result + 1;
if (newValue > 0x00FFFFFF) {
newValue = 1; // Roll over to 1, not 0.
}
if (nextGeneratedId.compareAndSet(result, newValue)) {
return result;
}
}
}
private FooterActionButton inflateButton(
FooterButton footerButton, FooterButtonPartnerConfig footerButtonPartnerConfig) {
FooterActionButton button =
createThemedButton(context, footerButtonPartnerConfig.getPartnerTheme());
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
button.setId(View.generateViewId());
} else {
button.setId(generateViewId());
}
// apply initial configuration into button view.
button.setText(footerButton.getText());
button.setOnClickListener(footerButton);
button.setVisibility(footerButton.getVisibility());
button.setEnabled(footerButton.isEnabled());
button.setFooterButton(footerButton);
footerButton.setOnButtonEventListener(createButtonEventListener(button.getId()));
return button;
}
// TODO: Make sure customize attributes in theme can be applied during setup flow.
@TargetApi(VERSION_CODES.Q)
private void onFooterButtonApplyPartnerResource(
Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
if (!applyPartnerResources) {
return;
}
updateButtonTextColorWithPartnerConfig(
button, footerButtonPartnerConfig.getButtonTextColorConfig());
updateButtonTextSizeWithPartnerConfig(
button, footerButtonPartnerConfig.getButtonTextSizeConfig());
updateButtonTypeFaceWithPartnerConfig(
button, footerButtonPartnerConfig.getButtonTextTypeFaceConfig());
updateButtonBackgroundWithPartnerConfig(
button,
footerButtonPartnerConfig.getButtonBackgroundConfig(),
footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
updateButtonRadiusWithPartnerConfig(button, footerButtonPartnerConfig.getButtonRadiusConfig());
updateButtonIconWithPartnerConfig(button, footerButtonPartnerConfig.getButtonIconConfig());
updateButtonRippleColorWithPartnerConfig(button, footerButtonPartnerConfig);
}
private void updateButtonTextColorWithPartnerConfig(
Button button, PartnerConfig buttonTextColorConfig) {
if (button.isEnabled()) {
@ColorInt
int color = PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig);
if (color != Color.TRANSPARENT) {
button.setTextColor(ColorStateList.valueOf(color));
}
} else {
// disable state will use the default disable state color
button.setTextColor(
button.getId() == primaryButtonId ? primaryDefaultTextColor : secondaryDefaultTextColor);
}
}
private void updateButtonTextSizeWithPartnerConfig(
Button button, PartnerConfig buttonTextSizeConfig) {
float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig);
if (size > 0) {
button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
}
}
private void updateButtonTypeFaceWithPartnerConfig(
Button button, PartnerConfig buttonTextTypeFaceConfig) {
String fontFamilyName =
PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig);
Typeface font = Typeface.create(fontFamilyName, Typeface.NORMAL);
if (font != null) {
button.setTypeface(font);
}
}
@TargetApi(VERSION_CODES.Q)
private void updateButtonBackgroundWithPartnerConfig(
Button button,
PartnerConfig buttonBackgroundConfig,
PartnerConfig buttonDisableAlphaConfig,
PartnerConfig buttonDisableBackgroundConfig) {
Preconditions.checkArgument(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
"Update button background only support on sdk Q or higher");
@ColorInt int color;
@ColorInt int disabledColor;
float disabledAlpha;
int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
int[] ENABLED_STATE_SET = {};
color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig);
disabledAlpha =
PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f);
disabledColor =
PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig);
if (color != Color.TRANSPARENT) {
if (disabledAlpha <= 0f) {
// if no partner resource, fallback to theme disable alpha
float alpha;
TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha});
alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA);
a.recycle();
disabledAlpha = alpha;
}
if (disabledColor == Color.TRANSPARENT) {
// if no partner resource, fallback to button background color
disabledColor = color;
}
// Set text color for ripple.
ColorStateList colorStateList =
new ColorStateList(
new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET},
new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color});
// b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are
// created, but without copying the state from the parent drawable. So even though the
// parent is getting the correct drawable state from the view, the children won't get those
// states until a state change happens.
// As a workaround, we mutate the drawable and forcibly set the state to empty, and then
// refresh the state so the children will have the updated states.
button.getBackground().mutate().setState(new int[0]);
button.refreshDrawableState();
button.setBackgroundTintList(colorStateList);
}
}
private void updateButtonBackground(Button button, @ColorInt int color) {
button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP);
}
private void updateButtonRadiusWithPartnerConfig(
Button button, PartnerConfig buttonRadiusConfig) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig);
GradientDrawable gradientDrawable = getGradientDrawable(button);
if (gradientDrawable != null) {
gradientDrawable.setCornerRadius(radius);
}
}
}
private void updateButtonRippleColorWithPartnerConfig(
Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
// RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
// unavailable. Since Stencil customization provider only works on Q+, there is no need to
// perform any customization for versions 21.
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
RippleDrawable rippleDrawable = getRippleDrawable(button);
if (rippleDrawable == null) {
return;
}
int[] pressedState = {android.R.attr.state_pressed};
@ColorInt int color;
// Get partner text color.
color =
PartnerConfigHelper.get(context)
.getColor(context, footerButtonPartnerConfig.getButtonTextColorConfig());
float alpha =
PartnerConfigHelper.get(context)
.getFraction(context, footerButtonPartnerConfig.getButtonRippleColorAlphaConfig());
// Set text color for ripple.
ColorStateList colorStateList =
new ColorStateList(
new int[][] {pressedState, StateSet.NOTHING},
new int[] {convertRgbToArgb(color, alpha), Color.TRANSPARENT});
rippleDrawable.setColor(colorStateList);
}
}
private void updateButtonIconWithPartnerConfig(Button button, PartnerConfig buttonIconConfig) {
if (button == null) {
return;
}
Drawable icon = null;
if (buttonIconConfig != null) {
icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig);
}
setButtonIcon(button, icon);
}
private void setButtonIcon(Button button, Drawable icon) {
if (button == null) {
return;
}
if (icon != null) {
// TODO: restrict the icons to a reasonable size
int h = icon.getIntrinsicHeight();
int w = icon.getIntrinsicWidth();
icon.setBounds(0, 0, w, h);
}
Drawable iconStart = null;
Drawable iconEnd = null;
if (button.getId() == primaryButtonId) {
iconEnd = icon;
} else if (button.getId() == secondaryButtonId) {
iconStart = icon;
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null);
} else {
button.setCompoundDrawables(iconStart, null, iconEnd, null);
}
}
private static PartnerConfig getDrawablePartnerConfig(@ButtonType int buttonType) {
PartnerConfig result;
switch (buttonType) {
case ButtonType.ADD_ANOTHER:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_ADD_ANOTHER;
break;
case ButtonType.CANCEL:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CANCEL;
break;
case ButtonType.CLEAR:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CLEAR;
break;
case ButtonType.DONE:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_DONE;
break;
case ButtonType.NEXT:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_NEXT;
break;
case ButtonType.OPT_IN:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_OPT_IN;
break;
case ButtonType.SKIP:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_SKIP;
break;
case ButtonType.STOP:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_STOP;
break;
case ButtonType.OTHER:
default:
result = null;
break;
}
return result;
}
GradientDrawable getGradientDrawable(Button button) {
// RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after
// sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only
// works on Q+, there is no need to perform any customization for versions 21.
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
Drawable drawable = button.getBackground();
if (drawable instanceof InsetDrawable) {
LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable();
return (GradientDrawable) layerDrawable.getDrawable(0);
} else if (drawable instanceof RippleDrawable) {
InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0);
return (GradientDrawable) insetDrawable.getDrawable();
}
}
return null;
}
RippleDrawable getRippleDrawable(Button button) {
// RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
// unavailable. Since Stencil customization provider only works on Q+, there is no need to
// perform any customization for versions 21.
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
Drawable drawable = button.getBackground();
if (drawable instanceof InsetDrawable) {
return (RippleDrawable) ((InsetDrawable) drawable).getDrawable();
} else if (drawable instanceof RippleDrawable) {
return (RippleDrawable) drawable;
}
}
return null;
}
@ColorInt
private static int convertRgbToArgb(@ColorInt int color, float alpha) {
return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color));
}
protected View inflateFooter(@LayoutRes int footer) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
LayoutInflater inflater =
LayoutInflater.from(
new ContextThemeWrapper(context, R.style.SucPartnerCustomizationButtonBar_Stackable));
footerStub.setLayoutInflater(inflater);
}
footerStub.setLayoutResource(footer);
return footerStub.inflate();
}
private void updateFooterBarPadding(
LinearLayout buttonContainer, int left, int top, int right, int bottom) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
buttonContainer.setPadding(left, top, right, bottom);
}
/** Returns the paddingTop of footer bar. */
@VisibleForTesting
int getPaddingTop() {
return (buttonContainer != null) ? buttonContainer.getPaddingTop() : footerStub.getPaddingTop();
}
/** Returns the paddingBottom of footer bar. */
@VisibleForTesting
int getPaddingBottom() {
return (buttonContainer != null)
? buttonContainer.getPaddingBottom()
: footerStub.getPaddingBottom();
}
/** Uses for notify mixin the view already attached to window. */
public void onAttachedToWindow() {
metrics.logPrimaryButtonInitialStateVisibility(
/* isVisible= */ isPrimaryButtonVisible(), /* isUsingXml= */ false);
metrics.logSecondaryButtonInitialStateVisibility(
/* isVisible= */ isSecondaryButtonVisible(), /* isUsingXml= */ false);
}
/** Uses for notify mixin the view already detached from window. */
public void onDetachedFromWindow() {
metrics.updateButtonVisibility(isPrimaryButtonVisible(), isSecondaryButtonVisible());
}
/**
* Assigns logging metrics to bundle for PartnerCustomizationLayout to log metrics to SetupWizard.
*/
@TargetApi(VERSION_CODES.Q)
public PersistableBundle getLoggingMetrics() {
return metrics.getMetrics();
}
}