From d6fc4afb2ea521d09c0ab12b171ef56a854ecdf9 Mon Sep 17 00:00:00 2001 From: Setup Wizard Team Date: Tue, 31 Dec 2019 20:58:11 +0800 Subject: Import updated Android SetupCompat Library 227351086 Test: mm PiperOrigin-RevId: 227351086 Change-Id: I50a18bf0fc7fe01569098838edee43384bd8f6a0 --- .../setupcompat/PartnerCustomizationLayout.java | 10 +- .../setupcompat/internal/PersistableBundles.java | 43 ++++++++ .../android/setupcompat/internal/Validations.java | 54 ++++++++++ .../android/setupcompat/item/FooterButton.java | 59 ++++++++--- .../android/setupcompat/logging/CustomEvent.java | 39 +++++++- .../android/setupcompat/logging/MetricKey.java | 22 +---- .../PartnerCustomizedResourceListMetric.java | 37 +++++++ .../setupcompat/template/ButtonFooterMixin.java | 72 ++++++++------ .../setupcompat/template/ColoredHeaderMixin.java | 70 +++++++++++++ .../android/setupcompat/template/HeaderMixin.java | 92 +++++++++++++++++ .../android/setupcompat/template/IconMixin.java | 109 +++++++++++++++++++++ .../android/setupcompat/util/PartnerConfig.java | 25 ++++- .../android/setupcompat/util/PartnerConfigKey.java | 27 ++++- 13 files changed, 583 insertions(+), 76 deletions(-) create mode 100644 main/java/com/google/android/setupcompat/internal/PersistableBundles.java create mode 100644 main/java/com/google/android/setupcompat/internal/Validations.java create mode 100644 main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java create mode 100644 main/java/com/google/android/setupcompat/template/ColoredHeaderMixin.java create mode 100644 main/java/com/google/android/setupcompat/template/HeaderMixin.java create mode 100644 main/java/com/google/android/setupcompat/template/IconMixin.java (limited to 'main/java/com/google/android/setupcompat') diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java index 7d0e816..fa4273f 100644 --- a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java +++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java @@ -31,6 +31,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.WindowManager; +import com.google.android.setupcompat.internal.PersistableBundles; import com.google.android.setupcompat.lifecycle.LifecycleFragment; import com.google.android.setupcompat.logging.CustomEvent; import com.google.android.setupcompat.logging.MetricKey; @@ -43,8 +44,7 @@ import com.google.android.setupcompat.util.WizardManagerHelper; /** A templatization layout with consistent style used in Setup Wizard or app itself. */ public class PartnerCustomizationLayout extends TemplateLayout { - private final boolean suwVersionSupportPartnerResource = - Build.VERSION.SDK_INT > VERSION_CODES.P; + private final boolean suwVersionSupportPartnerResource = Build.VERSION.SDK_INT > VERSION_CODES.P; private Activity activity; public PartnerCustomizationLayout(Context context) { @@ -147,9 +147,9 @@ public class PartnerCustomizationLayout extends TemplateLayout { if (WizardManagerHelper.isAnySetupWizard(activity.getIntent())) { ButtonFooterMixin buttonFooterMixin = getMixin(ButtonFooterMixin.class); buttonFooterMixin.onDetachedFromWindow(); - PersistableBundle persistableBundle = new PersistableBundle(); - persistableBundle.putPersistableBundle( - "FooterButtonVisibilityMetrics", buttonFooterMixin.getLoggingMetrics()); + PersistableBundle persistableBundle = + PersistableBundles.mergeBundles( + new PersistableBundle(), buttonFooterMixin.getLoggingMetrics()); SetupMetricsLogger.logCustomEvent( getContext(), CustomEvent.create( diff --git a/main/java/com/google/android/setupcompat/internal/PersistableBundles.java b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java new file mode 100644 index 0000000..6fce61f --- /dev/null +++ b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java @@ -0,0 +1,43 @@ +package com.google.android.setupcompat.internal; + +import android.os.PersistableBundle; +import com.google.common.base.Preconditions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Contains utility methods related to {@link PersistableBundle}. */ +public final class PersistableBundles { + + /** + * Merges two or more {@link PersistableBundle}. Ensures no conflict of keys occurred during + * merge. + * + * @return Returns a new {@link PersistableBundle} that contains all the data from {@code + * firstBundle}, {@code nextBundle} and {@code others}. + */ + public static PersistableBundle mergeBundles( + PersistableBundle firstBundle, PersistableBundle nextBundle, PersistableBundle... others) { + List allBundles = new ArrayList<>(); + allBundles.addAll(Arrays.asList(firstBundle, nextBundle)); + Collections.addAll(allBundles, others); + + PersistableBundle result = new PersistableBundle(); + for (PersistableBundle bundle : allBundles) { + for (String key : bundle.keySet()) { + Preconditions.checkArgument( + !result.containsKey(key), + "Found duplicate key [%s] while attempting to merge bundles.", + key); + } + result.putAll(bundle); + } + + return result; + } + + private PersistableBundles() { + throw new AssertionError("Should not be instantiated"); + } +} diff --git a/main/java/com/google/android/setupcompat/internal/Validations.java b/main/java/com/google/android/setupcompat/internal/Validations.java new file mode 100644 index 0000000..584cad9 --- /dev/null +++ b/main/java/com/google/android/setupcompat/internal/Validations.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 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.internal; + +import com.google.common.base.Preconditions; + +/** Commonly used validations and preconditions. */ +public final class Validations { + + /** + * Asserts that the {@code length} is in the expected range. + * + * @throws IllegalArgumentException if {@code input}'s length is than {@code minLength} or + * greather than {@code maxLength}. + */ + public static void assertLengthInRange(int length, String name, int minLength, int maxLength) { + Preconditions.checkArgument( + length <= maxLength && length >= minLength, + "Length of %s should be in the range [%s-%s]", + name, + minLength, + maxLength); + } + + /** + * Asserts that the {@code input}'s length is in the expected range. + * + * @throws NullPointerException if {@code input} is null. + * @throws IllegalArgumentException if {@code input}'s length is than {@code minLength} or + * greather than {@code maxLength}. + */ + public static void assertLengthInRange(String input, String name, int minLength, int maxLength) { + Preconditions.checkNotNull(input, "%s cannot be null.", name); + assertLengthInRange(input.length(), name, minLength, maxLength); + } + + private Validations() { + throw new AssertionError("Should not be instantiated"); + } +} diff --git a/main/java/com/google/android/setupcompat/item/FooterButton.java b/main/java/com/google/android/setupcompat/item/FooterButton.java index dbebc90..1b32449 100644 --- a/main/java/com/google/android/setupcompat/item/FooterButton.java +++ b/main/java/com/google/android/setupcompat/item/FooterButton.java @@ -35,7 +35,6 @@ import com.google.android.setupcompat.template.ButtonFooterMixin; * button type and click listener, and ButtonFooterMixin will inflate a corresponding Button view. */ public final class FooterButton { - private static final int BUTTON_TYPE_NONE = 0; private final ButtonType buttonType; private CharSequence text; @@ -52,7 +51,9 @@ public final class FooterButton { onClickListener = null; buttonType = ButtonType.valueOf( - a.getInt(R.styleable.SucFooterButton_sucButtonType, /* defValue= */ BUTTON_TYPE_NONE)); + a.getInt( + R.styleable.SucFooterButton_sucButtonType, + /* defValue= */ ButtonType.OTHER.getEnumValue())); theme = a.getResourceId(R.styleable.SucFooterButton_android_theme, /* defValue= */ 0); a.recycle(); } @@ -164,6 +165,7 @@ public final class FooterButton { * @param enabled True if this view is enabled, false otherwise. */ public void setEnabled(boolean enabled) { + this.enabled = enabled; if (buttonListener != null && id != 0) { buttonListener.onEnabledChanged(enabled, id); } @@ -242,27 +244,56 @@ public final class FooterButton { this.theme = theme; } - /** Types for footer button. The button appearance and behavior may change based on its type. */ + /** + * Types for footer button. The button appearance and behavior may change based on its type. In + * order to be backward compatible with application built with old version of setupcompat; the + * {@code enumValue} of each ButtonType should not be changed. + */ public enum ButtonType { /** A type of button that doesn't fit into any other categories. */ - OTHER, + OTHER(0), + /** + * A type of button that will set up additional elements of the ongoing setup step(s) when + * clicked. + */ + ADD_ANOTHER(1), + /** A type of button that will cancel the ongoing setup step(s) and exit setup when clicked. */ + CANCEL(2), + /** A type of button that will clear the progress when clicked. (eg: clear PIN code) */ + CLEAR(3), + /** A type of button that will exit the setup flow when clicked. */ + DONE(4), /** A type of button that will go to the next screen, or next step in the flow when clicked. */ - NEXT, + NEXT(5), + /** A type of button to opt-in or agree to the features described in the current screen. */ + OPT_IN(6), /** A type of button that will skip the current step when clicked. */ - SKIP, - /** A type of button that will cancel the ongoing setup step(s) and exit setup when clicked. */ - CANCEL, + SKIP(7), /** A type of button that will stop the ongoing setup step(s) and skip forward when clicked. */ - STOP; + STOP(8); + + private final int enumValue; + + public int getEnumValue() { + return enumValue; + } + + ButtonType(int enumValue) { + this.enumValue = enumValue; + } - public static ButtonType valueOf(int value) { - if (value >= 0 && value < ButtonType.values().length) { - return ButtonType.values()[value]; - } else { - return OTHER; + public static ButtonType valueOf(int enumValue) { + for (ButtonType type : ButtonType.values()) { + if (type.getEnumValue() == enumValue) { + return type; + } } + return OTHER; } } + // LINT.ThenChange( + // //depot/google3/third_party/java_src/android_libs/setupcompat/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java, + // //depot/google3/third_party/java_src/android_libs/setupcompat/main/res/values/attrs.xml) /** * Builder class for constructing {@code FooterButton} objects. diff --git a/main/java/com/google/android/setupcompat/logging/CustomEvent.java b/main/java/com/google/android/setupcompat/logging/CustomEvent.java index 363c3ef..f9aa3b9 100644 --- a/main/java/com/google/android/setupcompat/logging/CustomEvent.java +++ b/main/java/com/google/android/setupcompat/logging/CustomEvent.java @@ -16,9 +16,12 @@ package com.google.android.setupcompat.logging; +import static com.google.android.setupcompat.internal.Validations.assertLengthInRange; + import android.os.Parcel; import android.os.Parcelable; import android.os.PersistableBundle; +import androidx.annotation.VisibleForTesting; import com.google.android.setupcompat.internal.ClockProvider; import com.google.common.base.Preconditions; import java.util.Objects; @@ -71,7 +74,7 @@ public final class CustomEvent implements Parcelable { /** Returns the non PII values describing the event. Only primitive values are supported. */ public PersistableBundle values() { - return this.persistableBundle; + return new PersistableBundle(this.persistableBundle); } /** @@ -125,6 +128,7 @@ public final class CustomEvent implements Parcelable { Preconditions.checkNotNull(bundle, "Bundle cannot be null."); Preconditions.checkArgument(!bundle.isEmpty(), "Bundle cannot be empty."); Preconditions.checkNotNull(piiValues, "piiValues cannot be null."); + assertPersistableBundleIsValid(bundle); this.timestampMillis = timestampMillis; this.metricKey = metricKey; this.persistableBundle = bundle.deepCopy(); @@ -135,4 +139,37 @@ public final class CustomEvent implements Parcelable { private final MetricKey metricKey; private final PersistableBundle persistableBundle; private final PersistableBundle piiValues; + + private static void assertPersistableBundleIsValid(PersistableBundle bundle) { + for (String key : bundle.keySet()) { + assertLengthInRange(key, "bundle key", MIN_BUNDLE_KEY_LENGTH, MAX_STR_LENGTH); + Object value = bundle.get(key); + boolean valid = false; + for (Class clazz : CUSTOM_EVENT_ALLOWED_DATA_TYPES) { + if (clazz.isInstance(value)) { + valid = true; + break; + } + } + Preconditions.checkArgument( + valid, + "Invalid data type for key='%s'. Expected values of type %s, but found [%s].", + key, + CUSTOM_EVENT_ALLOWED_DATA_TYPES, + value); + + if (value instanceof String) { + Preconditions.checkArgument( + ((String) value).length() <= MAX_STR_LENGTH, + "Maximum length of string value for key='%s' cannot exceed %s.", + key, + MAX_STR_LENGTH); + } + } + } + + private static final Class[] CUSTOM_EVENT_ALLOWED_DATA_TYPES = + new Class[] {Integer.class, Long.class, Double.class, String.class, Boolean.class}; + @VisibleForTesting static final int MAX_STR_LENGTH = 50; + @VisibleForTesting static final int MIN_BUNDLE_KEY_LENGTH = 3; } diff --git a/main/java/com/google/android/setupcompat/logging/MetricKey.java b/main/java/com/google/android/setupcompat/logging/MetricKey.java index a74a2a6..fe310f2 100644 --- a/main/java/com/google/android/setupcompat/logging/MetricKey.java +++ b/main/java/com/google/android/setupcompat/logging/MetricKey.java @@ -16,6 +16,8 @@ package com.google.android.setupcompat.logging; +import static com.google.android.setupcompat.internal.Validations.assertLengthInRange; + import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.NonNull; @@ -42,15 +44,9 @@ public final class MetricKey implements Parcelable { * */ public static MetricKey get(@NonNull String name, @NonNull String screenName) { - Preconditions.checkNotNull(name); - Preconditions.checkNotNull(screenName); - assertLengthInRange( - name.length(), "MetricKey.name", MIN_METRIC_KEY_LENGTH, MAX_METRIC_KEY_LENGTH); + assertLengthInRange(name, "MetricKey.name", MIN_METRIC_KEY_LENGTH, MAX_METRIC_KEY_LENGTH); assertLengthInRange( - screenName.length(), - "MetricKey.screenName", - MIN_SCREEN_NAME_LENGTH, - MAX_SCREEN_NAME_LENGTH); + screenName, "MetricKey.screenName", MIN_SCREEN_NAME_LENGTH, MAX_SCREEN_NAME_LENGTH); Preconditions.checkArgument( METRIC_KEY_PATTERN.matcher(name).matches(), "Invalid MetricKey, only alpha numeric characters are allowed."); @@ -119,16 +115,6 @@ public final class MetricKey implements Parcelable { private final String name; private final String screenName; - private static void assertLengthInRange( - int foundLength, String name, int minLength, int maxLength) { - Preconditions.checkArgument( - foundLength <= maxLength && foundLength >= minLength, - "Length of %s should be in the range [%s-%s]", - name, - minLength, - maxLength); - } - private static final int MIN_SCREEN_NAME_LENGTH = 5; private static final int MIN_METRIC_KEY_LENGTH = 5; private static final int MAX_SCREEN_NAME_LENGTH = 50; diff --git a/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java new file mode 100644 index 0000000..5ca8ed1 --- /dev/null +++ b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java @@ -0,0 +1,37 @@ +package com.google.android.setupcompat.logging.internal; + +import android.content.Context; +import android.os.Bundle; +import android.os.PersistableBundle; +import androidx.annotation.VisibleForTesting; +import com.google.android.setupcompat.logging.CustomEvent; +import com.google.android.setupcompat.logging.MetricKey; +import com.google.android.setupcompat.logging.SetupMetricsLogger; + +/** Uses to log internal event customization resource list. */ +public class PartnerCustomizedResourceListMetric { + + public static void logMetrics(Context context, String screenName, Bundle bundle) { + PersistableBundle logBundle = + buildLogBundleFromResourceConfigBundle(context.getPackageName(), bundle); + if (!logBundle.isEmpty()) { + SetupMetricsLogger.logCustomEvent( + context, + CustomEvent.create(MetricKey.get("PartnerCustomizationResource", screenName), logBundle)); + } + } + + @VisibleForTesting + public static PersistableBundle buildLogBundleFromResourceConfigBundle( + String defaultPackageName, Bundle resourceConfigBundle) { + PersistableBundle persistableBundle = new PersistableBundle(); + for (String key : resourceConfigBundle.keySet()) { + Bundle resourceExtra = resourceConfigBundle.getBundle(key); + if (!resourceExtra.getString("packageName", defaultPackageName).equals(defaultPackageName)) { + persistableBundle.putBoolean(resourceExtra.getString("resourceName", key), true); + } + } + + return persistableBundle; + } +} diff --git a/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java b/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java index 47177af..4aca790 100644 --- a/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java +++ b/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java @@ -81,6 +81,7 @@ public class ButtonFooterMixin implements Mixin { private int footerBarPaddingTop; private int footerBarPaddingBottom; + @VisibleForTesting int defaultPadding; private static final AtomicInteger nextGeneratedId = new AtomicInteger(1); @@ -157,11 +158,9 @@ public class ButtonFooterMixin implements Mixin { footerStub = (ViewStub) layout.findManagedViewById(R.id.suc_layout_footer); this.applyPartnerResources = applyPartnerResources; - int defaultPadding = - context - .getResources() - .getDimensionPixelSize(R.dimen.suc_customization_footer_padding_vertical); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterBar, defStyleAttr, 0); + defaultPadding = + a.getDimensionPixelSize(R.styleable.SucFooterBar_sucFooterButtonPaddingVertical, 0); footerBarPaddingTop = a.getDimensionPixelSize(R.styleable.SucFooterBar_sucFooterBarPaddingTop, defaultPadding); footerBarPaddingBottom = @@ -476,32 +475,10 @@ public class ButtonFooterMixin implements Mixin { if (button == null) { return; } - Drawable icon; - switch (buttonType) { - case NEXT: - icon = - PartnerConfigHelper.get(context) - .getDrawable(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_NEXT); - break; - case SKIP: - icon = - PartnerConfigHelper.get(context) - .getDrawable(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_SKIP); - break; - case CANCEL: - icon = - PartnerConfigHelper.get(context) - .getDrawable(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CANCEL); - break; - case STOP: - icon = - PartnerConfigHelper.get(context) - .getDrawable(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_STOP); - break; - case OTHER: - default: - icon = null; - break; + Drawable icon = null; + PartnerConfig id = getDrawablePartnerConfig(buttonType); + if (id != null) { + icon = PartnerConfigHelper.get(context).getDrawable(context, id); } setButtonIcon(button, icon); } @@ -532,6 +509,41 @@ public class ButtonFooterMixin implements Mixin { } } + private static PartnerConfig getDrawablePartnerConfig(ButtonType buttonType) { + PartnerConfig result; + switch (buttonType) { + case ADD_ANOTHER: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_ADD_ANOTHER; + break; + case CANCEL: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CANCEL; + break; + case CLEAR: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CLEAR; + break; + case DONE: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_DONE; + break; + case NEXT: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_NEXT; + break; + case OPT_IN: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_OPT_IN; + break; + case SKIP: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_SKIP; + break; + case STOP: + result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_STOP; + break; + case OTHER: + default: + result = null; + break; + } + return result; + } + GradientDrawable getGradientDrawable(Button button) { Drawable drawable = button.getBackground(); if (drawable instanceof InsetDrawable) { diff --git a/main/java/com/google/android/setupcompat/template/ColoredHeaderMixin.java b/main/java/com/google/android/setupcompat/template/ColoredHeaderMixin.java new file mode 100644 index 0000000..85b7825 --- /dev/null +++ b/main/java/com/google/android/setupcompat/template/ColoredHeaderMixin.java @@ -0,0 +1,70 @@ +/* + * 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 android.content.res.ColorStateList; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.widget.TextView; +import com.google.android.setupcompat.R; +import com.google.android.setupcompat.TemplateLayout; + +/** + * A {@link Mixin} displaying a header text that can be set to different colors. This Mixin is + * registered to the template using HeaderMixin.class, and can be retrieved using: {@code + * (ColoredHeaderMixin) templateLayout.getMixin(HeaderMixin.class}. + */ +public class ColoredHeaderMixin extends HeaderMixin { + + /** {@inheritDoc} */ + public ColoredHeaderMixin(TemplateLayout layout, AttributeSet attrs, int defStyleAttr) { + super(layout, attrs, defStyleAttr); + + final TypedArray a = + layout + .getContext() + .obtainStyledAttributes(attrs, R.styleable.SucColoredHeaderMixin, defStyleAttr, 0); + + // Set the header color + final ColorStateList headerColor = + a.getColorStateList(R.styleable.SucColoredHeaderMixin_sucHeaderColor); + if (headerColor != null) { + setColor(headerColor); + } + + a.recycle(); + } + + /** + * Sets the color of the header text. This can also be set via XML using {@code + * app:sucHeaderColor}. + * + * @param color The text color of the header. + */ + public void setColor(ColorStateList color) { + final TextView titleView = getTextView(); + if (titleView != null) { + titleView.setTextColor(color); + } + } + + /** @return The current text color of the header. */ + public ColorStateList getColor() { + final TextView titleView = getTextView(); + return titleView != null ? titleView.getTextColors() : null; + } +} diff --git a/main/java/com/google/android/setupcompat/template/HeaderMixin.java b/main/java/com/google/android/setupcompat/template/HeaderMixin.java new file mode 100644 index 0000000..b5d06ec --- /dev/null +++ b/main/java/com/google/android/setupcompat/template/HeaderMixin.java @@ -0,0 +1,92 @@ +/* + * 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 android.content.res.TypedArray; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import android.util.AttributeSet; +import android.widget.TextView; +import com.google.android.setupcompat.R; +import com.google.android.setupcompat.TemplateLayout; + +/** + * A {@link com.google.android.setupcompat.template.Mixin} for setting and getting the header text. + */ +public class HeaderMixin implements Mixin { + + private final TemplateLayout templateLayout; + + /** + * @param layout The layout this Mixin belongs to. + * @param attrs XML attributes given to the layout. + * @param defStyleAttr The default style attribute as given to the constructor of the layout. + */ + public HeaderMixin( + @NonNull TemplateLayout layout, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + templateLayout = layout; + + final TypedArray a = + layout + .getContext() + .obtainStyledAttributes(attrs, R.styleable.SucHeaderMixin, defStyleAttr, 0); + + // Set the header text + final CharSequence headerText = a.getText(R.styleable.SucHeaderMixin_sucHeaderText); + if (headerText != null) { + setText(headerText); + } + + a.recycle(); + } + + /** @return The TextView displaying the header. */ + public TextView getTextView() { + return (TextView) templateLayout.findManagedViewById(R.id.suc_layout_title); + } + + /** + * Sets the header text. This can also be set via the XML attribute {@code app:sucHeaderText}. + * + * @param title The resource ID of the text to be set as header. + */ + public void setText(int title) { + final TextView titleView = getTextView(); + if (titleView != null) { + titleView.setText(title); + } + } + + /** + * Sets the header text. This can also be set via the XML attribute {@code app:sucHeaderText}. + * + * @param title The text to be set as header. + */ + public void setText(CharSequence title) { + final TextView titleView = getTextView(); + if (titleView != null) { + titleView.setText(title); + } + } + + /** @return The current header text. */ + public CharSequence getText() { + final TextView titleView = getTextView(); + return titleView != null ? titleView.getText() : null; + } +} diff --git a/main/java/com/google/android/setupcompat/template/IconMixin.java b/main/java/com/google/android/setupcompat/template/IconMixin.java new file mode 100644 index 0000000..c319277 --- /dev/null +++ b/main/java/com/google/android/setupcompat/template/IconMixin.java @@ -0,0 +1,109 @@ +/* + * 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 android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import androidx.annotation.DrawableRes; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ImageView; +import com.google.android.setupcompat.R; +import com.google.android.setupcompat.TemplateLayout; + +/** + * A {@link com.google.android.setupcompat.template.Mixin} for setting an icon on the template + * layout. + */ +public class IconMixin implements Mixin { + + private final TemplateLayout templateLayout; + + /** + * @param layout The template layout that this Mixin is a part of. + * @param attrs XML attributes given to the layout. + * @param defStyleAttr The default style attribute as given to the constructor of the layout. + */ + public IconMixin(TemplateLayout layout, AttributeSet attrs, int defStyleAttr) { + templateLayout = layout; + final Context context = layout.getContext(); + + final TypedArray a = + context.obtainStyledAttributes(attrs, R.styleable.SucIconMixin, defStyleAttr, 0); + + final @DrawableRes int icon = a.getResourceId(R.styleable.SucIconMixin_android_icon, 0); + if (icon != 0) { + setIcon(icon); + } + + a.recycle(); + } + + /** + * Sets the icon on this layout. The icon can also be set in XML using {@code android:icon}. + * + * @param icon A drawable icon. + */ + public void setIcon(Drawable icon) { + final ImageView iconView = getView(); + if (iconView != null) { + iconView.setImageDrawable(icon); + iconView.setVisibility(icon != null ? View.VISIBLE : View.GONE); + } + } + + /** + * Sets the icon on this layout. The icon can also be set in XML using {@code android:icon}. + * + * @param icon A drawable icon resource. + */ + public void setIcon(@DrawableRes int icon) { + final ImageView iconView = getView(); + if (iconView != null) { + // Note: setImageResource on the ImageView is overridden in AppCompatImageView for + // support lib users, which enables vector drawable compat to work on versions pre-L. + iconView.setImageResource(icon); + iconView.setVisibility(icon != 0 ? View.VISIBLE : View.GONE); + } + } + + /** @return The icon previously set in {@link #setIcon(Drawable)} or {@code android:icon} */ + public Drawable getIcon() { + final ImageView iconView = getView(); + return iconView != null ? iconView.getDrawable() : null; + } + + /** Sets the content description of the icon view */ + public void setContentDescription(CharSequence description) { + final ImageView iconView = getView(); + if (iconView != null) { + iconView.setContentDescription(description); + } + } + + /** @return The content description of the icon view */ + public CharSequence getContentDescription() { + final ImageView iconView = getView(); + return iconView != null ? iconView.getContentDescription() : null; + } + + /** @return The ImageView responsible for displaying the icon. */ + protected ImageView getView() { + return (ImageView) templateLayout.findManagedViewById(R.id.suc_layout_icon); + } +} diff --git a/main/java/com/google/android/setupcompat/util/PartnerConfig.java b/main/java/com/google/android/setupcompat/util/PartnerConfig.java index 083479d..f892524 100644 --- a/main/java/com/google/android/setupcompat/util/PartnerConfig.java +++ b/main/java/com/google/android/setupcompat/util/PartnerConfig.java @@ -16,6 +16,7 @@ package com.google.android.setupcompat.util; +// TODO(b/121371322): optimize the enum /** Resources that can be customized by partner overlay APK. */ public enum PartnerConfig { @@ -39,18 +40,34 @@ public enum PartnerConfig { CONFIG_FOOTER_BUTTON_FONT_FAMILY( PartnerConfigKey.KEY_FOOTER_BUTTON_FONT_FAMILY, ResourceType.STRING), + // The icon for "add another" action. Can be "@null" for no icon. + CONFIG_FOOTER_BUTTON_ICON_ADD_ANOTHER( + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_ADD_ANOTHER, ResourceType.DRAWABLE), + + // The icon for "cancel" action. Can be "@null" for no icon. + CONFIG_FOOTER_BUTTON_ICON_CANCEL( + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CANCEL, ResourceType.DRAWABLE), + + // The icon for "clear" action. Can be "@null" for no icon. + CONFIG_FOOTER_BUTTON_ICON_CLEAR( + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CLEAR, ResourceType.DRAWABLE), + + // The icon for "done" action. Can be "@null" for no icon. + CONFIG_FOOTER_BUTTON_ICON_DONE( + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_DONE, ResourceType.DRAWABLE), + // The icon for "next" action. Can be "@null" for no icon. CONFIG_FOOTER_BUTTON_ICON_NEXT( PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_NEXT, ResourceType.DRAWABLE), + // The icon for "opt-in" action. Can be "@null" for no icon. + CONFIG_FOOTER_BUTTON_ICON_OPT_IN( + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_OPT_IN, ResourceType.DRAWABLE), + // The icon for "skip" action. Can be "@null" for no icon. CONFIG_FOOTER_BUTTON_ICON_SKIP( PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_SKIP, ResourceType.DRAWABLE), - // The icon for "cancel" action. Can be "@null" for no icon. - CONFIG_FOOTER_BUTTON_ICON_CANCEL( - PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CANCEL, ResourceType.DRAWABLE), - // The icon for "stop" action. Can be "@null" for no icon. CONFIG_FOOTER_BUTTON_ICON_STOP( PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_STOP, ResourceType.DRAWABLE), diff --git a/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java b/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java index d9bdbb2..1c66940 100644 --- a/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java +++ b/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java @@ -28,8 +28,14 @@ import java.lang.annotation.RetentionPolicy; PartnerConfigKey.KEY_NAVIGATION_BAR_BG_COLOR, PartnerConfigKey.KEY_WINDOW_LIGHT_NAVIGATION_BAR, PartnerConfigKey.KEY_FOOTER_BUTTON_FONT_FAMILY, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_ADD_ANOTHER, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CANCEL, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_CLEAR, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_DONE, PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_NEXT, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_OPT_IN, PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_SKIP, + PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_STOP, PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_TOP, PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_BOTTOM, PartnerConfigKey.KEY_FOOTER_BUTTON_RADIUS, @@ -40,6 +46,7 @@ import java.lang.annotation.RetentionPolicy; PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR, PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_SIZE, }) +// TODO(121371322): can be removed and always reference PartnerConfig.getResourceName()? public @interface PartnerConfigKey { // Status bar background color or illustration. String KEY_STATUS_BAR_BACKGROUND = "setup_compat_status_bar_background"; @@ -59,16 +66,28 @@ public @interface PartnerConfigKey { // available in the system. Font references (@font or @xml) are not allowed. String KEY_FOOTER_BUTTON_FONT_FAMILY = "setup_compat_footer_button_font_family"; + // The icon for "add another" action. Can be "@null" for no icon. + String KEY_FOOTER_BUTTON_ICON_ADD_ANOTHER = "setup_compat_footer_button_icon_add_another"; + + // The icon for "cancel" action. Can be "@null" for no icon. + String KEY_FOOTER_BUTTON_ICON_CANCEL = "setup_compat_footer_button_icon_cancel"; + + // The icon for "clear" action. Can be "@null" for no icon. + String KEY_FOOTER_BUTTON_ICON_CLEAR = "setup_compat_footer_button_icon_clear"; + + // The icon for "done" action. Can be "@null" for no icon. + String KEY_FOOTER_BUTTON_ICON_DONE = "setup_compat_footer_button_icon_done"; + // The icon for "next" action. Can be "@null" for no icon. String KEY_FOOTER_BUTTON_ICON_NEXT = "setup_compat_footer_button_icon_next"; - // The icon for "skip" action. Can be "@null" for no icon. - String KEY_FOOTER_BUTTON_ICON_SKIP = "setup_compat_footer_button_icon_skip"; + // The icon for "opt-in" action. Can be "@null" for no icon. + String KEY_FOOTER_BUTTON_ICON_OPT_IN = "setup_compat_footer_button_icon_opt_in"; // The icon for "skip" action. Can be "@null" for no icon. - String KEY_FOOTER_BUTTON_ICON_CANCEL = "setup_compat_footer_button_icon_cancel"; + String KEY_FOOTER_BUTTON_ICON_SKIP = "setup_compat_footer_button_icon_skip"; - // The icon for "skip" action. Can be "@null" for no icon. + // The icon for "stop" action. Can be "@null" for no icon. String KEY_FOOTER_BUTTON_ICON_STOP = "setup_compat_footer_button_icon_stop"; // Top padding of the footer buttons -- cgit v1.2.3