summaryrefslogtreecommitdiff
path: root/main/java/com/google/android
diff options
context:
space:
mode:
Diffstat (limited to 'main/java/com/google/android')
-rw-r--r--main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java32
-rw-r--r--main/java/com/google/android/setupcompat/internal/Preconditions.java10
-rw-r--r--main/java/com/google/android/setupcompat/logging/ScreenKey.java178
-rw-r--r--main/java/com/google/android/setupcompat/logging/SetupMetric.java254
-rw-r--r--main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java70
-rw-r--r--main/java/com/google/android/setupcompat/logging/internal/MetricBundleConverter.java9
-rw-r--r--main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java54
-rw-r--r--main/java/com/google/android/setupcompat/template/FooterBarMixin.java7
-rw-r--r--main/java/com/google/android/setupcompat/util/BuildCompatUtils.java19
9 files changed, 617 insertions, 16 deletions
diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
index 37cc358..21928c8 100644
--- a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
+++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
@@ -76,6 +76,8 @@ public class PartnerCustomizationLayout extends TemplateLayout {
private Activity activity;
+ private PersistableBundle layoutTypeBundle;
+
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context) {
this(context, 0, 0);
@@ -92,10 +94,6 @@ public class PartnerCustomizationLayout extends TemplateLayout {
init(null, R.attr.sucLayoutTheme);
}
- @VisibleForTesting
- final ViewTreeObserver.OnWindowFocusChangeListener windowFocusChangeListener =
- this::onFocusChanged;
-
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -109,6 +107,10 @@ public class PartnerCustomizationLayout extends TemplateLayout {
init(attrs, defStyleAttr);
}
+ @VisibleForTesting
+ final ViewTreeObserver.OnWindowFocusChangeListener windowFocusChangeListener =
+ this::onFocusChanged;
+
private void init(AttributeSet attrs, int defStyleAttr) {
if (isInEditMode()) {
return;
@@ -242,9 +244,15 @@ public class PartnerCustomizationLayout extends TemplateLayout {
? secondaryButton.getMetrics("SecondaryFooterButton")
: PersistableBundle.EMPTY;
+ PersistableBundle layoutTypeMetrics =
+ (layoutTypeBundle != null) ? layoutTypeBundle : PersistableBundle.EMPTY;
+
PersistableBundle persistableBundle =
PersistableBundles.mergeBundles(
- footerBarMixin.getLoggingMetrics(), primaryButtonMetrics, secondaryButtonMetrics);
+ footerBarMixin.getLoggingMetrics(),
+ primaryButtonMetrics,
+ secondaryButtonMetrics,
+ layoutTypeMetrics);
SetupMetricsLogger.logCustomEvent(
getContext(),
@@ -256,6 +264,20 @@ public class PartnerCustomizationLayout extends TemplateLayout {
}
}
+ /**
+ * PartnerCustomizationLayout is a template layout for different type of GlifLayout.
+ * This method allows each type of layout to report its "GlifLayoutType".
+ */
+ public void setLayoutTypeMetrics(PersistableBundle bundle) {
+ this.layoutTypeBundle = bundle;
+ }
+
+ /** Returns a {@link PersistableBundle} contains key "GlifLayoutType". */
+ @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
+ public PersistableBundle getLayoutTypeMetrics() {
+ return this.layoutTypeBundle;
+ }
+
public static Activity lookupActivityFromContext(Context context) {
if (context instanceof Activity) {
return (Activity) context;
diff --git a/main/java/com/google/android/setupcompat/internal/Preconditions.java b/main/java/com/google/android/setupcompat/internal/Preconditions.java
index 259377c..1346c24 100644
--- a/main/java/com/google/android/setupcompat/internal/Preconditions.java
+++ b/main/java/com/google/android/setupcompat/internal/Preconditions.java
@@ -61,4 +61,14 @@ public final class Preconditions {
}
throw new IllegalStateException(whichMethod + " must be called from the UI thread.");
}
+ /**
+ * Ensure that this method is not called from the main thread, otherwise an exception will be
+ * thrown.
+ */
+ public static void ensureNotOnMainThread(String whichMethod) {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ return;
+ }
+ throw new IllegalThreadStateException(whichMethod + " cannot be called from the UI thread.");
+ }
}
diff --git a/main/java/com/google/android/setupcompat/logging/ScreenKey.java b/main/java/com/google/android/setupcompat/logging/ScreenKey.java
new file mode 100644
index 0000000..4fba32b
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/ScreenKey.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2022 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 static com.google.android.setupcompat.internal.Validations.assertLengthInRange;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.setupcompat.internal.Preconditions;
+import com.google.android.setupcompat.util.ObjectUtils;
+import java.util.regex.Pattern;
+
+/**
+ * A screen key represents a validated “string key” that is associated with the values reported by
+ * the API consumer.
+ */
+public class ScreenKey implements Parcelable {
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SCREEN_KEY_BUNDLE_NAME_KEY = "ScreenKey_name";
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SCREEN_KEY_BUNDLE_PACKAGE_KEY = "ScreenKey_package";
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SCREEN_KEY_BUNDLE_VERSION_KEY = "ScreenKey_version";
+ private static final int INVALID_VERSION = -1;
+ private static final int VERSION = 1;
+
+ /**
+ * Creates a new instance of {@link ScreenKey}.
+ *
+ * @param name screen name to identify what the metric belongs to. It should be in the range of
+ * 5-50 characters, only alphanumeric characters are allowed.
+ * @param context context associated to metric screen, uses to generate package name.
+ */
+ public static ScreenKey of(@NonNull String name, @NonNull Context context) {
+ Preconditions.checkNotNull(context, "Context can not be null.");
+ return ScreenKey.of(name, context.getPackageName());
+ }
+
+ private static ScreenKey of(@NonNull String name, @NonNull String packageName) {
+ Preconditions.checkArgument(
+ SCREEN_PACKAGENAME_PATTERN.matcher(packageName).matches(),
+ "Invalid ScreenKey#package, only alpha numeric characters are allowed.");
+ assertLengthInRange(
+ name, "ScreenKey.name", MIN_SCREEN_NAME_LENGTH, MAX_SCREEN_NAME_LENGTH);
+ Preconditions.checkArgument(
+ SCREEN_NAME_PATTERN.matcher(name).matches(),
+ "Invalid ScreenKey#name, only alpha numeric characters are allowed.");
+
+ return new ScreenKey(name, packageName);
+ }
+
+ /**
+ * Converts {@link ScreenKey} into {@link Bundle}.
+ * Throw {@link NullPointerException} if the screenKey is null.
+ */
+ public static Bundle toBundle(ScreenKey screenKey) {
+ Preconditions.checkNotNull(screenKey, "ScreenKey cannot be null.");
+ Bundle bundle = new Bundle();
+ bundle.putInt(SCREEN_KEY_BUNDLE_VERSION_KEY, VERSION);
+ bundle.putString(SCREEN_KEY_BUNDLE_NAME_KEY, screenKey.getName());
+ bundle.putString(SCREEN_KEY_BUNDLE_PACKAGE_KEY, screenKey.getPackageName());
+ return bundle;
+ }
+
+ /**
+ * Converts {@link Bundle} into {@link ScreenKey}.
+ * Throw {@link NullPointerException} if the bundle is null.
+ * Throw {@link IllegalArgumentException} if the bundle version is unsupported.
+ */
+ public static ScreenKey fromBundle(Bundle bundle) {
+ Preconditions.checkNotNull(bundle, "Bundle cannot be null");
+
+ int version = bundle.getInt(SCREEN_KEY_BUNDLE_VERSION_KEY, INVALID_VERSION);
+ if (version == 1) {
+ return ScreenKey.of(
+ bundle.getString(SCREEN_KEY_BUNDLE_NAME_KEY),
+ bundle.getString(SCREEN_KEY_BUNDLE_PACKAGE_KEY));
+ } else {
+ // Invalid version
+ throw new IllegalArgumentException("Unsupported version: " + version);
+ }
+ }
+
+ public static final Creator<ScreenKey> CREATOR =
+ new Creator<>() {
+ @Override
+ public ScreenKey createFromParcel(Parcel in) {
+ return new ScreenKey(in.readString(), in.readString());
+ }
+
+ @Override
+ public ScreenKey[] newArray(int size) {
+ return new ScreenKey[size];
+ }
+ };
+
+ /** Returns the name of the screen key. */
+ public String getName() {
+ return name;
+ }
+
+ /** Returns the package name of the screen key. */
+ public String getPackageName() {
+ return packageName;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int i) {
+ parcel.writeString(name);
+ parcel.writeString(packageName);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof ScreenKey)) {
+ return false;
+ }
+ ScreenKey screenKey = (ScreenKey) o;
+ return ObjectUtils.equals(name, screenKey.name)
+ && ObjectUtils.equals(packageName, screenKey.packageName);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.hashCode(name, packageName);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "ScreenKey {name="
+ + getName()
+ + ", package="
+ + getPackageName()
+ + "}";
+ }
+
+ private ScreenKey(String name, String packageName) {
+ this.name = name;
+ this.packageName = packageName;
+ }
+
+ private final String name;
+ private final String packageName;
+
+ private static final int MIN_SCREEN_NAME_LENGTH = 5;
+ private static final int MAX_SCREEN_NAME_LENGTH = 50;
+ private static final Pattern SCREEN_NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+");
+ private static final Pattern SCREEN_PACKAGENAME_PATTERN =
+ Pattern.compile("^([a-z]+[.])+[a-zA-Z][a-zA-Z0-9]+");
+}
diff --git a/main/java/com/google/android/setupcompat/logging/SetupMetric.java b/main/java/com/google/android/setupcompat/logging/SetupMetric.java
new file mode 100644
index 0000000..4015a53
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/logging/SetupMetric.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2022 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.annotation.TargetApi;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.setupcompat.internal.ClockProvider;
+import com.google.android.setupcompat.internal.PersistableBundles;
+import com.google.android.setupcompat.internal.Preconditions;
+import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.EventType;
+import com.google.android.setupcompat.util.ObjectUtils;
+
+/**
+ * This class represents a setup metric event at a particular point in time.
+ * The event is identified by {@link EventType} along with a string name. It can include
+ * additional key-value pairs providing more attributes associated with the given event. Only
+ * primitive values are supported for now (int, long, boolean, String).
+ */
+@TargetApi(VERSION_CODES.Q)
+public class SetupMetric implements Parcelable {
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SETUP_METRIC_BUNDLE_VERSION_KEY = "SetupMetric_version";
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SETUP_METRIC_BUNDLE_NAME_KEY = "SetupMetric_name";
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SETUP_METRIC_BUNDLE_TYPE_KEY = "SetupMetric_type";
+ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+ public static final String SETUP_METRIC_BUNDLE_VALUES_KEY = "SetupMetric_values";
+ private static final int VERSION = 1;
+ private static final int INVALID_VERSION = -1;
+
+ public static final String SETUP_METRIC_BUNDLE_OPTIN_KEY = "opt_in";
+ public static final String SETUP_METRIC_BUNDLE_ERROR_KEY = "error";
+ public static final String SETUP_METRIC_BUNDLE_TIMESTAMP_KEY = "timestamp";
+
+
+ /**
+ * A convenient function to create a setup event with event type {@link EventType#IMPRESSION}
+ * @param name A name represents this impression
+ * @return A {@link SetupMetric}
+ * @throws IllegalArgumentException if the {@code name} is empty.
+ */
+ @NonNull
+ public static SetupMetric ofImpression(@NonNull String name) {
+ Bundle bundle = new Bundle();
+ bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
+ return new SetupMetric(VERSION, name, EventType.IMPRESSION,
+ PersistableBundles.fromBundle(bundle));
+ }
+
+ /**
+ * A convenient function to create a setup event with event type {@link EventType#OPT_IN}
+ * @param name A name represents this opt-in
+ * @param status Opt-in status in {@code true} or {@code false}
+ * @return A {@link SetupMetric}
+ * @throws IllegalArgumentException if the {@code name} is empty.
+ */
+ @NonNull
+ public static SetupMetric ofOptIn(@NonNull String name, boolean status) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(SETUP_METRIC_BUNDLE_OPTIN_KEY, status);
+ bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
+ return new SetupMetric(VERSION, name, EventType.OPT_IN, PersistableBundles.fromBundle(bundle));
+ }
+
+ /**
+ * A convenient function to create a setup event with event type
+ * {@link EventType#WAITING_START}
+ * @param name A task name causes this waiting duration
+ * @return A {@link SetupMetric}
+ * @throws IllegalArgumentException if the {@code name} is empty.
+ */
+ @NonNull
+ public static SetupMetric ofWaitingStart(@NonNull String name) {
+ Bundle bundle = new Bundle();
+ bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
+ return new SetupMetric(VERSION, name, EventType.WAITING_START,
+ PersistableBundles.fromBundle(bundle));
+ }
+
+ /**
+ * A convenient function to create a setup event with event type
+ * {@link EventType#WAITING_END}
+ * @param name A task name causes this waiting duration
+ * @return A {@link SetupMetric}
+ * @throws IllegalArgumentException if the {@code name} is empty.
+ */
+ @NonNull
+ public static SetupMetric ofWaitingEnd(@NonNull String name) {
+ Bundle bundle = new Bundle();
+ bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
+ return new SetupMetric(VERSION, name, EventType.WAITING_END,
+ PersistableBundles.fromBundle(bundle));
+ }
+
+ /**
+ * A convenient function to create a setup event with event type {@link EventType#ERROR}
+ * @param name A name represents this error
+ * @param errorCode A error code
+ * @return A {@link SetupMetric}
+ * @throws IllegalArgumentException if the {@code name} is empty.
+ */
+ @NonNull
+ public static SetupMetric ofError(@NonNull String name, int errorCode) {
+ Bundle bundle = new Bundle();
+ bundle.putInt(SETUP_METRIC_BUNDLE_ERROR_KEY, errorCode);
+ bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
+ return new SetupMetric(VERSION, name, EventType.ERROR, PersistableBundles.fromBundle(bundle));
+ }
+
+ /** Converts {@link SetupMetric} into {@link Bundle}. */
+ @NonNull
+ public static Bundle toBundle(@NonNull SetupMetric setupMetric) {
+ Preconditions.checkNotNull(setupMetric, "SetupMetric cannot be null.");
+ Bundle bundle = new Bundle();
+ bundle.putInt(SETUP_METRIC_BUNDLE_VERSION_KEY, VERSION);
+ bundle.putString(SETUP_METRIC_BUNDLE_NAME_KEY, setupMetric.name);
+ bundle.putInt(SETUP_METRIC_BUNDLE_TYPE_KEY, setupMetric.type);
+ bundle.putBundle(
+ SETUP_METRIC_BUNDLE_VALUES_KEY, PersistableBundles.toBundle(setupMetric.values));
+ return bundle;
+ }
+
+ /**
+ * Converts {@link Bundle} into {@link SetupMetric}.
+ * Throw {@link IllegalArgumentException} if the bundle version is unsupported.
+ */
+ @NonNull
+ public static SetupMetric fromBundle(@NonNull Bundle bundle) {
+ Preconditions.checkNotNull(bundle, "Bundle cannot be null");
+ int version = bundle.getInt(SETUP_METRIC_BUNDLE_VERSION_KEY, INVALID_VERSION);
+ if (version == 1) {
+ return new SetupMetric(
+ bundle.getInt(SETUP_METRIC_BUNDLE_VERSION_KEY),
+ bundle.getString(SETUP_METRIC_BUNDLE_NAME_KEY),
+ bundle.getInt(SETUP_METRIC_BUNDLE_TYPE_KEY),
+ PersistableBundles.fromBundle(bundle.getBundle(SETUP_METRIC_BUNDLE_VALUES_KEY)));
+ } else {
+ throw new IllegalArgumentException("Unsupported version: " + version);
+ }
+ }
+
+ private SetupMetric(
+ int version, String name, @EventType int type, @NonNull PersistableBundle values) {
+ Preconditions.checkArgument(
+ name != null && name.length() != 0,
+ "name cannot be null or empty.");
+ this.version = version;
+ this.name = name;
+ this.type = type;
+ this.values = values;
+ }
+
+ private final int version;
+ private final String name;
+ @EventType private final int type;
+ private final PersistableBundle values;
+
+ public int getVersion() {
+ return version;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @EventType
+ public int getType() {
+ return type;
+ }
+
+ public PersistableBundle getValues() {
+ return values;
+ }
+
+ public static final Creator<SetupMetric> CREATOR =
+ new Creator<>() {
+ @Override
+ public SetupMetric createFromParcel(@NonNull Parcel in) {
+ return new SetupMetric(in.readInt(),
+ in.readString(),
+ in.readInt(),
+ in.readPersistableBundle(SetupMetric.class.getClassLoader()));
+ }
+
+ @Override
+ public SetupMetric[] newArray(int size) {
+ return new SetupMetric[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(name);
+ parcel.writeInt(type);
+ parcel.writePersistableBundle(values);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SetupMetric)) {
+ return false;
+ }
+ SetupMetric that = (SetupMetric) o;
+ return ObjectUtils.equals(name, that.name)
+ && ObjectUtils.equals(type, that.type)
+ && PersistableBundles.equals(values, that.values);
+ }
+
+ @Override
+ public int hashCode() {
+ return ObjectUtils.hashCode(name, type, values);
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return "SetupMetric {name="
+ + getName()
+ + ", type="
+ + getType()
+ + ", bundle="
+ + getValues().toString()
+ + "}";
+ }
+}
diff --git a/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java b/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java
index 8d696e0..fab38a2 100644
--- a/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java
+++ b/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java
@@ -16,17 +16,25 @@
package com.google.android.setupcompat.logging;
+import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.internal.SetupCompatServiceInvoker;
import com.google.android.setupcompat.logging.internal.MetricBundleConverter;
import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType;
+import com.google.android.setupcompat.util.Logger;
import java.util.concurrent.TimeUnit;
-/** SetupMetricsLogger provides an easy way to log custom metrics to SetupWizard. */
+/**
+ * SetupMetricsLogger provides an easy way to log custom metrics to SetupWizard.
+ * (go/suw-metrics-collection-api)
+ */
public class SetupMetricsLogger {
+ private static final Logger LOG = new Logger("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.");
@@ -71,4 +79,64 @@ public class SetupMetricsLogger {
MetricType.DURATION_EVENT,
MetricBundleConverter.createBundleForLoggingTimer(timerName, timeInMillis));
}
+
+ /**
+ * Logs setup collection metrics
+ */
+ public static void logMetrics(
+ @NonNull Context context, @NonNull ScreenKey screenKey, @NonNull SetupMetric... metrics) {
+ Preconditions.checkNotNull(context, "Context cannot be null.");
+ Preconditions.checkNotNull(screenKey, "ScreenKey cannot be null.");
+ Preconditions.checkNotNull(metrics, "SetupMetric cannot be null.");
+
+ for (SetupMetric metric : metrics) {
+ LOG.atDebug("Log metric: " + screenKey + ", " + metric);
+
+ SetupCompatServiceInvoker.get(context).logMetricEvent(
+ MetricType.SETUP_COLLECTION_EVENT,
+ MetricBundleConverter.createBundleForLoggingSetupMetric(screenKey, metric));
+ }
+ }
+
+ /**
+ * A non-static method to log setup collection metrics calling
+ * {@link #logMetrics(Context, ScreenKey, SetupMetric...)} as the actual implementation. This
+ * function is useful when performing unit tests in caller's implementation.
+ * <p>
+ * For unit testing, caller uses {@link #setInstanceForTesting(SetupMetricsLogger)} to inject the
+ * mocked SetupMetricsLogger instance and use {@link SetupMetricsLogger#get(Context)} to get the
+ * SetupMetricsLogger. And verify the this function is called with expected parameters.
+ *
+ * @see #logMetrics(Context, ScreenKey, SetupMetric...)
+ */
+ public void logMetrics(@NonNull ScreenKey screenKey, @NonNull SetupMetric... metrics) {
+ SetupMetricsLogger.logMetrics(context, screenKey, metrics);
+ }
+
+ private SetupMetricsLogger(Context context) {
+ this.context = context;
+ }
+
+ private final Context context;
+
+ /** Use this function to get a singleton of {@link SetupMetricsLogger} */
+ public static synchronized SetupMetricsLogger get(Context context) {
+ if (instance == null) {
+ instance = new SetupMetricsLogger(context.getApplicationContext());
+ }
+
+ return instance;
+ }
+
+ @VisibleForTesting
+ public static void setInstanceForTesting(SetupMetricsLogger testInstance) {
+ instance = testInstance;
+ }
+
+ // The instance is coming from Application context which alive during the application activate and
+ // it's not depend on the activities life cycle, so we can avoid memory leak. However linter
+ // cannot distinguish Application context or activity context, so we add @SuppressLint to avoid
+ // lint error.
+ @SuppressLint("StaticFieldLeak")
+ private static SetupMetricsLogger instance;
}
diff --git a/main/java/com/google/android/setupcompat/logging/internal/MetricBundleConverter.java b/main/java/com/google/android/setupcompat/logging/internal/MetricBundleConverter.java
index e1a3909..8e5ba20 100644
--- a/main/java/com/google/android/setupcompat/logging/internal/MetricBundleConverter.java
+++ b/main/java/com/google/android/setupcompat/logging/internal/MetricBundleConverter.java
@@ -3,6 +3,8 @@ package com.google.android.setupcompat.logging.internal;
import android.os.Bundle;
import com.google.android.setupcompat.logging.CustomEvent;
import com.google.android.setupcompat.logging.MetricKey;
+import com.google.android.setupcompat.logging.ScreenKey;
+import com.google.android.setupcompat.logging.SetupMetric;
import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricBundleKeys;
/** Collection of helper methods for reading and writing {@link CustomEvent}, {@link MetricKey}. */
@@ -28,6 +30,13 @@ public final class MetricBundleConverter {
return bundle;
}
+ public static Bundle createBundleForLoggingSetupMetric(ScreenKey screenKey, SetupMetric metric) {
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(MetricBundleKeys.SCREEN_KEY_BUNDLE, ScreenKey.toBundle(screenKey));
+ bundle.putParcelable(MetricBundleKeys.SETUP_METRIC_BUNDLE, SetupMetric.toBundle(metric));
+ return bundle;
+ }
+
private MetricBundleConverter() {
throw new AssertionError("Cannot instantiate MetricBundleConverter");
}
diff --git a/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java b/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java
index 57a7272..d4995b7 100644
--- a/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java
+++ b/main/java/com/google/android/setupcompat/logging/internal/SetupMetricsLoggingConstants.java
@@ -20,6 +20,8 @@ import android.content.Context;
import androidx.annotation.IntDef;
import androidx.annotation.StringDef;
import com.google.android.setupcompat.logging.MetricKey;
+import com.google.android.setupcompat.logging.ScreenKey;
+import com.google.android.setupcompat.logging.SetupMetric;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -28,7 +30,12 @@ public interface SetupMetricsLoggingConstants {
/** Enumeration of supported metric types logged to SetupWizard. */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({MetricType.CUSTOM_EVENT, MetricType.COUNTER_EVENT, MetricType.DURATION_EVENT})
+ @IntDef({
+ MetricType.CUSTOM_EVENT,
+ MetricType.DURATION_EVENT,
+ MetricType.COUNTER_EVENT,
+ MetricType.SETUP_COLLECTION_EVENT,
+ MetricType.INTERNAL})
@interface MetricType {
/**
* MetricType constant used when logging {@link
@@ -47,10 +54,39 @@ public interface SetupMetricsLoggingConstants {
*/
int COUNTER_EVENT = 3;
+ /**
+ * MetricType constant used when logging setup metric using {@link
+ * com.google.android.setupcompat.logging.SetupMetricsLogger#logMetrics(Context, ScreenKey,
+ * SetupMetric...)}.
+ */
+ int SETUP_COLLECTION_EVENT = 4;
+
/** MetricType constant used for internal logging purposes. */
int INTERNAL = 100;
}
+ /**
+ * Enumeration of supported EventType of {@link MetricType#SETUP_COLLECTION_EVENT} logged to
+ * SetupWizard. (go/suw-metrics-collection-api)
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ EventType.UNKNOWN,
+ EventType.IMPRESSION,
+ EventType.OPT_IN,
+ EventType.WAITING_START,
+ EventType.WAITING_END,
+ EventType.ERROR,
+ })
+ @interface EventType {
+ int UNKNOWN = 1;
+ int IMPRESSION = 2;
+ int OPT_IN = 3;
+ int WAITING_START = 4;
+ int WAITING_END = 5;
+ int ERROR = 6;
+ }
+
/** Keys of the bundle used while logging data to SetupWizard. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
@@ -59,7 +95,9 @@ public interface SetupMetricsLoggingConstants {
MetricBundleKeys.CUSTOM_EVENT,
MetricBundleKeys.CUSTOM_EVENT_BUNDLE,
MetricBundleKeys.TIME_MILLIS_LONG,
- MetricBundleKeys.COUNTER_INT
+ MetricBundleKeys.COUNTER_INT,
+ MetricBundleKeys.SCREEN_KEY_BUNDLE,
+ MetricBundleKeys.SETUP_METRIC_BUNDLE,
})
@interface MetricBundleKeys {
/**
@@ -104,5 +142,17 @@ public interface SetupMetricsLoggingConstants {
* com.google.android.setupcompat.logging.CustomEvent}.
*/
String CUSTOM_EVENT_BUNDLE = "CustomEvent_bundle";
+
+ /**
+ * This key will be used when {@code metricType} is {@link MetricType#SETUP_COLLECTION_EVENT}
+ * with the value being a Bundle which can be used to read {@link ScreenKey}
+ */
+ String SCREEN_KEY_BUNDLE = "ScreenKey_bundle";
+
+ /**
+ * This key will be used when {@code metricType} is {@link MetricType#SETUP_COLLECTION_EVENT}
+ * with the value being a Bundle which can be used to read {@link SetupMetric}
+ */
+ String SETUP_METRIC_BUNDLE = "SetupMetric_bundle";
}
}
diff --git a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
index b77eacf..2268b1e 100644
--- a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
+++ b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
@@ -73,6 +73,7 @@ public class FooterBarMixin implements Mixin {
@VisibleForTesting final boolean applyPartnerResources;
@VisibleForTesting final boolean applyDynamicColor;
@VisibleForTesting final boolean useFullDynamicColor;
+ @VisibleForTesting final boolean footerButtonAlignEnd;
@VisibleForTesting public LinearLayout buttonContainer;
private FooterButton primaryButton;
@@ -206,6 +207,8 @@ public class FooterBarMixin implements Mixin {
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterBackground, 0);
footerBarSecondaryBackgroundColor =
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterBackground, 0);
+ footerButtonAlignEnd =
+ a.getBoolean(R.styleable.SucFooterBarMixin_sucFooterBarButtonAlignEnd, false);
int primaryBtn =
a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterButton, 0);
@@ -234,7 +237,7 @@ public class FooterBarMixin implements Mixin {
return PartnerConfigHelper.get(context)
.getBoolean(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ALIGNED_END, false);
} else {
- return false;
+ return footerButtonAlignEnd;
}
}
@@ -617,7 +620,7 @@ public class FooterBarMixin implements Mixin {
return overrideTheme;
}
- @VisibleForTesting
+ /** Returns the {@link LinearLayout} of button container. */
public LinearLayout getButtonContainer() {
return buttonContainer;
}
diff --git a/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java
index 090e1df..cccc413 100644
--- a/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java
+++ b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java
@@ -74,7 +74,7 @@ public final class BuildCompatUtils {
* <li>For current Android release: while new API is not finalized yet (CODENAME =
* "UpsideDownCake", SDK_INT = 33)
* <li>For current Android release: when new API is finalized (CODENAME = "REL", SDK_INT = 34)
- * <li>For next Android release (CODENAME = "V", SDK_INT = 35+)
+ * <li>For next Android release (CODENAME = "VanillaIceCream", SDK_INT = 35+)
* </ul>
*
* <p>Note that Build.VERSION_CODES.T cannot be used here until final SDK is available in all
@@ -83,12 +83,19 @@ public final class BuildCompatUtils {
* @return Whether the current OS version is higher or equal to U.
*/
public static boolean isAtLeastU() {
- System.out.println("Build.VERSION.CODENAME=" + Build.VERSION.CODENAME);
return (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 34)
- || (Build.VERSION.CODENAME.length() == 1
- && Build.VERSION.CODENAME.charAt(0) >= 'U'
- && Build.VERSION.CODENAME.charAt(0) <= 'Z')
- || (Build.VERSION.CODENAME.equals("UpsideDownCake") && Build.VERSION.SDK_INT >= 33);
+ || isAtLeastPreReleaseCodename("UpsideDownCake");
+ }
+
+ private static boolean isAtLeastPreReleaseCodename(String codename) {
+ // Special case "REL", which means the build is not a pre-release build.
+ if (Build.VERSION.CODENAME.equals("REL")) {
+ return false;
+ }
+
+ // Otherwise lexically compare them. Return true if the build codename is equal to or
+ // greater than the requested codename.
+ return Build.VERSION.CODENAME.compareTo(codename) >= 0;
}
private BuildCompatUtils() {}