Merge Android 14

Bug: 298295554
Merged-In: Idfb8953bf8205b78627c9081a9ef6b52d462234b
Change-Id: I2b2945c11f7d07e595f944830feed0a6718640f6
diff --git a/Android.bp b/Android.bp
index d3a42a7..26a0270 100644
--- a/Android.bp
+++ b/Android.bp
@@ -84,4 +84,7 @@
     ],
     min_sdk_version: "14",
     sdk_version: "current",
+    optimize: {
+        proguard_flags_files: ["proguard.flags"],
+    }
 }
diff --git a/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskService.aidl b/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskService.aidl
new file mode 100644
index 0000000..1ab625f
--- /dev/null
+++ b/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskService.aidl
@@ -0,0 +1,38 @@
+/*
+ * 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.bts;
+
+import com.google.android.setupcompat.bts.IBtsTaskServiceCallback;
+
+/**
+ * Declare the interface for BTS task service.
+ *
+ * The SetupWizard will bind BtsTaskService when specific event triggerred. The service callback
+ * using {@link IBtsTaskServiceCallback#onTaskFinished} to notify SetupWizard the task is already
+ *  completed and SetupWizard will unbind the service.
+ *
+ * If the service can't be complete before end of SetupWizard, the SetupWizard still unbind the
+ * service since the background task is no longer helpful for SetupWizard.
+ */
+interface IBtsTaskService {
+
+  /**
+  * Set the callback for the client to notify the job already completed and can
+  * be disconnected.
+  */
+  oneway void setCallback(IBtsTaskServiceCallback callback) = 1;
+}
\ No newline at end of file
diff --git a/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskServiceCallback.aidl b/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskServiceCallback.aidl
new file mode 100644
index 0000000..c6ed3e5
--- /dev/null
+++ b/bts/aidl/com/google/android/setupcompat/bts/IBtsTaskServiceCallback.aidl
@@ -0,0 +1,33 @@
+/*
+ * 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.bts;
+
+import android.os.Bundle;
+
+/**
+ * Declare the callback interface for BTS task service to notice the SUW the
+ * status of task service.
+ */
+interface IBtsTaskServiceCallback {
+
+  /**
+   *  Called when the task is finished.
+   *
+   *  @param bundle The metrics bundle.
+   */
+  void onTaskFinished(in Bundle bundle) = 1;
+}
\ No newline at end of file
diff --git a/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java b/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java
new file mode 100644
index 0000000..80066ab
--- /dev/null
+++ b/bts/java/com/google/android/setupcompat/bts/AbstractSetupBtsService.java
@@ -0,0 +1,299 @@
+/*
+ * 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.bts;
+
+import android.app.Service;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.setupcompat.internal.Preconditions;
+import com.google.android.setupcompat.util.Logger;
+import java.util.concurrent.Executor;
+
+/** Class to handle service binding from SUW, and execute the client's task in the executor. */
+public abstract class AbstractSetupBtsService extends Service {
+  private static final Logger LOG = new Logger(AbstractSetupBtsService.class);
+
+  private static final String SETUP_WIZARD_PACKAGE_NAME = "com.google.android.setupwizard";
+
+  private static final String BTS_STARTER_FOR_TEST =
+      "com.google.android.apps.setupwizard.sample.bts.starter";
+
+  private static final String SETUP_BTS_PERMISSION = "com.google.android.setupwizard.SETUP_BTS";
+
+  @VisibleForTesting
+  static final String SETUP_WIZARD_RELEASE_CERTIFICATE_STRING =
+      "308204433082032ba003020102020900c2e08746644a308d300d06092a864886f70d01010405003074310b300"
+          + "9060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d"
+          + "4d6f756e7461696e205669657731143012060355040a130b476f6f676c6520496e632e3110300e06035"
+          + "5040b1307416e64726f69643110300e06035504031307416e64726f6964301e170d3038303832313233"
+          + "313333345a170d3336303130373233313333345a3074310b30090603550406130255533113301106035"
+          + "50408130a43616c69666f726e6961311630140603550407130d4d6f756e7461696e2056696577311430"
+          + "12060355040a130b476f6f676c6520496e632e3110300e060355040b1307416e64726f69643110300e0"
+          + "6035504031307416e64726f696430820120300d06092a864886f70d01010105000382010d0030820108"
+          + "0282010100ab562e00d83ba208ae0a966f124e29da11f2ab56d08f58e2cca91303e9b754d372f640a71"
+          + "b1dcb130967624e4656a7776a92193db2e5bfb724a91e77188b0e6a47a43b33d9609b77183145ccdf7b"
+          + "2e586674c9e1565b1f4c6a5955bff251a63dabf9c55c27222252e875e4f8154a645f897168c0b1bfc61"
+          + "2eabf785769bb34aa7984dc7e2ea2764cae8307d8c17154d7ee5f64a51a44a602c249054157dc02cd5f"
+          + "5c0e55fbef8519fbe327f0b1511692c5a06f19d18385f5c4dbc2d6b93f68cc2979c70e18ab93866b3bd"
+          + "5db8999552a0e3b4c99df58fb918bedc182ba35e003c1b4b10dd244a8ee24fffd333872ab5221985eda"
+          + "b0fc0d0b145b6aa192858e79020103a381d93081d6301d0603551d0e04160414c77d8cc2211756259a7"
+          + "fd382df6be398e4d786a53081a60603551d2304819e30819b8014c77d8cc2211756259a7fd382df6be3"
+          + "98e4d786a5a178a4763074310b3009060355040613025553311330110603550408130a43616c69666f7"
+          + "26e6961311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f"
+          + "676c6520496e632e3110300e060355040b1307416e64726f69643110300e06035504031307416e64726"
+          + "f6964820900c2e08746644a308d300c0603551d13040530030101ff300d06092a864886f70d01010405"
+          + "0003820101006dd252ceef85302c360aaace939bcff2cca904bb5d7a1661f8ae46b2994204d0ff4a68c"
+          + "7ed1a531ec4595a623ce60763b167297a7ae35712c407f208f0cb109429124d7b106219c084ca3eb3f9"
+          + "ad5fb871ef92269a8be28bf16d44c8d9a08e6cb2f005bb3fe2cb96447e868e731076ad45b33f6009ea1"
+          + "9c161e62641aa99271dfd5228c5c587875ddb7f452758d661f6cc0cccb7352e424cc4365c523532f732"
+          + "5137593c4ae341f4db41edda0d0b1071a7c440f0fe9ea01cb627ca674369d084bd2fd911ff06cdbf2cf"
+          + "a10dc0f893ae35762919048c7efc64c7144178342f70581c9de573af55b390dd7fdb9418631895d5f75"
+          + "9f30112687ff621410c069308a";
+
+  @VisibleForTesting
+  static final String SETUP_WIZARD_DEBUG_CERTIFICATE_STRING =
+      "308204a830820390a003020102020900d585b86c7dd34ef5300d06092a864886f70d0101040500308194310b3"
+          + "009060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130"
+          + "d4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e060355040b13"
+          + "07416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d0109011"
+          + "613616e64726f696440616e64726f69642e636f6d301e170d3038303431353233333635365a170d3335"
+          + "303930313233333635365a308194310b3009060355040613025553311330110603550408130a43616c6"
+          + "9666f726e6961311630140603550407130d4d6f756e7461696e20566965773110300e060355040a1307"
+          + "416e64726f69643110300e060355040b1307416e64726f69643110300e06035504031307416e64726f6"
+          + "9643122302006092a864886f70d0109011613616e64726f696440616e64726f69642e636f6d30820120"
+          + "300d06092a864886f70d01010105000382010d00308201080282010100d6ce2e080abfe2314dd18db3c"
+          + "fd3185cb43d33fa0c74e1bdb6d1db8913f62c5c39df56f846813d65bec0f3ca426b07c5a8ed5a3990c1"
+          + "67e76bc999b927894b8f0b22001994a92915e572c56d2a301ba36fc5fc113ad6cb9e7435a16d23ab7df"
+          + "aeee165e4df1f0a8dbda70a869d516c4e9d051196ca7c0c557f175bc375f948c56aae86089ba44f8aa6"
+          + "a4dd9a7dbf2c0a352282ad06b8cc185eb15579eef86d080b1d6189c0f9af98b1c2ebd107ea45abdb68a"
+          + "3c7838a5e5488c76c53d40b121de7bbd30e620c188ae1aa61dbbc87dd3c645f2f55f3d4c375ec4070a9"
+          + "3f7151d83670c16a971abe5ef2d11890e1b8aef3298cf066bf9e6ce144ac9ae86d1c1b0f020103a381f"
+          + "c3081f9301d0603551d0e041604148d1cc5be954c433c61863a15b04cbc03f24fe0b23081c90603551d"
+          + "230481c13081be80148d1cc5be954c433c61863a15b04cbc03f24fe0b2a1819aa48197308194310b300"
+          + "9060355040613025553311330110603550408130a43616c69666f726e6961311630140603550407130d"
+          + "4d6f756e7461696e20566965773110300e060355040a1307416e64726f69643110300e060355040b130"
+          + "7416e64726f69643110300e06035504031307416e64726f69643122302006092a864886f70d01090116"
+          + "13616e64726f696440616e64726f69642e636f6d820900d585b86c7dd34ef5300c0603551d130405300"
+          + "30101ff300d06092a864886f70d0101040500038201010019d30cf105fb78923f4c0d7dd223233d4096"
+          + "7acfce00081d5bd7c6e9d6ed206b0e11209506416ca244939913d26b4aa0e0f524cad2bb5c6e4ca1016"
+          + "a15916ea1ec5dc95a5e3a010036f49248d5109bbf2e1e618186673a3be56daf0b77b1c229e3c255e3e8"
+          + "4c905d2387efba09cbf13b202b4e5a22c93263484a23d2fc29fa9f1939759733afd8aa160f4296c2d01"
+          + "63e8182859c6643e9c1962fa0c18333335bc090ff9a6b22ded1ad444229a539a94eefadabd065ced24b"
+          + "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) {
+    if (verifyIntentAction(intent)) {
+      return binder;
+    } else {
+      LOG.w(
+          "["
+              + this.getClass().getSimpleName()
+              + "] Unauthorized binder uid="
+              + Binder.getCallingUid()
+              + ", intentAction="
+              + (intent == null ? "(null)" : intent.getAction()));
+      return null;
+    }
+  }
+
+  @Override
+  public boolean onUnbind(Intent intent) {
+    if (verifyIntentAction(intent)) {
+      callback = null;
+    }
+    return super.onUnbind(intent);
+  }
+
+  private boolean verifyIntentAction(Intent intent) {
+    if (intent != null
+        && intent.getAction() != null
+        && intent.getAction().equals(getIntentAction())) {
+      return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Called when the task is finished.
+   *
+   * @param succeed whether the task success or not.
+   * @param failedReason A simple phrase to explain the failed reason. Like "No network". Null if
+   *     task is success.
+   */
+  protected void onTaskFinished(boolean succeed, @Nullable String failedReason) {
+    LOG.atDebug("onTaskFinished callback " + ((callback == null) ? "is null." : "is not null."));
+    if (callback != null) {
+      try {
+        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");
+      }
+    }
+  }
+
+  /**
+   * Gets the intent action that expected to execute the task. Use to avoid the receiver launch
+   * unexpectedly.
+   */
+  @NonNull
+  protected abstract String getIntentAction();
+
+  /** Returns the executor used to execute the task. */
+  @NonNull
+  protected abstract Executor getExecutor();
+
+  /** Tasks can be done before activity launched, in order to remove the loading before activity. */
+  protected abstract void onStartTask();
+
+  @VisibleForTesting
+  final IBtsTaskService.Stub binder =
+      new IBtsTaskService.Stub() {
+        @Override
+        public void setCallback(IBtsTaskServiceCallback callback) {
+          LOG.atDebug("setCallback called.");
+          if (verifyCallingApp()) {
+            AbstractSetupBtsService.this.callback = callback;
+            Executor executor = getExecutor();
+
+            if (executor != null) {
+              executor.execute(
+                  () -> {
+                    Preconditions.ensureNotOnMainThread(
+                        AbstractSetupBtsService.this.getClass().getSimpleName() + "::onStartTask");
+                    onStartTask();
+                  });
+            }
+          } else {
+            if (callback != null) {
+              try {
+                callback.onTaskFinished(Bundle.EMPTY);
+              } catch (RemoteException e) {
+                LOG.e("Error occurred while invoke remote method onTaskFinished");
+              }
+            }
+            LOG.e(
+                "BTS service bound with untrusted application, callingUid="
+                    + Binder.getCallingUid());
+          }
+        }
+      };
+
+  @VisibleForTesting
+  boolean verifyCallingApp() {
+    if (verifyCallingPackageName() && verifyCallingSignature() && verifyCallingAppPermission()) {
+      LOG.atInfo("Trusted caller=" + getPackageManager().getNameForUid(Binder.getCallingUid()));
+      return true;
+    } else {
+      LOG.e("Untrusted caller=" + getPackageManager().getNameForUid(Binder.getCallingUid()));
+      return false;
+    }
+  }
+
+  @VisibleForTesting
+  boolean verifyCallingPackageName() {
+    String packageName = getPackageManager().getNameForUid(Binder.getCallingUid());
+    if (SETUP_WIZARD_PACKAGE_NAME.equals(packageName)
+        || (allowDebugKeys && BTS_STARTER_FOR_TEST.equals(packageName))) {
+      LOG.atDebug("Package name match to SetupWizard");
+      return true;
+    } else {
+      LOG.w("Untrusted package:" + packageName);
+      return false;
+    }
+  }
+
+  @VisibleForTesting
+  boolean verifyCallingSignature() {
+    String packageName = getPackageManager().getNameForUid(Binder.getCallingUid());
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.P) {
+      try {
+        PackageInfo info =
+            getPackageManager()
+                .getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES);
+
+        for (Signature signature : info.signingInfo.getApkContentsSigners()) {
+          if (SETUP_WIZARD_RELEASE_CERTIFICATE_STRING.equals(signature.toCharsString())
+              || (isAllowDebugKeysOrBuild()
+                  && SETUP_WIZARD_DEBUG_CERTIFICATE_STRING.equals(signature.toCharsString()))) {
+            return true;
+          }
+        }
+      } catch (NameNotFoundException | NullPointerException e) {
+        LOG.e("Exception occurred while verify signature", e);
+      }
+    } else {
+      LOG.w("Signature verify is not support before Android P.");
+      return false;
+    }
+
+    LOG.w("Signature not match to SetupWizard");
+    return false;
+  }
+
+  private boolean isAllowDebugKeysOrBuild() {
+    return Build.TYPE.equals("userdebug") || Build.TYPE.equals("eng") || allowDebugKeys;
+  }
+
+  @VisibleForTesting
+  boolean verifyCallingAppPermission() {
+    int checkPermission =
+        checkPermission(SETUP_BTS_PERMISSION, Binder.getCallingPid(), Binder.getCallingUid());
+    if (PackageManager.PERMISSION_GRANTED == checkPermission) {
+      LOG.atDebug(
+          "permission:"
+              + SETUP_BTS_PERMISSION
+              + ", grant pid="
+              + Binder.getCallingPid()
+              + ", uid="
+              + Binder.getCallingUid()
+              + ", checkPermission="
+              + checkPermission);
+      return true;
+    } else {
+      return false;
+    }
+  }
+}
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 @@
 
   private Activity activity;
 
