Import updated Android SetupCompat Library 369420590 am: 56ea8be381 am: 3e44677b94

Original change: https://googleplex-android-review.googlesource.com/c/platform/external/setupcompat/+/14260711

Change-Id: I924ee1edb6b169e0ec13c4fd2621fc0caebf2464
diff --git a/Android.bp b/Android.bp
index 353706d..01e654d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -19,6 +19,52 @@
     ],
 }
 
+filegroup {
+    name: "Aidls",
+    srcs: [
+        "main/aidl/com/google/android/setupcompat/ISetupCompatService.aidl",
+    ],
+    path: "main/aidl",
+}
+
+filegroup {
+    name: "AidlsPortal",
+    srcs: [
+        "main/aidl/com/google/android/setupcompat/portal/*.aidl",
+    ],
+    path: "main/aidl",
+}
+
+filegroup {
+    name: "Srcs",
+    srcs: [
+        "main/java/com/google/android/setupcompat/*.java",
+        "main/java/com/google/android/setupcompat/internal/*.java",
+        "main/java/com/google/android/setupcompat/logging/*.java",
+        "main/java/com/google/android/setupcompat/logging/internal/*.java",
+        "main/java/com/google/android/setupcompat/template/*.java",
+        "main/java/com/google/android/setupcompat/util/*.java",
+        "main/java/com/google/android/setupcompat/view/*.java",
+    ],
+    path: "main/java",
+}
+
+filegroup {
+    name: "SrcsPartnerConfig",
+    srcs: [
+        "partnerconfig/java/**/*.java",
+    ],
+    path: "partnerconfig/java",
+}
+
+filegroup {
+    name: "SrcsPortal",
+    srcs: [
+        "main/java/com/google/android/setupcompat/portal/*.java",
+    ],
+    path: "main/java",
+}
+
 android_library {
     name: "setupcompat",
     manifest: "AndroidManifest.xml",
@@ -26,9 +72,11 @@
         "main/res",
     ],
     srcs: [
-        "main/java/**/*.java",
-        "partnerconfig/java/**/*.java",
-        "main/aidl/**/*.aidl",
+        ":Aidls",
+        ":AidlsPortal",
+        ":Srcs",
+        ":SrcsPartnerConfig",
+        ":SrcsPortal",
     ],
     static_libs: [
         "androidx.annotation_annotation",
diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl
new file mode 100644
index 0000000..13303d8
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressCallback.aidl
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.os.Bundle;
+
+/**
+ * Interface for progress service to update progress to SUW. Clients should
+ * update progress at least once a minute, or set a pending reason to stop
+ * update progress. Without progress update and pending reason. We considering
+ * the progress service is no response will suspend it and unbinde service.
+ */
+interface IPortalProgressCallback {
+  /**
+   * Sets completed amount.
+   */
+  Bundle setProgressCount(int completed, int failed, int total) = 0;
+
+  /**
+   * Sets completed percentage.
+   */
+  Bundle setProgressPercentage(int percentage) = 1;
+
+  /**
+   * Sets the summary shows on portal activity.
+   */
+  Bundle setSummary(String summary) = 2;
+
+  /**
+   * Let SUW knows the progress is pending now, and stop update progress.
+   * @param reasonResId The resource identifier.
+   * @param quantity The number used to get the correct string for the current language's
+   *           plural rules
+   * @param formatArgs The format arguments that will be used for substitution.
+   */
+  Bundle setPendingReason(int reasonResId, int quantity, in int[] formatArgs, int reason) = 3;
+
+  /**
+   * Once the progress completed, and service can be destroy. Call this function.
+   * SUW will unbind the service.
+   * @param resId The resource identifier.
+   * @param quantity The number used to get the correct string for the current language's
+   *           plural rules
+   * @param formatArgs The format arguments that will be used for substitution.
+   */
+  Bundle setComplete(int resId, int quantity, in int[] formatArgs) = 4;
+
+  /**
+   * Once the progress failed, and not able to finish the progress. Should call
+   * this function. SUW will unbind service after receive setFailure. Client can
+   * registerProgressService again once the service is ready to execute.
+   * @param resId The resource identifier.
+   * @param quantity The number used to get the correct string for the current language's
+   *           plural rules
+   * @param formatArgs The format arguments that will be used for substitution.
+   */
+  Bundle setFailure(int resId, int quantity, in int[] formatArgs) = 5;
+}
\ No newline at end of file
diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl
new file mode 100644
index 0000000..d741125
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/IPortalProgressService.aidl
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.os.Bundle;
+import com.google.android.setupcompat.portal.IPortalProgressCallback;
+
+/**
+ * Interface of progress service, all servics needs to run during onboarding, and would like
+ * to consolidate notifications with SetupNotificationService, should implement this interface.
+ * So that SetupNotificationService can bind the progress service and run below
+ * SetupNotificationService.
+ */
+interface IPortalProgressService {
+  /**
+  * Called when the Portal notification is started.
+  */
+  oneway void onPortalSessionStart() = 0;
+
+  /**
+   * Provides a non-null callback after service connected.
+   */
+  oneway void onSetCallback(IPortalProgressCallback callback) = 1;
+
+  /**
+   * Called when progress timed out.
+   */
+  oneway void onSuspend() = 2;
+
+  /**
+   * User clicks "User mobile data" on portal activity.
+   * All running progress should agree to use mobile data.
+   */
+  oneway void onAllowMobileData(boolean allowed) = 3;
+
+  /**
+   * Portal service calls to get remaining downloading size(MB) from progress service.
+   */
+  @nullable
+  Bundle onGetRemainingValues() = 4;
+}
\ No newline at end of file
diff --git a/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl b/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl
new file mode 100644
index 0000000..56d39e5
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/IPortalRegisterResultListener.aidl
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+/** Listener to listen the result of registerProgressService */
+interface IPortalRegisterResultListener {
+  void onResult(in Bundle result) = 0;
+}
\ No newline at end of file
diff --git a/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl b/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl
new file mode 100644
index 0000000..9f9b1d8
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/ISetupNotificationService.aidl
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.os.UserHandle;
+import com.google.android.setupcompat.portal.IPortalRegisterResultListener;
+import com.google.android.setupcompat.portal.NotificationComponent;
+import com.google.android.setupcompat.portal.ProgressServiceComponent;
+
+/**
+ * Declares the interface for notification related service methods.
+ */
+interface ISetupNotificationService {
+  /** Register a notification without progress service */
+  boolean registerNotification(in NotificationComponent component) = 0;
+  void unregisterNotification(in NotificationComponent component) = 1;
+
+  /** Register a progress service */
+  void registerProgressService(in ProgressServiceComponent component, in UserHandle userHandle, IPortalRegisterResultListener listener) = 2;
+
+  /** Checks the progress connection still alive or not.  */
+  boolean isProgressServiceAlive(in ProgressServiceComponent component, in UserHandle userHandle) = 3;
+
+  /** Checks portal avaailable or not. */
+  boolean isPortalAvailable() = 4;
+}
\ No newline at end of file
diff --git a/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl b/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl
new file mode 100644
index 0000000..5de3f76
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/NotificationComponent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+parcelable NotificationComponent;
\ No newline at end of file
diff --git a/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl b/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl
new file mode 100644
index 0000000..6a6e120
--- /dev/null
+++ b/main/aidl/com/google/android/setupcompat/portal/ProgressServiceComponent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+ parcelable ProgressServiceComponent;
\ No newline at end of file
diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
index 0f0a58e..466f036 100644
--- a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
+++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java
@@ -280,9 +280,6 @@
    * {@code false}.
    */
   public boolean shouldApplyDynamicColor() {
-    if (!enablePartnerResourceLoading()) {
-      return false;
-    }
     if (!useDynamicColor) {
       return false;
     }
@@ -295,7 +292,11 @@
     return true;
   }
 
-  /** Returns boolean value of the {@code app:sucFullDynamicColor}. */
+  /**
+   * Returns {@code true} if the current layout/activity applies full dynamic color. Otherwise,
+   * returns {@code false}. This method combines the result of {@link #shouldApplyDynamicColor()}
+   * and the value of the {@code app:sucFullDynamicColor}.
+   */
   public boolean useFullDynamicColor() {
     return shouldApplyDynamicColor() && useFullDynamicColorAttr;
   }
diff --git a/main/java/com/google/android/setupcompat/portal/NotificationComponent.java b/main/java/com/google/android/setupcompat/portal/NotificationComponent.java
new file mode 100644
index 0000000..a90963b
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/portal/NotificationComponent.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class that represents how a persistent notification is to be presented to the user using the
+ * {@link com.google.android.setupcompat.portal.ISetupNotificationService}.
+ */
+public class NotificationComponent implements Parcelable {
+
+  @NotificationType private final int notificationType;
+  private Bundle extraBundle = new Bundle();
+
+  private NotificationComponent(@NotificationType int notificationType) {
+    this.notificationType = notificationType;
+  }
+
+  protected NotificationComponent(Parcel in) {
+    this(in.readInt());
+    extraBundle = in.readBundle(Bundle.class.getClassLoader());
+  }
+
+  public int getIntExtra(String key, int defValue) {
+    return extraBundle.getInt(key, defValue);
+  }
+
+  @NotificationType
+  public int getNotificationType() {
+    return notificationType;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeInt(notificationType);
+    dest.writeBundle(extraBundle);
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  public static final Creator<NotificationComponent> CREATOR =
+      new Creator<NotificationComponent>() {
+        @Override
+        public NotificationComponent createFromParcel(Parcel in) {
+          return new NotificationComponent(in);
+        }
+
+        @Override
+        public NotificationComponent[] newArray(int size) {
+          return new NotificationComponent[size];
+        }
+      };
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    NotificationType.INITIAL_ONGOING,
+    NotificationType.PREDEFERRED,
+    NotificationType.PREDEFERRED_PREPARING,
+    NotificationType.DEFERRED,
+    NotificationType.DEFERRED_ONGOING,
+    NotificationType.PORTAL
+  })
+  public @interface NotificationType {
+    int UNKNOWN = 0;
+    int INITIAL_ONGOING = 1;
+    int PREDEFERRED = 2;
+    int PREDEFERRED_PREPARING = 3;
+    int DEFERRED = 4;
+    int DEFERRED_ONGOING = 5;
+    int PORTAL = 6;
+    int MAX = 7;
+  }
+
+  public static class Builder {
+
+    private final NotificationComponent component;
+
+    public Builder(@NotificationType int notificationType) {
+      component = new NotificationComponent(notificationType);
+    }
+
+    public Builder putIntExtra(String key, int value) {
+      component.extraBundle.putInt(key, value);
+      return this;
+    }
+
+    public NotificationComponent build() {
+      return component;
+    }
+  }
+}
diff --git a/main/java/com/google/android/setupcompat/portal/PortalConstants.java b/main/java/com/google/android/setupcompat/portal/PortalConstants.java
new file mode 100644
index 0000000..52d8700
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/portal/PortalConstants.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.StringDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Constant values used for Portal */
+public class PortalConstants {
+
+  /** Enumeration of pending reasons, for {@link IPortalProgressCallback#setPendingReason}. */
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({
+    PendingReason.IN_PROGRESS,
+    PendingReason.PROGRESS_REQUEST_ANY_NETWORK,
+    PendingReason.PROGRESS_REQUEST_WIFI,
+    PendingReason.PROGRESS_REQUEST_MOBILE,
+    PendingReason.PROGRESS_RETRY,
+    PendingReason.PROGRESS_REQUEST_REMOVED,
+    PendingReason.MAX
+  })
+  public @interface PendingReason {
+    /**
+     * Don't used this, use {@link IPortalProgressCallback#setProgressCount} ot {@link
+     * IPortalProgressCallback#setProgressPercentage} will reset pending reason to in progress.
+     */
+    int IN_PROGRESS = 0;
+
+    /** Clients required network. */
+    int PROGRESS_REQUEST_ANY_NETWORK = 1;
+
+    /** Clients required a wifi network. */
+    int PROGRESS_REQUEST_WIFI = 2;
+
+    /** Client required a mobile data */
+    int PROGRESS_REQUEST_MOBILE = 3;
+
+    /** Client needs to wait for retry */
+    int PROGRESS_RETRY = 4;
+
+    /** Client required to remove added task */
+    int PROGRESS_REQUEST_REMOVED = 5;
+
+    int MAX = 6;
+  }
+
+  /** Bundle keys used in {@link IPortalProgressService#onGetRemainingValues}. */
+  @Retention(RetentionPolicy.SOURCE)
+  @StringDef({RemainingValues.REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB})
+  public @interface RemainingValues {
+    /** Remaining size to download in MB. */
+    String REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB = "RemainingSizeInKB";
+  }
+
+  private PortalConstants() {}
+}
diff --git a/main/java/com/google/android/setupcompat/portal/PortalHelper.java b/main/java/com/google/android/setupcompat/portal/PortalHelper.java
new file mode 100644
index 0000000..370db96
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/portal/PortalHelper.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Build.VERSION;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+import androidx.annotation.NonNull;
+import com.google.android.setupcompat.internal.Preconditions;
+import com.google.android.setupcompat.portal.PortalConstants.RemainingValues;
+
+/** This class is responsible for safely executing methods on SetupNotificationService. */
+public class PortalHelper {
+
+  private static final String TAG = "PortalHelper";
+
+  public static final String EXTRA_KEY_IS_SETUP_WIZARD = "isSetupWizard";
+
+  public static final String ACTION_BIND_SETUP_NOTIFICATION_SERVICE =
+      "com.google.android.setupcompat.portal.SetupNotificationService.BIND";
+
+  public static final String RESULT_BUNDLE_KEY_RESULT = "Result";
+  public static final String RESULT_BUNDLE_KEY_ERROR = "Error";
+  public static final String RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE =
+      "PortalNotificationAvailable";
+
+  public static final Intent NOTIFICATION_SERVICE_INTENT =
+      new Intent(ACTION_BIND_SETUP_NOTIFICATION_SERVICE)
+          .setPackage("com.google.android.setupwizard");
+
+  /**
+   * Binds SetupNotificationService. For more detail see {@link Context#bindService(Intent,
+   * ServiceConnection, int)}
+   */
+  public static boolean bindSetupNotificationService(
+      @NonNull Context context, @NonNull ServiceConnection connection) {
+    Preconditions.checkNotNull(context, "Context cannot be null");
+    Preconditions.checkNotNull(connection, "ServiceConnection cannot be null");
+    try {
+      return context.bindService(NOTIFICATION_SERVICE_INTENT, connection, Context.BIND_AUTO_CREATE);
+    } catch (SecurityException e) {
+      Log.e(TAG, "Exception occurred while binding SetupNotificationService", e);
+      return false;
+    }
+  }
+
+  /**
+   * Registers a progress service to SUW service. The function response for bind service and invoke
+   * function safely, and returns the result using {@link RegisterCallback}.
+   *
+   * @param context The application context.
+   * @param component Identifies the progress service to execute.
+   * @param callback Receives register result. {@link RegisterCallback#onSuccess} called while
+   *     register succeed. {@link RegisterCallback#onFailure} called while register failed.
+   */
+  public static void registerProgressService(
+      @NonNull Context context,
+      @NonNull ProgressServiceComponent component,
+      @NonNull RegisterCallback callback) {
+    Preconditions.checkNotNull(context, "Context cannot be null");
+    Preconditions.checkNotNull(component, "ProgressServiceComponent cannot be null");
+    Preconditions.checkNotNull(callback, "RegisterCallback cannot be null");
+
+    ServiceConnection connection =
+        new ServiceConnection() {
+          @Override
+          public void onServiceConnected(ComponentName name, IBinder binder) {
+            if (binder != null) {
+              ISetupNotificationService service =
+                  ISetupNotificationService.Stub.asInterface(binder);
+              try {
+                if (VERSION.SDK_INT >= VERSION_CODES.N) {
+                  final ServiceConnection serviceConnection = this;
+                  service.registerProgressService(
+                      component,
+                      getCurrentUserHandle(),
+                      new IPortalRegisterResultListener.Stub() {
+                        @Override
+                        public void onResult(Bundle result) {
+                          if (result.getBoolean(RESULT_BUNDLE_KEY_RESULT, false)) {
+                            callback.onSuccess(
+                                result.getBoolean(
+                                    RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE, false));
+                          } else {
+                            callback.onFailure(
+                                new IllegalStateException(
+                                    result.getString(RESULT_BUNDLE_KEY_ERROR, "Unknown error")));
+                          }
+                          context.unbindService(serviceConnection);
+                        }
+                      });
+                } else {
+                  callback.onFailure(
+                      new IllegalStateException(
+                          "SetupNotificationService is not supported before Android N"));
+                  context.unbindService(this);
+                }
+              } catch (RemoteException | NullPointerException e) {
+                callback.onFailure(e);
+                context.unbindService(this);
+              }
+            } else {
+              callback.onFailure(
+                  new IllegalStateException("SetupNotification should not return null binder"));
+              context.unbindService(this);
+            }
+          }
+
+          @Override
+          public void onServiceDisconnected(ComponentName name) {
+            // Do nothing when service disconnected
+          }
+        };
+
+    if (!bindSetupNotificationService(context, connection)) {
+      Log.e(TAG, "Failed to bind SetupNotificationService.");
+      callback.onFailure(new SecurityException("Failed to bind SetupNotificationService."));
+    }
+  }
+
+  public static void isPortalAvailable(
+      @NonNull Context context, @NonNull final PortalAvailableResultListener listener) {
+    ServiceConnection connection =
+        new ServiceConnection() {
+          @Override
+          public void onServiceConnected(ComponentName name, IBinder binder) {
+            if (binder != null) {
+              ISetupNotificationService service =
+                  ISetupNotificationService.Stub.asInterface(binder);
+
+              try {
+                listener.onResult(service.isPortalAvailable());
+              } catch (RemoteException e) {
+                Log.w(TAG, "Failed to invoke SetupNotificationService#isPortalAvailable");
+                listener.onResult(false);
+              }
+            }
+            context.unbindService(this);
+          }
+
+          @Override
+          public void onServiceDisconnected(ComponentName name) {}
+        };
+
+    if (!bindSetupNotificationService(context, connection)) {
+      Log.e(
+          TAG,
+          "Failed to bind SetupNotificationService. Do you have permission"
+              + " \"com.google.android.setupwizard.SETUP_PROGRESS_SERVICE\"");
+      listener.onResult(false);
+    }
+  }
+
+  public static void isProgressServiceAlive(
+      @NonNull final Context context,
+      @NonNull final ProgressServiceComponent component,
+      @NonNull final ProgressServiceAliveResultListener listener) {
+    Preconditions.checkNotNull(context, "Context cannot be null");
+    Preconditions.checkNotNull(component, "ProgressServiceComponent cannot be null");
+    Preconditions.checkNotNull(listener, "ProgressServiceAliveResultCallback cannot be null");
+
+    ServiceConnection connection =
+        new ServiceConnection() {
+          @Override
+          public void onServiceConnected(ComponentName name, IBinder binder) {
+            if (binder != null) {
+              ISetupNotificationService service =
+                  ISetupNotificationService.Stub.asInterface(binder);
+
+              try {
+                if (VERSION.SDK_INT >= VERSION_CODES.N) {
+                  listener.onResult(
+                      service.isProgressServiceAlive(component, getCurrentUserHandle()));
+                } else {
+                  listener.onResult(false);
+                }
+
+              } catch (RemoteException e) {
+                Log.w(TAG, "Failed to invoke SetupNotificationService#isProgressServiceAlive");
+                listener.onResult(false);
+              }
+            }
+            context.unbindService(this);
+          }
+
+          @Override
+          public void onServiceDisconnected(ComponentName name) {}
+        };
+
+    if (!bindSetupNotificationService(context, connection)) {
+      Log.e(
+          TAG,
+          "Failed to bind SetupNotificationService. Do you have permission"
+              + " \"com.google.android.setupwizard.SETUP_PROGRESS_SERVICE\"");
+      listener.onResult(false);
+    }
+  }
+
+  private static UserHandle getCurrentUserHandle() {
+    if (VERSION.SDK_INT >= VERSION_CODES.N) {
+      return UserHandle.getUserHandleForUid(Process.myUid());
+    } else {
+      return null;
+    }
+  }
+
+  /**
+   * Creates the {@code Bundle} including the bind progress service result.
+   *
+   * @param succeed whether bind service success or not.
+   * @param errorMsg describe the reason why bind service failed.
+   * @return A bundle include bind result and error message.
+   */
+  public static Bundle createResultBundle(
+      boolean succeed, String errorMsg, boolean isPortalNotificationAvailable) {
+    Bundle bundle = new Bundle();
+    bundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, succeed);
+    if (!succeed) {
+      bundle.putString(RESULT_BUNDLE_KEY_ERROR, errorMsg);
+    }
+    bundle.putBoolean(
+        RESULT_BUNDLE_KEY_PORTAL_NOTIFICATION_AVAILABLE, isPortalNotificationAvailable);
+    return bundle;
+  }
+
+  /**
+   * Returns {@code true}, if the intent is bound from SetupWizard, otherwise returns false.
+   *
+   * @param intent that received when onBind.
+   */
+  public static boolean isFromSUW(Intent intent) {
+    return intent != null && intent.getBooleanExtra(EXTRA_KEY_IS_SETUP_WIZARD, false);
+  }
+
+  /** A callback for accepting the results of SetupNotificationService. */
+  public interface RegisterCallback {
+    void onSuccess(boolean isPortalNow);
+
+    void onFailure(Throwable throwable);
+  }
+
+  public interface RegisterNotificationCallback {
+    void onSuccess();
+
+    void onFailure(Throwable throwable);
+  }
+
+  public interface ProgressServiceAliveResultListener {
+    void onResult(boolean isAlive);
+  }
+
+  public interface PortalAvailableResultListener {
+    void onResult(boolean isAvailable);
+  }
+
+  public static class RemainingValueBuilder {
+    private final Bundle bundle = new Bundle();
+
+    public static RemainingValueBuilder createBuilder() {
+      return new RemainingValueBuilder();
+    }
+
+    public RemainingValueBuilder setRemainingSizeInKB(int size) {
+      Preconditions.checkArgument(
+          size >= 0, "The remainingSize should be positive integer or zero.");
+      bundle.putInt(RemainingValues.REMAINING_SIZE_TO_BE_DOWNLOAD_IN_KB, size);
+      return this;
+    }
+
+    public Bundle build() {
+      return bundle;
+    }
+
+    private RemainingValueBuilder() {}
+  }
+
+  private PortalHelper() {}
+}
+
+
diff --git a/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java b/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java
new file mode 100644
index 0000000..cec2990
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/portal/PortalResultHelper.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2021 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.portal;
+
+import android.os.Bundle;
+
+public class PortalResultHelper {
+
+  public static final String RESULT_BUNDLE_KEY_RESULT = "Result";
+  public static final String RESULT_BUNDLE_KEY_ERROR = "Error";
+
+  public static boolean isSuccess(Bundle bundle) {
+    return bundle.getBoolean(RESULT_BUNDLE_KEY_RESULT, false);
+  }
+
+  public static String getErrorMessage(Bundle bundle) {
+    return bundle.getString(RESULT_BUNDLE_KEY_ERROR, null);
+  }
+
+  public static Bundle createSuccessBundle() {
+    Bundle resultBundle = new Bundle();
+    resultBundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, true);
+    return resultBundle;
+  }
+
+  public static Bundle createFailureBundle(String errorMessage) {
+    Bundle resultBundle = new Bundle();
+    resultBundle.putBoolean(RESULT_BUNDLE_KEY_RESULT, false);
+    resultBundle.putString(RESULT_BUNDLE_KEY_ERROR, errorMessage);
+    return resultBundle;
+  }
+
+  private PortalResultHelper() {}
+  ;
+}
diff --git a/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java b/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java
new file mode 100644
index 0000000..be11239
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/portal/ProgressServiceComponent.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2020 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.portal;
+
+import android.content.Intent;
+import android.os.Parcel;
+import android.os.Parcelable;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.StringRes;
+import com.google.android.setupcompat.internal.Preconditions;
+
+/**
+ * A class that represents how a progress service to be registered to {@link
+ * com.google.android.setupcompat.portal.ISetupNotificationService}.
+ */
+public class ProgressServiceComponent implements Parcelable {
+  private final String packageName;
+  private final String taskName;
+  private final boolean isSilent;
+  private final boolean autoRebind;
+  private final long timeoutForReRegister;
+  @StringRes private final int displayNameResId;
+  @DrawableRes private final int displayIconResId;
+  private final Intent serviceIntent;
+  private final Intent itemClickIntent;
+
+  private ProgressServiceComponent(
+      String packageName,
+      String taskName,
+      boolean isSilent,
+      boolean autoRebind,
+      long timeoutForReRegister,
+      @StringRes int displayNameResId,
+      @DrawableRes int displayIconResId,
+      Intent serviceIntent,
+      Intent itemClickIntent) {
+    this.packageName = packageName;
+    this.taskName = taskName;
+    this.isSilent = isSilent;
+    this.autoRebind = autoRebind;
+    this.timeoutForReRegister = timeoutForReRegister;
+    this.displayNameResId = displayNameResId;
+    this.displayIconResId = displayIconResId;
+    this.serviceIntent = serviceIntent;
+    this.itemClickIntent = itemClickIntent;
+  }
+
+  /** Returns a new instance of {@link Builder}. */
+  public static Builder newBuilder() {
+    return new ProgressServiceComponent.Builder();
+  }
+
+  /** Returns the package name where the service exist. */
+  @NonNull
+  public String getPackageName() {
+    return packageName;
+  }
+
+  /** Returns the service class name */
+  @NonNull
+  public String getTaskName() {
+    return taskName;
+  }
+
+  /** Returns the whether the service is silent or not */
+  public boolean isSilent() {
+    return isSilent;
+  }
+
+  /** Auto rebind progress service while service connection disconnect. Default: true */
+  public boolean isAutoRebind() {
+    return autoRebind;
+  }
+
+  /** The timeout period waiting for client register progress service again. */
+  public long getTimeoutForReRegister() {
+    return timeoutForReRegister;
+  }
+
+  /** Returns the string resource id of display name. */
+  @StringRes
+  public int getDisplayName() {
+    return displayNameResId;
+  }
+
+  /** Returns the drawable resource id of display icon. */
+  @DrawableRes
+  public int getDisplayIcon() {
+    return displayIconResId;
+  }
+
+  /** Returns the Intent used to bind progress service. */
+  public Intent getServiceIntent() {
+    return serviceIntent;
+  }
+
+  /** Returns the Intent to start the user interface while progress item click. */
+  public Intent getItemClickIntent() {
+    return itemClickIntent;
+  }
+
+  @Override
+  public void writeToParcel(Parcel dest, int flags) {
+    dest.writeString(getPackageName());
+    dest.writeString(getTaskName());
+    dest.writeInt(isSilent() ? 1 : 0);
+    dest.writeInt(getDisplayName());
+    dest.writeInt(getDisplayIcon());
+    dest.writeParcelable(getServiceIntent(), 0);
+    dest.writeParcelable(getItemClickIntent(), 0);
+    dest.writeInt(isAutoRebind() ? 1 : 0);
+    dest.writeLong(getTimeoutForReRegister());
+  }
+
+  @Override
+  public int describeContents() {
+    return 0;
+  }
+
+  public static final Creator<ProgressServiceComponent> CREATOR =
+      new Creator<ProgressServiceComponent>() {
+        @Override
+        public ProgressServiceComponent createFromParcel(Parcel in) {
+          return ProgressServiceComponent.newBuilder()
+              .setPackageName(in.readString())
+              .setTaskName(in.readString())
+              .setSilentMode(in.readInt() == 1)
+              .setDisplayName(in.readInt())
+              .setDisplayIcon(in.readInt())
+              .setServiceIntent(in.readParcelable(Intent.class.getClassLoader()))
+              .setItemClickIntent(in.readParcelable(Intent.class.getClassLoader()))
+              .setAutoRebind(in.readInt() == 1)
+              .setTimeoutForReRegister(in.readLong())
+              .build();
+        }
+
+        @Override
+        public ProgressServiceComponent[] newArray(int size) {
+          return new ProgressServiceComponent[size];
+        }
+      };
+
+  /** Builder class for {@link ProgressServiceComponent} objects */
+  public static class Builder {
+    private String packageName;
+    private String taskName;
+    private boolean isSilent = false;
+    private boolean autoRebind = true;
+    private long timeoutForReRegister = 0L;
+    @StringRes private int displayNameResId;
+    @DrawableRes private int displayIconResId;
+    private Intent serviceIntent;
+    private Intent itemClickIntent;
+
+    /** Sets the packages name which is the service exists */
+    public Builder setPackageName(@NonNull String packageName) {
+      this.packageName = packageName;
+      return this;
+    }
+
+    /** Sets a name to identify what task this progress is. */
+    public Builder setTaskName(@NonNull String taskName) {
+      this.taskName = taskName;
+      return this;
+    }
+
+    /** Sets the service as silent mode, it executes without UI on PortalActivity. */
+    public Builder setSilentMode(boolean isSilent) {
+      this.isSilent = isSilent;
+      return this;
+    }
+
+    /** Sets the service need auto rebind or not when service connection disconnected. */
+    public Builder setAutoRebind(boolean autoRebind) {
+      this.autoRebind = autoRebind;
+      return this;
+    }
+
+    /**
+     * Sets the timeout period waiting for the client register again, only works when auto-rebind
+     * disabled. When 0 is set, will read default configuration from SUW.
+     */
+    public Builder setTimeoutForReRegister(long timeoutForReRegister) {
+      this.timeoutForReRegister = timeoutForReRegister;
+      return this;
+    }
+
+    /** Sets the name which is displayed on PortalActivity */
+    public Builder setDisplayName(@StringRes int displayNameResId) {
+      this.displayNameResId = displayNameResId;
+      return this;
+    }
+
+    /** Sets the icon which is display on PortalActivity */
+    public Builder setDisplayIcon(@DrawableRes int displayIconResId) {
+      this.displayIconResId = displayIconResId;
+      return this;
+    }
+
+    public Builder setServiceIntent(Intent serviceIntent) {
+      this.serviceIntent = serviceIntent;
+      return this;
+    }
+
+    public Builder setItemClickIntent(Intent itemClickIntent) {
+      this.itemClickIntent = itemClickIntent;
+      return this;
+    }
+
+    public ProgressServiceComponent build() {
+      Preconditions.checkNotNull(packageName, "packageName cannot be null.");
+      Preconditions.checkNotNull(taskName, "serviceClass cannot be null.");
+      Preconditions.checkNotNull(serviceIntent, "Service intent cannot be null.");
+      Preconditions.checkNotNull(itemClickIntent, "Item click intent cannot be null");
+      if (!isSilent) {
+        Preconditions.checkArgument(
+            displayNameResId != 0, "Invalidate resource id of display name");
+        Preconditions.checkArgument(
+            displayIconResId != 0, "Invalidate resource id of display icon");
+      }
+      return new ProgressServiceComponent(
+          packageName,
+          taskName,
+          isSilent,
+          autoRebind,
+          timeoutForReRegister,
+          displayNameResId,
+          displayIconResId,
+          serviceIntent,
+          itemClickIntent);
+    }
+
+    private Builder() {}
+  }
+}
diff --git a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
index 8584527..8f807e6 100644
--- a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
+++ b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java
@@ -24,19 +24,10 @@
 import android.content.res.ColorStateList;
 import android.content.res.TypedArray;
 import android.graphics.Color;
