diff options
Diffstat (limited to 'main/java/com/google/android')
24 files changed, 1749 insertions, 339 deletions
diff --git a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java index fac4b39..e5ba0c5 100644 --- a/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java +++ b/main/java/com/google/android/setupcompat/PartnerCustomizationLayout.java @@ -26,7 +26,6 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.PersistableBundle; import android.util.AttributeSet; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -42,12 +41,14 @@ import com.google.android.setupcompat.template.FooterBarMixin; import com.google.android.setupcompat.template.FooterButton; import com.google.android.setupcompat.template.StatusBarMixin; import com.google.android.setupcompat.template.SystemNavBarMixin; +import com.google.android.setupcompat.util.BuildCompatUtils; +import com.google.android.setupcompat.util.Logger; import com.google.android.setupcompat.util.WizardManagerHelper; /** A templatization layout with consistent style used in Setup Wizard or app itself. */ public class PartnerCustomizationLayout extends TemplateLayout { - // Log tags can have at most 23 characters on N or before. - private static final String TAG = "PartnerCustomizedLayout"; + + private static final Logger LOG = new Logger("PartnerCustomizationLayout"); /** * Attribute indicating whether usage of partner theme resources is allowed. This corresponds to @@ -56,6 +57,18 @@ public class PartnerCustomizationLayout extends TemplateLayout { */ private boolean usePartnerResourceAttr; + /** + * Attribute indicating whether using full dynamic colors or not. This corresponds to the {@code + * app:sucFullDynamicColor} XML attribute. + */ + private boolean useFullDynamicColorAttr; + + /** + * Attribute indicating whether usage of dynamic is allowed. This corresponds to the existence of + * {@code app:sucFullDynamicColor} XML attribute. + */ + private boolean useDynamicColor; + private Activity activity; public PartnerCustomizationLayout(Context context) { @@ -83,6 +96,9 @@ public class PartnerCustomizationLayout extends TemplateLayout { } private void init(AttributeSet attrs, int defStyleAttr) { + if (isInEditMode()) { + return; + } TypedArray a = getContext() @@ -132,15 +148,13 @@ public class PartnerCustomizationLayout extends TemplateLayout { @Override protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) { - boolean isSetupFlow; - // Sets default value to true since this timing // before PartnerCustomization members initialization usePartnerResourceAttr = true; activity = lookupActivityFromContext(getContext()); - isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent()); + boolean isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent()); TypedArray a = getContext() @@ -149,27 +163,32 @@ public class PartnerCustomizationLayout extends TemplateLayout { if (!a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource)) { // TODO: Enable Log.WTF after other client already set sucUsePartnerResource. - Log.e(TAG, "Attribute sucUsePartnerResource not found in " + activity.getComponentName()); + LOG.e("Attribute sucUsePartnerResource not found in " + activity.getComponentName()); } usePartnerResourceAttr = isSetupFlow || a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource, true); + useDynamicColor = a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor); + useFullDynamicColorAttr = + a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucFullDynamicColor, false); + a.recycle(); - if (Log.isLoggable(TAG, Log.DEBUG)) { - Log.d( - TAG, - "activity=" - + activity.getClass().getSimpleName() - + " isSetupFlow=" - + isSetupFlow - + " enablePartnerResourceLoading=" - + enablePartnerResourceLoading() - + " usePartnerResourceAttr=" - + usePartnerResourceAttr); - } + LOG.atDebug( + "activity=" + + activity.getClass().getSimpleName() + + " isSetupFlow=" + + isSetupFlow + + " enablePartnerResourceLoading=" + + enablePartnerResourceLoading() + + " usePartnerResourceAttr=" + + usePartnerResourceAttr + + " useDynamicColor=" + + useDynamicColor + + " useFullDynamicColorAttr=" + + useFullDynamicColorAttr); } @Override @@ -216,7 +235,7 @@ public class PartnerCustomizationLayout extends TemplateLayout { } } - private static Activity lookupActivityFromContext(Context context) { + public static Activity lookupActivityFromContext(Context context) { if (context instanceof Activity) { return (Activity) context; } else if (context instanceof ContextWrapper) { @@ -252,4 +271,30 @@ public class PartnerCustomizationLayout extends TemplateLayout { } return true; } + + /** + * Returns {@code true} if the current layout/activity applies dynamic color. Otherwise, returns + * {@code false}. + */ + public boolean shouldApplyDynamicColor() { + if (!useDynamicColor) { + return false; + } + if (!BuildCompatUtils.isAtLeastS()) { + return false; + } + if (!PartnerConfigHelper.get(getContext()).isAvailable()) { + return false; + } + return true; + } + + /** + * 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/internal/FallbackThemeWrapper.java b/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java index af17a62..574f614 100644 --- a/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java +++ b/main/java/com/google/android/setupcompat/internal/FallbackThemeWrapper.java @@ -18,8 +18,8 @@ package com.google.android.setupcompat.internal; import android.content.Context; import android.content.res.Resources.Theme; -import androidx.annotation.StyleRes; import android.view.ContextThemeWrapper; +import androidx.annotation.StyleRes; /** * Same as {@link ContextThemeWrapper}, but the base context's theme attributes take precedence over diff --git a/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java b/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java index 39b50cf..5f8bf67 100644 --- a/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java +++ b/main/java/com/google/android/setupcompat/internal/FooterButtonPartnerConfig.java @@ -27,7 +27,9 @@ public class FooterButtonPartnerConfig { private final PartnerConfig buttonIconConfig; private final PartnerConfig buttonTextColorConfig; private final PartnerConfig buttonTextSizeConfig; + private final PartnerConfig buttonMinHeightConfig; private final PartnerConfig buttonTextTypeFaceConfig; + private final PartnerConfig buttonTextStyleConfig; private final PartnerConfig buttonRadiusConfig; private final PartnerConfig buttonRippleColorAlphaConfig; private final int partnerTheme; @@ -40,14 +42,18 @@ public class FooterButtonPartnerConfig { PartnerConfig buttonIconConfig, PartnerConfig buttonTextColorConfig, PartnerConfig buttonTextSizeConfig, + PartnerConfig buttonMinHeightConfig, PartnerConfig buttonTextTypeFaceConfig, + PartnerConfig buttonTextStyleConfig, PartnerConfig buttonRadiusConfig, PartnerConfig buttonRippleColorAlphaConfig) { this.partnerTheme = partnerTheme; this.buttonTextColorConfig = buttonTextColorConfig; this.buttonTextSizeConfig = buttonTextSizeConfig; + this.buttonMinHeightConfig = buttonMinHeightConfig; this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig; + this.buttonTextStyleConfig = buttonTextStyleConfig; this.buttonBackgroundConfig = buttonBackgroundConfig; this.buttonDisableAlphaConfig = buttonDisableAlphaConfig; this.buttonDisableBackgroundConfig = buttonDisableBackgroundConfig; @@ -80,6 +86,10 @@ public class FooterButtonPartnerConfig { return buttonTextColorConfig; } + public PartnerConfig getButtonMinHeightConfig() { + return buttonMinHeightConfig; + } + public PartnerConfig getButtonTextSizeConfig() { return buttonTextSizeConfig; } @@ -88,6 +98,10 @@ public class FooterButtonPartnerConfig { return buttonTextTypeFaceConfig; } + public PartnerConfig getButtonTextStyleConfig() { + return buttonTextStyleConfig; + } + public PartnerConfig getButtonRadiusConfig() { return buttonRadiusConfig; } @@ -105,15 +119,19 @@ public class FooterButtonPartnerConfig { private PartnerConfig buttonIconConfig = null; private PartnerConfig buttonTextColorConfig = null; private PartnerConfig buttonTextSizeConfig = null; + private PartnerConfig buttonMinHeight = null; private PartnerConfig buttonTextTypeFaceConfig = null; + private PartnerConfig buttonTextStyleConfig = null; private PartnerConfig buttonRadiusConfig = null; private PartnerConfig buttonRippleColorAlphaConfig = null; private int partnerTheme; public Builder(FooterButton footerButton) { this.footerButton = footerButton; - // default partnerTheme should be the same as footerButton.getTheme(); - this.partnerTheme = this.footerButton.getTheme(); + if (this.footerButton != null) { + // default partnerTheme should be the same as footerButton.getTheme(); + this.partnerTheme = this.footerButton.getTheme(); + } } public Builder setButtonBackgroundConfig(PartnerConfig buttonBackgroundConfig) { @@ -146,11 +164,21 @@ public class FooterButtonPartnerConfig { return this; } + public Builder setButtonMinHeight(PartnerConfig buttonMinHeightConfig) { + this.buttonMinHeight = buttonMinHeightConfig; + return this; + } + public Builder setTextTypeFaceConfig(PartnerConfig buttonTextTypeFaceConfig) { this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig; return this; } + public Builder setTextStyleConfig(PartnerConfig buttonTextStyleConfig) { + this.buttonTextStyleConfig = buttonTextStyleConfig; + return this; + } + public Builder setButtonRadiusConfig(PartnerConfig buttonRadiusConfig) { this.buttonRadiusConfig = buttonRadiusConfig; return this; @@ -175,7 +203,9 @@ public class FooterButtonPartnerConfig { buttonIconConfig, buttonTextColorConfig, buttonTextSizeConfig, + buttonMinHeight, buttonTextTypeFaceConfig, + buttonTextStyleConfig, buttonRadiusConfig, buttonRippleColorAlphaConfig); } diff --git a/main/java/com/google/android/setupcompat/internal/PersistableBundles.java b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java index 1197645..3b7d5a5 100644 --- a/main/java/com/google/android/setupcompat/internal/PersistableBundles.java +++ b/main/java/com/google/android/setupcompat/internal/PersistableBundles.java @@ -22,16 +22,18 @@ import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.PersistableBundle; import android.util.ArrayMap; -import android.util.Log; +import com.google.android.setupcompat.util.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; /** Contains utility methods related to {@link PersistableBundle}. */ -@TargetApi(VERSION_CODES.Q) +@TargetApi(VERSION_CODES.LOLLIPOP_MR1) public final class PersistableBundles { + private static final Logger LOG = new Logger("PersistableBundles"); + /** * Merges two or more {@link PersistableBundle}. Ensures no conflict of keys occurred during * merge. @@ -121,7 +123,7 @@ public final class PersistableBundles { for (String key : baseBundle.keySet()) { Object value = baseBundle.get(key); if (!isSupportedDataType(value)) { - Log.w(TAG, String.format("Unknown/unsupported data type [%s] for key %s", value, key)); + LOG.w(String.format("Unknown/unsupported data type [%s] for key %s", value, key)); continue; } map.put(key, baseBundle.get(key)); @@ -141,6 +143,4 @@ public final class PersistableBundles { private PersistableBundles() { throw new AssertionError("Should not be instantiated"); } - - private static final String TAG = "SetupCompat.PersistBls"; } diff --git a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java index a1ca156..149da54 100644 --- a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java +++ b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceInvoker.java @@ -21,9 +21,9 @@ import android.content.Context; import android.os.Bundle; import android.os.RemoteException; import androidx.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.setupcompat.ISetupCompatService; import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType; +import com.google.android.setupcompat.util.Logger; import java.util.concurrent.ExecutorService; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; @@ -41,11 +41,14 @@ import java.util.concurrent.TimeoutException; */ public class SetupCompatServiceInvoker { + private static final Logger LOG = new Logger("SetupCompatServiceInvoker"); + + @SuppressLint("DefaultLocale") public void logMetricEvent(@MetricType int metricType, Bundle args) { try { loggingExecutor.execute(() -> invokeLogMetric(metricType, args)); } catch (RejectedExecutionException e) { - Log.e(TAG, String.format("Metric of type %d dropped since queue is full.", metricType), e); + LOG.e(String.format("Metric of type %d dropped since queue is full.", metricType), e); } } @@ -53,7 +56,7 @@ public class SetupCompatServiceInvoker { try { setupCompatExecutor.execute(() -> invokeBindBack(screenName, bundle)); } catch (RejectedExecutionException e) { - Log.e(TAG, String.format("Screen %s bind back fail.", screenName), e); + LOG.e(String.format("Screen %s bind back fail.", screenName), e); } } @@ -66,10 +69,10 @@ public class SetupCompatServiceInvoker { if (setupCompatService != null) { setupCompatService.logMetric(metricType, args, Bundle.EMPTY); } else { - Log.w(TAG, "logMetric failed since service reference is null. Are the permissions valid?"); + LOG.w("logMetric failed since service reference is null. Are the permissions valid?"); } - } catch (InterruptedException | TimeoutException | RemoteException e) { - Log.e(TAG, String.format("Exception occurred while trying to log metric = [%s]", args), e); + } catch (InterruptedException | TimeoutException | RemoteException | IllegalStateException e) { + LOG.e(String.format("Exception occurred while trying to log metric = [%s]", args), e); } } @@ -81,11 +84,10 @@ public class SetupCompatServiceInvoker { if (setupCompatService != null) { setupCompatService.validateActivity(screenName, bundle); } else { - Log.w(TAG, "BindBack failed since service reference is null. Are the permissions valid?"); + LOG.w("BindBack failed since service reference is null. Are the permissions valid?"); } } catch (InterruptedException | TimeoutException | RemoteException e) { - Log.e( - TAG, + LOG.e( String.format("Exception occurred while %s trying bind back to SetupWizard.", screenName), e); } @@ -125,5 +127,4 @@ public class SetupCompatServiceInvoker { private static SetupCompatServiceInvoker instance; private static final long MAX_WAIT_TIME_FOR_CONNECTION_MS = TimeUnit.SECONDS.toMillis(10); - private static final String TAG = "SucServiceInvoker"; } diff --git a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java index 2043a81..e75d991 100644 --- a/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java +++ b/main/java/com/google/android/setupcompat/internal/SetupCompatServiceProvider.java @@ -26,8 +26,8 @@ import android.os.Looper; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.Log; import com.google.android.setupcompat.ISetupCompatService; +import com.google.android.setupcompat.util.Logger; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -40,6 +40,8 @@ import java.util.function.UnaryOperator; */ public class SetupCompatServiceProvider { + private static final Logger LOG = new Logger("SetupCompatServiceProvider"); + /** * Returns an instance of {@link ISetupCompatService} if one already exists. If not, attempts to * rebind if the current state allows such an operation and waits until {@code waitTime} for @@ -94,7 +96,7 @@ public class SetupCompatServiceProvider { } CountDownLatch connectedStateLatch = getConnectedCondition(); - Log.i(TAG, "Waiting for service to get connected"); + LOG.atInfo("Waiting for service to get connected"); boolean stateChanged = connectedStateLatch.await(timeout, timeUnit); if (!stateChanged) { // Even though documentation states that disconnected service should connect again, @@ -104,13 +106,10 @@ public class SetupCompatServiceProvider { String.format("Failed to acquire connection after [%s %s]", timeout, timeUnit)); } currentServiceState = getCurrentServiceState(); - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i( - TAG, - String.format( - "Finished waiting for service to get connected. Current state = %s", - currentServiceState.state)); - } + LOG.atInfo( + String.format( + "Finished waiting for service to get connected. Current state = %s", + currentServiceState.state)); return currentServiceState.compatService; } @@ -126,11 +125,11 @@ public class SetupCompatServiceProvider { private synchronized void requestServiceBind() { ServiceContext currentServiceState = getCurrentServiceState(); if (currentServiceState.state == State.CONNECTED) { - Log.i(TAG, "Refusing to rebind since current state is already connected"); + LOG.atInfo("Refusing to rebind since current state is already connected"); return; } if (currentServiceState.state != State.NOT_STARTED) { - Log.i(TAG, "Unbinding existing service connection."); + LOG.atInfo("Unbinding existing service connection."); context.unbindService(serviceConnection); } @@ -139,7 +138,7 @@ public class SetupCompatServiceProvider { bindAllowed = context.bindService(COMPAT_SERVICE_INTENT, serviceConnection, Context.BIND_AUTO_CREATE); } catch (SecurityException e) { - Log.e(TAG, "Unable to bind to compat service", e); + LOG.e("Unable to bind to compat service. " + e); bindAllowed = false; } @@ -149,12 +148,12 @@ public class SetupCompatServiceProvider { // in the normal world if (getCurrentState() != State.CONNECTED) { swapServiceContextAndNotify(new ServiceContext(State.BINDING)); - Log.i(TAG, "Context#bindService went through, now waiting for service connection"); + LOG.atInfo("Context#bindService went through, now waiting for service connection"); } } else { // SetupWizard is not installed/calling app does not have permissions to bind. swapServiceContextAndNotify(new ServiceContext(State.BIND_FAILED)); - Log.e(TAG, "Context#bindService did not succeed."); + LOG.e("Context#bindService did not succeed."); } } @@ -174,12 +173,9 @@ public class SetupCompatServiceProvider { } private void swapServiceContextAndNotify(ServiceContext latestServiceContext) { - if (Log.isLoggable(TAG, Log.INFO)) { - Log.i( - TAG, - String.format( - "State changed: %s -> %s", serviceContext.state, latestServiceContext.state)); - } + LOG.atInfo( + String.format("State changed: %s -> %s", serviceContext.state, latestServiceContext.state)); + serviceContext = latestServiceContext; CountDownLatch countDownLatch = getAndClearConnectedCondition(); if (countDownLatch != null) { @@ -221,7 +217,7 @@ public class SetupCompatServiceProvider { State state = State.CONNECTED; if (binder == null) { state = State.DISCONNECTED; - Log.w(TAG, "Binder is null when onServiceConnected was called!"); + LOG.w("Binder is null when onServiceConnected was called!"); } swapServiceContextAndNotify( new ServiceContext(state, ISetupCompatService.Stub.asInterface(binder))); @@ -336,6 +332,4 @@ public class SetupCompatServiceProvider { // lint error. @SuppressLint("StaticFieldLeak") private static volatile SetupCompatServiceProvider instance; - - private static final String TAG = "SucServiceProvider"; } diff --git a/main/java/com/google/android/setupcompat/internal/TemplateLayout.java b/main/java/com/google/android/setupcompat/internal/TemplateLayout.java index 34179d6..25a3c5b 100644 --- a/main/java/com/google/android/setupcompat/internal/TemplateLayout.java +++ b/main/java/com/google/android/setupcompat/internal/TemplateLayout.java @@ -20,15 +20,15 @@ import android.annotation.TargetApi; import android.content.Context; import android.content.res.TypedArray; import android.os.Build.VERSION_CODES; -import androidx.annotation.Keep; -import androidx.annotation.LayoutRes; -import androidx.annotation.StyleRes; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; +import androidx.annotation.Keep; +import androidx.annotation.LayoutRes; +import androidx.annotation.StyleRes; import com.google.android.setupcompat.R; import com.google.android.setupcompat.template.Mixin; import java.util.HashMap; diff --git a/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java index 7d0b731..2aa1240 100644 --- a/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java +++ b/main/java/com/google/android/setupcompat/logging/internal/PartnerCustomizedResourceListMetric.java @@ -30,24 +30,28 @@ import com.google.android.setupcompat.logging.SetupMetricsLogger; @TargetApi(VERSION_CODES.Q) public class PartnerCustomizedResourceListMetric { - public static void logMetrics(Context context, String screenName, Bundle bundle) { + public static void logMetrics(Context context, String deviceDisplayName, Bundle bundle) { PersistableBundle logBundle = - buildLogBundleFromResourceConfigBundle(context.getPackageName(), bundle); + buildLogBundleFromResourceConfigBundle(context.getPackageName(), deviceDisplayName, bundle); if (!logBundle.isEmpty()) { SetupMetricsLogger.logCustomEvent( context, - CustomEvent.create(MetricKey.get("PartnerCustomizationResource", screenName), logBundle)); + CustomEvent.create( + MetricKey.get("PartnerCustomizationResource", "NoScreenName"), logBundle)); } } @VisibleForTesting public static PersistableBundle buildLogBundleFromResourceConfigBundle( - String defaultPackageName, Bundle resourceConfigBundle) { + String defaultPackageName, String deviceDisplayName, Bundle resourceConfigBundle) { PersistableBundle persistableBundle = new PersistableBundle(); + persistableBundle.putString("deviceDisplayName", deviceDisplayName); for (String key : resourceConfigBundle.keySet()) { Bundle resourceExtra = resourceConfigBundle.getBundle(key); if (!resourceExtra.getString("packageName", defaultPackageName).equals(defaultPackageName)) { persistableBundle.putBoolean(resourceExtra.getString("resourceName", key), true); + } else { + persistableBundle.putBoolean(resourceExtra.getString("resourceName", key), false); } } 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..4d1965a --- /dev/null +++ b/main/java/com/google/android/setupcompat/portal/PortalHelper.java @@ -0,0 +1,301 @@ +/* + * 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 androidx.annotation.NonNull; +import com.google.android.setupcompat.internal.Preconditions; +import com.google.android.setupcompat.portal.PortalConstants.RemainingValues; +import com.google.android.setupcompat.util.Logger; + +/** This class is responsible for safely executing methods on SetupNotificationService. */ +public class PortalHelper { + + private static final Logger LOG = new Logger("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("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("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.e("Failed to invoke SetupNotificationService#isPortalAvailable"); + listener.onResult(false); + } + } + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + + if (!bindSetupNotificationService(context, connection)) { + LOG.e( + "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("Failed to invoke SetupNotificationService#isProgressServiceAlive"); + listener.onResult(false); + } + } + context.unbindService(this); + } + + @Override + public void onServiceDisconnected(ComponentName name) {} + }; + + if (!bindSetupNotificationService(context, connection)) { + LOG.e( + "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/FooterActionButton.java b/main/java/com/google/android/setupcompat/template/FooterActionButton.java index bb26d19..86a06d9 100644 --- a/main/java/com/google/android/setupcompat/template/FooterActionButton.java +++ b/main/java/com/google/android/setupcompat/template/FooterActionButton.java @@ -18,11 +18,11 @@ package com.google.android.setupcompat.template; import android.annotation.SuppressLint; import android.content.Context; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.widget.Button; +import androidx.annotation.Nullable; /** Button that can react to touch when disabled. */ public class FooterActionButton extends Button { diff --git a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java index bc9e5c1..b75d972 100644 --- a/main/java/com/google/android/setupcompat/template/FooterBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/FooterBarMixin.java @@ -24,16 +24,17 @@ 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.os.PersistableBundle; +import android.util.AttributeSet; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewStub; +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.LinearLayout.LayoutParams; import androidx.annotation.AttrRes; import androidx.annotation.CallSuper; import androidx.annotation.ColorInt; @@ -44,25 +45,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; import androidx.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.util.StateSet; -import android.util.TypedValue; -import android.view.ContextThemeWrapper; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewStub; -import android.widget.Button; -import android.widget.LinearLayout; -import android.widget.LinearLayout.LayoutParams; import com.google.android.setupcompat.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; import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper; import com.google.android.setupcompat.template.FooterButton.ButtonType; +import java.util.Locale; import java.util.concurrent.atomic.AtomicInteger; /** @@ -77,6 +68,8 @@ public class FooterBarMixin implements Mixin { @Nullable private final ViewStub footerStub; @VisibleForTesting final boolean applyPartnerResources; + @VisibleForTesting final boolean applyDynamicColor; + @VisibleForTesting final boolean useFullDynamicColor; private LinearLayout buttonContainer; private FooterButton primaryButton; @@ -94,8 +87,8 @@ public class FooterBarMixin implements Mixin { @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(); @@ -110,10 +103,10 @@ public class FooterBarMixin implements Mixin { Button button = buttonContainer.findViewById(id); if (button != null) { button.setEnabled(enabled); - if (applyPartnerResources) { - updateButtonTextColorWithPartnerConfig( + if (applyPartnerResources && !applyDynamicColor) { + updateButtonTextColorWithEnabledState( button, - (id == primaryButtonId) + (id == primaryButtonId || isSecondaryButtonInPrimaryStyle) ? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR : PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR); } @@ -141,6 +134,25 @@ public class FooterBarMixin implements Mixin { } } } + + @Override + @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) + public void onLocaleChanged(Locale locale) { + if (buttonContainer != null) { + Button button = buttonContainer.findViewById(id); + if (button != null && locale != null) { + button.setTextLocale(locale); + } + } + } + + @Override + @TargetApi(VERSION_CODES.JELLY_BEAN_MR1) + public void onDirectionChanged(int direction) { + if (buttonContainer != null && direction != -1) { + buttonContainer.setLayoutDirection(direction); + } + } }; } @@ -159,6 +171,14 @@ public class FooterBarMixin implements Mixin { layout instanceof PartnerCustomizationLayout && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource(); + applyDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).shouldApplyDynamicColor(); + + useFullDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).useFullDynamicColor(); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterBarMixin, defStyleAttr, 0); defaultPadding = @@ -253,11 +273,14 @@ public class FooterBarMixin implements Mixin { return; } - @ColorInt - int color = - PartnerConfigHelper.get(context) - .getColor(context, PartnerConfig.CONFIG_FOOTER_BAR_BG_COLOR); - buttonContainer.setBackgroundColor(color); + // 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) @@ -273,6 +296,17 @@ public class FooterBarMixin implements Mixin { footerBarPaddingTop, buttonContainer.getPaddingRight(), footerBarPaddingBottom); + + if (PartnerConfigHelper.get(context) + .isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT)) { + int minHeight = + (int) + PartnerConfigHelper.get(context) + .getDimension(context, PartnerConfig.CONFIG_FOOTER_BAR_MIN_HEIGHT); + if (minHeight > 0) { + buttonContainer.setMinimumHeight(minHeight); + } + } } /** @@ -310,7 +344,9 @@ public class FooterBarMixin implements Mixin { .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR) .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) .build(); FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig); @@ -346,7 +382,14 @@ public class FooterBarMixin implements Mixin { /** 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 @@ -355,18 +398,29 @@ public class FooterBarMixin implements Mixin { .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(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) + .setTextColorConfig( + 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) .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) .build(); FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig); @@ -395,6 +449,16 @@ public class FooterBarMixin implements Mixin { 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(); @@ -411,7 +475,7 @@ public class FooterBarMixin implements Mixin { 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 } @@ -544,187 +608,30 @@ public class FooterBarMixin implements Mixin { if (!applyPartnerResources) { return; } - updateButtonTextColorWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextColorConfig()); - updateButtonTextSizeWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextSizeConfig()); - updateButtonTypeFaceWithPartnerConfig( - button, footerButtonPartnerConfig.getButtonTextTypeFaceConfig()); - updateButtonBackgroundWithPartnerConfig( + FooterButtonStyleUtils.applyButtonPartnerResources( + context, button, - footerButtonPartnerConfig.getButtonBackgroundConfig(), - footerButtonPartnerConfig.getButtonDisableAlphaConfig(), - footerButtonPartnerConfig.getButtonDisableBackgroundConfig()); - updateButtonRadiusWithPartnerConfig(button, footerButtonPartnerConfig.getButtonRadiusConfig()); - updateButtonIconWithPartnerConfig(button, footerButtonPartnerConfig.getButtonIconConfig()); - updateButtonRippleColorWithPartnerConfig(button, footerButtonPartnerConfig); + applyDynamicColor, + /* isButtonIconAtEnd= */ (button.getId() == primaryButtonId), + footerButtonPartnerConfig); + if (!applyDynamicColor) { + // adjust text color based on enabled state + updateButtonTextColorWithEnabledState( + button, footerButtonPartnerConfig.getButtonTextColorConfig()); + } } - private void updateButtonTextColorWithPartnerConfig( + private void updateButtonTextColorWithEnabledState( 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 updateButtonTypeFaceWithPartnerConfig( - Button button, PartnerConfig buttonTextTypeFaceConfig) { - String fontFamilyName = - PartnerConfigHelper.get(context).getString(context, buttonTextTypeFaceConfig); - Typeface font = Typeface.create(fontFamilyName, Typeface.NORMAL); - 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); } } @@ -763,43 +670,6 @@ public class FooterBarMixin implements Mixin { 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/FooterButton.java b/main/java/com/google/android/setupcompat/template/FooterButton.java index 2fa8c7c..90c13ec 100644 --- a/main/java/com/google/android/setupcompat/template/FooterButton.java +++ b/main/java/com/google/android/setupcompat/template/FooterButton.java @@ -23,17 +23,18 @@ import android.content.Context; import android.content.res.TypedArray; import android.os.Build.VERSION_CODES; import android.os.PersistableBundle; +import android.util.AttributeSet; +import android.view.View; +import android.view.View.OnClickListener; import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.StyleRes; -import android.util.AttributeSet; -import android.view.View; -import android.view.View.OnClickListener; import com.google.android.setupcompat.R; import com.google.android.setupcompat.logging.CustomEvent; import java.lang.annotation.Retention; +import java.util.Locale; /** * Definition of a footer button. Clients can use this class to customize attributes like text, @@ -53,6 +54,8 @@ public final class FooterButton implements OnClickListener { private OnClickListener onClickListenerWhenDisabled; private OnButtonEventListener buttonListener; private int clickCount = 0; + private Locale locale; + private int direction; public FooterButton(Context context, AttributeSet attrs) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterButton); @@ -78,11 +81,15 @@ public final class FooterButton implements OnClickListener { CharSequence text, @Nullable OnClickListener listener, @ButtonType int buttonType, - @StyleRes int theme) { + @StyleRes int theme, + Locale locale, + int direction) { this.text = text; onClickListener = listener; this.buttonType = buttonType; this.theme = theme; + this.locale = locale; + this.direction = direction; } /** Returns the text that this footer button is displaying. */ @@ -142,6 +149,16 @@ public final class FooterButton implements OnClickListener { return enabled; } + /** Returns the layout direction for this footer button. */ + public int getLayoutDirection() { + return direction; + } + + /** Returns the text locale for this footer button. */ + public Locale getTextLocale() { + return locale; + } + /** * Sets the visibility state of this footer button. * @@ -172,6 +189,22 @@ public final class FooterButton implements OnClickListener { } } + /** Sets the text locale to be displayed on footer button. */ + public void setTextLocale(Locale locale) { + this.locale = locale; + if (buttonListener != null) { + buttonListener.onLocaleChanged(locale); + } + } + + /** Sets the layout direction to be displayed on footer button. */ + public void setLayoutDirection(int direction) { + this.direction = direction; + if (buttonListener != null) { + buttonListener.onDirectionChanged(direction); + } + } + /** * Registers a callback to be invoked when footer button API has set. * @@ -201,6 +234,10 @@ public final class FooterButton implements OnClickListener { void onVisibilityChanged(int visibility); void onTextChanged(CharSequence text); + + void onLocaleChanged(Locale locale); + + void onDirectionChanged(int direction); } /** Maximum valid value of ButtonType */ @@ -308,12 +345,16 @@ public final class FooterButton implements OnClickListener { * .setListener(primaryButton) * .setButtonType(ButtonType.NEXT) * .setTheme(R.style.SuwGlifButton_Primary) + * .setTextLocale(Locale.CANADA) + * .setLayoutDirection(View.LAYOUT_DIRECTION_LTR) * .build(); * </pre> */ public static class Builder { private final Context context; private String text = ""; + private Locale locale = null; + private int direction = -1; private OnClickListener onClickListener = null; @ButtonType private int buttonType = ButtonType.OTHER; private int theme = 0; @@ -334,6 +375,18 @@ public final class FooterButton implements OnClickListener { return this; } + /** Sets the {@code locale} of FooterButton. */ + public Builder setTextLocale(Locale locale) { + this.locale = locale; + return this; + } + + /** Sets the {@code direction} of FooterButton. */ + public Builder setLayoutDirection(int direction) { + this.direction = direction; + return this; + } + /** Sets the {@code listener} of FooterButton. */ public Builder setListener(@Nullable OnClickListener listener) { onClickListener = listener; @@ -353,7 +406,7 @@ public final class FooterButton implements OnClickListener { } public FooterButton build() { - return new FooterButton(text, onClickListener, buttonType, theme); + return new FooterButton(text, onClickListener, buttonType, theme, locale, direction); } } } diff --git a/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java b/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java index fe2538b..10aa052 100644 --- a/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java +++ b/main/java/com/google/android/setupcompat/template/FooterButtonInflater.java @@ -19,10 +19,10 @@ package com.google.android.setupcompat.template; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; -import androidx.annotation.NonNull; import android.util.AttributeSet; import android.util.Xml; import android.view.InflateException; +import androidx.annotation.NonNull; import java.io.IOException; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; 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..ef45b5c --- /dev/null +++ b/main/java/com/google/android/setupcompat/template/FooterButtonStyleUtils.java @@ -0,0 +1,412 @@ +/* + * 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.R; +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; + + /** Apply the partner primary button style to given {@code button}. */ + public static void applyPrimaryButtonPartnerResource( + Context context, Button button, boolean applyDynamicColor) { + + FooterButtonPartnerConfig footerButtonPartnerConfig = + new FooterButtonPartnerConfig.Builder(null) + .setPartnerTheme(R.style.SucPartnerCustomizationButton_Primary) + .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR) + .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) + .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) + .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) + .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) + .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR) + .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) + .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) + .build(); + applyButtonPartnerResources( + context, + button, + applyDynamicColor, + /* isButtonIconAtEnd= */ true, + footerButtonPartnerConfig); + } + + /** Apply the partner secondary button style to given {@code button}. */ + public static void applySecondaryButtonPartnerResource( + Context context, Button button, boolean applyDynamicColor) { + + int defaultTheme = R.style.SucPartnerCustomizationButton_Secondary; + int color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR); + if (color != Color.TRANSPARENT) { + defaultTheme = R.style.SucPartnerCustomizationButton_Primary; + } + // Setup button partner config + FooterButtonPartnerConfig footerButtonPartnerConfig = + new FooterButtonPartnerConfig.Builder(null) + .setPartnerTheme(defaultTheme) + .setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_BG_COLOR) + .setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA) + .setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR) + .setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS) + .setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA) + .setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR) + .setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE) + .setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT) + .setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY) + .setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE) + .build(); + applyButtonPartnerResources( + context, + button, + applyDynamicColor, + /* isButtonIconAtEnd= */ false, + footerButtonPartnerConfig); + } + + static void applyButtonPartnerResources( + Context context, + Button button, + boolean applyDynamicColor, + boolean isButtonIconAtEnd, + FooterButtonPartnerConfig footerButtonPartnerConfig) { + + // If dynamic color enabled, these colors won't be overrode by partner config. + // Instead, these colors align with the current theme colors. + if (!applyDynamicColor) { + // use default disable color util we support the partner disable text color + if (button.isEnabled()) { + FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonTextColorConfig()); + } + FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig( + context, + button, + footerButtonPartnerConfig.getButtonBackgroundConfig(), + footerButtonPartnerConfig.getButtonDisableAlphaConfig(), + footerButtonPartnerConfig.getButtonDisableBackgroundConfig()); + } + FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig( + context, + button, + applyDynamicColor, + footerButtonPartnerConfig.getButtonTextColorConfig(), + footerButtonPartnerConfig.getButtonRippleColorAlphaConfig()); + FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonTextSizeConfig()); + FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonMinHeightConfig()); + FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig( + context, + button, + footerButtonPartnerConfig.getButtonTextTypeFaceConfig(), + footerButtonPartnerConfig.getButtonTextStyleConfig()); + FooterButtonStyleUtils.updateButtonRadiusWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonRadiusConfig()); + FooterButtonStyleUtils.updateButtonIconWithPartnerConfig( + context, button, footerButtonPartnerConfig.getButtonIconConfig(), isButtonIconAtEnd); + } + + static void updateButtonTextEnabledColorWithPartnerConfig( + Context context, Button button, PartnerConfig buttonEnableTextColorConfig) { + @ColorInt + int color = PartnerConfigHelper.get(context).getColor(context, buttonEnableTextColorConfig); + updateButtonTextEnabledColor(button, color); + } + + static void updateButtonTextEnabledColor(Button button, @ColorInt int textColor) { + if (textColor != Color.TRANSPARENT) { + button.setTextColor(ColorStateList.valueOf(textColor)); + } + } + + static void updateButtonTextDisableColor(Button button, ColorStateList disabledTextColor) { + // TODO : add disable footer button text color partner config + + // disable state will use the default disable state color + button.setTextColor(disabledTextColor); + } + + @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 color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundConfig); + float disabledAlpha = + PartnerConfigHelper.get(context).getFraction(context, buttonDisableAlphaConfig, 0f); + @ColorInt + int disabledColor = + PartnerConfigHelper.get(context).getColor(context, buttonDisableBackgroundConfig); + + updateButtonBackgroundTintList(context, button, color, disabledAlpha, disabledColor); + } + + @TargetApi(VERSION_CODES.Q) + static void updateButtonBackgroundTintList( + Context context, + Button button, + @ColorInt int color, + float disabledAlpha, + @ColorInt int disabledColor) { + int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled}; + int[] ENABLED_STATE_SET = {}; + + 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); + } + } + + @TargetApi(VERSION_CODES.Q) + static void updateButtonRippleColorWithPartnerConfig( + Context context, + Button button, + boolean applyDynamicColor, + PartnerConfig buttonTextColorConfig, + PartnerConfig buttonRippleColorAlphaConfig) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { + + @ColorInt int textDefaultColor; + if (applyDynamicColor) { + // Get dynamic text color + textDefaultColor = button.getTextColors().getDefaultColor(); + } else { + // Get partner text color. + textDefaultColor = + PartnerConfigHelper.get(context).getColor(context, buttonTextColorConfig); + } + float alpha = + PartnerConfigHelper.get(context).getFraction(context, buttonRippleColorAlphaConfig); + updateButtonRippleColor(button, textDefaultColor, alpha); + } + } + + private static void updateButtonRippleColor( + Button button, @ColorInt int textColor, float rippleAlpha) { + // 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}; + + // Set text color for ripple. + ColorStateList colorStateList = + new ColorStateList( + new int[][] {pressedState, StateSet.NOTHING}, + new int[] {convertRgbToArgb(textColor, rippleAlpha), 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 isButtonIconAtEnd) { + if (button == null) { + return; + } + Drawable icon = null; + if (buttonIconConfig != null) { + icon = PartnerConfigHelper.get(context).getDrawable(context, buttonIconConfig); + } + setButtonIcon(button, icon, isButtonIconAtEnd); + } + + private static void setButtonIcon(Button button, Drawable icon, boolean isButtonIconAtEnd) { + 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 (isButtonIconAtEnd) { + 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) { + if (((RippleDrawable) drawable).getDrawable(0) instanceof GradientDrawable) { + return (GradientDrawable) ((RippleDrawable) drawable).getDrawable(0); + } + 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 1bd6949..c0f1c45 100644 --- a/main/java/com/google/android/setupcompat/template/StatusBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/StatusBarMixin.java @@ -25,13 +25,13 @@ import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Build.VERSION_CODES; -import androidx.annotation.AttrRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import android.util.AttributeSet; import android.view.View; import android.view.Window; import android.widget.LinearLayout; +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.google.android.setupcompat.PartnerCustomizationLayout; import com.google.android.setupcompat.R; import com.google.android.setupcompat.partnerconfig.PartnerConfig; @@ -112,10 +112,14 @@ public class StatusBarMixin implements Mixin { */ 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 e055d28..32c708c 100644 --- a/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java +++ b/main/java/com/google/android/setupcompat/template/SystemNavBarMixin.java @@ -24,13 +24,13 @@ import android.graphics.Color; import android.os.Build; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; +import android.util.AttributeSet; +import android.view.View; +import android.view.Window; import androidx.annotation.AttrRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import android.util.AttributeSet; -import android.view.View; -import android.view.Window; import com.google.android.setupcompat.PartnerCustomizationLayout; import com.google.android.setupcompat.R; import com.google.android.setupcompat.internal.TemplateLayout; @@ -47,6 +47,7 @@ public class SystemNavBarMixin implements Mixin { 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 @@ public class SystemNavBarMixin implements Mixin { this.applyPartnerResources = layout instanceof PartnerCustomizationLayout && ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource(); + + this.useFullDynamicColor = + layout instanceof PartnerCustomizationLayout + && ((PartnerCustomizationLayout) layout).useFullDynamicColor(); } /** @@ -83,6 +88,19 @@ public class SystemNavBarMixin implements Mixin { setLightSystemNavBar( a.getBoolean( R.styleable.SucSystemNavBarMixin_sucLightSystemNavBar, isLightSystemNavBar())); + + // Support updating system navigation bar divider color from P. + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // get fallback value from theme + int[] navBarDividerColorAttr = new int[] {android.R.attr.navigationBarDividerColor}; + TypedArray typedArray = + templateLayout.getContext().obtainStyledAttributes(navBarDividerColorAttr); + int defaultColor = typedArray.getColor(/* index= */ 0, /* defValue= */ 0); + int sucSystemNavBarDividerColor = + a.getColor(R.styleable.SucSystemNavBarMixin_sucSystemNavBarDividerColor, defaultColor); + setSystemNavBarDividerColor(sucSystemNavBarDividerColor); + typedArray.recycle(); + } a.recycle(); } } @@ -96,10 +114,14 @@ public class SystemNavBarMixin implements Mixin { 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); } @@ -120,6 +142,7 @@ public class SystemNavBarMixin implements Mixin { * * @param isLight true means compatible with light theme, otherwise compatible with dark theme */ + public void setLightSystemNavBar(boolean isLight) { if (Build.VERSION.SDK_INT >= VERSION_CODES.O && windowOfActivity != null) { if (applyPartnerResources) { @@ -158,6 +181,28 @@ public class SystemNavBarMixin implements Mixin { } /** + * Sets the divider color of navigation bar. The color will be overridden by partner resource if + * the activity is running in setup wizard flow. + * + * @param color the default divider color of navigation bar + */ + public void setSystemNavBarDividerColor(int color) { + if (Build.VERSION.SDK_INT >= VERSION_CODES.P && windowOfActivity != null) { + if (applyPartnerResources) { + Context context = templateLayout.getContext(); + // Do nothing if the old version partner provider did not contain the new config. + if (PartnerConfigHelper.get(context) + .isPartnerConfigAvailable(PartnerConfig.CONFIG_NAVIGATION_BAR_DIVIDER_COLOR)) { + color = + PartnerConfigHelper.get(context) + .getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_DIVIDER_COLOR); + } + } + windowOfActivity.setNavigationBarDividerColor(color); + } + } + + /** * Hides the navigation bar, make the color of the status and navigation bars transparent, and * specify {@link View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} flag so that the content is laid-out * behind the transparent status bar. This is commonly used with {@link diff --git a/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java new file mode 100644 index 0000000..ea54745 --- /dev/null +++ b/main/java/com/google/android/setupcompat/util/BuildCompatUtils.java @@ -0,0 +1,66 @@ +/* + * 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.util; + +import android.os.Build; + +/** + * An util class to check whether the current OS version is higher or equal to sdk version of + * device. + */ +public final class BuildCompatUtils { + + /** + * Implementation of BuildCompat.isAtLeast*() suitable for use in Setup + * + * <p>BuildCompat.isAtLeast*() can be changed by Android Release team, and once that is changed it + * may take weeks for that to propagate to stable/prerelease/experimental SDKs in Google3. Also it + * can be different in all these channels. This can cause random issues, especially with sidecars + * (i.e., the code running on R may not know that it runs on R). + * + * <p>This still should try using BuildCompat.isAtLeastR() as source of truth, but also checking + * for VERSION_SDK_INT and VERSION.CODENAME in case when BuildCompat implementation returned + * false. Note that both checks should be >= and not = to make sure that when Android version + * increases (i.e., from R to S), this does not stop working. + * + * <p>Supported configurations: + * + * <ul> + * <li>For current Android release: while new API is not finalized yet (CODENAME = "S", SDK_INT + * = 30|31) + * <li>For current Android release: when new API is finalized (CODENAME = "REL", SDK_INT = 31) + * <li>For next Android release (CODENAME = "T", SDK_INT = 30+) + * </ul> + * + * <p>Note that Build.VERSION_CODES.S cannot be used here until final SDK is available in all + * Google3 channels, because it is equal to Build.VERSION_CODES.CUR_DEVELOPMENT before API + * finalization. + * + * @return Whether the current OS version is higher or equal to S. + */ + public static boolean isAtLeastS() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return false; + } + return (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 31) + || (Build.VERSION.CODENAME.length() == 1 + && Build.VERSION.CODENAME.charAt(0) >= 'S' + && Build.VERSION.CODENAME.charAt(0) <= 'Z'); + } + + private BuildCompatUtils() {} +} diff --git a/main/java/com/google/android/setupcompat/util/Logger.java b/main/java/com/google/android/setupcompat/util/Logger.java new file mode 100644 index 0000000..3f8dfd1 --- /dev/null +++ b/main/java/com/google/android/setupcompat/util/Logger.java @@ -0,0 +1,83 @@ +/* + * 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.util; + +import android.util.Log; + +/** + * Helper class that wraps {@link Log} to log messages to logcat. This class consolidate the log + * {@link #TAG} in both SetupCompat and SetupDesign library. + * + * <p>When logging verbose and debug logs, the logs should either be guarded by {@code if + * (logger.isV())}, or a constant if (DEBUG). That DEBUG constant should be false on any submitted + * code. + */ +public final class Logger { + + public static final String TAG = "SetupLibrary"; + + private final String prefix; + + public Logger(Class<?> cls) { + this(cls.getSimpleName()); + } + + public Logger(String prefix) { + this.prefix = "[" + prefix + "] "; + } + + public boolean isV() { + return Log.isLoggable(TAG, Log.VERBOSE); + } + + public boolean isD() { + return Log.isLoggable(TAG, Log.DEBUG); + } + + public boolean isI() { + return Log.isLoggable(TAG, Log.INFO); + } + + public void atVerbose(String message) { + if (isV()) { + Log.v(TAG, prefix.concat(message)); + } + } + + public void atDebug(String message) { + if (isD()) { + Log.d(TAG, prefix.concat(message)); + } + } + + public void atInfo(String message) { + if (isI()) { + Log.i(TAG, prefix.concat(message)); + } + } + + public void w(String message) { + Log.w(TAG, prefix.concat(message)); + } + + public void e(String message) { + Log.e(TAG, prefix.concat(message)); + } + + public void e(String message, Throwable throwable) { + Log.e(TAG, prefix.concat(message), throwable); + } +} diff --git a/main/java/com/google/android/setupcompat/util/SystemBarHelper.java b/main/java/com/google/android/setupcompat/util/SystemBarHelper.java index 75e5dd3..dd92501 100644 --- a/main/java/com/google/android/setupcompat/util/SystemBarHelper.java +++ b/main/java/com/google/android/setupcompat/util/SystemBarHelper.java @@ -24,13 +24,12 @@ import android.content.res.TypedArray; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.os.Handler; -import androidx.annotation.RequiresPermission; -import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowInsets; import android.view.WindowManager; +import androidx.annotation.RequiresPermission; /** * A helper class to manage the system navigation bar and status bar. This will add various @@ -44,7 +43,7 @@ import android.view.WindowManager; */ public final class SystemBarHelper { - private static final String TAG = "SystemBarHelper"; + private static final Logger LOG = new Logger("SystemBarHelper"); /** Needs to be equal to View.STATUS_BAR_DISABLE_BACK */ private static final int STATUS_BAR_DISABLE_BACK = 0x00400000; @@ -329,7 +328,7 @@ public final class SystemBarHelper { // If the decor view is not installed yet, try again in the next loop. handler.post(checkDecorViewRunnable); } else { - Log.w(TAG, "Cannot get decor view of window: " + window); + LOG.e("Cannot get decor view of window: " + window); } } } diff --git a/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java index bfe1dbb..79976bc 100644 --- a/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java +++ b/main/java/com/google/android/setupcompat/util/WizardManagerHelper.java @@ -34,7 +34,7 @@ import java.util.Arrays; */ public final class WizardManagerHelper { - private static final String ACTION_NEXT = "com.android.wizard.NEXT"; + @VisibleForTesting public static final String ACTION_NEXT = "com.android.wizard.NEXT"; // EXTRA_SCRIPT_URI and EXTRA_ACTION_ID are used in setup wizard in versions before M and are // kept for backwards compatibility. @@ -43,10 +43,27 @@ public final class WizardManagerHelper { @VisibleForTesting static final String EXTRA_WIZARD_BUNDLE = "wizardBundle"; private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode"; - @VisibleForTesting public static final String EXTRA_IS_FIRST_RUN = "firstRun"; - @VisibleForTesting static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup"; - @VisibleForTesting static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup"; - @VisibleForTesting public static final String EXTRA_IS_SETUP_FLOW = "isSetupFlow"; + + /** Extra for notifying an Activity that it is inside the first SetupWizard flow or not. */ + public static final String EXTRA_IS_FIRST_RUN = "firstRun"; + + /** Extra for notifying an Activity that it is inside the Deferred SetupWizard flow or not. */ + public static final String EXTRA_IS_DEFERRED_SETUP = "deferredSetup"; + + /** Extra for notifying an Activity that it is inside the "Pre-Deferred Setup" flow. */ + public static final String EXTRA_IS_PRE_DEFERRED_SETUP = "preDeferredSetup"; + + /** Extra for notifying an Activity that it is inside the "Portal Setup" flow. */ + public static final String EXTRA_IS_PORTAL_SETUP = "portalSetup"; + + /** + * Extra for notifying an Activity that it is inside the any setup flow. + * + * <p>Apps that target API levels below {@link android.os.Build.VERSION_CODES#Q} is able to + * determine whether Activity is inside the any setup flow by one of {@link #EXTRA_IS_FIRST_RUN}, + * {@link #EXTRA_IS_DEFERRED_SETUP}, and {@link #EXTRA_IS_PRE_DEFERRED_SETUP} is true. + */ + public static final String EXTRA_IS_SETUP_FLOW = "isSetupFlow"; public static final String EXTRA_THEME = "theme"; public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode"; @@ -104,6 +121,7 @@ public final class WizardManagerHelper { EXTRA_IS_FIRST_RUN, EXTRA_IS_DEFERRED_SETUP, EXTRA_IS_PRE_DEFERRED_SETUP, + EXTRA_IS_PORTAL_SETUP, EXTRA_IS_SETUP_FLOW)) { dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false)); } |