diff options
author | David Liu <dswliu@google.com> | 2023-02-15 06:55:44 +0000 |
---|---|---|
committer | David Liu <dswliu@google.com> | 2023-02-16 01:02:50 +0000 |
commit | 534db6a965794ea080ed2203baeccccc912e9546 (patch) | |
tree | 6178f23c95dc885d4a87c905c413c80cd9399d52 | |
parent | 86b046d8e70d158e7a221ae222ba69ac8fc40b4f (diff) | |
download | setupcompat-534db6a965794ea080ed2203baeccccc912e9546.tar.gz |
Revert "Revert "Import updated Android SetupCompat Library 507643535""
This reverts commit 86b046d8e70d158e7a221ae222ba69ac8fc40b4f.
Reason for revert: Only submit setupcompat changes. Does not add new dependency.
BUG: 268572395
Change-Id: I761f5eabeb086fac07a8d08e588cbf7614f4f764
12 files changed, 720 insertions, 13 deletions
diff --git a/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java b/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java index c441b2d..eb10693 100644 --- a/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java +++ b/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java @@ -108,8 +108,15 @@ public abstract class AbstractSetupBtsService extends Service { + "3e51e5dd7b66787bef12fe97fba484c423fb4ff8cc494c02f0f5051612ff6529393e8e46eac5bb21f27" + "7c151aa5f2aa627d1e89da70ab6033569de3b9897bfff7ca9da3e1243f60b"; + @VisibleForTesting boolean allowDebugKeys = false; + @VisibleForTesting IBtsTaskServiceCallback callback; + /** Allow debug signature calling app when developing stage. */ + protected void setAllowDebugKeys(boolean allowed) { + allowDebugKeys = allowed; + } + @Nullable @Override public IBinder onBind(Intent intent) { @@ -156,7 +163,10 @@ public abstract class AbstractSetupBtsService extends Service { LOG.atDebug("onTaskFinished callback " + ((callback == null) ? "is null." : "is not null.")); if (callback != null) { try { - callback.onTaskFinished(Bundle.EMPTY); + Bundle metricBundle = new Bundle(); + metricBundle.putBoolean(Constants.EXTRA_KEY_TASK_SUCCEED, succeed); + metricBundle.putString(Constants.EXTRA_KEY_TASK_FAILED_REASON, failedReason); + callback.onTaskFinished(metricBundle); } catch (RemoteException e) { LOG.e( "[" + this.getClass().getSimpleName() + "] Fail to invoke remove method onJobFinished"); @@ -225,7 +235,8 @@ public abstract class AbstractSetupBtsService extends Service { @VisibleForTesting boolean verifyCallingPackageName() { String packageName = getPackageManager().getNameForUid(Binder.getCallingUid()); - if (SETUP_WIZARD_PACKAGE_NAME.equals(packageName) || BTS_STARTER_FOR_TEST.equals(packageName)) { + if (SETUP_WIZARD_PACKAGE_NAME.equals(packageName) + || (allowDebugKeys && BTS_STARTER_FOR_TEST.equals(packageName))) { LOG.atDebug("Package name match to SetupWizard"); return true; } else { @@ -242,9 +253,11 @@ public abstract class AbstractSetupBtsService extends Service { PackageInfo info = getPackageManager() .getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES); + for (Signature signature : info.signingInfo.getApkContentsSigners()) { if (SETUP_WIZARD_RELEASE_CERTIFICATE_STRING.equals(signature.toCharsString()) - || SETUP_WIZARD_DEBUG_CERTIFICATE_STRING.equals(signature.toCharsString())) { + || (allowDebugKeys + && SETUP_WIZARD_DEBUG_CERTIFICATE_STRING.equals(signature.toCharsString()))) { return true; } } diff --git a/bts/java/com/google/android/setupcompat/bts/Constants.java b/bts/java/com/google/android/setupcompat/bts/Constants.java new file mode 100644 index 0000000..7adcc33 --- /dev/null +++ b/bts/java/com/google/android/setupcompat/bts/Constants.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2023 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.bts; + +/** Constant values used by {@link com.google.android.setupcompat.bts.AbstractSetupBtsService}. */ +public class Constants { + + /** + * The extra key for {@link AbstractSetupBtsService} to send the task result to SUW for metric + * collection. + */ + public static final String EXTRA_KEY_TASK_SUCCEED = "succeed"; + + /** + * The extra key for {@link com.google.android.setupcompat.bts.AbstractSetupBtsService} to send + * the failed reason to SUW for metric collection. + */ + public static final String EXTRA_KEY_TASK_FAILED_REASON = "failed_reason"; + + private Constants() {} +} 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/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..786494e 100644 --- a/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java +++ b/main/java/com/google/android/setupcompat/logging/SetupMetricsLogger.java @@ -22,11 +22,14 @@ 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. */ 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 +74,22 @@ public class SetupMetricsLogger { MetricType.DURATION_EVENT, MetricBundleConverter.createBundleForLoggingTimer(timerName, timeInMillis)); } + + /** + * Logs setup collection metrics (go/suw-metrics-collection-api) + */ + 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)); + } + } } 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..d1b0ccb 100644 --- a/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java +++ b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java @@ -83,7 +83,6 @@ 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' diff --git a/main/res/values/attrs.xml b/main/res/values/attrs.xml index 07f87ed..0aaea8b 100644 --- a/main/res/values/attrs.xml +++ b/main/res/values/attrs.xml @@ -82,6 +82,7 @@ <!-- Button of footer attributes --> <declare-styleable name="SucFooterBarMixin"> <attr name="sucFooterBarButtonAllCaps" format="boolean" /> + <attr name="sucFooterBarButtonAlignEnd" format="boolean" /> <attr name="sucFooterBarButtonCornerRadius" format="dimension" /> <attr name="sucFooterBarButtonFontFamily" format="string|reference" /> <attr name="sucFooterBarPaddingTop" format="dimension" /> diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java index 1b73098..96a4317 100644 --- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java +++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java @@ -69,11 +69,18 @@ public class PartnerConfigHelper { public static final String IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD = "isNeutralButtonStyleEnabled"; @VisibleForTesting + public static final String IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD = + "isEmbeddedActivityOnePaneEnabled"; + + @VisibleForTesting public static final String GET_SUW_DEFAULT_THEME_STRING_METHOD = "suwDefaultThemeString"; @VisibleForTesting public static final String SUW_PACKAGE_NAME = "com.google.android.setupwizard"; @VisibleForTesting public static final String MATERIAL_YOU_RESOURCE_SUFFIX = "_material_you"; + @VisibleForTesting + public static final String EMBEDDED_ACTIVITY_RESOURCE_SUFFIX = "_embedded_activity"; + @VisibleForTesting static Bundle suwDayNightEnabledBundle = null; @VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null; @@ -84,6 +91,8 @@ public class PartnerConfigHelper { @VisibleForTesting public static Bundle applyNeutralButtonStyleBundle = null; + @VisibleForTesting public static Bundle applyEmbeddedActivityOnePaneBundle = null; + @VisibleForTesting public static Bundle suwDefaultThemeBundle = null; private static PartnerConfigHelper instance = null; @@ -97,8 +106,16 @@ public class PartnerConfigHelper { private static int savedConfigUiMode; + private static boolean savedConfigEmbeddedActivityMode; + + @VisibleForTesting static Bundle applyTransitionBundle = null; + @VisibleForTesting public static int savedOrientation = Configuration.ORIENTATION_PORTRAIT; + /** The method name to get if transition settings is set from client. */ + public static final String APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD = + "applyGlifThemeControlledTransition"; + /** * When testing related to fake PartnerConfigHelper instance, should sync the following saved * config with testing environment. @@ -117,6 +134,8 @@ public class PartnerConfigHelper { private static boolean isValidInstance(@NonNull Context context) { Configuration currentConfig = context.getResources().getConfiguration(); if (instance == null) { + savedConfigEmbeddedActivityMode = + isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU(); savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK; savedOrientation = currentConfig.orientation; savedScreenWidth = currentConfig.screenWidthDp; @@ -126,7 +145,10 @@ public class PartnerConfigHelper { boolean uiModeChanged = isSetupWizardDayNightEnabled(context) && (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode; + boolean embeddedActivityModeChanged = + isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU(); if (uiModeChanged + || embeddedActivityModeChanged != savedConfigEmbeddedActivityMode || currentConfig.orientation != savedOrientation || currentConfig.screenWidthDp != savedScreenWidth || currentConfig.screenHeightDp != savedScreenHeight) { @@ -554,6 +576,11 @@ public class PartnerConfigHelper { ResourceEntry adjustResourceEntry = adjustResourceEntryDefaultValue( context, ResourceEntry.fromBundle(context, resourceEntryBundle)); + ; + if (BuildCompatUtils.isAtLeastU() && isEmbeddedActivityOnePaneEnabled(context)) { + adjustResourceEntry = embeddedActivityResourceEntryDefaultValue(context, adjustResourceEntry); + } + return adjustResourceEntryDayNightMode(context, adjustResourceEntry); } @@ -617,6 +644,42 @@ public class PartnerConfigHelper { return inputResourceEntry; } + // Check the embedded acitvity flag and replace the inputResourceEntry.resourceName & + // inputResourceEntry.resourceId after U. + ResourceEntry embeddedActivityResourceEntryDefaultValue( + Context context, ResourceEntry inputResourceEntry) { + // If not overlay resource + try { + if (SUW_PACKAGE_NAME.equals(inputResourceEntry.getPackageName())) { + String resourceTypeName = + inputResourceEntry + .getResources() + .getResourceTypeName(inputResourceEntry.getResourceId()); + // try to update resourceName & resourceId + String embeddedActivityResourceName = + inputResourceEntry.getResourceName().concat(EMBEDDED_ACTIVITY_RESOURCE_SUFFIX); + int embeddedActivityResourceId = + inputResourceEntry + .getResources() + .getIdentifier( + embeddedActivityResourceName, + resourceTypeName, + inputResourceEntry.getPackageName()); + if (embeddedActivityResourceId != 0) { + Log.i(TAG, "use embedded activity resource:" + embeddedActivityResourceName); + return new ResourceEntry( + inputResourceEntry.getPackageName(), + embeddedActivityResourceName, + embeddedActivityResourceId, + inputResourceEntry.getResources()); + } + } + } catch (NotFoundException ex) { + // fall through + } + return inputResourceEntry; + } + @VisibleForTesting public static synchronized void resetInstance() { instance = null; @@ -625,7 +688,9 @@ public class PartnerConfigHelper { applyMaterialYouConfigBundle = null; applyDynamicColorBundle = null; applyNeutralButtonStyleBundle = null; + applyEmbeddedActivityOnePaneBundle = null; suwDefaultThemeBundle = null; + applyTransitionBundle = null; } /** @@ -766,6 +831,32 @@ public class PartnerConfigHelper { && applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false)); } + /** Returns true if the SetupWizard supports the one-pane embedded activity during setup flow. */ + public static boolean isEmbeddedActivityOnePaneEnabled(@NonNull Context context) { + if (applyEmbeddedActivityOnePaneBundle == null) { + try { + applyEmbeddedActivityOnePaneBundle = + context + .getContentResolver() + .call( + getContentUri(), + IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD, + /* arg= */ null, + /* extras= */ null); + } catch (IllegalArgumentException | SecurityException exception) { + Log.w( + TAG, + "SetupWizard one-pane support in embedded activity status unknown; return as false."); + applyEmbeddedActivityOnePaneBundle = null; + return false; + } + } + + return (applyEmbeddedActivityOnePaneBundle != null + && applyEmbeddedActivityOnePaneBundle.getBoolean( + IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD, false)); + } + /** Returns true if the SetupWizard supports the neutral button style during setup flow. */ public static boolean isNeutralButtonStyleEnabled(@NonNull Context context) { if (applyNeutralButtonStyleBundle == null) { @@ -789,6 +880,37 @@ public class PartnerConfigHelper { && applyNeutralButtonStyleBundle.getBoolean(IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD, false)); } + /** + * Returns the system property to indicate the transition settings is set by Glif theme rather + * than the client. + */ + public static boolean isGlifThemeControlledTransitionApplied(@NonNull Context context) { + if (applyTransitionBundle == null + || applyTransitionBundle.isEmpty()) { + try { + applyTransitionBundle = + context + .getContentResolver() + .call( + getContentUri(), + APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD, + /* arg= */ null, + /* extras= */ null); + } catch (IllegalArgumentException | SecurityException exception) { + Log.w( + TAG, + "applyGlifThemeControlledTransition unknown; return applyGlifThemeControlledTransition" + + " as default value"); + } + } + if (applyTransitionBundle != null + && !applyTransitionBundle.isEmpty()) { + return applyTransitionBundle.getBoolean( + APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD, true); + } + return true; + } + @VisibleForTesting static Uri getContentUri() { return new Uri.Builder() |