-import android.graphics.PorterDuff.Mode;
-import android.graphics.Typeface;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.RippleDrawable;
 import android.os.Build;
 import android.os.Build.VERSION_CODES;
 import android.os.PersistableBundle;
 import android.util.AttributeSet;
-import android.util.StateSet;
-import android.util.TypedValue;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
 import android.view.View;
@@ -57,7 +48,6 @@
 import com.google.android.setupcompat.PartnerCustomizationLayout;
 import com.google.android.setupcompat.R;
 import com.google.android.setupcompat.internal.FooterButtonPartnerConfig;
-import com.google.android.setupcompat.internal.Preconditions;
 import com.google.android.setupcompat.internal.TemplateLayout;
 import com.google.android.setupcompat.logging.internal.FooterBarMixinMetrics;
 import com.google.android.setupcompat.partnerconfig.PartnerConfig;
@@ -79,7 +69,7 @@
 
   @VisibleForTesting final boolean applyPartnerResources;
   @VisibleForTesting final boolean applyDynamicColor;
-  final boolean useFullDynamicColor;
+  @VisibleForTesting final boolean useFullDynamicColor;
 
   private LinearLayout buttonContainer;
   private FooterButton primaryButton;
@@ -97,8 +87,8 @@
   @ColorInt private final int footerBarPrimaryBackgroundColor;
   @ColorInt private final int footerBarSecondaryBackgroundColor;
   private boolean removeFooterBarWhenEmpty = true;
