1 /*
2  * Copyright (C) 2021 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 android.annotation.TargetApi;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.TypedArray;
23 import android.graphics.Color;
24 import android.graphics.PorterDuff.Mode;
25 import android.graphics.Typeface;
26 import android.graphics.drawable.Drawable;
27 import android.graphics.drawable.GradientDrawable;
28 import android.graphics.drawable.InsetDrawable;
29 import android.graphics.drawable.LayerDrawable;
30 import android.graphics.drawable.RippleDrawable;
31 import android.os.Build;
32 import android.os.Build.VERSION_CODES;
33 import android.util.StateSet;
34 import android.util.TypedValue;
35 import android.widget.Button;
36 import androidx.annotation.ColorInt;
37 import androidx.annotation.VisibleForTesting;
38 import com.google.android.setupcompat.R;
39 import com.google.android.setupcompat.internal.FooterButtonPartnerConfig;
40 import com.google.android.setupcompat.internal.Preconditions;
41 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
42 import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
43 
44 /** Utils for updating the button style. */
45 public class FooterButtonStyleUtils {
46   private static final float DEFAULT_DISABLED_ALPHA = 0.26f;
47 
48   /** Apply the partner primary button style to given {@code button}. */
applyPrimaryButtonPartnerResource( Context context, Button button, boolean applyDynamicColor)49   public static void applyPrimaryButtonPartnerResource(
50       Context context, Button button, boolean applyDynamicColor) {
51 
52     FooterButtonPartnerConfig footerButtonPartnerConfig =
53         new FooterButtonPartnerConfig.Builder(null)
54             .setPartnerTheme(R.style.SucPartnerCustomizationButton_Primary)
55             .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
56             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
57             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
58             .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
59             .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
60             .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR)
61             .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
62             .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
63             .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
64             .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
65             .build();
66     applyButtonPartnerResources(
67         context,
68         button,
69         applyDynamicColor,
70         /* isButtonIconAtEnd= */ true,
71         footerButtonPartnerConfig);
72   }
73 
74   /** Apply the partner secondary button style to given {@code button}. */
applySecondaryButtonPartnerResource( Context context, Button button, boolean applyDynamicColor)75   public static void applySecondaryButtonPartnerResource(
76       Context context, Button button, boolean applyDynamicColor) {
77 
78     int defaultTheme = R.style.SucPartnerCustomizationButton_Secondary;
79     int color =
80         PartnerConfigHelper.get(context)
81             .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR);
82     if (color != Color.TRANSPARENT) {
83       defaultTheme = R.style.SucPartnerCustomizationButton_Primary;
84     }
85     // Setup button partner config
86     FooterButtonPartnerConfig footerButtonPartnerConfig =
87         new FooterButtonPartnerConfig.Builder(null)
88             .setPartnerTheme(defaultTheme)
89             .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
90             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
91             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
92             .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
93             .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
94             .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
95             .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
96             .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
97             .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
98             .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
99             .build();
100     applyButtonPartnerResources(
101         context,
102         button,
103         applyDynamicColor,
104         /* isButtonIconAtEnd= */ false,
105         footerButtonPartnerConfig);
106   }
107 
applyButtonPartnerResources( Context context, Button button, boolean applyDynamicColor, boolean isButtonIconAtEnd, FooterButtonPartnerConfig footerButtonPartnerConfig)108   static void applyButtonPartnerResources(
109       Context context,
110       Button button,
111       boolean applyDynamicColor,
112       boolean isButtonIconAtEnd,
113       FooterButtonPartnerConfig footerButtonPartnerConfig) {
114 
115     // If dynamic color enabled, these colors won't be overrode by partner config.
116     // Instead, these colors align with the current theme colors.
117     if (!applyDynamicColor) {
118       // use default disable color util we support the partner disable text color
119       if (button.isEnabled()) {
120         FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig(
121             context, button, footerButtonPartnerConfig.getButtonTextColorConfig());
122       }
123       FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig(
124           context,
125           button,
126           footerButtonPartnerConfig.getButtonBackgroundConfig(),
127           footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
128           footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
129     }
130     FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig(
131         context,
132         button,
133         applyDynamicColor,
134         footerButtonPartnerConfig.getButtonTextColorConfig(),
135         footerButtonPartnerConfig.getButtonRippleColorAlphaConfig());
136     FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig(
137         context, button, footerButtonPartnerConfig.getButtonTextSizeConfig());
138     FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig(
139         context, button, footerButtonPartnerConfig.getButtonMinHeightConfig());
140     FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig(
141         context,
142         button,
143         footerButtonPartnerConfig.getButtonTextTypeFaceConfig(),
144         footerButtonPartnerConfig.getButtonTextStyleConfig());
145     FooterButtonStyleUtils.updateButtonRadiusWithPartnerConfig(
146         context, button, footerButtonPartnerConfig.getButtonRadiusConfig());
147     FooterButtonStyleUtils.updateButtonIconWithPartnerConfig(
148         context, button, footerButtonPartnerConfig.getButtonIconConfig(), isButtonIconAtEnd);
149   }
150 
updateButtonTextEnabledColorWithPartnerConfig( Context context, Button button, PartnerConfig buttonEnableTextColorConfig)151   static void updateButtonTextEnabledColorWithPartnerConfig(
152       Context context, Button button, PartnerConfig buttonEnableTextColorConfig) {
153     @ColorInt
154     int color = PartnerConfigHelper.get(context).getColor(context, buttonEnableTextColorConfig);
155     updateButtonTextEnabledColor(button, color);
156   }
157 
updateButtonTextEnabledColor(Button button, @ColorInt int textColor)158   static void updateButtonTextEnabledColor(Button button, @ColorInt int textColor) {
159     if (textColor != Color.TRANSPARENT) {
160       button.setTextColor(ColorStateList.valueOf(textColor));
161     }
162   }
163 
updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor)164   static void updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor) {
165     // TODO : add disable footer button text color partner config
166 
167     // disable state will use the default disable state color
168     button.setTextColor(disabledTextColor);
169   }
170 
171   @TargetApi(VERSION_CODES.Q)
updateButtonBackgroundWithPartnerConfig( Context context, Button button, PartnerConfig buttonBackgroundConfig, PartnerConfig buttonDisableAlphaConfig, PartnerConfig buttonDisableBackgroundConfig)172   static void updateButtonBackgroundWithPartnerConfig(
173       Context context,
174       Button button,
175       PartnerConfig buttonBackgroundConfig,
176       PartnerConfig buttonDisableAlphaConfig,
177       PartnerConfig buttonDisableBackgroundConfig) {
178     Preconditions.checkArgument(
179         Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
180         "Update button background only support on sdk Q or higher");
181     @ColorInt
182     int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig);
183     float disabledAlpha =
184         PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f);
185     @ColorInt
186     int disabledColor =
187         PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig);
188 
189     updateButtonBackgroundTintList(context, button, color, disabledAlpha, disabledColor);
190   }
191 
192   @TargetApi(VERSION_CODES.Q)
updateButtonBackgroundTintList( Context context, Button button, @ColorInt int color, float disabledAlpha, @ColorInt int disabledColor)193   static void updateButtonBackgroundTintList(
194       Context context,
195       Button button,
196       @ColorInt int color,
197       float disabledAlpha,
198       @ColorInt int disabledColor) {
199     int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
200     int[] ENABLED_STATE_SET = {};
201 
202     if (color != Color.TRANSPARENT) {
203       if (disabledAlpha <= 0f) {
204         // if no partner resource, fallback to theme disable alpha
205         TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha});
206         float alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA);
207         a.recycle();
208         disabledAlpha = alpha;
209       }
210       if (disabledColor == Color.TRANSPARENT) {
211         // if no partner resource, fallback to button background color
212         disabledColor = color;
213       }
214 
215       // Set text color for ripple.
216       ColorStateList colorStateList =
217           new ColorStateList(
218               new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET},
219               new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color});
220 
221       // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are
222       // created, but without copying the state from the parent drawable. So even though the
223       // parent is getting the correct drawable state from the view, the children won't get those
224       // states until a state change happens.
225       // As a workaround, we mutate the drawable and forcibly set the state to empty, and then
226       // refresh the state so the children will have the updated states.
227       button.getBackground().mutate().setState(new int[0]);
228       button.refreshDrawableState();
229       button.setBackgroundTintList(colorStateList);
230     }
231   }
232 
233   @TargetApi(VERSION_CODES.Q)
updateButtonRippleColorWithPartnerConfig( Context context, Button button, boolean applyDynamicColor, PartnerConfig buttonTextColorConfig, PartnerConfig buttonRippleColorAlphaConfig)234   static void updateButtonRippleColorWithPartnerConfig(
235       Context context,
236       Button button,
237       boolean applyDynamicColor,
238       PartnerConfig buttonTextColorConfig,
239       PartnerConfig buttonRippleColorAlphaConfig) {
240     if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
241 
242       @ColorInt int textDefaultColor;
243       if (applyDynamicColor) {
244         // Get dynamic text color
245         textDefaultColor = button.getTextColors().getDefaultColor();
246       } else {
247         // Get partner text color.
248         textDefaultColor =
249             PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig);
250       }
251       float alpha =
252           PartnerConfigHelper.get(context).getFraction(context, buttonRippleColorAlphaConfig);
253       updateButtonRippleColor(button, textDefaultColor, alpha);
254     }
255   }
256 
updateButtonRippleColor( Button button, @ColorInt int textColor, float rippleAlpha)257   private static void updateButtonRippleColor(
258       Button button, @ColorInt int textColor, float rippleAlpha) {
259     // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
260     // unavailable. Since Stencil customization provider only works on Q+, there is no need to
261     // perform any customization for versions 21.
262     if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
263       RippleDrawable rippleDrawable = getRippleDrawable(button);
264       if (rippleDrawable == null) {
265         return;
266       }
267 
268       int[] pressedState = {android.R.attr.state_pressed};
269 
270       // Set text color for ripple.
271       ColorStateList colorStateList =
272           new ColorStateList(
273               new int[][] {pressedState, StateSet.NOTHING},
274               new int[] {convertRgbToArgb(textColor, rippleAlpha), Color.TRANSPARENT});
275       rippleDrawable.setColor(colorStateList);
276     }
277   }
278 
updateButtonTextSizeWithPartnerConfig( Context context, Button button, PartnerConfig buttonTextSizeConfig)279   static void updateButtonTextSizeWithPartnerConfig(
280       Context context, Button button, PartnerConfig buttonTextSizeConfig) {
281     float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig);
282     if (size > 0) {
283       button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
284     }
285   }
286 
updateButtonMinHeightWithPartnerConfig( Context context, Button button, PartnerConfig buttonMinHeightConfig)287   static void updateButtonMinHeightWithPartnerConfig(
288       Context context, Button button, PartnerConfig buttonMinHeightConfig) {
289     if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMinHeightConfig)) {
290       float size = PartnerConfigHelper.get(context).getDimension(context, buttonMinHeightConfig);
291       if (size > 0) {
292         button.setMinHeight((int) size);
293       }
294     }
295   }
296 
updateButtonTypeFaceWithPartnerConfig( Context context, Button button, PartnerConfig buttonTextTypeFaceConfig, PartnerConfig buttonTextStyleConfig)297   static void updateButtonTypeFaceWithPartnerConfig(
298       Context context,
299       Button button,
300       PartnerConfig buttonTextTypeFaceConfig,
301       PartnerConfig buttonTextStyleConfig) {
302     String fontFamilyName =
303         PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig);
304 
305     int textStyleValue = Typeface.NORMAL;
306     if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextStyleConfig)) {
307       textStyleValue =
308           PartnerConfigHelper.get(context)
309               .getInteger(context, buttonTextStyleConfig, Typeface.NORMAL);
310     }
311     Typeface font = Typeface.create(fontFamilyName, textStyleValue);
312     if (font != null) {
313       button.setTypeface(font);
314     }
315   }
316 
updateButtonRadiusWithPartnerConfig( Context context, Button button, PartnerConfig buttonRadiusConfig)317   static void updateButtonRadiusWithPartnerConfig(
318       Context context, Button button, PartnerConfig buttonRadiusConfig) {
319     if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
320       float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig);
321       GradientDrawable gradientDrawable = getGradientDrawable(button);
322       if (gradientDrawable != null) {
323         gradientDrawable.setCornerRadius(radius);
324       }
325     }
326   }
327 
updateButtonIconWithPartnerConfig( Context context, Button button, PartnerConfig buttonIconConfig, boolean isButtonIconAtEnd)328   static void updateButtonIconWithPartnerConfig(
329       Context context, Button button, PartnerConfig buttonIconConfig, boolean isButtonIconAtEnd) {
330     if (button == null) {
331       return;
332     }
333     Drawable icon = null;
334     if (buttonIconConfig != null) {
335       icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig);
336     }
337     setButtonIcon(button, icon, isButtonIconAtEnd);
338   }
339 
setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd)340   private static void setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd) {
341     if (button == null) {
342       return;
343     }
344 
345     if (icon != null) {
346       // TODO: restrict the icons to a reasonable size
347       int h = icon.getIntrinsicHeight();
348       int w = icon.getIntrinsicWidth();
349       icon.setBounds(0, 0, w, h);
350     }
351 
352     Drawable iconStart = null;
353     Drawable iconEnd = null;
354     if (isButtonIconAtEnd) {
355       iconEnd = icon;
356     } else {
357       iconStart = icon;
358     }
359     if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
360       button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null);
361     } else {
362       button.setCompoundDrawables(iconStart, null, iconEnd, null);
363     }
364   }
365 
updateButtonBackground(Button button, @ColorInt int color)366   static void updateButtonBackground(Button button, @ColorInt int color) {
367     button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP);
368   }
369 
370   @VisibleForTesting
getGradientDrawable(Button button)371   public static GradientDrawable getGradientDrawable(Button button) {
372     // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after
373     // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only
374     // works on Q+, there is no need to perform any customization for versions 21.
375     if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
376       Drawable drawable = button.getBackground();
377       if (drawable instanceof InsetDrawable) {
378         LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable();
379         return (GradientDrawable) layerDrawable.getDrawable(0);
380       } else if (drawable instanceof RippleDrawable) {
381         if (((RippleDrawable) drawable).getDrawable(0) instanceof GradientDrawable) {
382           return (GradientDrawable) ((RippleDrawable) drawable).getDrawable(0);
383         }
384         InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0);
385         return (GradientDrawable) insetDrawable.getDrawable();
386       }
387     }
388     return null;
389   }
390 
getRippleDrawable(Button button)391   static RippleDrawable getRippleDrawable(Button button) {
392     // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
393     // unavailable. Since Stencil customization provider only works on Q+, there is no need to
394     // perform any customization for versions 21.
395     if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
396       Drawable drawable = button.getBackground();
397       if (drawable instanceof InsetDrawable) {
398         return (RippleDrawable) ((InsetDrawable) drawable).getDrawable();
399       } else if (drawable instanceof RippleDrawable) {
400         return (RippleDrawable) drawable;
401       }
402     }
403     return null;
404   }
405 
406   @ColorInt
convertRgbToArgb(@olorInt int color, float alpha)407   private static int convertRgbToArgb(@ColorInt int color, float alpha) {
408     return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color));
409   }
410 
FooterButtonStyleUtils()411   private FooterButtonStyleUtils() {}
412 }
413