+  private PersistableBundle layoutTypeBundle;
+
   @CanIgnoreReturnValue
   public PartnerCustomizationLayout(Context context) {
     this(context, 0, 0);
@@ -92,10 +94,6 @@
     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 @@
     init(attrs, defStyleAttr);
   }
 
+  @VisibleForTesting
+  final ViewTreeObserver.OnWindowFocusChangeListener windowFocusChangeListener =
+      this::onFocusChanged;
+
   private void init(AttributeSet attrs, int defStyleAttr) {
     if (isInEditMode()) {
       return;
@@ -242,9 +244,15 @@
               ? 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 @@
     }
   }
 
+  /**
+   * 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 @@
     }
     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 @@
             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 @@
 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 @@
     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 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 @@
 
   /** 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 @@
      */
     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 @@
     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 @@
      * 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 @@
   @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 @@
         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 @@
       return PartnerConfigHelper.get(context)
           .getBoolean(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ALIGNED_END, false);
     } else {
-      return false;
+      return footerButtonAlignEnd;
     }
   }
 
@@ -617,7 +620,7 @@
     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 @@
    *   <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 @@
    * @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() {}
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 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 @@
 
   @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 @@
 
   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 @@
   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 @@
       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 @@
     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 @@
     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 @@
     applyMaterialYouConfigBundle = null;
     applyDynamicColorBundle = null;
     applyNeutralButtonStyleBundle = null;
+    applyEmbeddedActivityOnePaneBundle = null;
     suwDefaultThemeBundle = null;
+    applyTransitionBundle = null;
   }
 
   /**
@@ -766,6 +831,32 @@
         && 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 @@
         && 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()