+  private boolean isSecondaryButtonInPrimaryStyle = false;
 
-  private static final float DEFAULT_DISABLED_ALPHA = 0.26f;
   private static final AtomicInteger nextGeneratedId = new AtomicInteger(1);
 
   @VisibleForTesting public final FooterBarMixinMetrics metrics = new FooterBarMixinMetrics();
@@ -116,7 +106,7 @@
             if (applyPartnerResources) {
               updateButtonTextColorWithPartnerConfig(
                   button,
-                  (id == primaryButtonId)
+                  (id == primaryButtonId || isSecondaryButtonInPrimaryStyle)
                       ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
                       : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR);
             }
@@ -283,13 +273,14 @@
       return;
     }
 
-    @ColorInt
-    int color =
-        PartnerConfigHelper.get(context)
-            .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR);
-    buttonContainer.setBackgroundColor(color);
-
-    // TODO: Apply dynamic color on footer bar background if useFullDynamicColor is true
+    // skip apply partner resources on footerbar background if dynamic color enabled
+    if (!useFullDynamicColor) {
+      @ColorInt
+      int color =
+          PartnerConfigHelper.get(context)
+              .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR);
+      buttonContainer.setBackgroundColor(color);
+    }
 
     footerBarPaddingTop =
         (int)
@@ -345,13 +336,7 @@
                     /* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Primary,
                     /* buttonBackgroundColorConfig= */ PartnerConfig
                         .CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR))
