summaryrefslogtreecommitdiff
path: root/main/java/com/google
diff options
context:
space:
mode:
authorSetup Wizard Team <android-setup-team-eng@google.com>2018-11-28 13:33:48 +0800
committerCn Chen <cnchen@google.com>2018-11-29 07:12:59 +0000
commit8ccc9e66eeabe7510f2175bc18deb2000245f64c (patch)
treec64f7870ff975cc1680caaab3ee0af407eb2f1bd /main/java/com/google
parenta4e3b960b3331ddd425844d2e7d4f980275d3fea (diff)
downloadsetupcompat-8ccc9e66eeabe7510f2175bc18deb2000245f64c.tar.gz
Import updated Android SetupCompat Library 223108899
Test: mm Bug: 119924155 PiperOrigin-RevId: 223108899 Change-Id: I82ed82e884f07d0a8828e665119a541b53144c7f
Diffstat (limited to 'main/java/com/google')
-rw-r--r--main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java169
-rw-r--r--main/java/com/google/android/setupcompat/TemplateLayout.java268
-rw-r--r--main/java/com/google/android/setupcompat/internal/ClockProvider.java57
-rw-r--r--main/java/com/google/android/setupcompat/item/FooterButton.java95
-rw-r--r--main/java/com/google/android/setupcompat/item/FooterButtonInflater.java102
-rw-r--r--main/java/com/google/android/setupcompat/lifecycle/LifecycleFragment.java95
-rw-r--r--main/java/com/google/android/setupcompat/logging/CustomEvent.java138
-rw-r--r--main/java/com/google/android/setupcompat/logging/MetricKey.java137
-rw-r--r--main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java75
-rw-r--r--main/java/com/google/android/setupcompat/logging/Timer.java85
-rw-r--r--main/java/com/google/android/setupcompat/logging/internal/DefaultSetupMetricsLogger.java96
-rw-r--r--main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java86
-rw-r--r--main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java287
-rw-r--r--main/java/com/google/android/setupcompat/template/Mixin.java25
-rw-r--r--main/java/com/google/android/setupcompat/template/StatusBarMixin.java162
-rw-r--r--main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java147
-rw-r--r--main/java/com/google/android/setupcompat/util/FallbackThemeWrapper.java50
-rw-r--r--main/java/com/google/android/setupcompat/util/FlagHelper.java39
-rw-r--r--main/java/com/google/android/setupcompat/util/PartnerConfig.java108
-rw-r--r--main/java/com/google/android/setupcompat/util/PartnerConfigHelper.java272
-rw-r--r--main/java/com/google/android/setupcompat/util/PartnerConfigKey.java94
-rw-r--r--main/java/com/google/android/setupcompat/util/ResourceEntry.java86
-rw-r--r--main/java/com/google/android/setupcompat/util/ResultCodes.java29
-rw-r--r--main/java/com/google/android/setupcompat/util/WizardManagerHelper.java206
-rw-r--r--main/java/com/google/android/setupcompat/view/ButtonBarLayout.java118
-rw-r--r--main/java/com/google/android/setupcompat/view/StatusBarBackgroundLayout.java98
26 files changed, 3124 insertions, 0 deletions
diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
new file mode 100644
index 0000000..885f01d
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 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;
+
+import android.annotation.TargetApi;
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.TypedArray;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.LayoutRes;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.WindowManager;
+import com.google.android.setupcompat.lifecycle.LifecycleFragment;
+import com.google.android.setupcompat.template.ButtonFooterMixin;
+import com.google.android.setupcompat.template.StatusBarMixin;
+import com.google.android.setupcompat.template.SystemNavBarMixin;
+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 Activity activity;
+
+ public PartnerCustomizationLayout(Context context) {
+ this(context, 0, 0);
+ }
+
+ public PartnerCustomizationLayout(Context context, int template) {
+ this(context, template, 0);
+ }
+
+ public PartnerCustomizationLayout(Context context, int template, int containerId) {
+ super(context, template, containerId);
+ init(null, R.attr.sucLayoutTheme);
+ }
+
+ public PartnerCustomizationLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(attrs, R.attr.sucLayoutTheme);
+ }
+
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public PartnerCustomizationLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(attrs, defStyleAttr);
+ }
+
+ private void init(AttributeSet attrs, int defStyleAttr) {
+ activity = lookupActivityFromContext(getContext());
+
+ boolean isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent());
+ registerMixin(
+ StatusBarMixin.class,
+ new StatusBarMixin(
+ this,
+ activity.getWindow(),
+ attrs,
+ defStyleAttr,
+ /* applyPartnerResources= */ isSetupFlow));
+ registerMixin(
+ SystemNavBarMixin.class,
+ new SystemNavBarMixin(
+ this,
+ activity.getWindow(),
+ attrs,
+ defStyleAttr,
+ /* applyPartnerResources= */ isSetupFlow));
+
+ TypedArray a =
+ getContext()
+ .obtainStyledAttributes(
+ attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0);
+
+ final int primaryBtn =
+ a.getResourceId(R.styleable.SucPartnerCustomizationLayout_sucPrimaryFooterButton, 0);
+ final int secondaryBtn =
+ a.getResourceId(R.styleable.SucPartnerCustomizationLayout_sucSecondaryFooterButton, 0);
+
+ registerMixin(
+ ButtonFooterMixin.class,
+ new ButtonFooterMixin(
+ this, primaryBtn, secondaryBtn, /* applyPartnerResources= */ isSetupFlow));
+
+ final int footer = a.getResourceId(R.styleable.SucPartnerCustomizationLayout_sucFooter, 0);
+ if (footer != 0) {
+ inflateFooter(footer);
+ }
+
+ boolean layoutFullscreen =
+ a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucLayoutFullscreen, true);
+ a.recycle();
+
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && layoutFullscreen) {
+ setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+
+ // Override the FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_TRANSLUCENT_STATUS,
+ // FLAG_TRANSLUCENT_NAVIGATION and SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN attributes of window forces
+ // showing status bar and navigation bar.
+ activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
+ }
+
+ @Override
+ protected View onInflateTemplate(LayoutInflater inflater, int template) {
+ if (template == 0) {
+ template = R.layout.partner_customization_layout;
+ }
+ return inflateTemplate(inflater, 0, template);
+ }
+
+ @Override
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ containerId = R.id.suc_layout_content;
+ }
+ return super.findContainer(containerId);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ LifecycleFragment.attachNow(lookupActivityFromContext(getContext()));
+ }
+
+ private static Activity lookupActivityFromContext(Context context) {
+ if (context instanceof Activity) {
+ return (Activity) context;
+ } else if (context instanceof ContextWrapper) {
+ return lookupActivityFromContext(((ContextWrapper) context).getBaseContext());
+ } else {
+ throw new IllegalArgumentException("Cannot find instance of Activity in parent tree");
+ }
+ }
+
+ /**
+ * Sets the footer of the layout, which is at the bottom of the content area outside the scrolling
+ * container. The footer can only be inflated once per instance of this layout.
+ *
+ * @param footer The layout to be inflated as footer.
+ * @return The root of the inflated footer view.
+ */
+ public View inflateFooter(@LayoutRes int footer) {
+ ViewStub footerStub = findManagedViewById(R.id.suc_layout_footer);
+ footerStub.setLayoutResource(footer);
+ return footerStub.inflate();
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/TemplateLayout.java b/main/java/com/google/android/setupcompat/TemplateLayout.java
new file mode 100644
index 0000000..644ab78
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/TemplateLayout.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2015 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;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.Keep;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.StyleRes;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.widget.FrameLayout;
+import com.google.android.setupcompat.template.Mixin;
+import com.google.android.setupcompat.util.FallbackThemeWrapper;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A generic template class that inflates a template, provided in the constructor or in {@code
+ * android:layout} through XML, and adds its children to a "container" in the template. When
+ * inflating this layout from XML, the {@code android:layout} and {@code suwContainer} attributes
+ * are required.
+ */
+public class TemplateLayout extends FrameLayout {
+
+ /**
+ * The container of the actual content. This will be a view in the template, which child views
+ * will be added to when {@link #addView(View)} is called.
+ */
+ private ViewGroup container;
+
+ private final Map<Class<? extends Mixin>, Mixin> mixins = new HashMap<>();
+
+ public TemplateLayout(Context context, int template, int containerId) {
+ super(context);
+ init(template, containerId, null, R.attr.sucLayoutTheme);
+ }
+
+ public TemplateLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(0, 0, attrs, R.attr.sucLayoutTheme);
+ }
+
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public TemplateLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(0, 0, attrs, defStyleAttr);
+ }
+
+ // All the constructors delegate to this init method. The 3-argument constructor is not
+ // available in LinearLayout before v11, so call super with the exact same arguments.
+ private void init(int template, int containerId, AttributeSet attrs, int defStyleAttr) {
+ final TypedArray a =
+ getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0);
+ if (template == 0) {
+ template = a.getResourceId(R.styleable.SucTemplateLayout_android_layout, 0);
+ }
+ if (containerId == 0) {
+ containerId = a.getResourceId(R.styleable.SucTemplateLayout_sucContainer, 0);
+ }
+ inflateTemplate(template, containerId);
+
+ a.recycle();
+ }
+
+ /**
+ * Registers a mixin with a given class. This method should be called in the constructor.
+ *
+ * @param cls The class to register the mixin. In most cases, {@code cls} is the same as {@code
+ * mixin.getClass()}, but {@code cls} can also be a super class of that. In the latter case
+ * the mixin must be retrieved using {@code cls} in {@link #getMixin(Class)}, not the
+ * subclass.
+ * @param mixin The mixin to be registered.
+ * @param <M> The class of the mixin to register. This is the same as {@code cls}
+ */
+ protected <M extends Mixin> void registerMixin(Class<M> cls, M mixin) {
+ mixins.put(cls, mixin);
+ }
+
+ /**
+ * Same as {@link View#findViewById(int)}, but may include views that are managed by this view but
+ * not currently added to the view hierarchy. e.g. recycler view or list view headers that are not
+ * currently shown.
+ */
+ // Returning generic type is the common pattern used for findViewBy* methods
+ @SuppressWarnings("TypeParameterUnusedInFormals")
+ public <T extends View> T findManagedViewById(int id) {
+ return findViewById(id);
+ }
+
+ /**
+ * Get a {@link Mixin} from this template registered earlier in {@link #registerMixin(Class,
+ * Mixin)}.
+ *
+ * @param cls The class marker of Mixin being requested. The actual Mixin returned may be a
+ * subclass of this marker. Note that this must be the same class as registered in {@link
+ * #registerMixin(Class, Mixin)}, which is not necessarily the same as the concrete class of
+ * the instance returned by this method.
+ * @param <M> The type of the class marker.
+ * @return The mixin marked by {@code cls}, or null if the template does not have a matching
+ * mixin.
+ */
+ @SuppressWarnings("unchecked")
+ public <M extends Mixin> M getMixin(Class<M> cls) {
+ return (M) mixins.get(cls);
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ container.addView(child, index, params);
+ }
+
+ private void addViewInternal(View child) {
+ super.addView(child, -1, generateDefaultLayoutParams());
+ }
+
+ private void inflateTemplate(int templateResource, int containerId) {
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ final View templateRoot = onInflateTemplate(inflater, templateResource);
+ addViewInternal(templateRoot);
+
+ container = findContainer(containerId);
+ if (container == null) {
+ throw new IllegalArgumentException("Container cannot be null in TemplateLayout");
+ }
+ onTemplateInflated();
+ }
+
+ /**
+ * Inflate the template using the given inflater and theme. The fallback theme will be applied to
+ * the theme without overriding the values already defined in the theme, but simply providing
+ * default values for values which have not been defined. This allows templates to add additional
+ * required theme attributes without breaking existing clients.
+ *
+ * <p>In general, clients should still set the activity theme to the corresponding theme in setup
+ * wizard lib, so that the content area gets the correct styles as well.
+ *
+ * @param inflater A LayoutInflater to inflate the template.
+ * @param fallbackTheme A fallback theme to apply to the template. If the values defined in the
+ * fallback theme is already defined in the original theme, the value in the original theme
+ * takes precedence.
+ * @param template The layout template to be inflated.
+ * @return Root of the inflated layout.
+ * @see FallbackThemeWrapper
+ */
+ protected final View inflateTemplate(
+ LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template) {
+ if (template == 0) {
+ throw new IllegalArgumentException("android:layout not specified for TemplateLayout");
+ }
+ if (fallbackTheme != 0) {
+ inflater =
+ LayoutInflater.from(new FallbackThemeWrapper(inflater.getContext(), fallbackTheme));
+ }
+ return inflater.inflate(template, this, false);
+ }
+
+ /**
+ * This method inflates the template. Subclasses can override this method to customize the
+ * template inflation, or change to a different default template. The root of the inflated layout
+ * should be returned, and not added to the view hierarchy.
+ *
+ * @param inflater A LayoutInflater to inflate the template.
+ * @param template The resource ID of the template to be inflated, or 0 if no template is
+ * specified.
+ * @return Root of the inflated layout.
+ */
+ protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
+ return inflateTemplate(inflater, 0, template);
+ }
+
+ protected ViewGroup findContainer(int containerId) {
+ if (containerId == 0) {
+ // Maintain compatibility with the deprecated way of specifying container ID.
+ containerId = getContainerId();
+ }
+ return (ViewGroup) findViewById(containerId);
+ }
+
+ /**
+ * This is called after the template has been inflated and added to the view hierarchy. Subclasses
+ * can implement this method to modify the template as necessary, such as caching views retrieved
+ * from findViewById, or other view operations that need to be done in code. You can think of this
+ * as {@link View#onFinishInflate()} but for inflation of the template instead of for child views.
+ */
+ protected void onTemplateInflated() {}
+
+ /**
+ * @return ID of the default container for this layout. This will be used to find the container
+ * ViewGroup, which all children views of this layout will be placed in.
+ * @deprecated Override {@link #findContainer(int)} instead.
+ */
+ @Deprecated
+ protected int getContainerId() {
+ return 0;
+ }
+
+ /* Animator support */
+
+ private float xFraction;
+ private ViewTreeObserver.OnPreDrawListener preDrawListener;
+
+ /**
+ * Set the X translation as a fraction of the width of this view. Make sure this method is not
+ * stripped out by proguard when using this with {@link android.animation.ObjectAnimator}. You may
+ * need to add <code>
+ * -keep @androidx.annotation.Keep class *
+ * </code> to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError}
+ * at runtime.
+ */
+ @Keep
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public void setXFraction(float fraction) {
+ xFraction = fraction;
+ final int width = getWidth();
+ if (width != 0) {
+ setTranslationX(width * fraction);
+ } else {
+ // If we haven't done a layout pass yet, wait for one and then set the fraction before
+ // the draw occurs using an OnPreDrawListener. Don't call translationX until we know
+ // getWidth() has a reliable, non-zero value or else we will see the fragment flicker on
+ // screen.
+ if (preDrawListener == null) {
+ preDrawListener =
+ new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
+ setXFraction(xFraction);
+ return true;
+ }
+ };
+ getViewTreeObserver().addOnPreDrawListener(preDrawListener);
+ }
+ }
+ }
+
+ /**
+ * Return the X translation as a fraction of the width, as previously set in {@link
+ * #setXFraction(float)}.
+ *
+ * @see #setXFraction(float)
+ */
+ @Keep
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public float getXFraction() {
+ return xFraction;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/internal/ClockProvider.java b/main/java/com/google/android/setupcompat/internal/ClockProvider.java
new file mode 100644
index 0000000..c54ac2e
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/internal/ClockProvider.java
@@ -0,0 +1,57 @@
+/*
+ * 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 androidx.annotation.VisibleForTesting;
+import com.google.common.base.Supplier;
+import com.google.common.base.Ticker;
+import java.util.concurrent.TimeUnit;
+
+/** Provider for time in nanos and millis. Allows overriding time during tests. */
+public class ClockProvider extends Ticker {
+
+ public static long timeInNanos() {
+ return ticker.read();
+ }
+
+ public static long timeInMillis() {
+ return TimeUnit.NANOSECONDS.toMillis(timeInNanos());
+ }
+
+ @VisibleForTesting
+ public static void resetInstance() {
+ ticker = Ticker.systemTicker();
+ }
+
+ @VisibleForTesting
+ public static void setInstance(Supplier<Long> nanoSecondSupplier) {
+ ticker =
+ new Ticker() {
+ @Override
+ public long read() {
+ return nanoSecondSupplier.get();
+ }
+ };
+ }
+
+ @Override
+ public long read() {
+ return ticker.read();
+ }
+
+ private static Ticker ticker = Ticker.systemTicker();
+}
diff --git a/main/java/com/google/android/setupcompat/item/FooterButton.java b/main/java/com/google/android/setupcompat/item/FooterButton.java
new file mode 100644
index 0000000..31c6e93
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/item/FooterButton.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 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.item;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.view.View.OnClickListener;
+import com.google.android.setupcompat.R;
+import com.google.android.setupcompat.template.ButtonFooterMixin;
+
+/**
+ * Definition of a footer button. Clients can use this class to customize attributes like text and
+ * click listener, and ButtonFooterMixin will inflate a corresponding Button view.
+ */
+public class FooterButton {
+ private final String text;
+ private final OnClickListener listener;
+ private int theme;
+
+ public FooterButton(Context context, AttributeSet attrs) {
+ TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterButton);
+ this.text = a.getString(R.styleable.SucFooterButton_android_text);
+ this.listener = null;
+ this.theme = a.getResourceId(R.styleable.SucFooterButton_android_theme, 0);
+ a.recycle();
+ }
+
+ /**
+ * Allows client customize text, click listener and theme for footer button
+ * before Button has been created. The {@link ButtonFooterMixin} will inflate a corresponding
+ * Button view.
+ *
+ * @param context The context of application.
+ * @param text The text for button.
+ * @param listener The listener for button.
+ * @param theme The theme for button.
+ */
+ public FooterButton(
+ Context context,
+ @StringRes int text,
+ @Nullable OnClickListener listener,
+ @StyleRes int theme) {
+ this(context.getString(text), listener, theme);
+ }
+
+ public FooterButton(
+ String text, @Nullable OnClickListener listener, @StyleRes int theme) {
+ this.text = text;
+ this.listener = listener;
+ this.theme = theme;
+ }
+
+ public String getText() {
+ return text;
+ }
+
+ public OnClickListener getListener() {
+ return listener;
+ }
+
+ @StyleRes
+ public int getTheme() {
+ return theme;
+ }
+
+ /**
+ * Sets the default theme for footer button, the method only for internal use in {@link
+ * ButtonFooterMixin} and there will have no influence during setup wizard flow.
+ *
+ * @param theme The theme for footer button.
+ */
+ @VisibleForTesting
+ public void setTheme(@StyleRes int theme) {
+ this.theme = theme;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/item/FooterButtonInflater.java b/main/java/com/google/android/setupcompat/item/FooterButtonInflater.java
new file mode 100644
index 0000000..cc354ab
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/item/FooterButtonInflater.java
@@ -0,0 +1,102 @@
+/*
+ * 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.item;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import androidx.annotation.NonNull;
+import android.util.AttributeSet;
+import android.util.Xml;
+import android.view.InflateException;
+import java.io.IOException;
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+public class FooterButtonInflater {
+ protected final Context context;
+
+ /**
+ * Creates a new inflater instance associated with a particular Resources bundle.
+ *
+ * @param context The Context using to get Resources and Generate FooterButton Object
+ */
+ public FooterButtonInflater(@NonNull Context context) {
+ this.context = context;
+ }
+
+ public Resources getResources() {
+ return context.getResources();
+ }
+
+ /**
+ * Inflates a new hierarchy from the specified XML resource. Throws InflaterException if there is
+ * an error.
+ *
+ * @param resId ID for an XML resource to load (e.g. <code>R.xml.my_xml</code>)
+ * @return The root of the inflated hierarchy.
+ */
+ public FooterButton inflate(int resId) {
+ XmlResourceParser parser = getResources().getXml(resId);
+ try {
+ return inflate(parser);
+ } finally {
+ parser.close();
+ }
+ }
+
+ /**
+ * Inflates a new hierarchy from the specified XML node. Throws InflaterException if there is an
+ * error.
+ *
+ * <p><em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance reasons, inflation
+ * relies heavily on pre-processing of XML files that is done at build time. Therefore, it is not
+ * currently possible to use inflater with an XmlPullParser over a plain XML file at runtime.
+ *
+ * @param parser XML dom node containing the description of the hierarchy.
+ * @return The root of the inflated hierarchy.
+ */
+ private FooterButton inflate(XmlPullParser parser) {
+ final AttributeSet attrs = Xml.asAttributeSet(parser);
+ FooterButton button;
+
+ try {
+ // Look for the root node.
+ int type;
+ while ((type = parser.next()) != XmlPullParser.START_TAG
+ && type != XmlPullParser.END_DOCUMENT) {
+ // continue
+ }
+
+ if (type != XmlPullParser.START_TAG) {
+ throw new InflateException(parser.getPositionDescription() + ": No start tag found!");
+ }
+
+ if (!parser.getName().equals("FooterButton")) {
+ throw new InflateException(parser.getPositionDescription() + ": not a FooterButton");
+ }
+
+ button = new FooterButton(context, attrs);
+ } catch (XmlPullParserException e) {
+ throw new InflateException(e.getMessage(), e);
+ } catch (IOException e) {
+ throw new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e);
+ }
+
+ return button;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/lifecycle/LifecycleFragment.java b/main/java/com/google/android/setupcompat/lifecycle/LifecycleFragment.java
new file mode 100644
index 0000000..0acec8c
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/lifecycle/LifecycleFragment.java
@@ -0,0 +1,95 @@
+/*
+ * 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.lifecycle;
+
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentManager;
+import android.content.Context;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.util.Log;
+import com.google.android.setupcompat.internal.ClockProvider;
+import com.google.android.setupcompat.logging.MetricKey;
+import com.google.android.setupcompat.logging.SetupMetricsLogger;
+import com.google.android.setupcompat.util.WizardManagerHelper;
+
+/** Fragment used to detect lifecycle of an activity for metrics logging. */
+public class LifecycleFragment extends Fragment {
+ private static final String LOG_TAG = LifecycleFragment.class.getSimpleName();
+ private static final String FRAGMENT_ID = "lifecycle_monitor";
+
+ private MetricKey metricKey;
+ private long startInNanos;
+ private long durationInNanos = 0;
+
+ public LifecycleFragment() {
+ setRetainInstance(true);
+ }
+
+ /**
+ * Attaches the lifecycle fragment if it is not attached yet.
+ *
+ * @param activity the activity to detect lifecycle for.
+ * @return fragment to monitor life cycle.
+ */
+ public static LifecycleFragment attachNow(Activity activity) {
+ if (WizardManagerHelper.isAnySetupWizard(activity.getIntent())) {
+ if (VERSION.SDK_INT > VERSION_CODES.M) {
+ FragmentManager fragmentManager = activity.getFragmentManager();
+ Fragment fragment = activity.getFragmentManager().findFragmentByTag(FRAGMENT_ID);
+ if (fragment == null) {
+ LifecycleFragment lifeCycleFragment = new LifecycleFragment();
+ fragmentManager.beginTransaction().add(lifeCycleFragment, FRAGMENT_ID).commitNow();
+ fragment = lifeCycleFragment;
+ } else if (!(fragment instanceof LifecycleFragment)) {
+ Log.wtf(
+ LOG_TAG,
+ activity.getClass().getSimpleName() + " Incorrect instance on lifecycle fragment.");
+ }
+
+ return (LifecycleFragment) fragment;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ metricKey = MetricKey.get("ScreenDuration", getActivity().getClass().getSimpleName());
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ SetupMetricsLogger.logDuration(getContext(), metricKey, durationInNanos);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ startInNanos = ClockProvider.timeInNanos();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ durationInNanos += (ClockProvider.timeInNanos() - startInNanos);
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/logging/CustomEvent.java b/main/java/com/google/android/setupcompat/logging/CustomEvent.java
new file mode 100644
index 0000000..363c3ef
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/CustomEvent.java
@@ -0,0 +1,138 @@
+/*
+ * 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.logging;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import com.google.android.setupcompat.internal.ClockProvider;
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+
+/**
+ * This class represents a interesting event at a particular point in time. The event is identified
+ * by {@link MetricKey} along with {@code timestamp}. It can include additional key-value pairs
+ * providing more attributes associated with the given event. Only primitive values are supported
+ * for now (int, long, double, float, String).
+ */
+public final class CustomEvent implements Parcelable {
+
+ /** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
+ public static CustomEvent create(
+ MetricKey metricKey, PersistableBundle bundle, PersistableBundle piiValues) {
+ return new CustomEvent(ClockProvider.timeInMillis(), metricKey, bundle, piiValues);
+ }
+
+ /** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
+ public static CustomEvent create(MetricKey metricKey, PersistableBundle bundle) {
+ return create(metricKey, bundle, PersistableBundle.EMPTY);
+ }
+
+ public static final Creator<CustomEvent> CREATOR =
+ new Creator<CustomEvent>() {
+ @Override
+ public CustomEvent createFromParcel(Parcel in) {
+ return new CustomEvent(
+ in.readLong(),
+ in.readParcelable(MetricKey.class.getClassLoader()),
+ in.readPersistableBundle(),
+ in.readPersistableBundle());
+ }
+
+ @Override
+ public CustomEvent[] newArray(int size) {
+ return new CustomEvent[size];
+ }
+ };
+
+ /** Returns the timestamp of when the event occurred. */
+ public long timestampMillis() {
+ return timestampMillis;
+ }
+
+ /** Returns the identifier of the event. */
+ public MetricKey metricKey() {
+ return this.metricKey;
+ }
+
+ /** Returns the non PII values describing the event. Only primitive values are supported. */
+ public PersistableBundle values() {
+ return this.persistableBundle;
+ }
+
+ /**
+ * Returns the PII(Personally identifiable information) values describing the event. These values
+ * will not be included in the aggregated logs. Only primitive values are supported.
+ */
+ public PersistableBundle piiValues() {
+ return this.piiValues;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeLong(timestampMillis);
+ parcel.writeParcelable(metricKey, i);
+ parcel.writePersistableBundle(persistableBundle);
+ parcel.writePersistableBundle(piiValues);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof CustomEvent)) {
+ return false;
+ }
+ CustomEvent that = (CustomEvent) o;
+ return timestampMillis == that.timestampMillis
+ && Objects.equals(metricKey, that.metricKey)
+ && Objects.equals(persistableBundle, that.persistableBundle)
+ && Objects.equals(piiValues, that.piiValues);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(timestampMillis, metricKey, persistableBundle, piiValues);
+ }
+
+ private CustomEvent(
+ long timestampMillis,
+ MetricKey metricKey,
+ PersistableBundle bundle,
+ PersistableBundle piiValues) {
+ Preconditions.checkArgument(timestampMillis >= 0, "Timestamp cannot be negative.");
+ Preconditions.checkNotNull(metricKey, "MetricKey cannot be null.");
+ Preconditions.checkNotNull(bundle, "Bundle cannot be null.");
+ Preconditions.checkArgument(!bundle.isEmpty(), "Bundle cannot be empty.");
+ Preconditions.checkNotNull(piiValues, "piiValues cannot be null.");
+ this.timestampMillis = timestampMillis;
+ this.metricKey = metricKey;
+ this.persistableBundle = bundle.deepCopy();
+ this.piiValues = piiValues.deepCopy();
+ }
+
+ private final long timestampMillis;
+ private final MetricKey metricKey;
+ private final PersistableBundle persistableBundle;
+ private final PersistableBundle piiValues;
+}
diff --git a/main/java/com/google/android/setupcompat/logging/MetricKey.java b/main/java/com/google/android/setupcompat/logging/MetricKey.java
new file mode 100644
index 0000000..a74a2a6
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/MetricKey.java
@@ -0,0 +1,137 @@
+/*
+ * 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.logging;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import com.google.common.base.Preconditions;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * A metric key represents a validated “string key” and a "screen name" that is associated with the
+ * values reported by the API consumer.
+ */
+public final class MetricKey implements Parcelable {
+
+ /**
+ * Creates a new instance of MetricKey.
+ *
+ * <p>NOTE:
+ *
+ * <ul>
+ * <li>Length of {@code name} should be in range of 5-30 characters, only alpha numeric
+ * characters are allowed.
+ * <li>Length of {@code screenName} should be in range of 5-50 characters, only alpha numeric
+ * characters are allowed.
+ * </ul>
+ */
+ 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(
+ screenName.length(),
+ "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.");
+ Preconditions.checkArgument(
+ METRIC_KEY_PATTERN.matcher(screenName).matches(),
+ "Invalid MetricKey, only alpha numeric characters are allowed.");
+ return new MetricKey(name, screenName);
+ }
+
+ public static final Creator<MetricKey> CREATOR =
+ new Creator<MetricKey>() {
+ @Override
+ public MetricKey createFromParcel(Parcel in) {
+ return new MetricKey(in.readString(), in.readString());
+ }
+
+ @Override
+ public MetricKey[] newArray(int size) {
+ return new MetricKey[size];
+ }
+ };
+
+ /** Returns the name of the metric key. */
+ public String name() {
+ return name;
+ }
+
+ /** Returns the name of the metric key. */
+ public String screenName() {
+ return screenName;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(name);
+ parcel.writeString(screenName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof MetricKey)) {
+ return false;
+ }
+ MetricKey metricKey = (MetricKey) o;
+ return Objects.equals(name, metricKey.name) && Objects.equals(screenName, metricKey.screenName);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(name, screenName);
+ }
+
+ private MetricKey(String name, String screenName) {
+ this.name = name;
+ this.screenName = screenName;
+ }
+
+ 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;
+ private static final int MAX_METRIC_KEY_LENGTH = 30;
+ private static final Pattern METRIC_KEY_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+");
+}
diff --git a/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java b/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java
new file mode 100644
index 0000000..e0c9371
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java
@@ -0,0 +1,75 @@
+/*
+ * 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.logging;
+
+import android.content.Context;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import com.google.android.setupcompat.logging.internal.DefaultSetupMetricsLogger;
+import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricBundleKeys;
+import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType;
+import com.google.common.base.Preconditions;
+import java.util.concurrent.TimeUnit;
+
+/** SetupMetricsLogger provides an easy way to log custom metrics to SetupWizard. */
+public class SetupMetricsLogger {
+
+ /** Logs an instance of {@link CustomEvent} to SetupWizard. */
+ public static void logCustomEvent(@NonNull Context context, @NonNull CustomEvent customEvent) {
+ Preconditions.checkNotNull(context, "Context cannot be null.");
+ Preconditions.checkNotNull(customEvent, "CustomEvent cannot be null.");
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MetricBundleKeys.CUSTOM_EVENT, customEvent);
+ DefaultSetupMetricsLogger.get(context).logEventSafely(MetricType.CUSTOM_EVENT, bundle);
+ }
+
+ /** Increments the counter value with the name {@code counterName} by {@code times}. */
+ public static void logCounter(
+ @NonNull Context context, @NonNull MetricKey counterName, int times) {
+ Preconditions.checkNotNull(context, "Context cannot be null.");
+ Preconditions.checkNotNull(counterName, "CounterName cannot be null.");
+ Preconditions.checkArgument(times > 0, "Counter cannot be negative.");
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MetricBundleKeys.METRIC_KEY, counterName);
+ bundle.putInt(MetricBundleKeys.COUNTER_INT, times);
+ DefaultSetupMetricsLogger.get(context).logEventSafely(MetricType.COUNTER_EVENT, bundle);
+ }
+
+ /**
+ * Logs the {@link Timer}'s duration by calling {@link #logDuration(Context, MetricKey, long)}.
+ */
+ public static void logDuration(@NonNull Context context, @NonNull Timer timer) {
+ Preconditions.checkNotNull(context, "Context cannot be null.");
+ Preconditions.checkNotNull(timer, "Timer cannot be null.");
+ Preconditions.checkArgument(
+ timer.isStopped(), "Timer should be stopped before calling logDuration.");
+ logDuration(
+ context, timer.getMetricKey(), TimeUnit.NANOSECONDS.toMillis(timer.getDurationInNanos()));
+ }
+
+ /** Logs a duration event to SetupWizard. */
+ public static void logDuration(
+ @NonNull Context context, @NonNull MetricKey timerName, long timeInMillis) {
+ Preconditions.checkNotNull(context, "Context cannot be null.");
+ Preconditions.checkNotNull(timerName, "Timer name cannot be null.");
+ Preconditions.checkArgument(timeInMillis >= 0, "Duration cannot be negative.");
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MetricBundleKeys.METRIC_KEY, timerName);
+ bundle.putLong(MetricBundleKeys.TIME_MILLIS_LONG, timeInMillis);
+ DefaultSetupMetricsLogger.get(context).logEventSafely(MetricType.DURATION_EVENT, bundle);
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/logging/Timer.java b/main/java/com/google/android/setupcompat/logging/Timer.java
new file mode 100644
index 0000000..7d2a2c9
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/Timer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.logging;
+
+import android.util.Log;
+import com.google.android.setupcompat.internal.ClockProvider;
+import com.google.common.base.Preconditions;
+
+/** Convenience utility to log duration events. Please note that this class is not thread-safe. */
+public final class Timer {
+ /** Creates a new instance of timer for the given {@code metricKey}. */
+ public Timer(MetricKey metricKey) {
+ this.metricKey = metricKey;
+ }
+
+ /**
+ * Starts the timer and notes the current clock time.
+ *
+ * @throws IllegalStateException if the timer was stopped.
+ */
+ public void start() {
+ Preconditions.checkState(!isStopped(), "Timer cannot be started once stopped.");
+ if (isStarted()) {
+ Log.wtf(
+ TAG,
+ String.format(
+ "Timer instance was already started for: %s at [%s].", metricKey, startInNanos));
+ return;
+ }
+ startInNanos = ClockProvider.timeInNanos();
+ }
+
+ /**
+ * Stops the watch and the current clock time is noted.
+ *
+ * @throws IllegalStateException if the watch was not started.
+ */
+ public void stop() {
+ Preconditions.checkState(isStarted(), "Timer must be started before it can be stopped.");
+ if (isStopped()) {
+ Log.wtf(
+ TAG,
+ String.format(
+ "Timer instance was already stopped for: %s at [%s]", metricKey, stopInNanos));
+ return;
+ }
+ stopInNanos = ClockProvider.timeInNanos();
+ }
+
+ boolean isStopped() {
+ return stopInNanos != 0;
+ }
+
+ private boolean isStarted() {
+ return startInNanos != 0;
+ }
+
+ long getDurationInNanos() {
+ return stopInNanos - startInNanos;
+ }
+
+ MetricKey getMetricKey() {
+ return metricKey;
+ }
+
+ private long startInNanos;
+ private long stopInNanos;
+ private final MetricKey metricKey;
+
+ private static final String TAG = "SetupCompat.Timer";
+}
diff --git a/main/java/com/google/android/setupcompat/logging/internal/DefaultSetupMetricsLogger.java b/main/java/com/google/android/setupcompat/logging/internal/DefaultSetupMetricsLogger.java
new file mode 100644
index 0000000..12e9f98
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/internal/DefaultSetupMetricsLogger.java
@@ -0,0 +1,96 @@
+/*
+ * 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.logging.internal;
+
+import android.content.Context;
+import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+import android.util.Log;
+import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class is responsible for safely publishing log events to SetupWizard. To avoid memory issues
+ * due to backed up queues, an upper bound of {@link #MAX_QUEUED} is set on the executor service's
+ * queue. Once the upper bound is reached, metrics published after this event are dropped silently.
+ *
+ * <p>NOTE: This class is not meant to be used directly. Please use {@link
+ * com.google.android.setupcompat.logging.SetupMetricsLogger} for publishing metric events.
+ */
+public class DefaultSetupMetricsLogger {
+
+ @SuppressWarnings("FutureReturnValueIgnored")
+ public void logEventSafely(@MetricType int metricType, Bundle args) {
+ try {
+ executorService.submit(() -> invokeService(metricType, args));
+ } catch (RejectedExecutionException e) {
+ Log.e(TAG, String.format("Metric of type %d dropped since queue is full.", metricType), e);
+ }
+ }
+
+ private void invokeService(@MetricType int metricType, @SuppressWarnings("unused") Bundle args) {
+ // TODO(b/117984473): Invoke service.
+ Log.w(
+ TAG,
+ String.format("invokeService not implemented yet. No action taken for: %d", metricType));
+ }
+
+ @VisibleForTesting
+ DefaultSetupMetricsLogger(Context context, int maxSize) {
+ this(context, createBoundedExecutor(maxSize));
+ }
+
+ private DefaultSetupMetricsLogger(Context context) {
+ this(context, MAX_QUEUED);
+ }
+
+ private DefaultSetupMetricsLogger(Context context, ExecutorService executorService) {
+ this.context = context;
+ this.executorService = executorService;
+ }
+
+ @SuppressWarnings("unused")
+ private final Context context;
+
+ private final ExecutorService executorService;
+
+ private static ExecutorService createBoundedExecutor(int maxSize) {
+ return new ThreadPoolExecutor(
+ /* corePoolSize= */ 1,
+ /* maximumPoolSize= */ 1,
+ /* keepAliveTime= */ 0,
+ TimeUnit.SECONDS,
+ new ArrayBlockingQueue<>(maxSize),
+ runnable -> new Thread(runnable, "DefaultSetupMetricsLogger"));
+ }
+
+ public static synchronized DefaultSetupMetricsLogger get(Context context) {
+ if (instance == null) {
+ instance = new DefaultSetupMetricsLogger(context);
+ }
+
+ return instance;
+ }
+
+ private static DefaultSetupMetricsLogger instance;
+ private static final int MAX_QUEUED = 50;
+ private static final String TAG = "SetupCompat.SetupMetricsLogger";
+}
diff --git a/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java b/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java
new file mode 100644
index 0000000..fd6f450
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java
@@ -0,0 +1,86 @@
+/*
+ * 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.logging.internal;
+
+import android.content.Context;
+import androidx.annotation.IntDef;
+import androidx.annotation.StringDef;
+import com.google.android.setupcompat.logging.MetricKey;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Constant values used by {@link com.google.android.setupcompat.logging.SetupMetricsLogger}. */
+public interface SetupMetricsLoggingConstants {
+
+ /** Enumeration of supported metric types logged to SetupWizard. */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({MetricType.CUSTOM_EVENT, MetricType.COUNTER_EVENT, MetricType.DURATION_EVENT})
+ @interface MetricType {
+ /**
+ * MetricType constant used when logging {@link
+ * com.google.android.setupcompat.logging.CustomEvent}.
+ */
+ int CUSTOM_EVENT = 0;
+ /**
+ * MetricType constant used when logging {@link com.google.android.setupcompat.logging.Timer}.
+ */
+ int DURATION_EVENT = 1;
+
+ /**
+ * MetricType constant used when logging counter value using {@link
+ * com.google.android.setupcompat.logging.SetupMetricsLogger#logCounter(Context, MetricKey,
+ * int)}.
+ */
+ int COUNTER_EVENT = 2;
+ }
+
+ /** Keys of the bundle used while logging data to SetupWizard. */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({
+ MetricBundleKeys.METRIC_KEY,
+ MetricBundleKeys.CUSTOM_EVENT,
+ MetricBundleKeys.TIME_MILLIS_LONG,
+ MetricBundleKeys.COUNTER_INT
+ })
+ @interface MetricBundleKeys {
+ /**
+ * {@link MetricKey} of the data being logged. This will be set when {@code metricType} is
+ * either {@link MetricType#COUNTER_EVENT} or {@link MetricType#DURATION_EVENT}.
+ */
+ String METRIC_KEY = "MetricKey";
+
+ /**
+ * This key will be used when {@code metricType} is {@link MetricType#CUSTOM_EVENT} with the
+ * value being a parcelable of type {@link com.google.android.setupcompat.logging.CustomEvent}.
+ */
+ String CUSTOM_EVENT = "CustomEvent";
+
+ /**
+ * This key will be set when {@code metricType} is {@link MetricType#DURATION_EVENT} with the
+ * value of type {@code long} representing the {@code duration} in milliseconds for the given
+ * {@link MetricKey}.
+ */
+ String TIME_MILLIS_LONG = "timeMillis";
+
+ /**
+ * This key will be set when {@code metricType} is {@link MetricType#COUNTER_EVENT} with the
+ * value of type {@code int} representing the {@code counter} value logged for the given {@link
+ * MetricKey}.
+ */
+ String COUNTER_INT = "counter";
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java b/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java
new file mode 100644
index 0000000..c122f07
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/template/ButtonFooterMixin.java
@@ -0,0 +1,287 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.content.Context;
+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 androidx.annotation.ColorInt;
+import androidx.annotation.IdRes;
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StyleRes;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.XmlRes;
+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.R;
+import com.google.android.setupcompat.TemplateLayout;
+import com.google.android.setupcompat.item.FooterButton;
+import com.google.android.setupcompat.item.FooterButtonInflater;
+import com.google.android.setupcompat.util.PartnerConfig;
+import com.google.android.setupcompat.util.PartnerConfigHelper;
+
+/**
+ * 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 ButtonFooterMixin implements Mixin {
+
+ private final Context context;
+
+ @Nullable private final ViewStub footerStub;
+
+ private LinearLayout buttonContainer;
+
+ @VisibleForTesting final boolean applyPartnerResources;
+
+ /**
+ * Creates a mixin for managing buttons on the footer.
+ *
+ * @param layout The {@link TemplateLayout} containing this mixin.
+ * @param applyPartnerResources determine applies partner resources or not.
+ */
+ public ButtonFooterMixin(
+ TemplateLayout layout,
+ @XmlRes int attrPrimaryButton,
+ @XmlRes int attrSecondaryButton,
+ boolean applyPartnerResources) {
+ context = layout.getContext();
+ footerStub = (ViewStub) layout.findManagedViewById(R.id.suc_layout_footer);
+ this.applyPartnerResources = applyPartnerResources;
+
+ FooterButtonInflater inflater = new FooterButtonInflater(context);
+
+ if (attrPrimaryButton != 0) {
+ setPrimaryButton(inflater.inflate(attrPrimaryButton));
+ }
+
+ if (attrSecondaryButton != 0) {
+ setSecondaryButton(inflater.inflate(attrSecondaryButton));
+ }
+ }
+
+ // TODO(b/119537553): The button position abnormal due to set button order different.
+ 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");
+ }
+ footerStub.setLayoutResource(R.layout.suc_footer_button_bar);
+ buttonContainer = (LinearLayout) footerStub.inflate();
+ }
+ return buttonContainer;
+ }
+
+ @SuppressLint("InflateParams")
+ private Button 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 (Button) inflater.inflate(R.layout.suc_button, null, false);
+ }
+
+ /** Sets primary button for footer. */
+ public void setPrimaryButton(FooterButton footerButton) {
+ addSpace();
+ LinearLayout buttonContainer = ensureFooterInflated();
+
+ // Set the default theme if theme is not set, or when running in setup flow.
+ if (footerButton.getTheme() == 0 || applyPartnerResources) {
+ footerButton.setTheme(R.style.SucPartnerCustomizationButton_Primary);
+ }
+ // TODO(b/120055778): 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
+ && PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
+ == Color.TRANSPARENT) {
+ footerButton.setTheme(R.style.SucPartnerCustomizationButton_Secondary);
+ }
+
+ Button button = inflateButton(footerButton, R.id.suc_customization_primary_button);
+ buttonContainer.addView(button);
+ }
+
+ public Button getPrimaryButton() {
+ return buttonContainer.findViewById(R.id.suc_customization_primary_button);
+ }
+
+ /** Sets secondary button for footer. */
+ public void setSecondaryButton(FooterButton footerButton) {
+ LinearLayout buttonContainer = ensureFooterInflated();
+
+ // Set the default theme if theme is not set, or when running in setup flow.
+ if (footerButton.getTheme() == 0 || applyPartnerResources) {
+ footerButton.setTheme(R.style.SucPartnerCustomizationButton_Secondary);
+ }
+ // TODO(b/120055778): Make sure customize attributes in theme can be applied during setup flow.
+ // If doesn't set background color to full transparent, the button changes to colored bordered
+ // ink button style.
+ if (applyPartnerResources
+ && PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
+ != Color.TRANSPARENT) {
+ footerButton.setTheme(R.style.SucPartnerCustomizationButton_Primary);
+ }
+
+ Button button = inflateButton(footerButton, R.id.suc_customization_secondary_button);
+ buttonContainer.addView(button);
+ addSpace();
+ }
+
+ public Button getSecondaryButton() {
+ return buttonContainer.findViewById(R.id.suc_customization_secondary_button);
+ }
+
+ private Button inflateButton(FooterButton footerButton, @IdRes int id) {
+ Button button = createThemedButton(context, footerButton.getTheme());
+ button.setId(id);
+ button.setText(footerButton.getText());
+ button.setOnClickListener(footerButton.getListener());
+ if (applyPartnerResources) {
+ updateButtonAttrsWithPartnerConfig(button, id);
+ }
+ return button;
+ }
+
+ // TODO(b/120055778): Make sure customize attributes in theme can be applied during setup flow.
+ private void updateButtonAttrsWithPartnerConfig(Button button, @IdRes int id) {
+ updateButtonTextColorWithPartnerConfig(button, id);
+ updateButtonTextSizeWithPartnerConfig(button, id);
+ updateButtonTypeFaceWithPartnerConfig(button);
+ updateButtonBackgroundWithPartnerConfig(button, id);
+ updateButtonRadiusWithPartnerConfig(button);
+ }
+
+ private void updateButtonTextColorWithPartnerConfig(Button button, @IdRes int id) {
+ @ColorInt int color = 0;
+ if (id == R.id.suc_customization_primary_button) {
+ color =
+ PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR);
+ } else if (id == R.id.suc_customization_secondary_button) {
+ color =
+ PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR);
+ }
+ button.setTextColor(color);
+ }
+
+ private void updateButtonTextSizeWithPartnerConfig(Button button, @IdRes int id) {
+ float size = 0.0f;
+ if (id == R.id.suc_customization_primary_button) {
+ size =
+ PartnerConfigHelper.get(context)
+ .getDimension(context, PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_SIZE);
+ } else if (id == R.id.suc_customization_secondary_button) {
+ size =
+ PartnerConfigHelper.get(context)
+ .getDimension(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_SIZE);
+ }
+ button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
+ }
+
+ private void updateButtonTypeFaceWithPartnerConfig(Button button) {
+ String fontFamilyName =
+ PartnerConfigHelper.get(context)
+ .getString(context, PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY);
+ Typeface font = Typeface.create(fontFamilyName, Typeface.NORMAL);
+ if (font != null) {
+ button.setTypeface(font);
+ }
+ }
+
+ private void updateButtonBackgroundWithPartnerConfig(Button button, @IdRes int id) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ if (id == R.id.suc_customization_primary_button) {
+ int color =
+ PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR);
+ if (color != Color.TRANSPARENT) {
+ button.getBackground().setColorFilter(color, Mode.MULTIPLY);
+ }
+ } else if (id == R.id.suc_customization_secondary_button) {
+ int color =
+ PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR);
+ if (color != Color.TRANSPARENT) {
+ button.getBackground().setColorFilter(color, Mode.MULTIPLY);
+ }
+ }
+ }
+ }
+
+ private void updateButtonRadiusWithPartnerConfig(Button button) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
+ float defaultRadius =
+ context.getResources().getDimension(R.dimen.suc_customization_button_corner_radius);
+ float radius =
+ PartnerConfigHelper.get(context)
+ .getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS, defaultRadius);
+ GradientDrawable gradientDrawable = getGradientDrawable(button);
+ if (gradientDrawable != null) {
+ gradientDrawable.setCornerRadius(radius);
+ }
+ }
+ }
+
+ GradientDrawable getGradientDrawable(Button button) {
+ 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;
+ }
+
+ protected View inflateFooter(@LayoutRes int footer) {
+ footerStub.setLayoutResource(footer);
+ return footerStub.inflate();
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/template/Mixin.java b/main/java/com/google/android/setupcompat/template/Mixin.java
new file mode 100644
index 0000000..473ba6f
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/template/Mixin.java
@@ -0,0 +1,25 @@
+/*
+ * 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;
+
+/**
+ * Marker interface to indicate Mixin classes.
+ *
+ * @see com.google.android.setupcompat.TemplateLayout#registerMixin(Class, Mixin)
+ * @see com.google.android.setupcompat.TemplateLayout#getMixin(Class)
+ */
+public interface Mixin {}
diff --git a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java
new file mode 100644
index 0000000..ef482bf
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java
@@ -0,0 +1,162 @@
+/*
+ * 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.template;
+
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.Window;
+import com.google.android.setupcompat.PartnerCustomizationLayout;
+import com.google.android.setupcompat.R;
+import com.google.android.setupcompat.util.PartnerConfig;
+import com.google.android.setupcompat.util.PartnerConfigHelper;
+import com.google.android.setupcompat.view.StatusBarBackgroundLayout;
+
+/**
+ * A {@link Mixin} for setting and getting background color, and window compatible light/dark theme
+ * of status bar.
+ */
+public class StatusBarMixin implements Mixin {
+
+ private final PartnerCustomizationLayout partnerCustomizationLayout;
+ private final StatusBarBackgroundLayout statusBarLayout;
+ private final View decorView;
+ @VisibleForTesting final boolean applyPartnerResources;
+
+ /**
+ * Creates a mixin for managing status bar.
+ *
+ * @param layout The layout this Mixin belongs to.
+ * @param window The window this activity of 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.
+ * @param applyPartnerResources determine applies partner resources or not.
+ */
+ public StatusBarMixin(
+ @NonNull PartnerCustomizationLayout layout,
+ @NonNull Window window,
+ @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr,
+ boolean applyPartnerResources) {
+ partnerCustomizationLayout = layout;
+ statusBarLayout = partnerCustomizationLayout.findManagedViewById(R.id.suc_layout_status);
+ decorView = window.getDecorView();
+ this.applyPartnerResources = applyPartnerResources;
+
+ if (statusBarLayout == null) {
+ throw new NullPointerException("StatusBarBackgroundLayout cannot be null in StatusBarMixin");
+ }
+
+ // Override the color of status bar to transparent such that the color of
+ // StatusBarBackgroundLayout can be seen.
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ window.setStatusBarColor(Color.TRANSPARENT);
+ }
+
+ TypedArray a =
+ layout
+ .getContext()
+ .obtainStyledAttributes(attrs, R.styleable.SucStatusBarMixin, defStyleAttr, 0);
+ setStatusBarWindowLight(
+ a.getBoolean(
+ R.styleable.SucStatusBarMixin_sucStatusBarWindowLight, isStatusBarWindowLight()));
+ setStatusBarBackground(a.getDrawable(R.styleable.SucStatusBarMixin_sucStatusBarBackground));
+ a.recycle();
+ }
+
+ /**
+ * Sets the background color of status bar. The color will be overridden by partner resource if
+ * the activity is running in setup wizard flow.
+ *
+ * @param color The background color of status bar.
+ */
+ public void setStatusBarBackground(int color) {
+ setStatusBarBackground(new ColorDrawable(color));
+ }
+
+ /**
+ * Sets the background image of status bar. The drawable will be overridden by partner resource if
+ * the activity is running in setup wizard flow.
+ *
+ * @param background The drawable of status bar.
+ */
+ public void setStatusBarBackground(Drawable background) {
+ if (applyPartnerResources) {
+ Context context = partnerCustomizationLayout.getContext();
+ background =
+ PartnerConfigHelper.get(context)
+ .getDrawable(context, PartnerConfig.CONFIG_STATUS_BAR_BACKGROUND);
+ }
+
+ statusBarLayout.setStatusBarBackground(background);
+ }
+
+ /** Returns the background of status bar. */
+ public Drawable getStatusBarBackground() {
+ return statusBarLayout.getStatusBarBackground();
+ }
+
+ /**
+ * Sets the status bar to draw in a mode that is compatible with light or dark status bar
+ * backgrounds. The status bar drawing mode will be overridden by partner resource if the activity
+ * is running in setup wizard flow.
+ *
+ * @param isLight true means compatible with light theme, otherwise compatible with dark theme
+ */
+ public void setStatusBarWindowLight(boolean isLight) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ if (applyPartnerResources) {
+ Context context = partnerCustomizationLayout.getContext();
+ isLight =
+ PartnerConfigHelper.get(context)
+ .getBoolean(context, PartnerConfig.CONFIG_WINDOW_LIGHT_STATUS_BAR, false);
+ }
+
+ if (isLight) {
+ decorView.setSystemUiVisibility(
+ decorView.getSystemUiVisibility() | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ } else {
+ decorView.setSystemUiVisibility(
+ decorView.getSystemUiVisibility() & ~SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+ }
+
+ /**
+ * Returns true if status bar icons should be drawn on light background, false if the icons should
+ * be light-on-dark.
+ */
+ public boolean isStatusBarWindowLight() {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ return (decorView.getSystemUiVisibility() & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
+ == SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ }
+ return true;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java
new file mode 100644
index 0000000..e9d7799
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java
@@ -0,0 +1,147 @@
+/*
+ * 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.template;
+
+import static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.Window;
+import com.google.android.setupcompat.PartnerCustomizationLayout;
+import com.google.android.setupcompat.R;
+import com.google.android.setupcompat.util.PartnerConfig;
+import com.google.android.setupcompat.util.PartnerConfigHelper;
+
+/**
+ * A {@link Mixin} for setting and getting background color and window compatible with light theme
+ * of system navigation bar.
+ */
+public class SystemNavBarMixin implements Mixin {
+
+ private final PartnerCustomizationLayout partnerCustomizationLayout;
+ private final Window windowOfActivity;
+ private final View decorView;
+ @VisibleForTesting final boolean applyPartnerResources;
+
+ /**
+ * Creates a mixin for managing system navigation bar.
+ *
+ * @param layout The layout this Mixin belongs to.
+ * @param window The window this activity of 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.
+ * @param applyPartnerResources determine applies partner resources or not.
+ */
+ public SystemNavBarMixin(
+ @NonNull PartnerCustomizationLayout layout,
+ @NonNull Window window,
+ @Nullable AttributeSet attrs,
+ @AttrRes int defStyleAttr,
+ boolean applyPartnerResources) {
+ partnerCustomizationLayout = layout;
+ windowOfActivity = window;
+ decorView = window.getDecorView();
+ this.applyPartnerResources = applyPartnerResources;
+
+ TypedArray a =
+ partnerCustomizationLayout
+ .getContext()
+ .obtainStyledAttributes(attrs, R.styleable.SucSystemNavBarMixin, defStyleAttr, 0);
+ int navigationBarBackground =
+ a.getColor(R.styleable.SucSystemNavBarMixin_sucSystemNavBarBackgroundColor, 0);
+ setSystemNavBarBackground(navigationBarBackground);
+ setSystemNavBarWindowLight(
+ a.getBoolean(
+ R.styleable.SucSystemNavBarMixin_sucSystemNavBarWindowLight,
+ isSystemNavBarWindowLight()));
+ a.recycle();
+ }
+
+ /**
+ * Sets the background color of navigation bar. The color will be overridden by partner resource
+ * if the activity is running in setup wizard flow.
+ *
+ * @param color The background color of navigation bar.
+ */
+ public void setSystemNavBarBackground(int color) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ if (applyPartnerResources) {
+ Context context = partnerCustomizationLayout.getContext();
+ color =
+ PartnerConfigHelper.get(context)
+ .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR);
+ }
+
+ windowOfActivity.setNavigationBarColor(color);
+ }
+ }
+
+ /** Returns the background color of navigation bar. */
+ public int getSystemNavBarBackground() {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ return windowOfActivity.getNavigationBarColor();
+ }
+ return Color.BLACK;
+ }
+
+ /**
+ * Sets the navigation bar to draw in a mode that is compatible with light or dark navigation bar
+ * backgrounds. The navigation bar drawing mode will be overridden by partner resource if the
+ * activity is running in setup wizard flow.
+ *
+ * @param isLight true means compatible with light theme, otherwise compatible with dark theme
+ */
+ public void setSystemNavBarWindowLight(boolean isLight) {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
+ if (applyPartnerResources) {
+ Context context = partnerCustomizationLayout.getContext();
+ isLight =
+ PartnerConfigHelper.get(context)
+ .getBoolean(context, PartnerConfig.CONFIG_WINDOW_LIGHT_NAVIGATION_BAR, false);
+ }
+
+ if (isLight) {
+ decorView.setSystemUiVisibility(
+ decorView.getSystemUiVisibility() | SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+ } else {
+ decorView.setSystemUiVisibility(
+ decorView.getSystemUiVisibility() & ~SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the navigation bar icon should be drawn on light background, false if the icons
+ * should be drawn light-on-dark.
+ */
+ public boolean isSystemNavBarWindowLight() {
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
+ return (decorView.getSystemUiVisibility() & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
+ == SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
+ }
+ return true;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/FallbackThemeWrapper.java b/main/java/com/google/android/setupcompat/util/FallbackThemeWrapper.java
new file mode 100644
index 0000000..917b9d7
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/FallbackThemeWrapper.java
@@ -0,0 +1,50 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.content.res.Resources.Theme;
+import androidx.annotation.StyleRes;
+import android.view.ContextThemeWrapper;
+
+/**
+ * Same as {@link ContextThemeWrapper}, but the base context's theme attributes take precedence over
+ * the wrapper context's. This is used to provide default values for theme attributes referenced in
+ * layouts, to remove the risk of crashing the client because of using the wrong theme.
+ */
+public class FallbackThemeWrapper extends ContextThemeWrapper {
+
+ /**
+ * Creates a new context wrapper with the specified theme.
+ *
+ * <p>The specified theme will be applied as fallbacks to the base context's theme. Any attributes
+ * defined in the base context's theme will retain their original values. Otherwise values in
+ * {@code themeResId} will be used.
+ *
+ * @param base The base context.
+ * @param themeResId The theme to use as fallback.
+ */
+ public FallbackThemeWrapper(Context base, @StyleRes int themeResId) {
+ super(base, themeResId);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void onApplyThemeResource(Theme theme, int resId, boolean first) {
+ theme.applyStyle(resId, false /* force */);
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/FlagHelper.java b/main/java/com/google/android/setupcompat/util/FlagHelper.java
new file mode 100644
index 0000000..b2b700f
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/FlagHelper.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.util;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import com.google.android.setupcompat.R;
+
+/** Helper utilities to get flags that enable/disable features of this library. */
+public final class FlagHelper {
+
+ private FlagHelper() {}
+
+ /**
+ * Returns whether customization styles should be loaded from the partner overlay APK. Default
+ * value is currently false, but will be changed to true once the feature is ready for general
+ * consumption. Outside of setup wizard flow, this flag should default to false.
+ */
+ public static final boolean isPartnerStyleEnabled(Context context) {
+ TypedArray typedArray = context.obtainStyledAttributes(new int[] {R.attr.sucEnablePartnerStyle});
+ boolean enablePartnerStyle = typedArray.getBoolean(0, false);
+ typedArray.recycle();
+ return enablePartnerStyle;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/PartnerConfig.java b/main/java/com/google/android/setupcompat/util/PartnerConfig.java
new file mode 100644
index 0000000..bf888f3
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/PartnerConfig.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 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.util;
+
+/** Resources that can be customized by partner overlay APK. */
+public enum PartnerConfig {
+
+ // Status bar background color or illustration.
+ CONFIG_STATUS_BAR_BACKGROUND(PartnerConfigKey.KEY_STATUS_BAR_BACKGROUND, ResourceType.DRAWABLE),
+
+ // The same as "WindowLightStatusBar". If set true, the status bar icons will be drawn such
+ // that it is compatible with a light status bar background
+ CONFIG_WINDOW_LIGHT_STATUS_BAR(PartnerConfigKey.KEY_WINDOW_LIGHT_STATUS_BAR, ResourceType.BOOL),
+
+ // Navigation bar background color
+ CONFIG_NAVIGATION_BAR_BG_COLOR(PartnerConfigKey.KEY_NAVIGATION_BAR_BG_COLOR, ResourceType.COLOR),
+
+ // The same as "windowLightNavigationBar". If set true, the navigation bar icons will be drawn
+ // such that it is compatible with a light navigation bar background.
+ CONFIG_WINDOW_LIGHT_NAVIGATION_BAR(
+ PartnerConfigKey.KEY_WINDOW_LIGHT_NAVIGATION_BAR, ResourceType.BOOL),
+
+ // The font face used in footer buttons. This must be a string reference to a font that is
+ // available in the system. Font references (@font or @xml) are not allowed.
+ CONFIG_FOOTER_BUTTON_FONT_FAMILY(
+ PartnerConfigKey.KEY_FOOTER_BUTTON_FONT_FAMILY, ResourceType.STRING),
+
+ // 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 "skip" action. Can be "@null" for no icon.
+ CONFIG_FOOTER_BUTTON_ICON_SKIP(
+ PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_SKIP, ResourceType.DRAWABLE),
+
+ // Top padding of the footer buttons
+ CONFIG_FOOTER_BUTTON_PADDING_TOP(
+ PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_TOP, ResourceType.DIMENSION),
+
+ // Bottom padding of the footer buttons
+ CONFIG_FOOTER_BUTTON_PADDING_BOTTOM(
+ PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_BOTTOM, ResourceType.DIMENSION),
+
+ // Corner radius of the footer buttons
+ CONFIG_FOOTER_BUTTON_RADIUS(PartnerConfigKey.KEY_FOOTER_BUTTON_RADIUS, ResourceType.DIMENSION),
+
+ // Background color of the primary footer button
+ CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR(
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR, ResourceType.COLOR),
+
+ // Text color of the primary footer button
+ CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR(
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR, ResourceType.COLOR),
+
+ // Text size of the primary footer button
+ CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_SIZE(
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_SIZE, ResourceType.DIMENSION),
+
+ // Background color of the secondary footer button
+ CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR(
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_BG_COLOR, ResourceType.COLOR),
+
+ // Text color of the secondary footer button
+ CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR(
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR, ResourceType.COLOR),
+
+ // Text size of the secondary footer button
+ CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_SIZE(
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_SIZE, ResourceType.DIMENSION);
+
+ public enum ResourceType {
+ BOOL,
+ COLOR,
+ DRAWABLE,
+ STRING,
+ DIMENSION;
+ }
+
+ private final String resourceName;
+ private final ResourceType resourceType;
+
+ public ResourceType getResourceType() {
+ return resourceType;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ PartnerConfig(@PartnerConfigKey String resourceName, ResourceType type) {
+ this.resourceName = resourceName;
+ this.resourceType = type;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/PartnerConfigHelper.java b/main/java/com/google/android/setupcompat/util/PartnerConfigHelper.java
new file mode 100644
index 0000000..87a3706
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/PartnerConfigHelper.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright 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.util;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import android.util.Log;
+import com.google.android.setupcompat.util.PartnerConfig.ResourceType;
+import java.util.EnumMap;
+
+/** The helper reads and caches the partner configurations from SUW. */
+public class PartnerConfigHelper {
+
+ private static final String TAG = PartnerConfigHelper.class.getSimpleName();
+
+ @VisibleForTesting
+ public static final String SUW_AUTHORITY = "com.google.android.setupwizard.partner";
+
+ @VisibleForTesting public static final String SUW_GET_PARTNER_CONFIG_METHOD = "getOverlayConfig";
+ private static PartnerConfigHelper instance = null;
+
+ @VisibleForTesting Bundle resultBundle = null;
+
+ @VisibleForTesting
+ final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class);
+
+ public static synchronized PartnerConfigHelper get(@NonNull Context context) {
+ if (instance == null) {
+ instance = new PartnerConfigHelper(context);
+ }
+ return instance;
+ }
+
+ private PartnerConfigHelper(Context context) {
+ getPartnerConfigBundle(context);
+ }
+
+ /**
+ * Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is
+ * not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color,
+ * IllegalArgumentException will be thrown.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ */
+ @ColorInt
+ public int getColor(@NonNull Context context, PartnerConfig resourceConfig) {
+ if (resourceConfig.getResourceType() != ResourceType.COLOR) {
+ throw new IllegalArgumentException("Not a color resource");
+ }
+
+ if (partnerResourceCache.containsKey(resourceConfig)) {
+ return (int) partnerResourceCache.get(resourceConfig);
+ }
+
+ int result = 0;
+ try {
+ String resourceName = resourceConfig.getResourceName();
+ ResourceEntry resourceEntry = getResourceEntryFromKey(resourceName);
+ Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
+ result = resource.getColor(resourceEntry.getResourceId(), null);
+ } else {
+ result = resource.getColor(resourceEntry.getResourceId());
+ }
+ partnerResourceCache.put(resourceConfig, result);
+ } catch (PackageManager.NameNotFoundException | NullPointerException exception) {
+ // fall through
+ }
+ return result;
+ }
+
+ /**
+ * Returns the {@code Drawable} of given {@code resourceConfig}, or {@code null} if the given
+ * {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
+ * resourceConfig} is not drawable, IllegalArgumentException will be thrown.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ */
+ @Nullable
+ public Drawable getDrawable(@NonNull Context context, PartnerConfig resourceConfig) {
+ if (resourceConfig.getResourceType() != ResourceType.DRAWABLE) {
+ throw new IllegalArgumentException("Not a drawable resource");
+ }
+
+ if (partnerResourceCache.containsKey(resourceConfig)) {
+ return (Drawable) partnerResourceCache.get(resourceConfig);
+ }
+
+ Drawable result = null;
+ try {
+ ResourceEntry resourceEntry = getResourceEntryFromKey(resourceConfig.getResourceName());
+ Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ result = resource.getDrawable(resourceEntry.getResourceId(), null);
+ } else {
+ result = resource.getDrawable(resourceEntry.getResourceId());
+ }
+ partnerResourceCache.put(resourceConfig, result);
+ } catch (PackageManager.NameNotFoundException | NullPointerException exception) {
+ // fall through
+ }
+ return result;
+ }
+
+ /**
+ * Returns the string of the given {@code resourceConfig}, or {@code null} if the given {@code
+ * resourceConfig} is not found. If the {@code ResourceType} of the given {@code resourceConfig}
+ * is not string, IllegalArgumentException will be thrown.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ */
+ @Nullable
+ public String getString(@NonNull Context context, PartnerConfig resourceConfig) {
+ if (resourceConfig.getResourceType() != ResourceType.STRING) {
+ throw new IllegalArgumentException("Not a string resource");
+ }
+
+ if (partnerResourceCache.containsKey(resourceConfig)) {
+ return (String) partnerResourceCache.get(resourceConfig);
+ }
+
+ String result = null;
+ try {
+ ResourceEntry resourceEntry = getResourceEntryFromKey(resourceConfig.getResourceName());
+ Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
+ result = resource.getString(resourceEntry.getResourceId());
+ partnerResourceCache.put(resourceConfig, result);
+ } catch (PackageManager.NameNotFoundException | NullPointerException exception) {
+ // fall through
+ }
+ return result;
+ }
+
+ /**
+ * Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given
+ * {@code resourceName} is not found. If the {@code ResourceType} of the given {@code
+ * resourceConfig} is not boolean, IllegalArgumentException will be thrown.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ * @param defaultValue The default value
+ */
+ public boolean getBoolean(
+ @NonNull Context context, PartnerConfig resourceConfig, boolean defaultValue) {
+ if (resourceConfig.getResourceType() != ResourceType.BOOL) {
+ throw new IllegalArgumentException("Not a bool resource");
+ }
+
+ if (partnerResourceCache.containsKey(resourceConfig)) {
+ return (boolean) partnerResourceCache.get(resourceConfig);
+ }
+
+ boolean result = defaultValue;
+ try {
+ ResourceEntry resourceEntry = getResourceEntryFromKey(resourceConfig.getResourceName());
+ Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
+ result = resource.getBoolean(resourceEntry.getResourceId());
+ partnerResourceCache.put(resourceConfig, result);
+ } catch (PackageManager.NameNotFoundException | NullPointerException exception) {
+ // fall through
+ }
+ return result;
+ }
+
+ /**
+ * Returns the dimension of given {@code resourceConfig}. The default return value is 0.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ */
+ public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
+ return getDimension(context, resourceConfig, 0);
+ }
+
+ /**
+ * Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} not
+ * found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
+ * resourceConfig} is not dimension, will throw IllegalArgumentException.
+ *
+ * @param context The context of client activity
+ * @param resourceConfig The {@code PartnerConfig} of target resource
+ * @param defaultValue The default value
+ */
+ public float getDimension(
+ @NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
+ if (resourceConfig.getResourceType() != ResourceType.DIMENSION) {
+ throw new IllegalArgumentException("Not a dimension resource");
+ }
+
+ if (partnerResourceCache.containsKey(resourceConfig)) {
+ return (float) partnerResourceCache.get(resourceConfig);
+ }
+
+ float result = defaultValue;
+ try {
+ ResourceEntry resourceEntry = getResourceEntryFromKey(resourceConfig.getResourceName());
+ Resources resource = getResourcesByPackageName(context, resourceEntry.getPackageName());
+ result = resource.getDimension(resourceEntry.getResourceId());
+ partnerResourceCache.put(resourceConfig, result);
+ } catch (PackageManager.NameNotFoundException | NullPointerException exception) {
+ // fall through
+ }
+ return result;
+ }
+
+ private void getPartnerConfigBundle(Context context) {
+ if (resultBundle == null || resultBundle.isEmpty()) {
+ try {
+ Uri contentUri =
+ new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SUW_AUTHORITY)
+ .appendPath(SUW_GET_PARTNER_CONFIG_METHOD)
+ .build();
+ resultBundle =
+ context
+ .getContentResolver()
+ .call(
+ contentUri, SUW_GET_PARTNER_CONFIG_METHOD, /* arg= */ null, /* extras= */ null);
+ partnerResourceCache.clear();
+ } catch (IllegalArgumentException exception) {
+ Log.w(TAG, "Fail to get config from suw provider");
+ }
+ }
+ }
+
+ private Resources getResourcesByPackageName(Context context, String packageName)
+ throws PackageManager.NameNotFoundException {
+ PackageManager manager = context.getPackageManager();
+ return manager.getResourcesForApplication(packageName);
+ }
+
+ private ResourceEntry getResourceEntryFromKey(String resourceName) {
+ if (resultBundle == null) {
+ return null;
+ }
+ return ResourceEntry.fromBundle(resultBundle.getBundle(resourceName));
+ }
+
+ @VisibleForTesting
+ public static synchronized void resetForTesting() {
+ instance = null;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java b/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java
new file mode 100644
index 0000000..dfd4add
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/PartnerConfigKey.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 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.util;
+
+import androidx.annotation.StringDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Resource names that can be customized by partner overlay APK. */
+@Retention(RetentionPolicy.SOURCE)
+@StringDef({
+ PartnerConfigKey.KEY_STATUS_BAR_BACKGROUND,
+ PartnerConfigKey.KEY_WINDOW_LIGHT_STATUS_BAR,
+ PartnerConfigKey.KEY_NAVIGATION_BAR_BG_COLOR,
+ PartnerConfigKey.KEY_WINDOW_LIGHT_NAVIGATION_BAR,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_FONT_FAMILY,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_NEXT,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_ICON_SKIP,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_TOP,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_PADDING_BOTTOM,
+ PartnerConfigKey.KEY_FOOTER_BUTTON_RADIUS,
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR,
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR,
+ PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_SIZE,
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_BG_COLOR,
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR,
+ PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_SIZE,
+})
+public @interface PartnerConfigKey {
+ // Status bar background color or illustration.
+ String KEY_STATUS_BAR_BACKGROUND = "setup_compat_status_bar_background";
+
+ // The same as "WindowLightStatusBar". If set true, the status bar icons will be drawn such
+ // that it is compatible with a light status bar background
+ String KEY_WINDOW_LIGHT_STATUS_BAR = "setup_compat_window_light_status_bar";
+
+ // Navigation bar background color
+ String KEY_NAVIGATION_BAR_BG_COLOR = "setup_compat_navigation_bar_bg_color";
+
+ // The same as "windowLightNavigationBar". If set true, the navigation bar icons will be drawn
+ // such that it is compatible with a light navigation bar background.
+ String KEY_WINDOW_LIGHT_NAVIGATION_BAR = "setup_compat_window_light_navigation_bar";
+
+ // The font face used in footer buttons. This must be a string reference to a font that is
+ // 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 "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";
+
+ // Top padding of the footer buttons
+ String KEY_FOOTER_BUTTON_PADDING_TOP = "setup_compat_footer_button_padding_top";
+
+ // Bottom padding of the footer buttons
+ String KEY_FOOTER_BUTTON_PADDING_BOTTOM = "setup_compat_footer_button_padding_bottom";
+
+ // Corner radius of the footer buttons
+ String KEY_FOOTER_BUTTON_RADIUS = "setup_compat_footer_button_radius";
+
+ // Background color of the primary footer button
+ String KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR = "setup_compat_footer_primary_button_bg_color";
+
+ // Text color of the primary footer button
+ String KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR = "setup_compat_footer_primary_button_text_color";
+
+ // Text size of the primary footer button
+ String KEY_FOOTER_PRIMARY_BUTTON_TEXT_SIZE = "setup_compat_footer_primary_button_text_size";
+
+ // Background color of the secondary footer button
+ String KEY_FOOTER_SECONDARY_BUTTON_BG_COLOR = "setup_compat_footer_secondary_button_bg_color";
+
+ // Text color of the secondary footer button
+ String KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR = "setup_compat_footer_secondary_button_text_color";
+
+ // Text size of the secondary footer button
+ String KEY_FOOTER_SECONDARY_BUTTON_TEXT_SIZE = "setup_compat_footer_secondary_button_text_size";
+}
diff --git a/main/java/com/google/android/setupcompat/util/ResourceEntry.java b/main/java/com/google/android/setupcompat/util/ResourceEntry.java
new file mode 100644
index 0000000..0cf7fc3
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/ResourceEntry.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright 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.util;
+
+import android.os.Bundle;
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * A potentially cross-package resource entry, which can then be retrieved using {@link
+ * PackageManager#getApplicationForResources}. This class can also be sent across to other packages
+ * on IPC via the Bundle representation.
+ */
+public final class ResourceEntry {
+ @VisibleForTesting static final String KEY_PACKAGE_NAME = "packageName";
+ @VisibleForTesting static final String KEY_RESOURCE_NAME = "resourceName";
+ @VisibleForTesting static final String KEY_RESOURCE_ID = "resourceId";
+
+ private final String packageName;
+ private final String resourceName;
+ private final int resourceId;
+
+ /**
+ * Creates a {@code ResourceEntry} object from a provided bundle.
+ *
+ * @param bundle the source bundle needs to have all the information for a {@code ResourceEntry}
+ */
+ public static ResourceEntry fromBundle(Bundle bundle) {
+ String packageName;
+ String resourceName;
+ int resourceId;
+ if (!bundle.containsKey(KEY_PACKAGE_NAME)
+ || !bundle.containsKey(KEY_RESOURCE_NAME)
+ || !bundle.containsKey(KEY_RESOURCE_ID)) {
+ return null;
+ }
+ packageName = bundle.getString(KEY_PACKAGE_NAME);
+ resourceName = bundle.getString(KEY_RESOURCE_NAME);
+ resourceId = bundle.getInt(KEY_RESOURCE_ID);
+ return new ResourceEntry(packageName, resourceName, resourceId);
+ }
+
+ public ResourceEntry(String packageName, @PartnerConfigKey String resourceName, int resourceId) {
+ this.packageName = packageName;
+ this.resourceName = resourceName;
+ this.resourceId = resourceId;
+ }
+
+ public String getPackageName() {
+ return this.packageName;
+ }
+
+ public String getResourceName() {
+ return this.resourceName;
+ }
+
+ public int getResourceId() {
+ return this.resourceId;
+ }
+
+ /**
+ * Returns a bundle representation of this resource entry, which can then be sent over IPC.
+ *
+ * @see #fromBundle(Bundle)
+ */
+ public Bundle toBundle() {
+ Bundle result = new Bundle();
+ result.putString(KEY_PACKAGE_NAME, packageName);
+ result.putString(KEY_RESOURCE_NAME, resourceName);
+ result.putInt(KEY_RESOURCE_ID, resourceId);
+ return result;
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/util/ResultCodes.java b/main/java/com/google/android/setupcompat/util/ResultCodes.java
new file mode 100644
index 0000000..3934b21
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/ResultCodes.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import static android.app.Activity.RESULT_FIRST_USER;
+
+/** Result codes for activities to return to Wizard Manager. */
+public final class ResultCodes {
+
+ public static final int RESULT_SKIP = RESULT_FIRST_USER;
+ public static final int RESULT_RETRY = RESULT_FIRST_USER + 1;
+ public static final int RESULT_ACTIVITY_NOT_FOUND = RESULT_FIRST_USER + 2;
+
+ public static final int RESULT_FIRST_SETUP_USER = RESULT_FIRST_USER + 100;
+}
diff --git a/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java
new file mode 100644
index 0000000..780105d
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.provider.Settings;
+import androidx.annotation.VisibleForTesting;
+import java.util.Arrays;
+
+/**
+ * Helper to interact with Wizard Manager in setup wizard, which should be used when a screen is
+ * shown inside the setup flow. This includes things like parsing extras passed by Wizard Manager,
+ * and invoking Wizard Manager to start the next action.
+ */
+public class WizardManagerHelper {
+
+ private static final String ACTION_NEXT = "com.android.wizard.NEXT";
+
+ // EXTRA_SCRIPT_URI and EXTRA_ACTION_ID are used in setup wizard in versions before M and are
+ // kept for backwards compatibility.
+ @VisibleForTesting static final String EXTRA_SCRIPT_URI = "scriptUri";
+ @VisibleForTesting static final String EXTRA_ACTION_ID = "actionId";
+
+ @VisibleForTesting static final String EXTRA_WIZARD_BUNDLE = "wizardBundle";
+ private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode";
+ @VisibleForTesting public static final String EXTRA_IS_FIRST_RUN = "firstRun";
+ @VisibleForTesting static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup";
+ @VisibleForTesting static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup";
+ @VisibleForTesting public static final String EXTRA_IS_SETUP_FLOW = "isSetupFlow";
+
+ public static final String EXTRA_THEME = "theme";
+ public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
+
+ public static final String SETTINGS_GLOBAL_DEVICE_PROVISIONED = "device_provisioned";
+ public static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
+
+ /**
+ * Gets an intent that will invoke the next step of setup wizard.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via {@link
+ * Activity#getIntent()}.
+ * @param resultCode The result code of the step. See {@link ResultCodes}.
+ * @return A new intent that can be used with {@link Activity#startActivityForResult(Intent, int)}
+ * to start the next step of the setup flow.
+ */
+ public static Intent getNextIntent(Intent originalIntent, int resultCode) {
+ return getNextIntent(originalIntent, resultCode, null);
+ }
+
+ /**
+ * Gets an intent that will invoke the next step of setup wizard.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via {@link
+ * Activity#getIntent()}.
+ * @param resultCode The result code of the step. See {@link ResultCodes}.
+ * @param data An intent containing extra result data.
+ * @return A new intent that can be used with {@link Activity#startActivityForResult(Intent, int)}
+ * to start the next step of the setup flow.
+ */
+ public static Intent getNextIntent(Intent originalIntent, int resultCode, Intent data) {
+ Intent intent = new Intent(ACTION_NEXT);
+ copyWizardManagerExtras(originalIntent, intent);
+ intent.putExtra(EXTRA_RESULT_CODE, resultCode);
+ if (data != null && data.getExtras() != null) {
+ intent.putExtras(data.getExtras());
+ }
+ intent.putExtra(EXTRA_THEME, originalIntent.getStringExtra(EXTRA_THEME));
+
+ return intent;
+ }
+
+ /**
+ * Copies the internal extras used by setup wizard from one intent to another. For low-level use
+ * only, such as when using {@link Intent#FLAG_ACTIVITY_FORWARD_RESULT} to relay to another
+ * intent.
+ *
+ * @param srcIntent Intent to get the wizard manager extras from.
+ * @param dstIntent Intent to copy the wizard manager extras to.
+ */
+ public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) {
+ dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
+ for (String key :
+ Arrays.asList(EXTRA_IS_FIRST_RUN, EXTRA_IS_DEFERRED_SETUP, EXTRA_IS_PRE_DEFERRED_SETUP)) {
+ dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false));
+ }
+
+ for (String key : Arrays.asList(EXTRA_THEME, EXTRA_SCRIPT_URI, EXTRA_ACTION_ID)) {
+ dstIntent.putExtra(key, srcIntent.getStringExtra(key));
+ }
+ }
+
+ /**
+ * Checks whether an intent is intended to be used within the setup wizard flow.
+ *
+ * @param intent The intent to be checked, usually from {@link Activity#getIntent()}.
+ * @return true if the intent passed in was intended to be used with setup wizard.
+ */
+ public static boolean isSetupWizardIntent(Intent intent) {
+ return intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false);
+ }
+
+ /**
+ * Checks whether the current user has completed Setup Wizard. This is true if the current user
+ * has gone through Setup Wizard. The current user may or may not be the device owner and the
+ * device owner may have already completed setup wizard.
+ *
+ * @param context The context to retrieve the settings.
+ * @return true if the current user has completed Setup Wizard.
+ * @see #isDeviceProvisioned(Context)
+ */
+ public static boolean isUserSetupComplete(Context context) {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ return Settings.Secure.getInt(
+ context.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0)
+ == 1;
+ } else {
+ // For versions below JB MR1, there are no user profiles. Just return the global device
+ // provisioned state.
+ return Settings.Secure.getInt(
+ context.getContentResolver(), SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0)
+ == 1;
+ }
+ }
+
+ /**
+ * Checks whether the device is provisioned. This means that the device has gone through Setup
+ * Wizard at least once. Note that the user can still be in Setup Wizard even if this is true, for
+ * a secondary user profile triggered through Settings > Add account.
+ *
+ * @param context The context to retrieve the settings.
+ * @return true if the device is provisioned.
+ * @see #isUserSetupComplete(Context)
+ */
+ public static boolean isDeviceProvisioned(Context context) {
+ if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+ return Settings.Global.getInt(
+ context.getContentResolver(), SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0)
+ == 1;
+ } else {
+ return Settings.Secure.getInt(
+ context.getContentResolver(), SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0)
+ == 1;
+ }
+ }
+
+ /**
+ * Checks whether an intent is running in the deferred setup wizard flow.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via {@link
+ * Activity#getIntent()}.
+ * @return true if the intent passed in was running in deferred setup wizard.
+ */
+ public static boolean isDeferredSetupWizard(Intent originalIntent) {
+ return originalIntent != null && originalIntent.getBooleanExtra(EXTRA_IS_DEFERRED_SETUP, false);
+ }
+
+ /**
+ * Checks whether an intent is running in "pre-deferred" setup wizard flow.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via {@link
+ * Activity#getIntent()}.
+ * @return true if the intent passed in was running in "pre-deferred" setup wizard.
+ */
+ public static boolean isPreDeferredSetupWizard(Intent originalIntent) {
+ return originalIntent != null
+ && originalIntent.getBooleanExtra(EXTRA_IS_PRE_DEFERRED_SETUP, false);
+ }
+
+ /**
+ * Returns true if the intent passed in indicates that it is running in any setup wizard flow,
+ * including initial setup and deferred setup etc.
+ *
+ * @param originalIntent The original intent that was used to start the step, usually via {@link
+ * Activity#getIntent()}.
+ */
+ public static boolean isAnySetupWizard(Intent originalIntent) {
+ // TODO(b/119455685): change this to >= VERSION_CODES.Q when VERSION_CODES.Q is available
+ if (Build.VERSION.SDK_INT > VERSION_CODES.P) {
+ return originalIntent.getBooleanExtra(EXTRA_IS_SETUP_FLOW, false);
+ } else {
+ return originalIntent != null
+ && (isSetupWizardIntent(originalIntent)
+ || isPreDeferredSetupWizard(originalIntent)
+ || isDeferredSetupWizard(originalIntent));
+ }
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/view/ButtonBarLayout.java b/main/java/com/google/android/setupcompat/view/ButtonBarLayout.java
new file mode 100644
index 0000000..da1ab34
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/view/ButtonBarLayout.java
@@ -0,0 +1,118 @@
+/*
+ * 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.LinearLayout;
+import com.google.android.setupcompat.R;
+
+/**
+ * An extension of LinearLayout that automatically switches to vertical orientation when it can't
+ * fit its child views horizontally.
+ *
+ * <p>Modified from {@code com.android.internal.widget.ButtonBarLayout}
+ */
+public class ButtonBarLayout extends LinearLayout {
+
+ private boolean stacked = false;
+ private int originalPaddingLeft;
+ private int originalPaddingRight;
+
+ public ButtonBarLayout(Context context) {
+ super(context);
+ }
+
+ public ButtonBarLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+
+ setStacked(false);
+
+ boolean needsRemeasure = false;
+
+ int initialWidthMeasureSpec = widthMeasureSpec;
+ if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ // Measure with WRAP_CONTENT, so that we can compare the measured size with the
+ // available size to see if we need to stack.
+ initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+
+ // We'll need to remeasure again to fill excess space.
+ needsRemeasure = true;
+ }
+
+ super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
+
+ if (getMeasuredWidth() > widthSize) {
+ setStacked(true);
+
+ // Measure again in the new orientation.
+ needsRemeasure = true;
+ }
+
+ if (needsRemeasure) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ private void setStacked(boolean stacked) {
+ if (this.stacked == stacked) {
+ return;
+ }
+ this.stacked = stacked;
+ int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ View child = getChildAt(i);
+ LayoutParams childParams = (LayoutParams) child.getLayoutParams();
+ if (stacked) {
+ child.setTag(R.id.suc_customization_original_weight, childParams.weight);
+ childParams.weight = 0;
+ } else {
+ Float weight = (Float) child.getTag(R.id.suc_customization_original_weight);
+ if (weight != null) {
+ childParams.weight = weight;
+ }
+ }
+ child.setLayoutParams(childParams);
+ }
+
+ setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
+
+ // Reverse the child order, so that the primary button is towards the top when vertical
+ for (int i = childCount - 1; i >= 0; i--) {
+ bringChildToFront(getChildAt(i));
+ }
+
+ if (stacked) {
+ // HACK: In the default button bar style, the left and right paddings are not
+ // balanced to compensate for different alignment for borderless (left) button and
+ // the raised (right) button. When it's stacked, we want the buttons to be centered,
+ // so we balance out the paddings here.
+ originalPaddingLeft = getPaddingLeft();
+ originalPaddingRight = getPaddingRight();
+ int paddingHorizontal = Math.max(originalPaddingLeft, originalPaddingRight);
+ setPadding(paddingHorizontal, getPaddingTop(), paddingHorizontal, getPaddingBottom());
+ } else {
+ setPadding(originalPaddingLeft, getPaddingTop(), originalPaddingRight, getPaddingBottom());
+ }
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/view/StatusBarBackgroundLayout.java b/main/java/com/google/android/setupcompat/view/StatusBarBackgroundLayout.java
new file mode 100644
index 0000000..9cee894
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/view/StatusBarBackgroundLayout.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2015 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.view;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.util.AttributeSet;
+import android.view.WindowInsets;
+import android.widget.FrameLayout;
+
+/**
+ * A FrameLayout subclass that will responds to onApplyWindowInsets to draw a drawable in the top
+ * inset area, making a background effect for the navigation bar. To make use of this layout,
+ * specify the system UI visibility {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} and
+ * set specify fitsSystemWindows.
+ *
+ * <p>This view is a normal FrameLayout if either of those are not set, or if the platform version
+ * is lower than Lollipop.
+ */
+public class StatusBarBackgroundLayout extends FrameLayout {
+
+ private Drawable statusBarBackground;
+ private Object lastInsets; // Use generic Object type for compatibility
+
+ public StatusBarBackgroundLayout(Context context) {
+ super(context);
+ }
+
+ public StatusBarBackgroundLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @TargetApi(VERSION_CODES.HONEYCOMB)
+ public StatusBarBackgroundLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ if (lastInsets == null) {
+ requestApplyInsets();
+ }
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ if (lastInsets != null) {
+ final int insetTop = ((WindowInsets) lastInsets).getSystemWindowInsetTop();
+ if (insetTop > 0) {
+ statusBarBackground.setBounds(0, 0, getWidth(), insetTop);
+ statusBarBackground.draw(canvas);
+ }
+ }
+ }
+ }
+
+ public void setStatusBarBackground(Drawable background) {
+ statusBarBackground = background;
+ if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+ setWillNotDraw(background == null);
+ setFitsSystemWindows(background != null);
+ invalidate();
+ }
+ }
+
+ public Drawable getStatusBarBackground() {
+ return statusBarBackground;
+ }
+
+ @Override
+ public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ lastInsets = insets;
+ return super.onApplyWindowInsets(insets);
+ }
+}