-            .setButtonBackgroundConfig(
-                (applyDynamicColor
-                        && PartnerConfigHelper.get(context)
-                            .isPartnerConfigAvailable(
-                                PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR))
-                    ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_DYNAMIC_COLOR
-                    : PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
+            .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
             .setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
@@ -397,7 +382,14 @@
   /** Sets secondary button for footer. */
   @MainThread
   public void setSecondaryButton(FooterButton footerButton) {
+    setSecondaryButton(footerButton, /*usePrimaryStyle= */ false);
+  }
+
+  /** Sets secondary button for footer. Allow to use the primary button style. */
+  @MainThread
+  public void setSecondaryButton(FooterButton footerButton, boolean usePrimaryStyle) {
     ensureOnMainThread("setSecondaryButton");
+    isSecondaryButtonInPrimaryStyle = usePrimaryStyle;
     ensureFooterInflated();
 
     // Setup button partner config
@@ -406,21 +398,24 @@
             .setPartnerTheme(
                 getPartnerTheme(
                     footerButton,
-                    /* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Secondary,
-                    /* buttonBackgroundColorConfig= */ PartnerConfig
-                        .CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR))
-            .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
+                    /* defaultPartnerTheme= */ usePrimaryStyle
+                        ? R.style.SucPartnerCustomizationButton_Primary
+                        : R.style.SucPartnerCustomizationButton_Secondary,
+                    /* buttonBackgroundColorConfig= */ usePrimaryStyle
+                        ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR
+                        : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR))
+            .setButtonBackgroundConfig(
+                usePrimaryStyle
+                    ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR
+                    : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR)
             .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
             .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
             .setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
             .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
             .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
             .setTextColorConfig(
-                (applyDynamicColor
-                        && PartnerConfigHelper.get(context)
-                            .isPartnerConfigAvailable(
-                                PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR))
-                    ? PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR
+                usePrimaryStyle
+                    ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
                     : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
             .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
             .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
@@ -454,6 +449,16 @@
     buttonContainer.removeAllViews();
 
     if (tempSecondaryButton != null) {
+      if (isSecondaryButtonInPrimaryStyle) {
+        // Since the secondary button has the same style (with background) as the primary button,
+        // we need to have the left padding equal to the right padding.
+        updateFooterBarPadding(
+            buttonContainer,
+            buttonContainer.getPaddingRight(),
+            buttonContainer.getPaddingTop(),
+            buttonContainer.getPaddingRight(),
+            buttonContainer.getPaddingBottom());
+      }
       buttonContainer.addView(tempSecondaryButton);
     }
     addSpace();
@@ -470,7 +475,7 @@
   protected void onFooterButtonInflated(Button button, @ColorInt int defaultButtonBackgroundColor) {
     // Try to set default background
     if (defaultButtonBackgroundColor != 0) {
-      updateButtonBackground(button, defaultButtonBackgroundColor);
+      FooterButtonStyleUtils.updateButtonBackground(button, defaultButtonBackgroundColor);
     } else {
       // TODO: get button background color from activity theme
     }
@@ -601,224 +606,53 @@
   private void onFooterButtonApplyPartnerResource(
       Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
     if (!applyPartnerResources) {
-      // dynamic color should still work if not applying partner resource
-      if (applyDynamicColor) {
-        // override config related to dynamic color
-        updateButtonTextColorWithPartnerConfig(
-            button, footerButtonPartnerConfig.getButtonTextColorConfig());
-        updateButtonBackgroundWithPartnerConfig(
-            button,
-            footerButtonPartnerConfig.getButtonBackgroundConfig(),
-            footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
-            footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
-        updateButtonRippleColorWithPartnerConfig(button, footerButtonPartnerConfig);
-
-        // TODO: Apply full dynamic color if useFullDynamicColor is true
-      }
       return;
     }
-    updateButtonTextColorWithPartnerConfig(
-        button, footerButtonPartnerConfig.getButtonTextColorConfig());
-    updateButtonTextSizeWithPartnerConfig(
-        button, footerButtonPartnerConfig.getButtonTextSizeConfig());
-    updateButtonMinHeightWithPartnerConfig(
-        button, footerButtonPartnerConfig.getButtonMinHeightConfig());
-    updateButtonTypeFaceWithPartnerConfig(
+
+    // If dynamic color enabled, these colors won't be overrode by partner config.
+    // Instead, these colors align with the current theme colors.
+    if (!applyDynamicColor) {
+      updateButtonTextColorWithPartnerConfig(
+          button, footerButtonPartnerConfig.getButtonTextColorConfig());
+      FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig(
+          context,
+          button,
+          footerButtonPartnerConfig.getButtonBackgroundConfig(),
+          footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
+          footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
+      FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig(
+          context, button, footerButtonPartnerConfig);
+    }
+
+    FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig(
+        context, button, footerButtonPartnerConfig.getButtonTextSizeConfig());
+    FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig(
+        context, button, footerButtonPartnerConfig.getButtonMinHeightConfig());
+    FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig(
+        context,
         button,
         footerButtonPartnerConfig.getButtonTextTypeFaceConfig(),
         footerButtonPartnerConfig.getButtonTextStyleConfig());
-    updateButtonBackgroundWithPartnerConfig(
+    FooterButtonStyleUtils.updateButtonRadiusWithPartnerConfig(
+        context, button, footerButtonPartnerConfig.getButtonRadiusConfig());
+    FooterButtonStyleUtils.updateButtonIconWithPartnerConfig(
+        context,
         button,
-        footerButtonPartnerConfig.getButtonBackgroundConfig(),
-        footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
-        footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
-    updateButtonRadiusWithPartnerConfig(button, footerButtonPartnerConfig.getButtonRadiusConfig());
-    updateButtonIconWithPartnerConfig(button, footerButtonPartnerConfig.getButtonIconConfig());
-    updateButtonRippleColorWithPartnerConfig(button, footerButtonPartnerConfig);
+        footerButtonPartnerConfig.getButtonIconConfig(),
+        primaryButtonId == button.getId());
   }
 
   private void updateButtonTextColorWithPartnerConfig(
       Button button, PartnerConfig buttonTextColorConfig) {
     if (button.isEnabled()) {
-      @ColorInt
-      int color = PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig);
-      if (color != Color.TRANSPARENT) {
-        button.setTextColor(ColorStateList.valueOf(color));
-      }
+      FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig(
+          context, button, buttonTextColorConfig);
     } else {
-      // disable state will use the default disable state color
-      button.setTextColor(
-          button.getId() == primaryButtonId ? primaryDefaultTextColor : secondaryDefaultTextColor);
-    }
-  }
-
-  private void updateButtonTextSizeWithPartnerConfig(
-      Button button, PartnerConfig buttonTextSizeConfig) {
-    float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig);
-    if (size > 0) {
-      button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
-    }
-  }
-
-  private void updateButtonMinHeightWithPartnerConfig(
-      Button button, PartnerConfig buttonMinHeightConfig) {
-    if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMinHeightConfig)) {
-      float size = PartnerConfigHelper.get(context).getDimension(context, buttonMinHeightConfig);
-      if (size > 0) {
-        button.setMinHeight((int) size);
-      }
-    }
-  }
-
-  private void updateButtonTypeFaceWithPartnerConfig(
-      Button button, PartnerConfig buttonTextTypeFaceConfig, PartnerConfig buttonTextStyleConfig) {
-    String fontFamilyName =
-        PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig);
-
-    int textStyleValue = Typeface.NORMAL;
-    if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextStyleConfig)) {
-      textStyleValue =
-          PartnerConfigHelper.get(context)
-              .getInteger(context, buttonTextStyleConfig, Typeface.NORMAL);
-    }
-    Typeface font = Typeface.create(fontFamilyName, textStyleValue);
-    if (font != null) {
-      button.setTypeface(font);
-    }
-  }
-
-  @TargetApi(VERSION_CODES.Q)
-  private void updateButtonBackgroundWithPartnerConfig(
-      Button button,
-      PartnerConfig buttonBackgroundConfig,
-      PartnerConfig buttonDisableAlphaConfig,
-      PartnerConfig buttonDisableBackgroundConfig) {
-    Preconditions.checkArgument(
-        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
-        "Update button background only support on sdk Q or higher");
-    @ColorInt int color;
-    @ColorInt int disabledColor;
-    float disabledAlpha;
-    int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
-    int[] ENABLED_STATE_SET = {};
-    color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig);
-    disabledAlpha =
-        PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f);
-    disabledColor =
-        PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig);
-
-    if (color != Color.TRANSPARENT) {
-      if (disabledAlpha <= 0f) {
-        // if no partner resource, fallback to theme disable alpha
-        float alpha;
-        TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha});
-        alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA);
-        a.recycle();
-        disabledAlpha = alpha;
-      }
-      if (disabledColor == Color.TRANSPARENT) {
-        // if no partner resource, fallback to button background color
-        disabledColor = color;
-      }
-
-      // Set text color for ripple.
-      ColorStateList colorStateList =
-          new ColorStateList(
-              new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET},
-              new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color});
-
-      // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are
-      // created, but without copying the state from the parent drawable. So even though the
-      // parent is getting the correct drawable state from the view, the children won't get those
-      // states until a state change happens.
-      // As a workaround, we mutate the drawable and forcibly set the state to empty, and then
-      // refresh the state so the children will have the updated states.
-      button.getBackground().mutate().setState(new int[0]);
-      button.refreshDrawableState();
-      button.setBackgroundTintList(colorStateList);
-    }
-  }
-
-  private void updateButtonBackground(Button button, @ColorInt int color) {
-    button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP);
-  }
-
-  private void updateButtonRadiusWithPartnerConfig(
-      Button button, PartnerConfig buttonRadiusConfig) {
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
-      float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig);
-      GradientDrawable gradientDrawable = getGradientDrawable(button);
-      if (gradientDrawable != null) {
-        gradientDrawable.setCornerRadius(radius);
-      }
-    }
-  }
-
-  private void updateButtonRippleColorWithPartnerConfig(
-      Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
-    // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
-    // unavailable. Since Stencil customization provider only works on Q+, there is no need to
-    // perform any customization for versions 21.
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
-      RippleDrawable rippleDrawable = getRippleDrawable(button);
-      if (rippleDrawable == null) {
-        return;
-      }
-
-      int[] pressedState = {android.R.attr.state_pressed};
-      @ColorInt int color;
-      // Get partner text color.
-      color =
-          PartnerConfigHelper.get(context)
-              .getColor(context, footerButtonPartnerConfig.getButtonTextColorConfig());
-
-      float alpha =
-          PartnerConfigHelper.get(context)
-              .getFraction(context, footerButtonPartnerConfig.getButtonRippleColorAlphaConfig());
-
-      // Set text color for ripple.
-      ColorStateList colorStateList =
-          new ColorStateList(
-              new int[][] {pressedState, StateSet.NOTHING},
-              new int[] {convertRgbToArgb(color, alpha), Color.TRANSPARENT});
-      rippleDrawable.setColor(colorStateList);
-    }
-  }
-
-  private void updateButtonIconWithPartnerConfig(Button button, PartnerConfig buttonIconConfig) {
-    if (button == null) {
-      return;
-    }
-    Drawable icon = null;
-    if (buttonIconConfig != null) {
-      icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig);
-    }
-    setButtonIcon(button, icon);
-  }
-
-  private void setButtonIcon(Button button, Drawable icon) {
-    if (button == null) {
-      return;
-    }
-
-    if (icon != null) {
-      // TODO: restrict the icons to a reasonable size
-      int h = icon.getIntrinsicHeight();
-      int w = icon.getIntrinsicWidth();
-      icon.setBounds(0, 0, w, h);
-    }
-
-    Drawable iconStart = null;
-    Drawable iconEnd = null;
-    if (button.getId() == primaryButtonId) {
-      iconEnd = icon;
-    } else if (button.getId() == secondaryButtonId) {
-      iconStart = icon;
-    }
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
-      button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null);
-    } else {
-      button.setCompoundDrawables(iconStart, null, iconEnd, null);
+      FooterButtonStyleUtils.updateButtonTextDisableColor(
+          button,
+          /* is Primary= */ (primaryButtonId == button.getId() || isSecondaryButtonInPrimaryStyle)
+              ? primaryDefaultTextColor
+              : secondaryDefaultTextColor);
     }
   }
 
@@ -857,43 +691,6 @@
     return result;
   }
 
-  GradientDrawable getGradientDrawable(Button button) {
-    // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after
-    // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only
-    // works on Q+, there is no need to perform any customization for versions 21.
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
-      Drawable drawable = button.getBackground();
-      if (drawable instanceof InsetDrawable) {
-        LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable();
-        return (GradientDrawable) layerDrawable.getDrawable(0);
-      } else if (drawable instanceof RippleDrawable) {
-        InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0);
-        return (GradientDrawable) insetDrawable.getDrawable();
-      }
-    }
-    return null;
-  }
-
-  RippleDrawable getRippleDrawable(Button button) {
-    // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
-    // unavailable. Since Stencil customization provider only works on Q+, there is no need to
-    // perform any customization for versions 21.
-    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
-      Drawable drawable = button.getBackground();
-      if (drawable instanceof InsetDrawable) {
-        return (RippleDrawable) ((InsetDrawable) drawable).getDrawable();
-      } else if (drawable instanceof RippleDrawable) {
-        return (RippleDrawable) drawable;
-      }
-    }
-    return null;
-  }
-
-  @ColorInt
-  private static int convertRgbToArgb(@ColorInt int color, float alpha) {
-    return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color));
-  }
-
   protected View inflateFooter(@LayoutRes int footer) {
     if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
       LayoutInflater inflater =
diff --git a/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java b/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java
new file mode 100644
index 0000000..98a1426
--- /dev/null
+++ b/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.android.setupcompat.template;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.graphics.drawable.InsetDrawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.widget.Button;
+import androidx.annotation.ColorInt;
+import androidx.annotation.VisibleForTesting;
+import com.google.android.setupcompat.internal.FooterButtonPartnerConfig;
+import com.google.android.setupcompat.internal.Preconditions;
+import com.google.android.setupcompat.partnerconfig.PartnerConfig;
+import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
+
+/** Utils for updating the button style. */
+public class FooterButtonStyleUtils {
+  private static final float DEFAULT_DISABLED_ALPHA = 0.26f;
+
+  static void updateButtonTextEnabledColorWithPartnerConfig(
+      Context context, Button button, PartnerConfig buttonEnableTextColorConfig) {
+    @ColorInt
+    int color = PartnerConfigHelper.get(context).getColor(context, buttonEnableTextColorConfig);
+    if (color != Color.TRANSPARENT) {
+      button.setTextColor(ColorStateList.valueOf(color));
+    }
+  }
+
+  static void updateButtonTextDisableColor(Button button, ColorStateList defaultTextColor) {
+    // TODO : add disable footer button text color partner config
+
+    // disable state will use the default disable state color
+    button.setTextColor(defaultTextColor);
+  }
+
+  @TargetApi(VERSION_CODES.Q)
+  static void updateButtonBackgroundWithPartnerConfig(
+      Context context,
+      Button button,
+      PartnerConfig buttonBackgroundConfig,
+      PartnerConfig buttonDisableAlphaConfig,
+      PartnerConfig buttonDisableBackgroundConfig) {
+    Preconditions.checkArgument(
+        Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
+        "Update button background only support on sdk Q or higher");
+    @ColorInt int disabledColor;
+    float disabledAlpha;
+    int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
+    int[] ENABLED_STATE_SET = {};
+    @ColorInt
+    int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig);
+    disabledAlpha =
+        PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f);
+    disabledColor =
+        PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig);
+
+    if (color != Color.TRANSPARENT) {
+      if (disabledAlpha <= 0f) {
+        // if no partner resource, fallback to theme disable alpha
+        TypedArray a = context.obtainStyledAttributes(new int[] {android.R.attr.disabledAlpha});
+        float alpha = a.getFloat(0, DEFAULT_DISABLED_ALPHA);
+        a.recycle();
+        disabledAlpha = alpha;
+      }
+      if (disabledColor == Color.TRANSPARENT) {
+        // if no partner resource, fallback to button background color
+        disabledColor = color;
+      }
+
+      // Set text color for ripple.
+      ColorStateList colorStateList =
+          new ColorStateList(
+              new int[][] {DISABLED_STATE_SET, ENABLED_STATE_SET},
+              new int[] {convertRgbToArgb(disabledColor, disabledAlpha), color});
+
+      // b/129482013: When a LayerDrawable is mutated, a new clone of its children drawables are
+      // created, but without copying the state from the parent drawable. So even though the
+      // parent is getting the correct drawable state from the view, the children won't get those
+      // states until a state change happens.
+      // As a workaround, we mutate the drawable and forcibly set the state to empty, and then
+      // refresh the state so the children will have the updated states.
+      button.getBackground().mutate().setState(new int[0]);
+      button.refreshDrawableState();
+      button.setBackgroundTintList(colorStateList);
+    }
+  }
+
+  static void updateButtonRippleColorWithPartnerConfig(
+      Context context, Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
+    // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
+    // unavailable. Since Stencil customization provider only works on Q+, there is no need to
+    // perform any customization for versions 21.
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+      RippleDrawable rippleDrawable = getRippleDrawable(button);
+      if (rippleDrawable == null) {
+        return;
+      }
+
+      int[] pressedState = {android.R.attr.state_pressed};
+      // Get partner text color.
+      @ColorInt
+      int color =
+          PartnerConfigHelper.get(context)
+              .getColor(context, footerButtonPartnerConfig.getButtonTextColorConfig());
+
+      float alpha =
+          PartnerConfigHelper.get(context)
+              .getFraction(context, footerButtonPartnerConfig.getButtonRippleColorAlphaConfig());
+
+      // Set text color for ripple.
+      ColorStateList colorStateList =
+          new ColorStateList(
+              new int[][] {pressedState, StateSet.NOTHING},
+              new int[] {convertRgbToArgb(color, alpha), Color.TRANSPARENT});
+      rippleDrawable.setColor(colorStateList);
+    }
+  }
+
+  static void updateButtonTextSizeWithPartnerConfig(
+      Context context, Button button, PartnerConfig buttonTextSizeConfig) {
+    float size = PartnerConfigHelper.get(context).getDimension(context, buttonTextSizeConfig);
+    if (size > 0) {
+      button.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
+    }
+  }
+
+  static void updateButtonMinHeightWithPartnerConfig(
+      Context context, Button button, PartnerConfig buttonMinHeightConfig) {
+    if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMinHeightConfig)) {
+      float size = PartnerConfigHelper.get(context).getDimension(context, buttonMinHeightConfig);
+      if (size > 0) {
+        button.setMinHeight((int) size);
+      }
+    }
+  }
+
+  static void updateButtonTypeFaceWithPartnerConfig(
+      Context context,
+      Button button,
+      PartnerConfig buttonTextTypeFaceConfig,
+      PartnerConfig buttonTextStyleConfig) {
+    String fontFamilyName =
+        PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig);
+
+    int textStyleValue = Typeface.NORMAL;
+    if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextStyleConfig)) {
+      textStyleValue =
+          PartnerConfigHelper.get(context)
+              .getInteger(context, buttonTextStyleConfig, Typeface.NORMAL);
+    }
+    Typeface font = Typeface.create(fontFamilyName, textStyleValue);
+    if (font != null) {
+      button.setTypeface(font);
+    }
+  }
+
+  static void updateButtonRadiusWithPartnerConfig(
+      Context context, Button button, PartnerConfig buttonRadiusConfig) {
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
+      float radius = PartnerConfigHelper.get(context).getDimension(context, buttonRadiusConfig);
+      GradientDrawable gradientDrawable = getGradientDrawable(button);
+      if (gradientDrawable != null) {
+        gradientDrawable.setCornerRadius(radius);
+      }
+    }
+  }
+
+  static void updateButtonIconWithPartnerConfig(
+      Context context, Button button, PartnerConfig buttonIconConfig, boolean isPrimary) {
+    if (button == null) {
+      return;
+    }
+    Drawable icon = null;
+    if (buttonIconConfig != null) {
+      icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig);
+    }
+    setButtonIcon(button, icon, isPrimary);
+  }
+
+  private static void setButtonIcon(Button button, Drawable icon, boolean isPrimary) {
+    if (button == null) {
+      return;
+    }
+
+    if (icon != null) {
+      // TODO: restrict the icons to a reasonable size
+      int h = icon.getIntrinsicHeight();
+      int w = icon.getIntrinsicWidth();
+      icon.setBounds(0, 0, w, h);
+    }
+
+    Drawable iconStart = null;
+    Drawable iconEnd = null;
+    if (isPrimary) {
+      iconEnd = icon;
+    } else {
+      iconStart = icon;
+    }
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR1) {
+      button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null);
+    } else {
+      button.setCompoundDrawables(iconStart, null, iconEnd, null);
+    }
+  }
+
+  static void updateButtonBackground(Button button, @ColorInt int color) {
+    button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP);
+  }
+
+  @VisibleForTesting
+  public static GradientDrawable getGradientDrawable(Button button) {
+    // RippleDrawable is available after sdk 21, InsetDrawable#getDrawable is available after
+    // sdk 19. So check the sdk is higher than sdk 21 and since Stencil customization provider only
+    // works on Q+, there is no need to perform any customization for versions 21.
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+      Drawable drawable = button.getBackground();
+      if (drawable instanceof InsetDrawable) {
+        LayerDrawable layerDrawable = (LayerDrawable) ((InsetDrawable) drawable).getDrawable();
+        return (GradientDrawable) layerDrawable.getDrawable(0);
+      } else if (drawable instanceof RippleDrawable) {
+        InsetDrawable insetDrawable = (InsetDrawable) ((RippleDrawable) drawable).getDrawable(0);
+        return (GradientDrawable) insetDrawable.getDrawable();
+      }
+    }
+    return null;
+  }
+
+  static RippleDrawable getRippleDrawable(Button button) {
+    // RippleDrawable is available after sdk 21. And because on lower sdk the RippleDrawable is
+    // unavailable. Since Stencil customization provider only works on Q+, there is no need to
+    // perform any customization for versions 21.
+    if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
+      Drawable drawable = button.getBackground();
+      if (drawable instanceof InsetDrawable) {
+        return (RippleDrawable) ((InsetDrawable) drawable).getDrawable();
+      } else if (drawable instanceof RippleDrawable) {
+        return (RippleDrawable) drawable;
+      }
+    }
+    return null;
+  }
+
+  @ColorInt
+  private static int convertRgbToArgb(@ColorInt int color, float alpha) {
+    return Color.argb((int) (alpha * 255), Color.red(color), Color.green(color), Color.blue(color));
+  }
+
+  private FooterButtonStyleUtils() {}
+}
diff --git a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java
index a818793..c0f1c45 100644
--- a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java
+++ b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java
@@ -112,10 +112,14 @@
    */
   public void setStatusBarBackground(Drawable background) {
     if (partnerCustomizationLayout.shouldApplyPartnerResource()) {
+      // If full dynamic color enabled which means this activity is running outside of setup
+      // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3.
+      if (!partnerCustomizationLayout.useFullDynamicColor()) {
       Context context = partnerCustomizationLayout.getContext();
       background =
           PartnerConfigHelper.get(context)
               .getDrawable(context, PartnerConfig.CONFIG_STATUS_BAR_BACKGROUND);
+      }
     }
 
     if (statusBarLayout == null) {
diff --git a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java
index 8050406..32c708c 100644
--- a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java
+++ b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java
@@ -47,6 +47,7 @@
   private final TemplateLayout templateLayout;
   @Nullable private final Window windowOfActivity;
   @VisibleForTesting final boolean applyPartnerResources;
+  @VisibleForTesting final boolean useFullDynamicColor;
   private int sucSystemNavBarBackgroundColor = 0;
 
   /**
@@ -61,6 +62,10 @@
     this.applyPartnerResources =
         layout instanceof PartnerCustomizationLayout
             && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource();
+
+    this.useFullDynamicColor =
+        layout instanceof PartnerCustomizationLayout
+            && ((PartnerCustomizationLayout) layout).useFullDynamicColor();
   }
 
   /**
@@ -109,10 +114,14 @@
   public void setSystemNavBarBackground(int color) {
     if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && windowOfActivity != null) {
       if (applyPartnerResources) {
-        Context context = templateLayout.getContext();
-        color =
-            PartnerConfigHelper.get(context)
-                .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR);
+        // If full dynamic color enabled which means this activity is running outside of setup
+        // flow, the colors should refer to R.style.SudFullDynamicColorThemeGlifV3.
+        if (!useFullDynamicColor) {
+          Context context = templateLayout.getContext();
+          color =
+              PartnerConfigHelper.get(context)
+                  .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR);
+        }
       }
       windowOfActivity.setNavigationBarColor(color);
     }
diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java
index cacbf80..c10055e 100644
--- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java
+++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfig.java
@@ -120,10 +120,6 @@
   CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR(
       PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR, ResourceType.COLOR),
 
-  // Dynamic background color of the primary footer button
-  CONFIG_FOOTER_PRIMARY_BUTTON_BG_DYNAMIC_COLOR(
-      PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_DYNAMIC_COLOR, ResourceType.COLOR),
-
   // Text color of the primary footer button
   CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR(
       PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR, ResourceType.COLOR),
@@ -136,10 +132,6 @@
   CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR(
       PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR, ResourceType.COLOR),
 
-  // Dynamic text color of the secondary footer button
-  CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR(
-      PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR, ResourceType.COLOR),
-
   // Background color of layout
   CONFIG_LAYOUT_BACKGROUND_COLOR(PartnerConfigKey.KEY_LAYOUT_BACKGROUND_COLOR, ResourceType.COLOR),
 
@@ -394,7 +386,27 @@
 
   // The list of keypath and color map, applied to final hold animation when dark theme.
   CONFIG_LOTTIE_DARK_THEME_CUSTOMIZATION_FINAL_HOLD(
-      PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD, ResourceType.STRING_ARRAY);
+      PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD, ResourceType.STRING_ARRAY),
+
+  // The padding top of the content frame of loading layout.
+  CONFIG_LOADING_LAYOUT_PADDING_TOP(
+      PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP, ResourceType.DIMENSION),
+
+  // The padding start of the content frame of loading layout.
+  CONFIG_LOADING_LAYOUT_PADDING_START(
+      PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_START, ResourceType.DIMENSION),
+
+  // The padding end of the content frame of loading layout.
+  CONFIG_LOADING_LAYOUT_PADDING_END(
+      PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_END, ResourceType.DIMENSION),
+
+  // The padding bottom of the content frame of loading layout.
+  CONFIG_LOADING_LAYOUT_PADDING_BOTTOM(
+      PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM, ResourceType.DIMENSION),
+
+  // The height of the header of the loading layout.
+  CONFIG_LOADING_LAYOUT_HEADER_HEIGHT(
+      PartnerConfigKey.KEY_LOADING_LAYOUT_HEADER_HEIGHT, ResourceType.DIMENSION);
 
   /** Resource type of the partner resources type. */
   public enum ResourceType {
diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java
index dd43989..2ca8876 100644
--- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java
+++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigHelper.java
@@ -607,7 +607,7 @@
   }
 
   /** Returns true if the SetupWizard supports the dynamic color during setup flow. */
-  public static boolean shouldApplyDynamicColor(@NonNull Context context) {
+  public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) {
     if (applyDynamicColorBundle == null) {
       try {
         applyDynamicColorBundle =
diff --git a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java
index aa77ee8..a810908 100644
--- a/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java
+++ b/partnerconfig/java/com/google/android/setupcompat/partnerconfig/PartnerConfigKey.java
@@ -50,11 +50,9 @@
   PartnerConfigKey.KEY_FOOTER_BUTTON_DISABLED_ALPHA,
   PartnerConfigKey.KEY_FOOTER_BUTTON_DISABLED_BG_COLOR,
   PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR,
-  PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_BG_DYNAMIC_COLOR,
   PartnerConfigKey.KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR,
   PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_BG_COLOR,
   PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR,
-  PartnerConfigKey.KEY_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR,
   PartnerConfigKey.KEY_LAYOUT_BACKGROUND_COLOR,
   PartnerConfigKey.KEY_LAYOUT_MARGIN_START,
   PartnerConfigKey.KEY_LAYOUT_MARGIN_END,
@@ -123,6 +121,11 @@
   PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_UPDATE,
   PartnerConfigKey.KEY_LOADING_DARK_THEME_CUSTOMIZATION_FINAL_HOLD,
   PartnerConfigKey.KEY_TRANSITION_TYPE,
+  PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP,
+  PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_START,
+  PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_END,
+  PartnerConfigKey.KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM,
+  PartnerConfigKey.KEY_LOADING_LAYOUT_HEADER_HEIGHT,
 })
 // TODO: can be removed and always reference PartnerConfig.getResourceName()?
 @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
@@ -208,10 +211,6 @@
   // Background color of the primary footer button
   String KEY_FOOTER_PRIMARY_BUTTON_BG_COLOR = "setup_compat_footer_primary_button_bg_color";
 
-  // Dynamic background color of the primary footer button
-  String KEY_FOOTER_PRIMARY_BUTTON_BG_DYNAMIC_COLOR =
-      "setup_compat_footer_1st_button_bg_dynamic_color";
-
   // Text color of the primary footer button
   String KEY_FOOTER_PRIMARY_BUTTON_TEXT_COLOR = "setup_compat_footer_primary_button_text_color";
 
@@ -221,10 +220,6 @@
   // Text color of the secondary footer button
   String KEY_FOOTER_SECONDARY_BUTTON_TEXT_COLOR = "setup_compat_footer_secondary_button_text_color";
 
-  // Dynamic text color of the secondary footer button
-  String KEY_FOOTER_SECONDARY_BUTTON_TEXT_DYNAMIC_COLOR =
-      "setup_compat_footer_2nd_button_text_dynamic_color";
-
   // Background color of layout
   String KEY_LAYOUT_BACKGROUND_COLOR = "setup_design_layout_bg_color";
 
@@ -456,4 +451,19 @@
 
   // The transition type between activities
   String KEY_TRANSITION_TYPE = "setup_design_transition_type";
+
+  // A padding top of the content frame of loading layout.
+  String KEY_LOADING_LAYOUT_CONTENT_PADDING_TOP = "loading_layout_content_padding_top";
+
+  // A padding start of the content frame of loading layout.
+  String KEY_LOADING_LAYOUT_CONTENT_PADDING_START = "loading_layout_content_padding_start";
+
+  // A padding end of the content frame of loading layout.
+  String KEY_LOADING_LAYOUT_CONTENT_PADDING_END = "loading_layout_content_padding_end";
+
+  // A padding bottom of the content frame of loading layout.
+  String KEY_LOADING_LAYOUT_CONTENT_PADDING_BOTTOM = "loading_layout_content_padding_bottom";
+
+  // A height of the header of loading layout.
+  String KEY_LOADING_LAYOUT_HEADER_HEIGHT = "loading_layout_header_height";
 }