fix: 首次提交

This commit is contained in:
2024-12-09 11:25:23 +08:00
parent d0c01071e9
commit 2c2109a5f3
4741 changed files with 290641 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
// IMyAidlInterface.aidl
package com.google.android.setupcompat;
// Declare any non-default types here with import statements
interface IMyAidlInterface {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright (C) 2018 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;
import android.os.Bundle;
/**
* Declares the interface for compat related service methods.
*/
interface ISetupCompatService {
/** Notifies SetupWizard that the screen is using PartnerCustomizationLayout */
oneway void validateActivity(String screenName, in Bundle arguments) = 0;
oneway void logMetric(int metricType, in Bundle arguments, in Bundle extras) = 1;
oneway void onFocusStatusChanged(in Bundle bundle) = 2;
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
import android.os.Bundle;
/**
* Interface for progress service to update progress to SUW. Clients should
* update progress at least once a minute, or set a pending reason to stop
* update progress. Without progress update and pending reason. We considering
* the progress service is no response will suspend it and unbinde service.
*/
interface IPortalProgressCallback {
/**
* Sets completed amount.
*/
Bundle setProgressCount(int completed, int failed, int total) = 0;
/**
* Sets completed percentage.
*/
Bundle setProgressPercentage(int percentage) = 1;
/**
* Sets the summary shows on portal activity.
*/
Bundle setSummary(String summary) = 2;
/**
* Let SUW knows the progress is pending now, and stop update progress.
* @param reasonResId The resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setPendingReason(int reasonResId, int quantity, in int[] formatArgs, int reason) = 3;
/**
* Once the progress completed, and service can be destroy. Call this function.
* SUW will unbind the service.
* @param resId The resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setComplete(int resId, int quantity, in int[] formatArgs) = 4;
/**
* Once the progress failed, and not able to finish the progress. Should call
* this function. SUW will unbind service after receive setFailure. Client can
* registerProgressService again once the service is ready to execute.
* @param resId The resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setFailure(int resId, int quantity, in int[] formatArgs) = 5;
}

View File

@@ -0,0 +1,55 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
import android.os.Bundle;
import com.google.android.setupcompat.portal.IPortalProgressCallback;
/**
* Interface of progress service, all servics needs to run during onboarding, and would like
* to consolidate notifications with SetupNotificationService, should implement this interface.
* So that SetupNotificationService can bind the progress service and run below
* SetupNotificationService.
*/
interface IPortalProgressService {
/**
* Called when the Portal notification is started.
*/
oneway void onPortalSessionStart() = 0;
/**
* Provides a non-null callback after service connected.
*/
oneway void onSetCallback(IPortalProgressCallback callback) = 1;
/**
* Called when progress timed out.
*/
oneway void onSuspend() = 2;
/**
* User clicks "User mobile data" on portal activity.
* All running progress should agree to use mobile data.
*/
oneway void onAllowMobileData(boolean allowed) = 3;
/**
* Portal service calls to get remaining downloading size(MB) from progress service.
*/
@nullable
Bundle onGetRemainingValues() = 4;
}

View File

@@ -0,0 +1,22 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
/** Listener to listen the result of registerProgressService */
interface IPortalRegisterResultListener {
void onResult(in Bundle result) = 0;
}

View File

@@ -0,0 +1,40 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
import android.os.UserHandle;
import com.google.android.setupcompat.portal.IPortalRegisterResultListener;
import com.google.android.setupcompat.portal.NotificationComponent;
import com.google.android.setupcompat.portal.ProgressServiceComponent;
/**
* Declares the interface for notification related service methods.
*/
interface ISetupNotificationService {
/** Register a notification without progress service */
boolean registerNotification(in NotificationComponent component) = 0;
void unregisterNotification(in NotificationComponent component) = 1;
/** Register a progress service */
void registerProgressService(in ProgressServiceComponent component, in UserHandle userHandle, IPortalRegisterResultListener listener) = 2;
/** Checks the progress connection still alive or not. */
boolean isProgressServiceAlive(in ProgressServiceComponent component, in UserHandle userHandle) = 3;
/** Checks portal avaailable or not. */
boolean isPortalAvailable() = 4;
}

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
parcelable NotificationComponent;

View File

@@ -0,0 +1,19 @@
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal;
parcelable ProgressServiceComponent;

View File

@@ -0,0 +1,72 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.portal.v1_1;
import android.os.Bundle;
/**
* Interface for progress service to update progress to SUW. Clients should
* update progress at least once a minute, or set a pending reason to stop
* update progress. Without progress update and pending reason. We considering
* the progress service is no response will suspend it and unbinde service.
*/
interface IPortalProgressCallback {
/**
* Sets completed amount.
*/
Bundle setProgressCount(int completed, int failed, int total) = 0;
/**
* Sets completed percentage.
*/
Bundle setProgressPercentage(int percentage) = 1;
/**
* Sets the summary shows on portal activity.
*/
Bundle setSummary(String summary) = 2;
/**
* Let SUW knows the progress is pending now, and stop update progress.
* @param reasonResName The name of resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setPendingReason(String reasonResName, int quantity, in int[] formatArgs, int reason) = 3;
/**
* Once the progress completed, and service can be destroy. Call this function.
* SUW will unbind the service.
* @param resName The name of resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setComplete(String resName, int quantity, in int[] formatArgs) = 4;
/**
* Once the progress failed, and not able to finish the progress. Should call
* this function. SUW will unbind service after receive setFailure. Client can
* registerProgressService again once the service is ready to execute.
* @param resName The name of resource identifier.
* @param quantity The number used to get the correct string for the current language's
* plural rules
* @param formatArgs The format arguments that will be used for substitution.
*/
Bundle setFailure(String resName, int quantity, in int[] formatArgs) = 5;
}

View File

@@ -0,0 +1,355 @@
/*
* Copyright 2018 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;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.PersistableBundle;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.FocusChangedMetricHelper;
import com.google.android.setupcompat.internal.LifecycleFragment;
import com.google.android.setupcompat.internal.PersistableBundles;
import com.google.android.setupcompat.internal.SetupCompatServiceInvoker;
import com.google.android.setupcompat.internal.TemplateLayout;
import com.google.android.setupcompat.logging.CustomEvent;
import com.google.android.setupcompat.logging.LoggingObserver;
import com.google.android.setupcompat.logging.LoggingObserver.SetupCompatUiEvent.LayoutInflatedEvent;
import com.google.android.setupcompat.logging.MetricKey;
import com.google.android.setupcompat.logging.SetupMetricsLogger;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
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;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** A templatization layout with consistent style used in Setup Wizard or app itself. */
public class PartnerCustomizationLayout extends TemplateLayout {
private static final Logger LOG = new Logger("PartnerCustomizationLayout");
/**
* Attribute indicating whether usage of partner theme resources is allowed. This corresponds to
* the {@code app:sucUsePartnerResource} XML attribute. Note that when running in setup wizard,
* this is always overridden to true.
*/
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;
private PersistableBundle layoutTypeBundle;
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context) {
this(context, 0, 0);
}
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context, int template) {
this(context, template, 0);
}
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context, int template, int containerId) {
super(context, template, containerId);
init(null, R.attr.sucLayoutTheme);
}
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs, R.attr.sucLayoutTheme);
}
@CanIgnoreReturnValue
public PartnerCustomizationLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(attrs, defStyleAttr);
}
@VisibleForTesting
final ViewTreeObserver.OnWindowFocusChangeListener windowFocusChangeListener =
this::onFocusChanged;
private void init(AttributeSet attrs, int defStyleAttr) {
if (isInEditMode()) {
return;
}
TypedArray a =
getContext()
.obtainStyledAttributes(
attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0);
boolean layoutFullscreen =
a.getBoolean(R.styleable.SucPartnerCustomizationLayout_sucLayoutFullscreen, true);
a.recycle();
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && layoutFullscreen) {
setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
registerMixin(
StatusBarMixin.class, new StatusBarMixin(this, activity.getWindow(), attrs, defStyleAttr));
registerMixin(SystemNavBarMixin.class, new SystemNavBarMixin(this, activity.getWindow()));
registerMixin(FooterBarMixin.class, new FooterBarMixin(this, attrs, defStyleAttr));
getMixin(SystemNavBarMixin.class).applyPartnerCustomizations(attrs, defStyleAttr);
// Override the FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_TRANSLUCENT_STATUS,
// FLAG_TRANSLUCENT_NAVIGATION and SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN attributes of window forces
// showing status bar and navigation bar.
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
}
@Override
protected View onInflateTemplate(LayoutInflater inflater, int template) {
if (template == 0) {
template = R.layout.partner_customization_layout;
}
return inflateTemplate(inflater, 0, template);
}
/**
* {@inheritDoc}
*
* <p>This method sets all these flags before onTemplateInflated since it will be too late and get
* incorrect flag value on PartnerCustomizationLayout if sets them after onTemplateInflated.
*/
@Override
protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) {
// Sets default value to true since this timing
// before PartnerCustomization members initialization
usePartnerResourceAttr = true;
activity = lookupActivityFromContext(getContext());
boolean isSetupFlow = WizardManagerHelper.isAnySetupWizard(activity.getIntent());
TypedArray a =
getContext()
.obtainStyledAttributes(
attrs, R.styleable.SucPartnerCustomizationLayout, defStyleAttr, 0);
if (!a.hasValue(R.styleable.SucPartnerCustomizationLayout_sucUsePartnerResource)) {
// TODO: Enable Log.WTF after other client already set sucUsePartnerResource.
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();
LOG.atDebug(
"activity="
+ activity.getClass().getSimpleName()
+ " isSetupFlow="
+ isSetupFlow
+ " enablePartnerResourceLoading="
+ enablePartnerResourceLoading()
+ " usePartnerResourceAttr="
+ usePartnerResourceAttr
+ " useDynamicColor="
+ useDynamicColor
+ " useFullDynamicColorAttr="
+ useFullDynamicColorAttr);
}
@Override
protected ViewGroup findContainer(int containerId) {
if (containerId == 0) {
containerId = R.id.suc_layout_content;
}
return super.findContainer(containerId);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
LifecycleFragment.attachNow(activity);
if (WizardManagerHelper.isAnySetupWizard(activity.getIntent())) {
getViewTreeObserver().addOnWindowFocusChangeListener(windowFocusChangeListener);
}
getMixin(FooterBarMixin.class).onAttachedToWindow();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (VERSION.SDK_INT >= Build.VERSION_CODES.Q
&& WizardManagerHelper.isAnySetupWizard(activity.getIntent())) {
FooterBarMixin footerBarMixin = getMixin(FooterBarMixin.class);
footerBarMixin.onDetachedFromWindow();
FooterButton primaryButton = footerBarMixin.getPrimaryButton();
FooterButton secondaryButton = footerBarMixin.getSecondaryButton();
PersistableBundle primaryButtonMetrics =
primaryButton != null
? primaryButton.getMetrics("PrimaryFooterButton")
: PersistableBundle.EMPTY;
PersistableBundle secondaryButtonMetrics =
secondaryButton != null
? secondaryButton.getMetrics("SecondaryFooterButton")
: PersistableBundle.EMPTY;
PersistableBundle layoutTypeMetrics =
(layoutTypeBundle != null) ? layoutTypeBundle : PersistableBundle.EMPTY;
PersistableBundle persistableBundle =
PersistableBundles.mergeBundles(
footerBarMixin.getLoggingMetrics(),
primaryButtonMetrics,
secondaryButtonMetrics,
layoutTypeMetrics);
SetupMetricsLogger.logCustomEvent(
getContext(),
CustomEvent.create(MetricKey.get("SetupCompatMetrics", activity), persistableBundle));
}
getViewTreeObserver().removeOnWindowFocusChangeListener(windowFocusChangeListener);
}
/**
* PartnerCustomizationLayout is a template layout for different type of GlifLayout. This method
* allows each type of layout to report its "GlifLayoutType".
*/
public void setLayoutTypeMetrics(PersistableBundle bundle) {
this.layoutTypeBundle = bundle;
}
/** Returns a {@link PersistableBundle} contains key "GlifLayoutType". */
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
public PersistableBundle getLayoutTypeMetrics() {
return this.layoutTypeBundle;
}
public static Activity lookupActivityFromContext(Context context) {
return PartnerConfigHelper.lookupActivityFromContext(context);
}
/**
* Returns true if partner resource loading is enabled. If true, and other necessary conditions
* for loading theme attributes are met, this layout will use customized theme attributes from OEM
* overlays. This is intended to be used with flag-based development, to allow a flag to control
* the rollout of partner resource loading.
*/
protected boolean enablePartnerResourceLoading() {
return true;
}
/** Returns if the current layout/activity applies partner customized configurations or not. */
public boolean shouldApplyPartnerResource() {
if (!enablePartnerResourceLoading()) {
return false;
}
if (!usePartnerResourceAttr) {
return false;
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return false;
}
if (!PartnerConfigHelper.get(getContext()).isAvailable()) {
return false;
}
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;
}
/**
* Sets a logging observer for {@link FooterBarMixin}. The logging observer is used to log
* impressions and clicks on the layout and footer bar buttons.
*
* @throws UnsupportedOperationException if the primary or secondary button has been set before
* the logging observer is set
*/
public void setLoggingObserver(LoggingObserver loggingObserver) {
getMixin(FooterBarMixin.class).setLoggingObserver(loggingObserver);
loggingObserver.log(new LayoutInflatedEvent(this));
}
/**
* Invoke the method onFocusStatusChanged when onWindowFocusChangeListener receive onFocusChanged.
*/
private void onFocusChanged(boolean hasFocus) {
SetupCompatServiceInvoker.get(getContext())
.onFocusStatusChanged(
FocusChangedMetricHelper.getScreenName(activity),
FocusChangedMetricHelper.getExtraBundle(
activity, PartnerCustomizationLayout.this, hasFocus));
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2018 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.internal;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.TimeUnit;
/** Provider for time in nanos and millis. Allows overriding time during tests. */
public class ClockProvider {
public static long timeInNanos() {
return ticker.read();
}
public static long timeInMillis() {
return TimeUnit.NANOSECONDS.toMillis(timeInNanos());
}
@VisibleForTesting
public static void resetInstance() {
ticker = SYSTEM_TICKER;
}
@VisibleForTesting
public static void setInstance(Supplier<Long> nanoSecondSupplier) {
ticker = () -> nanoSecondSupplier.get();
}
public long read() {
return ticker.read();
}
private static final Ticker SYSTEM_TICKER = () -> System.nanoTime();
private static Ticker ticker = SYSTEM_TICKER;
@VisibleForTesting
public interface Supplier<T> {
T get();
}
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 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.internal;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* Utility class to provide executors.
*
* <p>It allows the executors to be mocked in Robolectric, redirecting to Robolectric's schedulers
* rather than using real threads.
*/
public final class ExecutorProvider<T extends Executor> {
private static final int SETUP_METRICS_LOGGER_MAX_QUEUED = 50;
/**
* Creates a single threaded {@link ExecutorService} with a maximum pool size {@code maxSize}.
* Jobs submitted when the pool is full causes {@link
* java.util.concurrent.RejectedExecutionException} to be thrown.
*/
public static final ExecutorProvider<ExecutorService> setupCompatServiceInvoker =
new ExecutorProvider<>(
createSizeBoundedExecutor("SetupCompatServiceInvoker", SETUP_METRICS_LOGGER_MAX_QUEUED));
private final T executor;
@Nullable private T injectedExecutor;
private ExecutorProvider(T executor) {
this.executor = executor;
}
public T get() {
if (injectedExecutor != null) {
return injectedExecutor;
}
return executor;
}
/**
* Injects an executor for testing use for this provider. Subsequent calls to {@link #get} will
* return this instance instead, until {@link #resetExecutors()} is called.
*/
@VisibleForTesting
public void injectExecutor(T executor) {
this.injectedExecutor = executor;
}
@VisibleForTesting
public static void resetExecutors() {
setupCompatServiceInvoker.injectedExecutor = null;
}
@VisibleForTesting
public static ExecutorService createSizeBoundedExecutor(String threadName, int maxSize) {
return new ThreadPoolExecutor(
/* corePoolSize= */ 1,
/* maximumPoolSize= */ 1,
/* keepAliveTime= */ 0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(maxSize),
runnable -> new Thread(runnable, threadName));
}
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright (C) 2017 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.internal;
import android.content.Context;
import android.content.res.Resources.Theme;
import android.view.ContextThemeWrapper;
import androidx.annotation.StyleRes;
/**
* Same as {@link ContextThemeWrapper}, but the base context's theme attributes take precedence over
* the wrapper context's. This is used to provide default values for theme attributes referenced in
* layouts, to remove the risk of crashing the client because of using the wrong theme.
*/
public class FallbackThemeWrapper extends ContextThemeWrapper {
/**
* Creates a new context wrapper with the specified theme.
*
* <p>The specified theme will be applied as fallbacks to the base context's theme. Any attributes
* defined in the base context's theme will retain their original values. Otherwise values in
* {@code themeResId} will be used.
*
* @param base The base context.
* @param themeResId The theme to use as fallback.
*/
public FallbackThemeWrapper(Context base, @StyleRes int themeResId) {
super(base, themeResId);
}
/** {@inheritDoc} */
@Override
protected void onApplyThemeResource(Theme theme, int resId, boolean first) {
theme.applyStyle(resId, false /* force */);
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.internal;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.StringDef;
import com.google.android.setupcompat.internal.FocusChangedMetricHelper.Constants.ExtraKey;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* A help class response to generate extra bundle and capture screen name for interruption metric.
*/
public class FocusChangedMetricHelper {
private FocusChangedMetricHelper() {}
public static final String getScreenName(Activity activity) {
return activity.getComponentName().toShortString();
}
public static final Bundle getExtraBundle(
Activity activity, TemplateLayout layout, boolean hasFocus) {
Bundle bundle = new Bundle();
bundle.putString(ExtraKey.PACKAGE_NAME, activity.getComponentName().getPackageName());
bundle.putString(ExtraKey.SCREEN_NAME, activity.getComponentName().getShortClassName());
bundle.putInt(ExtraKey.HASH_CODE, layout.hashCode());
bundle.putBoolean(ExtraKey.HAS_FOCUS, hasFocus);
bundle.putLong(ExtraKey.TIME_IN_MILLIS, System.currentTimeMillis());
return bundle;
}
/**
* Constant values used by {@link
* com.google.android.setupcompat.internal.FocusChangedMetricHelper}.
*/
public static final class Constants {
@Retention(RetentionPolicy.SOURCE)
@StringDef({
ExtraKey.PACKAGE_NAME,
ExtraKey.SCREEN_NAME,
ExtraKey.HASH_CODE,
ExtraKey.HAS_FOCUS,
ExtraKey.TIME_IN_MILLIS
})
public @interface ExtraKey {
/** This key will be used to save the package name. */
String PACKAGE_NAME = "packageName";
/** This key will be used to save the activity name. */
String SCREEN_NAME = "screenName";
/**
* This key will be used to save the has code of {@link
* com.google.android.setupcompat.PartnerCustomizationLayout}.
*/
String HASH_CODE = "hash";
/**
* This key will be used to save whether the window which is including the {@link
* com.google.android.setupcompat.PartnerCustomizationLayout}. has focus or not.
*/
String HAS_FOCUS = "focus";
/** This key will be use to save the time stamp in milliseconds. */
String TIME_IN_MILLIS = "timeInMillis";
}
private Constants() {}
}
}

View File

@@ -0,0 +1,255 @@
/*
* Copyright 2019 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.internal;
import com.google.android.setupcompat.partnerconfig.PartnerConfig;
import com.google.android.setupcompat.template.FooterButton;
/** Keep the partner configuration of a footer button. Used when the button is inflated. */
public class FooterButtonPartnerConfig {
private final PartnerConfig buttonBackgroundConfig;
private final PartnerConfig buttonDisableAlphaConfig;
private final PartnerConfig buttonDisableBackgroundConfig;
private final PartnerConfig buttonDisableTextColorConfig;
private final PartnerConfig buttonIconConfig;
private final PartnerConfig buttonTextColorConfig;
private final PartnerConfig buttonMarginStartConfig;
private final PartnerConfig buttonTextSizeConfig;
private final PartnerConfig buttonMinHeightConfig;
private final PartnerConfig buttonTextTypeFaceConfig;
private final PartnerConfig buttonTextWeightConfig;
private final PartnerConfig buttonTextStyleConfig;
private final PartnerConfig buttonRadiusConfig;
private final PartnerConfig buttonRippleColorAlphaConfig;
private final int partnerTheme;
private FooterButtonPartnerConfig(
int partnerTheme,
PartnerConfig buttonBackgroundConfig,
PartnerConfig buttonDisableAlphaConfig,
PartnerConfig buttonDisableBackgroundConfig,
PartnerConfig buttonDisableTextColorConfig,
PartnerConfig buttonIconConfig,
PartnerConfig buttonTextColorConfig,
PartnerConfig buttonMarginStartConfig,
PartnerConfig buttonTextSizeConfig,
PartnerConfig buttonMinHeightConfig,
PartnerConfig buttonTextTypeFaceConfig,
PartnerConfig buttonTextWeightConfig,
PartnerConfig buttonTextStyleConfig,
PartnerConfig buttonRadiusConfig,
PartnerConfig buttonRippleColorAlphaConfig) {
this.partnerTheme = partnerTheme;
this.buttonTextColorConfig = buttonTextColorConfig;
this.buttonMarginStartConfig = buttonMarginStartConfig;
this.buttonTextSizeConfig = buttonTextSizeConfig;
this.buttonMinHeightConfig = buttonMinHeightConfig;
this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig;
this.buttonTextWeightConfig = buttonTextWeightConfig;
this.buttonTextStyleConfig = buttonTextStyleConfig;
this.buttonBackgroundConfig = buttonBackgroundConfig;
this.buttonDisableAlphaConfig = buttonDisableAlphaConfig;
this.buttonDisableBackgroundConfig = buttonDisableBackgroundConfig;
this.buttonDisableTextColorConfig = buttonDisableTextColorConfig;
this.buttonRadiusConfig = buttonRadiusConfig;
this.buttonIconConfig = buttonIconConfig;
this.buttonRippleColorAlphaConfig = buttonRippleColorAlphaConfig;
}
public int getPartnerTheme() {
return partnerTheme;
}
public PartnerConfig getButtonBackgroundConfig() {
return buttonBackgroundConfig;
}
public PartnerConfig getButtonDisableAlphaConfig() {
return buttonDisableAlphaConfig;
}
public PartnerConfig getButtonDisableBackgroundConfig() {
return buttonDisableBackgroundConfig;
}
public PartnerConfig getButtonDisableTextColorConfig() {
return buttonDisableTextColorConfig;
}
public PartnerConfig getButtonIconConfig() {
return buttonIconConfig;
}
public PartnerConfig getButtonTextColorConfig() {
return buttonTextColorConfig;
}
public PartnerConfig getButtonMarginStartConfig() {
return buttonMarginStartConfig;
}
public PartnerConfig getButtonMinHeightConfig() {
return buttonMinHeightConfig;
}
public PartnerConfig getButtonTextSizeConfig() {
return buttonTextSizeConfig;
}
public PartnerConfig getButtonTextTypeFaceConfig() {
return buttonTextTypeFaceConfig;
}
public PartnerConfig getButtonTextWeightConfig() {
return buttonTextWeightConfig;
}
public PartnerConfig getButtonTextStyleConfig() {
return buttonTextStyleConfig;
}
public PartnerConfig getButtonRadiusConfig() {
return buttonRadiusConfig;
}
public PartnerConfig getButtonRippleColorAlphaConfig() {
return buttonRippleColorAlphaConfig;
}
/** Builder class for constructing {@code FooterButtonPartnerConfig} objects. */
public static class Builder {
private final FooterButton footerButton;
private PartnerConfig buttonBackgroundConfig = null;
private PartnerConfig buttonDisableAlphaConfig = null;
private PartnerConfig buttonDisableBackgroundConfig = null;
private PartnerConfig buttonDisableTextColorConfig = null;
private PartnerConfig buttonIconConfig = null;
private PartnerConfig buttonTextColorConfig = null;
private PartnerConfig buttonMarginStartConfig = null;
private PartnerConfig buttonTextSizeConfig = null;
private PartnerConfig buttonMinHeight = null;
private PartnerConfig buttonTextTypeFaceConfig = null;
private PartnerConfig buttonTextWeightConfig = null;
private PartnerConfig buttonTextStyleConfig = null;
private PartnerConfig buttonRadiusConfig = null;
private PartnerConfig buttonRippleColorAlphaConfig = null;
private int partnerTheme;
public Builder(FooterButton footerButton) {
this.footerButton = footerButton;
if (this.footerButton != null) {
// default partnerTheme should be the same as footerButton.getTheme();
this.partnerTheme = this.footerButton.getTheme();
}
}
public Builder setButtonBackgroundConfig(PartnerConfig buttonBackgroundConfig) {
this.buttonBackgroundConfig = buttonBackgroundConfig;
return this;
}
public Builder setButtonDisableAlphaConfig(PartnerConfig buttonDisableAlphaConfig) {
this.buttonDisableAlphaConfig = buttonDisableAlphaConfig;
return this;
}
public Builder setButtonDisableBackgroundConfig(PartnerConfig buttonDisableBackgroundConfig) {
this.buttonDisableBackgroundConfig = buttonDisableBackgroundConfig;
return this;
}
public Builder setButtonDisableTextColorConfig(PartnerConfig buttonDisableTextColorConfig) {
this.buttonDisableTextColorConfig = buttonDisableTextColorConfig;
return this;
}
public Builder setButtonIconConfig(PartnerConfig buttonIconConfig) {
this.buttonIconConfig = buttonIconConfig;
return this;
}
public Builder setMarginStartConfig(PartnerConfig buttonMarginStartConfig) {
this.buttonMarginStartConfig = buttonMarginStartConfig;
return this;
}
public Builder setTextColorConfig(PartnerConfig buttonTextColorConfig) {
this.buttonTextColorConfig = buttonTextColorConfig;
return this;
}
public Builder setTextSizeConfig(PartnerConfig buttonTextSizeConfig) {
this.buttonTextSizeConfig = buttonTextSizeConfig;
return this;
}
public Builder setButtonMinHeight(PartnerConfig buttonMinHeightConfig) {
this.buttonMinHeight = buttonMinHeightConfig;
return this;
}
public Builder setTextTypeFaceConfig(PartnerConfig buttonTextTypeFaceConfig) {
this.buttonTextTypeFaceConfig = buttonTextTypeFaceConfig;
return this;
}
public Builder setTextWeightConfig(PartnerConfig buttonTextWeightFaceConfig) {
this.buttonTextWeightConfig = buttonTextWeightFaceConfig;
return this;
}
public Builder setTextStyleConfig(PartnerConfig buttonTextStyleConfig) {
this.buttonTextStyleConfig = buttonTextStyleConfig;
return this;
}
public Builder setButtonRadiusConfig(PartnerConfig buttonRadiusConfig) {
this.buttonRadiusConfig = buttonRadiusConfig;
return this;
}
public Builder setButtonRippleColorAlphaConfig(PartnerConfig buttonRippleColorAlphaConfig) {
this.buttonRippleColorAlphaConfig = buttonRippleColorAlphaConfig;
return this;
}
public Builder setPartnerTheme(int partnerTheme) {
this.partnerTheme = partnerTheme;
return this;
}
public FooterButtonPartnerConfig build() {
return new FooterButtonPartnerConfig(
partnerTheme,
buttonBackgroundConfig,
buttonDisableAlphaConfig,
buttonDisableBackgroundConfig,
buttonDisableTextColorConfig,
buttonIconConfig,
buttonTextColorConfig,
buttonMarginStartConfig,
buttonTextSizeConfig,
buttonMinHeight,
buttonTextTypeFaceConfig,
buttonTextWeightConfig,
buttonTextStyleConfig,
buttonRadiusConfig,
buttonRippleColorAlphaConfig);
}
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright (C) 2019 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.internal;
import android.app.Activity;
import android.os.Bundle;
public final class LayoutBindBackHelper {
private LayoutBindBackHelper() {}
public static final String getScreenName(Activity activity) {
return activity.getComponentName().toString();
}
public static final Bundle getExtraBundle(Activity activity) {
Bundle bundle = new Bundle();
bundle.putString("screenName", getScreenName(activity));
bundle.putString("intentAction", activity.getIntent().getAction());
return bundle;
}
}

View File

@@ -0,0 +1,123 @@
/*
* Copyright (C) 2018 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.internal;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.PersistableBundle;
import android.util.Log;
import com.google.android.setupcompat.logging.CustomEvent;
import com.google.android.setupcompat.logging.MetricKey;
import com.google.android.setupcompat.logging.SetupMetricsLogger;
import com.google.android.setupcompat.util.WizardManagerHelper;
/** Fragment used to detect lifecycle of an activity for metrics logging. */
public class LifecycleFragment extends Fragment {
private static final String LOG_TAG = LifecycleFragment.class.getSimpleName();
private static final String FRAGMENT_ID = "lifecycle_monitor";
private MetricKey metricKey;
private long startInNanos;
private long durationInNanos = 0;
public LifecycleFragment() {
setRetainInstance(true);
}
/**
* Attaches the lifecycle fragment if it is not attached yet.
*
* @param activity the activity to detect lifecycle for.
* @return fragment to monitor life cycle.
*/
public static LifecycleFragment attachNow(Activity activity) {
if (WizardManagerHelper.isAnySetupWizard(activity.getIntent())) {
SetupCompatServiceInvoker.get(activity.getApplicationContext())
.bindBack(
LayoutBindBackHelper.getScreenName(activity),
LayoutBindBackHelper.getExtraBundle(activity));
if (VERSION.SDK_INT > VERSION_CODES.M) {
FragmentManager fragmentManager = activity.getFragmentManager();
if (fragmentManager != null && !fragmentManager.isDestroyed()) {
Fragment fragment = fragmentManager.findFragmentByTag(FRAGMENT_ID);
if (fragment == null) {
LifecycleFragment lifeCycleFragment = new LifecycleFragment();
try {
fragmentManager.beginTransaction().add(lifeCycleFragment, FRAGMENT_ID).commitNow();
fragment = lifeCycleFragment;
} catch (IllegalStateException e) {
Log.e(
LOG_TAG,
"Error occurred when attach to Activity:" + activity.getComponentName(),
e);
}
} else if (!(fragment instanceof LifecycleFragment)) {
Log.wtf(
LOG_TAG,
activity.getClass().getSimpleName() + " Incorrect instance on lifecycle fragment.");
return null;
}
return (LifecycleFragment) fragment;
}
}
}
return null;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
metricKey = MetricKey.get("ScreenDuration", getActivity());
}
@Override
public void onDetach() {
super.onDetach();
SetupMetricsLogger.logDuration(getActivity(), metricKey, NANOSECONDS.toMillis(durationInNanos));
}
@Override
public void onResume() {
super.onResume();
startInNanos = ClockProvider.timeInNanos();
logScreenResume();
}
@Override
public void onPause() {
super.onPause();
durationInNanos += (ClockProvider.timeInNanos() - startInNanos);
}
private void logScreenResume() {
if (VERSION.SDK_INT >= VERSION_CODES.Q) {
PersistableBundle bundle = new PersistableBundle();
bundle.putLong("onScreenResume", System.nanoTime());
SetupMetricsLogger.logCustomEvent(
getActivity(),
CustomEvent.create(MetricKey.get("ScreenActivity", getActivity()), bundle));
}
}
}

View File

@@ -0,0 +1,146 @@
/*
* Copyright (C) 2018 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.internal;
import android.annotation.TargetApi;
import android.os.BaseBundle;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.PersistableBundle;
import android.util.ArrayMap;
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.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.
*
* @return Returns a new {@link PersistableBundle} that contains all the data from {@code
* firstBundle}, {@code nextBundle} and {@code others}.
*/
public static PersistableBundle mergeBundles(
PersistableBundle firstBundle, PersistableBundle nextBundle, PersistableBundle... others) {
List<PersistableBundle> allBundles = new ArrayList<>();
allBundles.addAll(Arrays.asList(firstBundle, nextBundle));
Collections.addAll(allBundles, others);
PersistableBundle result = new PersistableBundle();
for (PersistableBundle bundle : allBundles) {
for (String key : bundle.keySet()) {
Preconditions.checkArgument(
!result.containsKey(key),
String.format("Found duplicate key [%s] while attempting to merge bundles.", key));
}
result.putAll(bundle);
}
return result;
}
/** Returns a {@link Bundle} that contains all the values from {@code persistableBundle}. */
public static Bundle toBundle(PersistableBundle persistableBundle) {
Bundle bundle = new Bundle();
bundle.putAll(persistableBundle);
return bundle;
}
/**
* Returns a {@link PersistableBundle} that contains values from {@code bundle} that are supported
* by the logging API. Un-supported value types are dropped.
*/
public static PersistableBundle fromBundle(Bundle bundle) {
PersistableBundle to = new PersistableBundle();
ArrayMap<String, Object> map = toMap(bundle);
for (String key : map.keySet()) {
Object value = map.get(key);
if (value instanceof Long) {
to.putLong(key, (Long) value);
} else if (value instanceof Integer) {
to.putInt(key, (Integer) value);
} else if (value instanceof Double) {
to.putDouble(key, (Double) value);
} else if (value instanceof Boolean) {
to.putBoolean(key, (Boolean) value);
} else if (value instanceof String) {
to.putString(key, (String) value);
} else {
throw new AssertionError(String.format("Missing put* for valid data type? = %s", value));
}
}
return to;
}
/** Returns {@code true} if {@code left} contains same set of values as {@code right}. */
public static boolean equals(PersistableBundle left, PersistableBundle right) {
return (left == right) || toMap(left).equals(toMap(right));
}
/** Asserts that {@code persistableBundle} contains only supported data types. */
public static PersistableBundle assertIsValid(PersistableBundle persistableBundle) {
Preconditions.checkNotNull(persistableBundle, "PersistableBundle cannot be null!");
for (String key : persistableBundle.keySet()) {
Object value = persistableBundle.get(key);
Preconditions.checkArgument(
isSupportedDataType(value),
String.format("Unknown/unsupported data type [%s] for key %s", value, key));
}
return persistableBundle;
}
/**
* Returns a new {@link ArrayMap} that contains values from {@code bundle} that are supported by
* the logging API.
*/
private static ArrayMap<String, Object> toMap(BaseBundle baseBundle) {
if (baseBundle == null || baseBundle.isEmpty()) {
return new ArrayMap<>(0);
}
ArrayMap<String, Object> map = new ArrayMap<>(baseBundle.size());
for (String key : baseBundle.keySet()) {
Object value = baseBundle.get(key);
if (!isSupportedDataType(value)) {
LOG.w(String.format("Unknown/unsupported data type [%s] for key %s", value, key));
continue;
}
map.put(key, baseBundle.get(key));
}
return map;
}
private static boolean isSupportedDataType(Object value) {
return value instanceof Integer
|| value instanceof Long
|| value instanceof Double
|| value instanceof Float
|| value instanceof String
|| value instanceof Boolean;
}
private PersistableBundles() {
throw new AssertionError("Should not be instantiated");
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2019 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.internal;
import android.os.Looper;
/**
* Static convenience methods that help a method or constructor check whether it was invoked
* correctly (that is, whether its <i>preconditions</i> were met).
*
* <p>If the precondition is not met, the {@code Preconditions} method throws an unchecked exception
* of a specified type, which helps the method in which the exception was thrown communicate that
* its caller has made a mistake.
*/
public final class Preconditions {
/** Ensures the truth of an expression involving one or more parameters to the calling method. */
public static void checkArgument(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalArgumentException(errorMessage);
}
}
/**
* Ensures the truth of an expression involving the state of the calling instance, but not
* involving any parameters to the calling method.
*/
public static void checkState(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalStateException(errorMessage);
}
}
/** Ensures that an object reference passed as a parameter to the calling method is not null. */
public static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
}
return reference;
}
/**
* Ensures that this method is called from the main thread, otherwise an exception will be thrown.
*/
public static void ensureOnMainThread(String whichMethod) {
if (Looper.myLooper() == Looper.getMainLooper()) {
return;
}
throw new IllegalStateException(whichMethod + " must be called from the UI thread.");
}
/**
* Ensure that this method is not called from the main thread, otherwise an exception will be
* thrown.
*/
public static void ensureNotOnMainThread(String whichMethod) {
if (Looper.myLooper() != Looper.getMainLooper()) {
return;
}
throw new IllegalThreadStateException(whichMethod + " cannot be called from the UI thread.");
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright (C) 2018 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.internal;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.os.RemoteException;
import androidx.annotation.VisibleForTesting;
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;
import java.util.concurrent.TimeoutException;
/**
* This class is responsible for safely executing methods on SetupCompatService. To avoid memory
* issues due to backed up queues, an upper bound of {@link
* ExecutorProvider#SETUP_METRICS_LOGGER_MAX_QUEUED} is set on the logging executor service's queue
* and {@link ExecutorProvider#SETUP_COMPAT_BINDBACK_MAX_QUEUED} on the overall executor service.
* Once the upper bound is reached, metrics published after this event are dropped silently.
*
* <p>NOTE: This class is not meant to be used directly. Please use {@link
* com.google.android.setupcompat.logging.SetupMetricsLogger} for publishing metric events.
*/
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(String.format("Metric of type %d dropped since queue is full.", metricType), e);
}
}
public void bindBack(String screenName, Bundle bundle) {
try {
loggingExecutor.execute(() -> invokeBindBack(screenName, bundle));
} catch (RejectedExecutionException e) {
LOG.e(String.format("Screen %s bind back fail.", screenName), e);
}
}
/**
* Help invoke the {@link ISetupCompatService#onFocusStatusChanged} using {@code loggingExecutor}.
*/
public void onFocusStatusChanged(String screenName, Bundle bundle) {
try {
loggingExecutor.execute(() -> invokeOnWindowFocusChanged(screenName, bundle));
} catch (RejectedExecutionException e) {
LOG.e(String.format("Screen %s report focus changed failed.", screenName), e);
}
}
private void invokeLogMetric(
@MetricType int metricType, @SuppressWarnings("unused") Bundle args) {
try {
ISetupCompatService setupCompatService =
SetupCompatServiceProvider.get(
context, waitTimeInMillisForServiceConnection, TimeUnit.MILLISECONDS);
if (setupCompatService != null) {
setupCompatService.logMetric(metricType, args, Bundle.EMPTY);
} else {
LOG.w("logMetric failed since service reference is null. Are the permissions valid?");
}
} catch (InterruptedException | TimeoutException | RemoteException | IllegalStateException e) {
LOG.e(String.format("Exception occurred while trying to log metric = [%s]", args), e);
}
}
private void invokeOnWindowFocusChanged(String screenName, Bundle bundle) {
try {
ISetupCompatService setupCompatService =
SetupCompatServiceProvider.get(
context, waitTimeInMillisForServiceConnection, TimeUnit.MILLISECONDS);
if (setupCompatService != null) {
setupCompatService.onFocusStatusChanged(bundle);
} else {
LOG.w(
"Report focusChange failed since service reference is null. Are the permission valid?");
}
} catch (InterruptedException
| TimeoutException
| RemoteException
| UnsupportedOperationException e) {
LOG.e(
String.format(
"Exception occurred while %s trying report windowFocusChange to SetupWizard.",
screenName),
e);
}
}
private void invokeBindBack(String screenName, Bundle bundle) {
try {
ISetupCompatService setupCompatService =
SetupCompatServiceProvider.get(
context, waitTimeInMillisForServiceConnection, TimeUnit.MILLISECONDS);
if (setupCompatService != null) {
setupCompatService.validateActivity(screenName, bundle);
} else {
LOG.w("BindBack failed since service reference is null. Are the permissions valid?");
}
} catch (InterruptedException | TimeoutException | RemoteException e) {
LOG.e(
String.format("Exception occurred while %s trying bind back to SetupWizard.", screenName),
e);
}
}
private SetupCompatServiceInvoker(Context context) {
this.context = context;
this.loggingExecutor = ExecutorProvider.setupCompatServiceInvoker.get();
this.waitTimeInMillisForServiceConnection = MAX_WAIT_TIME_FOR_CONNECTION_MS;
}
private final Context context;
private final ExecutorService loggingExecutor;
private final long waitTimeInMillisForServiceConnection;
public static synchronized SetupCompatServiceInvoker get(Context context) {
if (instance == null) {
instance = new SetupCompatServiceInvoker(context.getApplicationContext());
}
return instance;
}
@VisibleForTesting
static void setInstanceForTesting(SetupCompatServiceInvoker testInstance) {
instance = testInstance;
}
// The instance is coming from Application context which alive during the application activate and
// it's not depend on the activities life cycle, so we can avoid memory leak. However linter
// cannot distinguish Application context or activity context, so we add @SuppressLint to avoid
// lint error.
@SuppressLint("StaticFieldLeak")
private static SetupCompatServiceInvoker instance;
private static final long MAX_WAIT_TIME_FOR_CONNECTION_MS = TimeUnit.SECONDS.toMillis(10);
}

View File

@@ -0,0 +1,338 @@
/*
* Copyright (C) 2018 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.internal;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Looper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
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;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.UnaryOperator;
/**
* This class provides an instance of {@link ISetupCompatService}. It keeps track of the connection
* state and reconnects if necessary.
*/
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
* receiving the stub reference via {@link ServiceConnection#onServiceConnected(ComponentName,
* IBinder)}.
*
* @throws IllegalStateException if called from the main thread since this is a blocking
* operation.
* @throws TimeoutException if timed out waiting for {@code waitTime}.
*/
public static ISetupCompatService get(Context context, long waitTime, @NonNull TimeUnit timeUnit)
throws TimeoutException, InterruptedException {
return getInstance(context).getService(waitTime, timeUnit);
}
@VisibleForTesting
public ISetupCompatService getService(long timeout, TimeUnit timeUnit)
throws TimeoutException, InterruptedException {
Preconditions.checkState(
disableLooperCheckForTesting || Looper.getMainLooper() != Looper.myLooper(),
"getService blocks and should not be called from the main thread.");
ServiceContext serviceContext = getCurrentServiceState();
switch (serviceContext.state) {
case CONNECTED:
return serviceContext.compatService;
case SERVICE_NOT_USABLE:
case BIND_FAILED:
// End states, no valid connection can be obtained ever.
return null;
case DISCONNECTED:
case BINDING:
return waitForConnection(timeout, timeUnit);
case REBIND_REQUIRED:
requestServiceBind();
return waitForConnection(timeout, timeUnit);
case NOT_STARTED:
LOG.w("NOT_STARTED state only possible before instance is created.");
return null;
}
throw new IllegalStateException("Unknown state = " + serviceContext.state);
}
private ISetupCompatService waitForConnection(long timeout, TimeUnit timeUnit)
throws TimeoutException, InterruptedException {
ServiceContext currentServiceState = getCurrentServiceState();
if (currentServiceState.state == State.CONNECTED) {
return currentServiceState.compatService;
}
CountDownLatch connectedStateLatch = getConnectedCondition();
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,
// requesting rebind reduces the wait time to acquire a new connection.
requestServiceBind();
throw new TimeoutException(
String.format("Failed to acquire connection after [%s %s]", timeout, timeUnit));
}
currentServiceState = getCurrentServiceState();
LOG.atInfo(
String.format(
"Finished waiting for service to get connected. Current state = %s",
currentServiceState.state));
return currentServiceState.compatService;
}
/**
* This method is being overwritten by {@link SetupCompatServiceProviderTest} for injecting an
* instance of {@link CountDownLatch}.
*/
@VisibleForTesting
protected CountDownLatch createCountDownLatch() {
return new CountDownLatch(1);
}
private synchronized void requestServiceBind() {
ServiceContext currentServiceState = getCurrentServiceState();
if (currentServiceState.state == State.CONNECTED) {
LOG.atInfo("Refusing to rebind since current state is already connected");
return;
}
if (currentServiceState.state != State.NOT_STARTED) {
LOG.atInfo("Unbinding existing service connection.");
context.unbindService(serviceConnection);
}
boolean bindAllowed;
try {
bindAllowed =
context.bindService(COMPAT_SERVICE_INTENT, serviceConnection, Context.BIND_AUTO_CREATE);
} catch (SecurityException e) {
LOG.e("Unable to bind to compat service. " + e);
bindAllowed = false;
}
if (bindAllowed) {
// Robolectric calls ServiceConnection#onServiceConnected inline during Context#bindService.
// This check prevents us from overriding connected state which usually arrives much later
// in the normal world
if (getCurrentState() != State.CONNECTED) {
swapServiceContextAndNotify(new ServiceContext(State.BINDING));
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("Context#bindService did not succeed.");
}
}
@VisibleForTesting
static final Intent COMPAT_SERVICE_INTENT =
new Intent()
.setPackage("com.google.android.setupwizard")
.setAction("com.google.android.setupcompat.SetupCompatService.BIND");
@VisibleForTesting
State getCurrentState() {
return serviceContext.state;
}
private synchronized ServiceContext getCurrentServiceState() {
return serviceContext;
}
@VisibleForTesting
void swapServiceContextAndNotify(ServiceContext latestServiceContext) {
LOG.atInfo(
String.format("State changed: %s -> %s", serviceContext.state, latestServiceContext.state));
serviceContext = latestServiceContext;
CountDownLatch countDownLatch = getAndClearConnectedCondition();
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
private CountDownLatch getAndClearConnectedCondition() {
return connectedConditionRef.getAndSet(/* newValue= */ null);
}
/**
* Cannot use {@link AtomicReference#updateAndGet(UnaryOperator)} to fix null reference since the
* library needs to be compatible with legacy android devices.
*/
private CountDownLatch getConnectedCondition() {
CountDownLatch countDownLatch;
// Loop until either count down latch is found or successfully able to update atomic reference.
do {
countDownLatch = connectedConditionRef.get();
if (countDownLatch != null) {
return countDownLatch;
}
countDownLatch = createCountDownLatch();
} while (!connectedConditionRef.compareAndSet(/* expectedValue= */ null, countDownLatch));
return countDownLatch;
}
@VisibleForTesting
SetupCompatServiceProvider(Context context) {
this.context = context.getApplicationContext();
}
@VisibleForTesting
final ServiceConnection serviceConnection =
new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder binder) {
State state = State.CONNECTED;
if (binder == null) {
state = State.DISCONNECTED;
LOG.w("Binder is null when onServiceConnected was called!");
}
swapServiceContextAndNotify(
new ServiceContext(state, ISetupCompatService.Stub.asInterface(binder)));
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
swapServiceContextAndNotify(new ServiceContext(State.DISCONNECTED));
}
@Override
public void onBindingDied(ComponentName name) {
swapServiceContextAndNotify(new ServiceContext(State.REBIND_REQUIRED));
}
@Override
public void onNullBinding(ComponentName name) {
swapServiceContextAndNotify(new ServiceContext(State.SERVICE_NOT_USABLE));
}
};
private volatile ServiceContext serviceContext = new ServiceContext(State.NOT_STARTED);
private final Context context;
private final AtomicReference<CountDownLatch> connectedConditionRef = new AtomicReference<>();
@VisibleForTesting
enum State {
/** Initial state of the service instance is completely created. */
NOT_STARTED,
/**
* Attempt to call {@link Context#bindService(Intent, ServiceConnection, int)} failed because,
* either Setupwizard is not installed or the app does not have permission to bind. This is an
* unrecoverable situation.
*/
BIND_FAILED,
/**
* Call to bind with the service went through, now waiting for {@link
* ServiceConnection#onServiceConnected(ComponentName, IBinder)}.
*/
BINDING,
/** Provider is connected to the service and can call the API(s). */
CONNECTED,
/**
* Not connected since provider received the call {@link
* ServiceConnection#onServiceDisconnected(ComponentName)}, and waiting for {@link
* ServiceConnection#onServiceConnected(ComponentName, IBinder)}.
*/
DISCONNECTED,
/**
* Similar to {@link #BIND_FAILED}, the bind call went through but we received a "null" binding
* via {@link ServiceConnection#onNullBinding(ComponentName)}. This is an unrecoverable
* situation.
*/
SERVICE_NOT_USABLE,
/**
* The provider has requested rebind via {@link Context#bindService(Intent, ServiceConnection,
* int)} and is waiting for a service connection.
*/
REBIND_REQUIRED
}
@VisibleForTesting
static final class ServiceContext {
final State state;
@Nullable final ISetupCompatService compatService;
private ServiceContext(State state, @Nullable ISetupCompatService compatService) {
this.state = state;
this.compatService = compatService;
if (state == State.CONNECTED) {
Preconditions.checkNotNull(
compatService, "CompatService cannot be null when state is connected");
}
}
@VisibleForTesting
ServiceContext(State state) {
this(state, /* compatService= */ null);
}
}
@VisibleForTesting
static SetupCompatServiceProvider getInstance(@NonNull Context context) {
Preconditions.checkNotNull(context, "Context object cannot be null.");
SetupCompatServiceProvider result = instance;
if (result == null) {
synchronized (SetupCompatServiceProvider.class) {
result = instance;
if (result == null) {
instance = result = new SetupCompatServiceProvider(context.getApplicationContext());
instance.requestServiceBind();
}
}
}
return result;
}
@VisibleForTesting
public static void setInstanceForTesting(SetupCompatServiceProvider testInstance) {
instance = testInstance;
}
@VisibleForTesting static boolean disableLooperCheckForTesting = false;
// The instance is coming from Application context which alive during the application activate and
// it's not depend on the activities life cycle, so we can avoid memory leak. However linter
// cannot distinguish Application context or activity context, so we add @SuppressLint to avoid
// lint error.
@SuppressLint("StaticFieldLeak")
private static volatile SetupCompatServiceProvider instance;
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2015 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.internal;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build.VERSION_CODES;
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;
import java.util.Map;
/**
* A generic template class that inflates a template, provided in the constructor or in {@code
* android:layout} through XML, and adds its children to a "container" in the template. When
* inflating this layout from XML, the {@code android:layout} and {@code suwContainer} attributes
* are required.
*
* <p>This class is designed to use inside the library; it is not suitable for external use.
*/
public class TemplateLayout extends FrameLayout {
/**
* The container of the actual content. This will be a view in the template, which child views
* will be added to when {@link #addView(View)} is called.
*/
private ViewGroup container;
private final Map<Class<? extends Mixin>, Mixin> mixins = new HashMap<>();
public TemplateLayout(Context context, int template, int containerId) {
super(context);
init(template, containerId, null, R.attr.sucLayoutTheme);
}
public TemplateLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(0, 0, attrs, R.attr.sucLayoutTheme);
}
@TargetApi(VERSION_CODES.HONEYCOMB)
public TemplateLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(0, 0, attrs, defStyleAttr);
}
// All the constructors delegate to this init method. The 3-argument constructor is not
// available in LinearLayout before v11, so call super with the exact same arguments.
private void init(int template, int containerId, AttributeSet attrs, int defStyleAttr) {
final TypedArray a =
getContext().obtainStyledAttributes(attrs, R.styleable.SucTemplateLayout, defStyleAttr, 0);
if (template == 0) {
template = a.getResourceId(R.styleable.SucTemplateLayout_android_layout, 0);
}
if (containerId == 0) {
containerId = a.getResourceId(R.styleable.SucTemplateLayout_sucContainer, 0);
}
onBeforeTemplateInflated(attrs, defStyleAttr);
inflateTemplate(template, containerId);
a.recycle();
}
/**
* Registers a mixin with a given class. This method should be called in the constructor.
*
* @param cls The class to register the mixin. In most cases, {@code cls} is the same as {@code
* mixin.getClass()}, but {@code cls} can also be a super class of that. In the latter case
* the mixin must be retrieved using {@code cls} in {@link #getMixin(Class)}, not the
* subclass.
* @param mixin The mixin to be registered.
* @param <M> The class of the mixin to register. This is the same as {@code cls}
*/
protected <M extends Mixin> void registerMixin(Class<M> cls, M mixin) {
mixins.put(cls, mixin);
}
/**
* Same as {@link View#findViewById(int)}, but may include views that are managed by this view but
* not currently added to the view hierarchy. e.g. recycler view or list view headers that are not
* currently shown.
*/
// Returning generic type is the common pattern used for findViewBy* methods
@SuppressWarnings("TypeParameterUnusedInFormals")
public <T extends View> T findManagedViewById(int id) {
return findViewById(id);
}
/**
* Get a {@link Mixin} from this template registered earlier in {@link #registerMixin(Class,
* Mixin)}.
*
* @param cls The class marker of Mixin being requested. The actual Mixin returned may be a
* subclass of this marker. Note that this must be the same class as registered in {@link
* #registerMixin(Class, Mixin)}, which is not necessarily the same as the concrete class of
* the instance returned by this method.
* @param <M> The type of the class marker.
* @return The mixin marked by {@code cls}, or null if the template does not have a matching
* mixin.
*/
@SuppressWarnings("unchecked")
public <M extends Mixin> M getMixin(Class<M> cls) {
return (M) mixins.get(cls);
}
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
container.addView(child, index, params);
}
private void addViewInternal(View child) {
super.addView(child, -1, generateDefaultLayoutParams());
}
private void inflateTemplate(int templateResource, int containerId) {
final LayoutInflater inflater = LayoutInflater.from(getContext());
final View templateRoot = onInflateTemplate(inflater, templateResource);
addViewInternal(templateRoot);
container = findContainer(containerId);
if (container == null) {
throw new IllegalArgumentException("Container cannot be null in TemplateLayout");
}
onTemplateInflated();
}
/**
* Inflate the template using the given inflater and theme. The fallback theme will be applied to
* the theme without overriding the values already defined in the theme, but simply providing
* default values for values which have not been defined. This allows templates to add additional
* required theme attributes without breaking existing clients.
*
* <p>In general, clients should still set the activity theme to the corresponding theme in setup
* wizard lib, so that the content area gets the correct styles as well.
*
* @param inflater A LayoutInflater to inflate the template.
* @param fallbackTheme A fallback theme to apply to the template. If the values defined in the
* fallback theme is already defined in the original theme, the value in the original theme
* takes precedence.
* @param template The layout template to be inflated.
* @return Root of the inflated layout.
* @see FallbackThemeWrapper
*/
protected final View inflateTemplate(
LayoutInflater inflater, @StyleRes int fallbackTheme, @LayoutRes int template) {
if (template == 0) {
throw new IllegalArgumentException("android:layout not specified for TemplateLayout");
}
if (fallbackTheme != 0) {
inflater =
LayoutInflater.from(new FallbackThemeWrapper(inflater.getContext(), fallbackTheme));
}
return inflater.inflate(template, this, false);
}
/**
* This method inflates the template. Subclasses can override this method to customize the
* template inflation, or change to a different default template. The root of the inflated layout
* should be returned, and not added to the view hierarchy.
*
* @param inflater A LayoutInflater to inflate the template.
* @param template The resource ID of the template to be inflated, or 0 if no template is
* specified.
* @return Root of the inflated layout.
*/
protected View onInflateTemplate(LayoutInflater inflater, @LayoutRes int template) {
return inflateTemplate(inflater, 0, template);
}
protected ViewGroup findContainer(int containerId) {
return (ViewGroup) findViewById(containerId);
}
/**
* This is called after the template has been inflated and added to the view hierarchy. Subclasses
* can implement this method to modify the template as necessary, such as caching views retrieved
* from findViewById, or other view operations that need to be done in code. You can think of this
* as {@link View#onFinishInflate()} but for inflation of the template instead of for child views.
*/
protected void onTemplateInflated() {}
/**
* This is called before the template has been inflated and added to the view hierarchy.
* Subclasses can implement this method to modify the template as necessary, such as something
* need to be done before onTemplateInflated which is called while still in the constructor.
*/
protected void onBeforeTemplateInflated(AttributeSet attrs, int defStyleAttr) {}
/* Animator support */
private float xFraction;
private ViewTreeObserver.OnPreDrawListener preDrawListener;
/**
* Set the X translation as a fraction of the width of this view. Make sure this method is not
* stripped out by proguard when using this with {@link android.animation.ObjectAnimator}. You may
* need to add <code>
* -keep @androidx.annotation.Keep class *
* </code> to your proguard configuration if you are seeing mysterious {@link NoSuchMethodError}
* at runtime.
*/
@Keep
@TargetApi(VERSION_CODES.HONEYCOMB)
public void setXFraction(float fraction) {
xFraction = fraction;
final int width = getWidth();
if (width != 0) {
setTranslationX(width * fraction);
} else {
// If we haven't done a layout pass yet, wait for one and then set the fraction before
// the draw occurs using an OnPreDrawListener. Don't call translationX until we know
// getWidth() has a reliable, non-zero value or else we will see the fragment flicker on
// screen.
if (preDrawListener == null) {
preDrawListener =
new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(preDrawListener);
setXFraction(xFraction);
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(preDrawListener);
}
}
}
/**
* Return the X translation as a fraction of the width, as previously set in {@link
* #setXFraction(float)}.
*
* @see #setXFraction(float)
*/
@Keep
@TargetApi(VERSION_CODES.HONEYCOMB)
public float getXFraction() {
return xFraction;
}
}

View File

@@ -0,0 +1,29 @@
/*
* Copyright (C) 2019 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.internal;
/**
* A time source; returns a time value representing the number of nanoseconds elapsed since some
* fixed but arbitrary point in time.
*
* <p><b>Warning:</b> this interface can only be used to measure elapsed time, not wall time.
*/
public interface Ticker {
/** Returns the number of nanoseconds elapsed since this ticker's fixed point of reference. */
long read();
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright (C) 2018 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.internal;
/** Commonly used validations and preconditions. */
public final class Validations {
/**
* Asserts that the {@code length} is in the expected range.
*
* @throws IllegalArgumentException if {@code input}'s length is than {@code minLength} or
* greather than {@code maxLength}.
*/
public static void assertLengthInRange(int length, String name, int minLength, int maxLength) {
Preconditions.checkArgument(
length <= maxLength && length >= minLength,
String.format("Length of %s should be in the range [%s-%s]", name, minLength, maxLength));
}
/**
* Asserts that the {@code input}'s length is in the expected range.
*
* @throws NullPointerException if {@code input} is null.
* @throws IllegalArgumentException if {@code input}'s length is than {@code minLength} or
* greather than {@code maxLength}.
*/
public static void assertLengthInRange(String input, String name, int minLength, int maxLength) {
Preconditions.checkNotNull(input, String.format("%s cannot be null.", name));
assertLengthInRange(input.length(), name, minLength, maxLength);
}
private Validations() {
throw new AssertionError("Should not be instantiated");
}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import static com.google.android.setupcompat.internal.Validations.assertLengthInRange;
import android.annotation.TargetApi;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.ClockProvider;
import com.google.android.setupcompat.internal.PersistableBundles;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.util.ObjectUtils;
/**
* This class represents a interesting event at a particular point in time. The event is identified
* by {@link MetricKey} along with {@code timestamp}. It can include additional key-value pairs
* providing more attributes associated with the given event. Only primitive values are supported
* for now (int, long, double, float, String).
*/
@TargetApi(VERSION_CODES.Q)
public final class CustomEvent implements Parcelable {
private static final String BUNDLE_KEY_TIMESTAMP = "CustomEvent_timestamp";
private static final String BUNDLE_KEY_METRICKEY = "CustomEvent_metricKey";
private static final String BUNDLE_KEY_BUNDLE_VALUES = "CustomEvent_bundleValues";
private static final String BUNDLE_KEY_BUNDLE_PII_VALUES = "CustomEvent_pii_bundleValues";
private static final String BUNDLE_VERSION = "CustomEvent_version";
private static final int VERSION = 1;
/** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
public static CustomEvent create(
MetricKey metricKey, PersistableBundle bundle, PersistableBundle piiValues) {
Preconditions.checkArgument(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
"The constructor only support on sdk Q or higher");
return new CustomEvent(
ClockProvider.timeInMillis(),
metricKey,
// Assert only in factory methods since these methods are directly used by API consumers
// while constructor is used directly only when data is de-serialized from bundle (which
// might have been sent by a client using a newer API)
PersistableBundles.assertIsValid(bundle),
PersistableBundles.assertIsValid(piiValues));
}
/** Creates a new instance of {@code CustomEvent}. Null arguments are not allowed. */
public static CustomEvent create(MetricKey metricKey, PersistableBundle bundle) {
Preconditions.checkArgument(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q,
"The constructor only support on sdk Q or higher.");
return create(metricKey, bundle, PersistableBundle.EMPTY);
}
/** Converts {@link Bundle} into {@link CustomEvent}. */
public static CustomEvent toCustomEvent(Bundle bundle) {
return new CustomEvent(
bundle.getLong(BUNDLE_KEY_TIMESTAMP, /* defaultValue= */ Long.MIN_VALUE),
MetricKey.toMetricKey(bundle.getBundle(BUNDLE_KEY_METRICKEY)),
PersistableBundles.fromBundle(bundle.getBundle(BUNDLE_KEY_BUNDLE_VALUES)),
PersistableBundles.fromBundle(bundle.getBundle(BUNDLE_KEY_BUNDLE_PII_VALUES)));
}
/** Converts {@link CustomEvent} into {@link Bundle}. */
public static Bundle toBundle(CustomEvent customEvent) {
Preconditions.checkNotNull(customEvent, "CustomEvent cannot be null");
Bundle bundle = new Bundle();
bundle.putInt(BUNDLE_VERSION, VERSION);
bundle.putLong(BUNDLE_KEY_TIMESTAMP, customEvent.timestampMillis());
bundle.putBundle(BUNDLE_KEY_METRICKEY, MetricKey.fromMetricKey(customEvent.metricKey()));
bundle.putBundle(BUNDLE_KEY_BUNDLE_VALUES, PersistableBundles.toBundle(customEvent.values()));
bundle.putBundle(
BUNDLE_KEY_BUNDLE_PII_VALUES, PersistableBundles.toBundle(customEvent.piiValues()));
return bundle;
}
public static final Creator<CustomEvent> CREATOR =
new Creator<CustomEvent>() {
@Override
public CustomEvent createFromParcel(Parcel in) {
return new CustomEvent(
in.readLong(),
in.readParcelable(MetricKey.class.getClassLoader()),
in.readPersistableBundle(MetricKey.class.getClassLoader()),
in.readPersistableBundle(MetricKey.class.getClassLoader()));
}
@Override
public CustomEvent[] newArray(int size) {
return new CustomEvent[size];
}
};
/** Returns the timestamp of when the event occurred. */
public long timestampMillis() {
return timestampMillis;
}
/** Returns the identifier of the event. */
public MetricKey metricKey() {
return this.metricKey;
}
/** Returns the non PII values describing the event. Only primitive values are supported. */
public PersistableBundle values() {
return new PersistableBundle(this.persistableBundle);
}
/**
* Returns the PII(Personally identifiable information) values describing the event. These values
* will not be included in the aggregated logs. Only primitive values are supported.
*/
public PersistableBundle piiValues() {
return this.piiValues;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeLong(timestampMillis);
parcel.writeParcelable(metricKey, i);
parcel.writePersistableBundle(persistableBundle);
parcel.writePersistableBundle(piiValues);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof CustomEvent)) {
return false;
}
CustomEvent that = (CustomEvent) o;
return timestampMillis == that.timestampMillis
&& ObjectUtils.equals(metricKey, that.metricKey)
&& PersistableBundles.equals(persistableBundle, that.persistableBundle)
&& PersistableBundles.equals(piiValues, that.piiValues);
}
@Override
public int hashCode() {
return ObjectUtils.hashCode(timestampMillis, metricKey, persistableBundle, piiValues);
}
private CustomEvent(
long timestampMillis,
MetricKey metricKey,
PersistableBundle bundle,
PersistableBundle piiValues) {
Preconditions.checkArgument(timestampMillis >= 0, "Timestamp cannot be negative.");
Preconditions.checkNotNull(metricKey, "MetricKey cannot be null.");
Preconditions.checkNotNull(bundle, "Bundle cannot be null.");
Preconditions.checkArgument(!bundle.isEmpty(), "Bundle cannot be empty.");
Preconditions.checkNotNull(piiValues, "piiValues cannot be null.");
assertPersistableBundleIsValid(bundle);
this.timestampMillis = timestampMillis;
this.metricKey = metricKey;
this.persistableBundle = new PersistableBundle(bundle);
this.piiValues = new PersistableBundle(piiValues);
}
private final long timestampMillis;
private final MetricKey metricKey;
private final PersistableBundle persistableBundle;
private final PersistableBundle piiValues;
private static void assertPersistableBundleIsValid(PersistableBundle bundle) {
for (String key : bundle.keySet()) {
assertLengthInRange(key, "bundle key", MIN_BUNDLE_KEY_LENGTH, MAX_STR_LENGTH);
Object value = bundle.get(key);
if (value instanceof String) {
Preconditions.checkArgument(
((String) value).length() <= MAX_STR_LENGTH,
String.format(
"Maximum length of string value for key='%s' cannot exceed %s.",
key, MAX_STR_LENGTH));
}
}
}
/**
* Trims the string longer than {@code MAX_STR_LENGTH} character, only keep the first {@code
* MAX_STR_LENGTH} - 1 characters and attached … in the end.
*/
@NonNull
public static String trimsStringOverMaxLength(@NonNull String str) {
if (str.length() <= MAX_STR_LENGTH) {
return str;
} else {
return String.format("%s…", str.substring(0, MAX_STR_LENGTH - 1));
}
}
@VisibleForTesting static final int MAX_STR_LENGTH = 50;
@VisibleForTesting static final int MIN_BUNDLE_KEY_LENGTH = 3;
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging
import android.view.View
/**
* An abstract class which can be attached to a Setupcompat layout and provides methods for logging
* impressions and interactions of its views and buttons.
*/
interface LoggingObserver {
fun log(event: SetupCompatUiEvent)
sealed class SetupCompatUiEvent {
data class LayoutInflatedEvent(val view: View) : SetupCompatUiEvent()
data class LayoutShownEvent(val view: View) : SetupCompatUiEvent()
data class ButtonInflatedEvent(val view: View, val buttonType: ButtonType) :
SetupCompatUiEvent()
data class ButtonShownEvent(val view: View, val buttonType: ButtonType) : SetupCompatUiEvent()
data class ButtonInteractionEvent(val view: View, val interactionType: InteractionType) :
SetupCompatUiEvent()
}
enum class ButtonType {
UNKNOWN,
PRIMARY,
SECONDARY
}
enum class InteractionType {
UNKNOWN,
TAP,
LONG_PRESS,
DOUBLE_TAP
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import static com.google.android.setupcompat.internal.Validations.assertLengthInRange;
import android.app.Activity;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.util.ObjectUtils;
import java.util.regex.Pattern;
/**
* A metric key represents a validated “string key” and a "screen name" that is associated with the
* values reported by the API consumer.
*/
public final class MetricKey implements Parcelable {
private static final String METRIC_KEY_BUNDLE_NAME_KEY = "MetricKey_name";
private static final String METRIC_KEY_BUNDLE_SCREEN_NAME_KEY = "MetricKey_screenName";
private static final String METRIC_KEY_BUNDLE_VERSION = "MetricKey_version";
private static final int VERSION = 1;
/**
* Creates a new instance of MetricKey.
*
* @param name metric name to identify what we log
* @param activity activity of metric screen, uses to generate screenName
*/
public static MetricKey get(@NonNull String name, @NonNull Activity activity) {
String screenName = activity.getComponentName().getClassName();
assertLengthInRange(name, "MetricKey.name", MIN_METRIC_KEY_LENGTH, MAX_METRIC_KEY_LENGTH);
Preconditions.checkArgument(
METRIC_KEY_PATTERN.matcher(name).matches(),
"Invalid MetricKey, only alpha numeric characters are allowed.");
return new MetricKey(name, screenName);
}
/**
* Creates a new instance of MetricKey.
*
* <p>NOTE:
*
* <ul>
* <li>Length of {@code name} should be in range of 5-30 characters, only alpha numeric
* characters are allowed.
* <li>Length of {@code screenName} should be in range of 5-50 characters, only alpha numeric
* characters are allowed.
* </ul>
*/
public static MetricKey get(@NonNull String name, @NonNull String screenName) {
// We only checked the length of customized screen name, by the reason if the screenName match
// to the class name skip check it
if (!SCREEN_COMPONENTNAME_PATTERN.matcher(screenName).matches()) {
assertLengthInRange(
screenName, "MetricKey.screenName", MIN_SCREEN_NAME_LENGTH, MAX_SCREEN_NAME_LENGTH);
Preconditions.checkArgument(
SCREEN_NAME_PATTERN.matcher(screenName).matches(),
"Invalid ScreenName, only alpha numeric characters are allowed.");
}
assertLengthInRange(name, "MetricKey.name", MIN_METRIC_KEY_LENGTH, MAX_METRIC_KEY_LENGTH);
Preconditions.checkArgument(
METRIC_KEY_PATTERN.matcher(name).matches(),
"Invalid MetricKey, only alpha numeric characters are allowed.");
return new MetricKey(name, screenName);
}
/** Converts {@link MetricKey} into {@link Bundle}. */
public static Bundle fromMetricKey(MetricKey metricKey) {
Preconditions.checkNotNull(metricKey, "MetricKey cannot be null.");
Bundle bundle = new Bundle();
bundle.putInt(METRIC_KEY_BUNDLE_VERSION, VERSION);
bundle.putString(METRIC_KEY_BUNDLE_NAME_KEY, metricKey.name());
bundle.putString(METRIC_KEY_BUNDLE_SCREEN_NAME_KEY, metricKey.screenName());
return bundle;
}
/** Converts {@link Bundle} into {@link MetricKey}. */
public static MetricKey toMetricKey(Bundle bundle) {
Preconditions.checkNotNull(bundle, "Bundle cannot be null");
return MetricKey.get(
bundle.getString(METRIC_KEY_BUNDLE_NAME_KEY),
bundle.getString(METRIC_KEY_BUNDLE_SCREEN_NAME_KEY));
}
public static final Creator<MetricKey> CREATOR =
new Creator<MetricKey>() {
@Override
public MetricKey createFromParcel(Parcel in) {
return new MetricKey(in.readString(), in.readString());
}
@Override
public MetricKey[] newArray(int size) {
return new MetricKey[size];
}
};
/** Returns the name of the metric key. */
public String name() {
return name;
}
/** Returns the name of the metric key. */
public String screenName() {
return screenName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeString(screenName);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof MetricKey)) {
return false;
}
MetricKey metricKey = (MetricKey) o;
return ObjectUtils.equals(name, metricKey.name)
&& ObjectUtils.equals(screenName, metricKey.screenName);
}
@Override
public int hashCode() {
return ObjectUtils.hashCode(name, screenName);
}
private MetricKey(String name, String screenName) {
this.name = name;
this.screenName = screenName;
}
private final String name;
private final String screenName;
private static final int MIN_SCREEN_NAME_LENGTH = 5;
private static final int MIN_METRIC_KEY_LENGTH = 5;
private static final int MAX_SCREEN_NAME_LENGTH = 50;
private static final int MAX_METRIC_KEY_LENGTH = 30;
private static final Pattern METRIC_KEY_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+");
private static final Pattern SCREEN_COMPONENTNAME_PATTERN =
Pattern.compile("^([a-z]+[.])+[A-Z][a-zA-Z0-9]+");
private static final Pattern SCREEN_NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+");
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import static com.google.android.setupcompat.internal.Validations.assertLengthInRange;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.util.ObjectUtils;
import java.util.regex.Pattern;
/**
* A screen key represents a validated “string key” that is associated with the values reported by
* the API consumer.
*/
public class ScreenKey implements Parcelable {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SCREEN_KEY_BUNDLE_NAME_KEY = "ScreenKey_name";
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SCREEN_KEY_BUNDLE_PACKAGE_KEY = "ScreenKey_package";
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SCREEN_KEY_BUNDLE_VERSION_KEY = "ScreenKey_version";
private static final int INVALID_VERSION = -1;
private static final int VERSION = 1;
/**
* Creates a new instance of {@link ScreenKey}.
*
* @param name screen name to identify what the metric belongs to. It should be in the range of
* 5-50 characters, only alphanumeric characters are allowed.
* @param context context associated to metric screen, uses to generate package name.
*/
public static ScreenKey of(@NonNull String name, @NonNull Context context) {
Preconditions.checkNotNull(context, "Context can not be null.");
return ScreenKey.of(name, context.getPackageName());
}
private static ScreenKey of(@NonNull String name, @NonNull String packageName) {
Preconditions.checkArgument(
SCREEN_PACKAGENAME_PATTERN.matcher(packageName).matches(),
"Invalid ScreenKey#package, only alpha numeric characters are allowed.");
assertLengthInRange(
name, "ScreenKey.name", MIN_SCREEN_NAME_LENGTH, MAX_SCREEN_NAME_LENGTH);
Preconditions.checkArgument(
SCREEN_NAME_PATTERN.matcher(name).matches(),
"Invalid ScreenKey#name, only alpha numeric characters are allowed.");
return new ScreenKey(name, packageName);
}
/**
* Converts {@link ScreenKey} into {@link Bundle}.
* Throw {@link NullPointerException} if the screenKey is null.
*/
public static Bundle toBundle(ScreenKey screenKey) {
Preconditions.checkNotNull(screenKey, "ScreenKey cannot be null.");
Bundle bundle = new Bundle();
bundle.putInt(SCREEN_KEY_BUNDLE_VERSION_KEY, VERSION);
bundle.putString(SCREEN_KEY_BUNDLE_NAME_KEY, screenKey.getName());
bundle.putString(SCREEN_KEY_BUNDLE_PACKAGE_KEY, screenKey.getPackageName());
return bundle;
}
/**
* Converts {@link Bundle} into {@link ScreenKey}.
* Throw {@link NullPointerException} if the bundle is null.
* Throw {@link IllegalArgumentException} if the bundle version is unsupported.
*/
public static ScreenKey fromBundle(Bundle bundle) {
Preconditions.checkNotNull(bundle, "Bundle cannot be null");
int version = bundle.getInt(SCREEN_KEY_BUNDLE_VERSION_KEY, INVALID_VERSION);
if (version == 1) {
return ScreenKey.of(
bundle.getString(SCREEN_KEY_BUNDLE_NAME_KEY),
bundle.getString(SCREEN_KEY_BUNDLE_PACKAGE_KEY));
} else {
// Invalid version
throw new IllegalArgumentException("Unsupported version: " + version);
}
}
public static final Creator<ScreenKey> CREATOR =
new Creator<>() {
@Override
public ScreenKey createFromParcel(Parcel in) {
return new ScreenKey(in.readString(), in.readString());
}
@Override
public ScreenKey[] newArray(int size) {
return new ScreenKey[size];
}
};
/** Returns the name of the screen key. */
public String getName() {
return name;
}
/** Returns the package name of the screen key. */
public String getPackageName() {
return packageName;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(name);
parcel.writeString(packageName);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ScreenKey)) {
return false;
}
ScreenKey screenKey = (ScreenKey) o;
return ObjectUtils.equals(name, screenKey.name)
&& ObjectUtils.equals(packageName, screenKey.packageName);
}
@Override
public int hashCode() {
return ObjectUtils.hashCode(name, packageName);
}
@NonNull
@Override
public String toString() {
return "ScreenKey {name="
+ getName()
+ ", package="
+ getPackageName()
+ "}";
}
private ScreenKey(String name, String packageName) {
this.name = name;
this.packageName = packageName;
}
private final String name;
private final String packageName;
private static final int MIN_SCREEN_NAME_LENGTH = 5;
private static final int MAX_SCREEN_NAME_LENGTH = 50;
private static final Pattern SCREEN_NAME_PATTERN = Pattern.compile("^[a-zA-Z][a-zA-Z0-9_]+");
private static final Pattern SCREEN_PACKAGENAME_PATTERN =
Pattern.compile("^([a-z]+[.])+[a-zA-Z][a-zA-Z0-9]+");
}

View File

@@ -0,0 +1,254 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PersistableBundle;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.ClockProvider;
import com.google.android.setupcompat.internal.PersistableBundles;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.EventType;
import com.google.android.setupcompat.util.ObjectUtils;
/**
* This class represents a setup metric event at a particular point in time.
* The event is identified by {@link EventType} along with a string name. It can include
* additional key-value pairs providing more attributes associated with the given event. Only
* primitive values are supported for now (int, long, boolean, String).
*/
@TargetApi(VERSION_CODES.Q)
public class SetupMetric implements Parcelable {
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SETUP_METRIC_BUNDLE_VERSION_KEY = "SetupMetric_version";
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SETUP_METRIC_BUNDLE_NAME_KEY = "SetupMetric_name";
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SETUP_METRIC_BUNDLE_TYPE_KEY = "SetupMetric_type";
@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
public static final String SETUP_METRIC_BUNDLE_VALUES_KEY = "SetupMetric_values";
private static final int VERSION = 1;
private static final int INVALID_VERSION = -1;
public static final String SETUP_METRIC_BUNDLE_OPTIN_KEY = "opt_in";
public static final String SETUP_METRIC_BUNDLE_ERROR_KEY = "error";
public static final String SETUP_METRIC_BUNDLE_TIMESTAMP_KEY = "timestamp";
/**
* A convenient function to create a setup event with event type {@link EventType#IMPRESSION}
* @param name A name represents this impression
* @return A {@link SetupMetric}
* @throws IllegalArgumentException if the {@code name} is empty.
*/
@NonNull
public static SetupMetric ofImpression(@NonNull String name) {
Bundle bundle = new Bundle();
bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
return new SetupMetric(VERSION, name, EventType.IMPRESSION,
PersistableBundles.fromBundle(bundle));
}
/**
* A convenient function to create a setup event with event type {@link EventType#OPT_IN}
* @param name A name represents this opt-in
* @param status Opt-in status in {@code true} or {@code false}
* @return A {@link SetupMetric}
* @throws IllegalArgumentException if the {@code name} is empty.
*/
@NonNull
public static SetupMetric ofOptIn(@NonNull String name, boolean status) {
Bundle bundle = new Bundle();
bundle.putBoolean(SETUP_METRIC_BUNDLE_OPTIN_KEY, status);
bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
return new SetupMetric(VERSION, name, EventType.OPT_IN, PersistableBundles.fromBundle(bundle));
}
/**
* A convenient function to create a setup event with event type
* {@link EventType#WAITING_START}
* @param name A task name causes this waiting duration
* @return A {@link SetupMetric}
* @throws IllegalArgumentException if the {@code name} is empty.
*/
@NonNull
public static SetupMetric ofWaitingStart(@NonNull String name) {
Bundle bundle = new Bundle();
bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
return new SetupMetric(VERSION, name, EventType.WAITING_START,
PersistableBundles.fromBundle(bundle));
}
/**
* A convenient function to create a setup event with event type
* {@link EventType#WAITING_END}
* @param name A task name causes this waiting duration
* @return A {@link SetupMetric}
* @throws IllegalArgumentException if the {@code name} is empty.
*/
@NonNull
public static SetupMetric ofWaitingEnd(@NonNull String name) {
Bundle bundle = new Bundle();
bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
return new SetupMetric(VERSION, name, EventType.WAITING_END,
PersistableBundles.fromBundle(bundle));
}
/**
* A convenient function to create a setup event with event type {@link EventType#ERROR}
* @param name A name represents this error
* @param errorCode A error code
* @return A {@link SetupMetric}
* @throws IllegalArgumentException if the {@code name} is empty.
*/
@NonNull
public static SetupMetric ofError(@NonNull String name, int errorCode) {
Bundle bundle = new Bundle();
bundle.putInt(SETUP_METRIC_BUNDLE_ERROR_KEY, errorCode);
bundle.putLong(SETUP_METRIC_BUNDLE_TIMESTAMP_KEY, ClockProvider.timeInMillis());
return new SetupMetric(VERSION, name, EventType.ERROR, PersistableBundles.fromBundle(bundle));
}
/** Converts {@link SetupMetric} into {@link Bundle}. */
@NonNull
public static Bundle toBundle(@NonNull SetupMetric setupMetric) {
Preconditions.checkNotNull(setupMetric, "SetupMetric cannot be null.");
Bundle bundle = new Bundle();
bundle.putInt(SETUP_METRIC_BUNDLE_VERSION_KEY, VERSION);
bundle.putString(SETUP_METRIC_BUNDLE_NAME_KEY, setupMetric.name);
bundle.putInt(SETUP_METRIC_BUNDLE_TYPE_KEY, setupMetric.type);
bundle.putBundle(
SETUP_METRIC_BUNDLE_VALUES_KEY, PersistableBundles.toBundle(setupMetric.values));
return bundle;
}
/**
* Converts {@link Bundle} into {@link SetupMetric}.
* Throw {@link IllegalArgumentException} if the bundle version is unsupported.
*/
@NonNull
public static SetupMetric fromBundle(@NonNull Bundle bundle) {
Preconditions.checkNotNull(bundle, "Bundle cannot be null");
int version = bundle.getInt(SETUP_METRIC_BUNDLE_VERSION_KEY, INVALID_VERSION);
if (version == 1) {
return new SetupMetric(
bundle.getInt(SETUP_METRIC_BUNDLE_VERSION_KEY),
bundle.getString(SETUP_METRIC_BUNDLE_NAME_KEY),
bundle.getInt(SETUP_METRIC_BUNDLE_TYPE_KEY),
PersistableBundles.fromBundle(bundle.getBundle(SETUP_METRIC_BUNDLE_VALUES_KEY)));
} else {
throw new IllegalArgumentException("Unsupported version: " + version);
}
}
private SetupMetric(
int version, String name, @EventType int type, @NonNull PersistableBundle values) {
Preconditions.checkArgument(
name != null && name.length() != 0,
"name cannot be null or empty.");
this.version = version;
this.name = name;
this.type = type;
this.values = values;
}
private final int version;
private final String name;
@EventType private final int type;
private final PersistableBundle values;
public int getVersion() {
return version;
}
public String getName() {
return name;
}
@EventType
public int getType() {
return type;
}
public PersistableBundle getValues() {
return values;
}
public static final Creator<SetupMetric> CREATOR =
new Creator<>() {
@Override
public SetupMetric createFromParcel(@NonNull Parcel in) {
return new SetupMetric(in.readInt(),
in.readString(),
in.readInt(),
in.readPersistableBundle(SetupMetric.class.getClassLoader()));
}
@Override
public SetupMetric[] newArray(int size) {
return new SetupMetric[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(name);
parcel.writeInt(type);
parcel.writePersistableBundle(values);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SetupMetric)) {
return false;
}
SetupMetric that = (SetupMetric) o;
return ObjectUtils.equals(name, that.name)
&& ObjectUtils.equals(type, that.type)
&& PersistableBundles.equals(values, that.values);
}
@Override
public int hashCode() {
return ObjectUtils.hashCode(name, type, values);
}
@NonNull
@Override
public String toString() {
return "SetupMetric {name="
+ getName()
+ ", type="
+ getType()
+ ", bundle="
+ getValues().toString()
+ "}";
}
}

View File

@@ -0,0 +1,142 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.google.android.setupcompat.internal.Preconditions;
import com.google.android.setupcompat.internal.SetupCompatServiceInvoker;
import com.google.android.setupcompat.logging.internal.MetricBundleConverter;
import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricType;
import com.google.android.setupcompat.util.Logger;
import java.util.concurrent.TimeUnit;
/**
* SetupMetricsLogger provides an easy way to log custom metrics to SetupWizard.
* (go/suw-metrics-collection-api)
*/
public class SetupMetricsLogger {
private static final Logger LOG = new Logger("SetupMetricsLogger");
/** Logs an instance of {@link CustomEvent} to SetupWizard. */
public static void logCustomEvent(@NonNull Context context, @NonNull CustomEvent customEvent) {
Preconditions.checkNotNull(context, "Context cannot be null.");
Preconditions.checkNotNull(customEvent, "CustomEvent cannot be null.");
SetupCompatServiceInvoker.get(context)
.logMetricEvent(
MetricType.CUSTOM_EVENT, MetricBundleConverter.createBundleForLogging(customEvent));
}
/** Increments the counter value with the name {@code counterName} by {@code times}. */
public static void logCounter(
@NonNull Context context, @NonNull MetricKey counterName, int times) {
Preconditions.checkNotNull(context, "Context cannot be null.");
Preconditions.checkNotNull(counterName, "CounterName cannot be null.");
Preconditions.checkArgument(times > 0, "Counter cannot be negative.");
SetupCompatServiceInvoker.get(context)
.logMetricEvent(
MetricType.COUNTER_EVENT,
MetricBundleConverter.createBundleForLoggingCounter(counterName, times));
}
/**
* Logs the {@link Timer}'s duration by calling {@link #logDuration(Context, MetricKey, long)}.
*/
public static void logDuration(@NonNull Context context, @NonNull Timer timer) {
Preconditions.checkNotNull(context, "Context cannot be null.");
Preconditions.checkNotNull(timer, "Timer cannot be null.");
Preconditions.checkArgument(
timer.isStopped(), "Timer should be stopped before calling logDuration.");
logDuration(
context, timer.getMetricKey(), TimeUnit.NANOSECONDS.toMillis(timer.getDurationInNanos()));
}
/** Logs a duration event to SetupWizard. */
public static void logDuration(
@NonNull Context context, @NonNull MetricKey timerName, long timeInMillis) {
Preconditions.checkNotNull(context, "Context cannot be null.");
Preconditions.checkNotNull(timerName, "Timer name cannot be null.");
Preconditions.checkArgument(timeInMillis >= 0, "Duration cannot be negative.");
SetupCompatServiceInvoker.get(context)
.logMetricEvent(
MetricType.DURATION_EVENT,
MetricBundleConverter.createBundleForLoggingTimer(timerName, timeInMillis));
}
/**
* Logs setup collection metrics
*/
public static void logMetrics(
@NonNull Context context, @NonNull ScreenKey screenKey, @NonNull SetupMetric... metrics) {
Preconditions.checkNotNull(context, "Context cannot be null.");
Preconditions.checkNotNull(screenKey, "ScreenKey cannot be null.");
Preconditions.checkNotNull(metrics, "SetupMetric cannot be null.");
for (SetupMetric metric : metrics) {
LOG.atDebug("Log metric: " + screenKey + ", " + metric);
SetupCompatServiceInvoker.get(context).logMetricEvent(
MetricType.SETUP_COLLECTION_EVENT,
MetricBundleConverter.createBundleForLoggingSetupMetric(screenKey, metric));
}
}
/**
* A non-static method to log setup collection metrics calling
* {@link #logMetrics(Context, ScreenKey, SetupMetric...)} as the actual implementation. This
* function is useful when performing unit tests in caller's implementation.
* <p>
* For unit testing, caller uses {@link #setInstanceForTesting(SetupMetricsLogger)} to inject the
* mocked SetupMetricsLogger instance and use {@link SetupMetricsLogger#get(Context)} to get the
* SetupMetricsLogger. And verify the this function is called with expected parameters.
*
* @see #logMetrics(Context, ScreenKey, SetupMetric...)
*/
public void logMetrics(@NonNull ScreenKey screenKey, @NonNull SetupMetric... metrics) {
SetupMetricsLogger.logMetrics(context, screenKey, metrics);
}
private SetupMetricsLogger(Context context) {
this.context = context;
}
private final Context context;
/** Use this function to get a singleton of {@link SetupMetricsLogger} */
public static synchronized SetupMetricsLogger get(Context context) {
if (instance == null) {
instance = new SetupMetricsLogger(context.getApplicationContext());
}
return instance;
}
@VisibleForTesting
public static void setInstanceForTesting(SetupMetricsLogger testInstance) {
instance = testInstance;
}
// The instance is coming from Application context which alive during the application activate and
// it's not depend on the activities life cycle, so we can avoid memory leak. However linter
// cannot distinguish Application context or activity context, so we add @SuppressLint to avoid
// lint error.
@SuppressLint("StaticFieldLeak")
private static SetupMetricsLogger instance;
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging;
import android.util.Log;
import com.google.android.setupcompat.internal.ClockProvider;
import com.google.android.setupcompat.internal.Preconditions;
/** Convenience utility to log duration events. Please note that this class is not thread-safe. */
public final class Timer {
/** Creates a new instance of timer for the given {@code metricKey}. */
public Timer(MetricKey metricKey) {
this.metricKey = metricKey;
}
/**
* Starts the timer and notes the current clock time.
*
* @throws IllegalStateException if the timer was stopped.
*/
public void start() {
Preconditions.checkState(!isStopped(), "Timer cannot be started once stopped.");
if (isStarted()) {
Log.wtf(
TAG,
String.format(
"Timer instance was already started for: %s at [%s].", metricKey, startInNanos));
return;
}
startInNanos = ClockProvider.timeInNanos();
}
/**
* Stops the watch and the current clock time is noted.
*
* @throws IllegalStateException if the watch was not started.
*/
public void stop() {
Preconditions.checkState(isStarted(), "Timer must be started before it can be stopped.");
if (isStopped()) {
Log.wtf(
TAG,
String.format(
"Timer instance was already stopped for: %s at [%s]", metricKey, stopInNanos));
return;
}
stopInNanos = ClockProvider.timeInNanos();
}
boolean isStopped() {
return stopInNanos != 0;
}
private boolean isStarted() {
return startInNanos != 0;
}
long getDurationInNanos() {
return stopInNanos - startInNanos;
}
MetricKey getMetricKey() {
return metricKey;
}
private long startInNanos;
private long stopInNanos;
private final MetricKey metricKey;
private static final String TAG = "SetupCompat.Timer";
}

View File

@@ -0,0 +1,133 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging.internal;
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.TargetApi;
import android.os.Build.VERSION_CODES;
import android.os.PersistableBundle;
import androidx.annotation.StringDef;
import androidx.annotation.VisibleForTesting;
import java.lang.annotation.Retention;
/** Uses to log internal event footer button metric */
public class FooterBarMixinMetrics {
@VisibleForTesting
public static final String EXTRA_PRIMARY_BUTTON_VISIBILITY = "PrimaryButtonVisibility";
@VisibleForTesting
public static final String EXTRA_SECONDARY_BUTTON_VISIBILITY = "SecondaryButtonVisibility";
@Retention(SOURCE)
@StringDef({
FooterButtonVisibility.UNKNOWN,
FooterButtonVisibility.VISIBLE_USING_XML,
FooterButtonVisibility.VISIBLE,
FooterButtonVisibility.VISIBLE_USING_XML_TO_INVISIBLE,
FooterButtonVisibility.VISIBLE_TO_INVISIBLE,
FooterButtonVisibility.INVISIBLE_TO_VISIBLE,
FooterButtonVisibility.INVISIBLE,
})
@VisibleForTesting
public @interface FooterButtonVisibility {
String UNKNOWN = "Unknown";
String VISIBLE_USING_XML = "VisibleUsingXml";
String VISIBLE = "Visible";
String VISIBLE_USING_XML_TO_INVISIBLE = "VisibleUsingXml_to_Invisible";
String VISIBLE_TO_INVISIBLE = "Visible_to_Invisible";
String INVISIBLE_TO_VISIBLE = "Invisible_to_Visible";
String INVISIBLE = "Invisible";
}
@FooterButtonVisibility String primaryButtonVisibility = FooterButtonVisibility.UNKNOWN;
@FooterButtonVisibility String secondaryButtonVisibility = FooterButtonVisibility.UNKNOWN;
/** Creates a metric object for metric logging */
public FooterBarMixinMetrics() {}
/** Gets initial state visibility */
@FooterButtonVisibility
public String getInitialStateVisibility(boolean isVisible, boolean isUsingXml) {
@FooterButtonVisibility String visibility;
if (isVisible) {
visibility =
isUsingXml ? FooterButtonVisibility.VISIBLE_USING_XML : FooterButtonVisibility.VISIBLE;
} else {
visibility = FooterButtonVisibility.INVISIBLE;
}
return visibility;
}
/** Saves primary footer button visibility when initial state */
public void logPrimaryButtonInitialStateVisibility(boolean isVisible, boolean isUsingXml) {
primaryButtonVisibility =
primaryButtonVisibility.equals(FooterButtonVisibility.UNKNOWN)
? getInitialStateVisibility(isVisible, isUsingXml)
: primaryButtonVisibility;
}
/** Saves secondary footer button visibility when initial state */
public void logSecondaryButtonInitialStateVisibility(boolean isVisible, boolean isUsingXml) {
secondaryButtonVisibility =
secondaryButtonVisibility.equals(FooterButtonVisibility.UNKNOWN)
? getInitialStateVisibility(isVisible, isUsingXml)
: secondaryButtonVisibility;
}
/** Saves footer button visibility when finish state */
public void updateButtonVisibility(
boolean isPrimaryButtonVisible, boolean isSecondaryButtonVisible) {
primaryButtonVisibility =
updateButtonVisibilityState(primaryButtonVisibility, isPrimaryButtonVisible);
secondaryButtonVisibility =
updateButtonVisibilityState(secondaryButtonVisibility, isSecondaryButtonVisible);
}
@FooterButtonVisibility
static String updateButtonVisibilityState(
@FooterButtonVisibility String originalVisibility, boolean isVisible) {
if (!FooterButtonVisibility.VISIBLE_USING_XML.equals(originalVisibility)
&& !FooterButtonVisibility.VISIBLE.equals(originalVisibility)
&& !FooterButtonVisibility.INVISIBLE.equals(originalVisibility)) {
throw new IllegalStateException("Illegal visibility state: " + originalVisibility);
}
if (isVisible && FooterButtonVisibility.INVISIBLE.equals(originalVisibility)) {
return FooterButtonVisibility.INVISIBLE_TO_VISIBLE;
} else if (!isVisible) {
if (FooterButtonVisibility.VISIBLE_USING_XML.equals(originalVisibility)) {
return FooterButtonVisibility.VISIBLE_USING_XML_TO_INVISIBLE;
} else if (FooterButtonVisibility.VISIBLE.equals(originalVisibility)) {
return FooterButtonVisibility.VISIBLE_TO_INVISIBLE;
}
}
return originalVisibility;
}
/** Returns metrics data for logging */
@TargetApi(VERSION_CODES.Q)
public PersistableBundle getMetrics() {
PersistableBundle persistableBundle = new PersistableBundle();
persistableBundle.putString(EXTRA_PRIMARY_BUTTON_VISIBILITY, primaryButtonVisibility);
persistableBundle.putString(EXTRA_SECONDARY_BUTTON_VISIBILITY, secondaryButtonVisibility);
return persistableBundle;
}
}

View File

@@ -0,0 +1,43 @@
package com.google.android.setupcompat.logging.internal;
import android.os.Bundle;
import com.google.android.setupcompat.logging.CustomEvent;
import com.google.android.setupcompat.logging.MetricKey;
import com.google.android.setupcompat.logging.ScreenKey;
import com.google.android.setupcompat.logging.SetupMetric;
import com.google.android.setupcompat.logging.internal.SetupMetricsLoggingConstants.MetricBundleKeys;
/** Collection of helper methods for reading and writing {@link CustomEvent}, {@link MetricKey}. */
public final class MetricBundleConverter {
public static Bundle createBundleForLogging(CustomEvent customEvent) {
Bundle bundle = new Bundle();
bundle.putParcelable(MetricBundleKeys.CUSTOM_EVENT_BUNDLE, CustomEvent.toBundle(customEvent));
return bundle;
}
public static Bundle createBundleForLoggingCounter(MetricKey counterName, int times) {
Bundle bundle = new Bundle();
bundle.putParcelable(MetricBundleKeys.METRIC_KEY_BUNDLE, MetricKey.fromMetricKey(counterName));
bundle.putInt(MetricBundleKeys.COUNTER_INT, times);
return bundle;
}
public static Bundle createBundleForLoggingTimer(MetricKey timerName, long timeInMillis) {
Bundle bundle = new Bundle();
bundle.putParcelable(MetricBundleKeys.METRIC_KEY_BUNDLE, MetricKey.fromMetricKey(timerName));
bundle.putLong(MetricBundleKeys.TIME_MILLIS_LONG, timeInMillis);
return bundle;
}
public static Bundle createBundleForLoggingSetupMetric(ScreenKey screenKey, SetupMetric metric) {
Bundle bundle = new Bundle();
bundle.putParcelable(MetricBundleKeys.SCREEN_KEY_BUNDLE, ScreenKey.toBundle(screenKey));
bundle.putParcelable(MetricBundleKeys.SETUP_METRIC_BUNDLE, SetupMetric.toBundle(metric));
return bundle;
}
private MetricBundleConverter() {
throw new AssertionError("Cannot instantiate MetricBundleConverter");
}
}

View File

@@ -0,0 +1,158 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.setupcompat.logging.internal;
import android.content.Context;
import androidx.annotation.IntDef;
import androidx.annotation.StringDef;
import com.google.android.setupcompat.logging.MetricKey;
import com.google.android.setupcompat.logging.ScreenKey;
import com.google.android.setupcompat.logging.SetupMetric;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/** Constant values used by {@link com.google.android.setupcompat.logging.SetupMetricsLogger}. */
public interface SetupMetricsLoggingConstants {
/** Enumeration of supported metric types logged to SetupWizard. */
@Retention(RetentionPolicy.SOURCE)
@IntDef({
MetricType.CUSTOM_EVENT,
MetricType.DURATION_EVENT,
MetricType.COUNTER_EVENT,
MetricType.SETUP_COLLECTION_EVENT,
MetricType.INTERNAL})
@interface MetricType {
/**
* MetricType constant used when logging {@link
* com.google.android.setupcompat.logging.CustomEvent}.
*/
int CUSTOM_EVENT = 1;
/**
* MetricType constant used when logging {@link com.google.android.setupcompat.logging.Timer}.
*/
int DURATION_EVENT = 2;
/**
* MetricType constant used when logging counter value using {@link
* com.google.android.setupcompat.logging.SetupMetricsLogger#logCounter(Context, MetricKey,
* int)}.
*/
int COUNTER_EVENT = 3;
/**
* MetricType constant used when logging setup metric using {@link
* com.google.android.setupcompat.logging.SetupMetricsLogger#logMetrics(Context, ScreenKey,
* SetupMetric...)}.
*/
int SETUP_COLLECTION_EVENT = 4;
/** MetricType constant used for internal logging purposes. */
int INTERNAL = 100;
}
/**
* Enumeration of supported EventType of {@link MetricType#SETUP_COLLECTION_EVENT} logged to
* SetupWizard. (go/suw-metrics-collection-api)
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({
EventType.UNKNOWN,
EventType.IMPRESSION,
EventType.OPT_IN,
EventType.WAITING_START,
EventType.WAITING_END,
EventType.ERROR,
})
@interface EventType {
int UNKNOWN = 1;
int IMPRESSION = 2;
int OPT_IN = 3;
int WAITING_START = 4;
int WAITING_END = 5;
int ERROR = 6;
}
/** Keys of the bundle used while logging data to SetupWizard. */
@Retention(RetentionPolicy.SOURCE)
@StringDef({
MetricBundleKeys.METRIC_KEY,
MetricBundleKeys.METRIC_KEY_BUNDLE,
MetricBundleKeys.CUSTOM_EVENT,
MetricBundleKeys.CUSTOM_EVENT_BUNDLE,
MetricBundleKeys.TIME_MILLIS_LONG,
MetricBundleKeys.COUNTER_INT,
MetricBundleKeys.SCREEN_KEY_BUNDLE,
MetricBundleKeys.SETUP_METRIC_BUNDLE,
})
@interface MetricBundleKeys {
/**
* {@link MetricKey} of the data being logged. This will be set when {@code metricType} is
* either {@link MetricType#COUNTER_EVENT} or {@link MetricType#DURATION_EVENT}.
*
* @deprecated Use {@link #METRIC_KEY_BUNDLE} instead.
*/
@Deprecated String METRIC_KEY = "MetricKey";
/**
* This key will be used when {@code metricType} is {@link MetricType#CUSTOM_EVENT} with the
* value being a parcelable of type {@link com.google.android.setupcompat.logging.CustomEvent}.
*
* @deprecated Use {@link #CUSTOM_EVENT_BUNDLE} instead.
*/
@Deprecated String CUSTOM_EVENT = "CustomEvent";
/**
* This key will be set when {@code metricType} is {@link MetricType#DURATION_EVENT} with the
* value of type {@code long} representing the {@code duration} in milliseconds for the given
* {@link MetricKey}.
*/
String TIME_MILLIS_LONG = "timeMillis";
/**
* This key will be set when {@code metricType} is {@link MetricType#COUNTER_EVENT} with the
* value of type {@code int} representing the {@code counter} value logged for the given {@link
* MetricKey}.
*/
String COUNTER_INT = "counter";
/**
* {@link MetricKey} of the data being logged. This will be set when {@code metricType} is
* either {@link MetricType#COUNTER_EVENT} or {@link MetricType#DURATION_EVENT}.
*/
String METRIC_KEY_BUNDLE = "MetricKey_bundle";
/**
* This key will be used when {@code metricType} is {@link MetricType#CUSTOM_EVENT} with the
* value being a Bundle which can be used to read {@link
* com.google.android.setupcompat.logging.CustomEvent}.
*/
String CUSTOM_EVENT_BUNDLE = "CustomEvent_bundle";
/**
* This key will be used when {@code metricType} is {@link MetricType#SETUP_COLLECTION_EVENT}
* with the value being a Bundle which can be used to read {@link ScreenKey}
*/
String SCREEN_KEY_BUNDLE = "ScreenKey_bundle";
/**
* This key will be used when {@code metricType} is {@link MetricType#SETUP_COLLECTION_EVENT}
* with the value being a Bundle which can be used to read {@link SetupMetric}
*/
String SETUP_METRIC_BUNDLE = "SetupMetric_bundle";
}
}

View File

@@ -0,0 +1,115 @@
/*
* 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.UNKNOWN,
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;
}
}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
}

View File

@@ -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() {}
;
}

View File

@@ -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() {}
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2019 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.SuppressLint;
import android.content.Context;
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 {
@Nullable private FooterButton footerButton;
private boolean isPrimaryButtonStyle = false;
public FooterActionButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
void setFooterButton(FooterButton footerButton) {
this.footerButton = footerButton;
}
// getOnClickListenerWhenDisabled is responsible for handling accessibility correctly, calling
// performClick if necessary.
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (footerButton != null
&& !footerButton.isEnabled()
&& footerButton.getVisibility() == View.VISIBLE) {
OnClickListener listener = footerButton.getOnClickListenerWhenDisabled();
if (listener != null) {
listener.onClick(this);
}
}
}
return super.onTouchEvent(event);
}
/**
* Sets this footer button is primary button style.
*
* @param isPrimaryButtonStyle True if this button is primary button style.
*/
void setPrimaryButtonStyle(boolean isPrimaryButtonStyle) {
this.isPrimaryButtonStyle = isPrimaryButtonStyle;
}
/** Returns true when the footer button is primary button style. */
public boolean isPrimaryButtonStyle() {
return isPrimaryButtonStyle;
}
}

View File

@@ -0,0 +1,853 @@
/*
* Copyright (C) 2017 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 static com.google.android.setupcompat.internal.Preconditions.ensureOnMainThread;
import static java.lang.Math.max;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Build.VERSION_CODES;
import android.os.PersistableBundle;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
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;
import androidx.annotation.IdRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
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.TemplateLayout;
import com.google.android.setupcompat.logging.LoggingObserver;
import com.google.android.setupcompat.logging.LoggingObserver.SetupCompatUiEvent.ButtonInflatedEvent;
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;
/**
* A {@link Mixin} for managing buttons. By default, the button bar expects that buttons on the
* start (left for LTR) are "secondary" borderless buttons, while buttons on the end (right for LTR)
* are "primary" accent-colored buttons.
*/
public class FooterBarMixin implements Mixin {
private final Context context;
@Nullable private final ViewStub footerStub;
@VisibleForTesting final boolean applyPartnerResources;
@VisibleForTesting final boolean applyDynamicColor;
@VisibleForTesting final boolean useFullDynamicColor;
@VisibleForTesting final boolean footerButtonAlignEnd;
@VisibleForTesting public LinearLayout buttonContainer;
private FooterButton primaryButton;
private FooterButton secondaryButton;
private LoggingObserver loggingObserver;
@IdRes private int primaryButtonId;
@IdRes private int secondaryButtonId;
@VisibleForTesting public FooterButtonPartnerConfig primaryButtonPartnerConfigForTesting;
@VisibleForTesting public FooterButtonPartnerConfig secondaryButtonPartnerConfigForTesting;
private int footerBarPaddingTop;
private int footerBarPaddingBottom;
@VisibleForTesting int footerBarPaddingStart;
@VisibleForTesting int footerBarPaddingEnd;
@VisibleForTesting int defaultPadding;
@ColorInt private final int footerBarPrimaryBackgroundColor;
@ColorInt private final int footerBarSecondaryBackgroundColor;
private boolean removeFooterBarWhenEmpty = true;
private boolean isSecondaryButtonInPrimaryStyle = false;
@VisibleForTesting public final FooterBarMixinMetrics metrics = new FooterBarMixinMetrics();
private FooterButton.OnButtonEventListener createButtonEventListener(@IdRes int id) {
return new FooterButton.OnButtonEventListener() {
@Override
public void onEnabledChanged(boolean enabled) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setEnabled(enabled);
if (applyPartnerResources && !applyDynamicColor) {
updateButtonTextColorWithStates(
button,
(id == primaryButtonId || isSecondaryButtonInPrimaryStyle)
? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
: PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR,
(id == primaryButtonId || isSecondaryButtonInPrimaryStyle)
? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_DISABLED_TEXT_COLOR
: PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_DISABLED_TEXT_COLOR);
}
}
}
}
@Override
public void onVisibilityChanged(int visibility) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setVisibility(visibility);
autoSetButtonBarVisibility();
}
}
}
@Override
public void onTextChanged(CharSequence text) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null) {
button.setText(text);
}
}
}
@Override
public void onLocaleChanged(Locale locale) {
if (buttonContainer != null) {
Button button = buttonContainer.findViewById(id);
if (button != null && locale != null) {
button.setTextLocale(locale);
}
}
}
@Override
public void onDirectionChanged(int direction) {
if (buttonContainer != null && direction != -1) {
buttonContainer.setLayoutDirection(direction);
}
}
};
}
/**
* Creates a mixin for managing buttons on the footer.
*
* @param layout The {@link TemplateLayout} containing this mixin.
* @param attrs XML attributes given to the layout.
* @param defStyleAttr The default style attribute as given to the constructor of the layout.
*/
public FooterBarMixin(
TemplateLayout layout, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
context = layout.getContext();
footerStub = layout.findManagedViewById(R.id.suc_layout_footer);
FooterButtonStyleUtils.clearSavedDefaultTextColor();
this.applyPartnerResources =
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 =
a.getDimensionPixelSize(R.styleable.SucFooterBarMixin_sucFooterBarPaddingVertical, 0);
footerBarPaddingTop =
a.getDimensionPixelSize(
R.styleable.SucFooterBarMixin_sucFooterBarPaddingTop, defaultPadding);
footerBarPaddingBottom =
a.getDimensionPixelSize(
R.styleable.SucFooterBarMixin_sucFooterBarPaddingBottom, defaultPadding);
footerBarPaddingStart =
a.getDimensionPixelSize(R.styleable.SucFooterBarMixin_sucFooterBarPaddingStart, 0);
footerBarPaddingEnd =
a.getDimensionPixelSize(R.styleable.SucFooterBarMixin_sucFooterBarPaddingEnd, 0);
footerBarPrimaryBackgroundColor =
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterBackground, 0);
footerBarSecondaryBackgroundColor =
a.getColor(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterBackground, 0);
footerButtonAlignEnd =
a.getBoolean(R.styleable.SucFooterBarMixin_sucFooterBarButtonAlignEnd, false);
int primaryBtn =
a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarPrimaryFooterButton, 0);
int secondaryBtn =
a.getResourceId(R.styleable.SucFooterBarMixin_sucFooterBarSecondaryFooterButton, 0);
a.recycle();
FooterButtonInflater inflater = new FooterButtonInflater(context);
if (secondaryBtn != 0) {
setSecondaryButton(inflater.inflate(secondaryBtn));
metrics.logPrimaryButtonInitialStateVisibility(/* isVisible= */ true, /* isUsingXml= */ true);
}
if (primaryBtn != 0) {
setPrimaryButton(inflater.inflate(primaryBtn));
metrics.logSecondaryButtonInitialStateVisibility(
/* isVisible= */ true, /* isUsingXml= */ true);
}
}
public void setLoggingObserver(LoggingObserver observer) {
loggingObserver = observer;
// If primary button is already created, it's likely that {@code setPrimaryButton()} was called
// before an {@link LoggingObserver} is set, we need to set an observer and call the right
// logging method here.
if (primaryButtonId != 0) {
loggingObserver.log(
new ButtonInflatedEvent(getPrimaryButtonView(), LoggingObserver.ButtonType.PRIMARY));
getPrimaryButton().setLoggingObserver(observer);
}
// Same for secondary button.
if (secondaryButtonId != 0) {
loggingObserver.log(
new ButtonInflatedEvent(getSecondaryButtonView(), LoggingObserver.ButtonType.SECONDARY));
getSecondaryButton().setLoggingObserver(observer);
}
}
protected boolean isFooterButtonAlignedEnd() {
if (PartnerConfigHelper.get(context)
.isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BUTTON_ALIGNED_END)) {
return PartnerConfigHelper.get(context)
.getBoolean(context, PartnerConfig.CONFIG_FOOTER_BUTTON_ALIGNED_END, false);
} else {
return footerButtonAlignEnd;
}
}
protected boolean isFooterButtonsEvenlyWeighted() {
if (!isSecondaryButtonInPrimaryStyle) {
return false;
}
PartnerConfigHelper.get(context);
return PartnerConfigHelper.isNeutralButtonStyleEnabled(context);
}
private View addSpace() {
LinearLayout buttonContainerlayout = ensureFooterInflated();
View space = new View(context);
space.setLayoutParams(new LayoutParams(0, 0, 1.0f));
space.setVisibility(View.INVISIBLE);
buttonContainerlayout.addView(space);
return space;
}
@NonNull
private LinearLayout ensureFooterInflated() {
if (buttonContainer == null) {
if (footerStub == null) {
throw new IllegalStateException("Footer stub is not found in this template");
}
buttonContainer = (LinearLayout) inflateFooter(R.layout.suc_footer_button_bar);
onFooterBarInflated(buttonContainer);
onFooterBarApplyPartnerResource(buttonContainer);
}
return buttonContainer;
}
/**
* Notifies that the footer bar has been inflated to the view hierarchy. Calling super is
* necessary while subclass implement it.
*/
@CallSuper
protected void onFooterBarInflated(LinearLayout buttonContainer) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
buttonContainer.setId(View.generateViewId());
updateFooterBarPadding(
buttonContainer,
footerBarPaddingStart,
footerBarPaddingTop,
footerBarPaddingEnd,
footerBarPaddingBottom);
if (isFooterButtonAlignedEnd()) {
buttonContainer.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
}
}
/**
* Notifies while the footer bar apply Partner Resource. Calling super is necessary while subclass
* implement it.
*/
@CallSuper
protected void onFooterBarApplyPartnerResource(LinearLayout buttonContainer) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
if (!applyPartnerResources) {
return;
}
// 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);
}
if (PartnerConfigHelper.get(context)
.isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_TOP)) {
footerBarPaddingTop =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_TOP);
}
if (PartnerConfigHelper.get(context)
.isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_BOTTOM)) {
footerBarPaddingBottom =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BUTTON_PADDING_BOTTOM);
}
if (PartnerConfigHelper.get(context)
.isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BAR_PADDING_START)) {
footerBarPaddingStart =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BAR_PADDING_START);
}
if (PartnerConfigHelper.get(context)
.isPartnerConfigAvailable(PartnerConfig.CONFIG_FOOTER_BAR_PADDING_END)) {
footerBarPaddingEnd =
(int)
PartnerConfigHelper.get(context)
.getDimension(context, PartnerConfig.CONFIG_FOOTER_BAR_PADDING_END);
}
updateFooterBarPadding(
buttonContainer,
footerBarPaddingStart,
footerBarPaddingTop,
footerBarPaddingEnd,
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);
}
}
}
/**
* Inflate FooterActionButton with layout "suc_button". Subclasses can implement this method to
* modify the footer button layout as necessary.
*/
@SuppressLint("InflateParams")
protected FooterActionButton createThemedButton(Context context, @StyleRes int theme) {
// Inflate a single button from XML, which when using support lib, will take advantage of
// the injected layout inflater and give us AppCompatButton instead.
LayoutInflater inflater = LayoutInflater.from(new ContextThemeWrapper(context, theme));
return (FooterActionButton) inflater.inflate(R.layout.suc_button, null, false);
}
/** Sets primary button for footer. */
@MainThread
public void setPrimaryButton(FooterButton footerButton) {
ensureOnMainThread("setPrimaryButton");
ensureFooterInflated();
// Setup button partner config
FooterButtonPartnerConfig footerButtonPartnerConfig =
new FooterButtonPartnerConfig.Builder(footerButton)
.setPartnerTheme(
getPartnerTheme(
footerButton,
/* defaultPartnerTheme= */ R.style.SucPartnerCustomizationButton_Primary,
/* buttonBackgroundColorConfig= */ PartnerConfig
.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR))
.setButtonBackgroundConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_BG_COLOR)
.setButtonDisableAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_ALPHA)
.setButtonDisableBackgroundConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_DISABLED_BG_COLOR)
.setButtonDisableTextColorConfig(
PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_DISABLED_TEXT_COLOR)
.setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR)
.setMarginStartConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_MARGIN_START)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.setTextWeightConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_WEIGHT)
.setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
.build();
FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
// update information for primary button. Need to update as long as the button inflated.
primaryButtonId = button.getId();
button.setPrimaryButtonStyle(/* isPrimaryButtonStyle= */ true);
primaryButton = footerButton;
primaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
onFooterButtonInflated(button, footerBarPrimaryBackgroundColor);
onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
if (loggingObserver != null) {
loggingObserver.log(
new ButtonInflatedEvent(getPrimaryButtonView(), LoggingObserver.ButtonType.PRIMARY));
footerButton.setLoggingObserver(loggingObserver);
}
// Make sure the position of buttons are correctly and prevent primary button create twice or
// more.
repopulateButtons();
}
/** Returns the {@link FooterButton} of primary button. */
public FooterButton getPrimaryButton() {
return primaryButton;
}
/**
* Returns the {@link Button} of primary button.
*
* @apiNote It is not recommended to apply style to the view directly. The setup library will
* handle the button style. There is no guarantee that changes made directly to the button
* style will not cause unexpected behavior.
*/
public Button getPrimaryButtonView() {
return buttonContainer == null ? null : buttonContainer.findViewById(primaryButtonId);
}
@VisibleForTesting
boolean isPrimaryButtonVisible() {
return getPrimaryButtonView() != null && getPrimaryButtonView().getVisibility() == View.VISIBLE;
}
/** 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
FooterButtonPartnerConfig footerButtonPartnerConfig =
new FooterButtonPartnerConfig.Builder(footerButton)
.setPartnerTheme(
getPartnerTheme(
footerButton,
/* 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)
.setButtonDisableTextColorConfig(
usePrimaryStyle
? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_DISABLED_TEXT_COLOR
: PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_DISABLED_TEXT_COLOR)
.setButtonIconConfig(getDrawablePartnerConfig(footerButton.getButtonType()))
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(
usePrimaryStyle
? PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR
: PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
.setMarginStartConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_MARGIN_START)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.setTextWeightConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_WEIGHT)
.setTextStyleConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_STYLE)
.build();
FooterActionButton button = inflateButton(footerButton, footerButtonPartnerConfig);
// update information for secondary button. Need to update as long as the button inflated.
secondaryButtonId = button.getId();
button.setPrimaryButtonStyle(usePrimaryStyle);
secondaryButton = footerButton;
secondaryButtonPartnerConfigForTesting = footerButtonPartnerConfig;
onFooterButtonInflated(button, footerBarSecondaryBackgroundColor);
onFooterButtonApplyPartnerResource(button, footerButtonPartnerConfig);
if (loggingObserver != null) {
loggingObserver.log(new ButtonInflatedEvent(button, LoggingObserver.ButtonType.SECONDARY));
footerButton.setLoggingObserver(loggingObserver);
}
// Make sure the position of buttons are correctly and prevent secondary button create twice or
// more.
repopulateButtons();
}
/**
* Corrects the order of footer buttons after the button has been inflated to the view hierarchy.
* Subclasses can implement this method to modify the order of footer buttons as necessary.
*/
protected void repopulateButtons() {
LinearLayout buttonContainer = ensureFooterInflated();
Button tempPrimaryButton = getPrimaryButtonView();
Button tempSecondaryButton = getSecondaryButtonView();
buttonContainer.removeAllViews();
boolean isEvenlyWeightedButtons = isFooterButtonsEvenlyWeighted();
boolean isLandscape =
context.getResources().getConfiguration().orientation
== Configuration.ORIENTATION_LANDSCAPE;
if (isLandscape && isEvenlyWeightedButtons && isFooterButtonAlignedEnd()) {
addSpace();
}
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);
}
if (!isFooterButtonAlignedEnd()) {
addSpace();
}
if (tempPrimaryButton != null) {
buttonContainer.addView(tempPrimaryButton);
}
setEvenlyWeightedButtons(tempPrimaryButton, tempSecondaryButton, isEvenlyWeightedButtons);
}
private void setEvenlyWeightedButtons(
Button primaryButton, Button secondaryButton, boolean isEvenlyWeighted) {
if (primaryButton != null && secondaryButton != null && isEvenlyWeighted) {
primaryButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int primaryButtonMeasuredWidth = primaryButton.getMeasuredWidth();
secondaryButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int secondaryButtonMeasuredWidth = secondaryButton.getMeasuredWidth();
int maxButtonMeasureWidth = max(primaryButtonMeasuredWidth, secondaryButtonMeasuredWidth);
primaryButton.getLayoutParams().width = maxButtonMeasureWidth;
secondaryButton.getLayoutParams().width = maxButtonMeasureWidth;
} else {
if (primaryButton != null) {
LinearLayout.LayoutParams primaryLayoutParams =
(LinearLayout.LayoutParams) primaryButton.getLayoutParams();
if (null != primaryLayoutParams) {
primaryLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
primaryLayoutParams.weight = 0;
primaryButton.setLayoutParams(primaryLayoutParams);
}
}
if (secondaryButton != null) {
LinearLayout.LayoutParams secondaryLayoutParams =
(LinearLayout.LayoutParams) secondaryButton.getLayoutParams();
if (null != secondaryLayoutParams) {
secondaryLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
secondaryLayoutParams.weight = 0;
secondaryButton.setLayoutParams(secondaryLayoutParams);
}
}
}
}
/**
* Notifies that the footer button has been inInflated and add to the view hierarchy. Calling
* super is necessary while subclass implement it.
*/
@CallSuper
protected void onFooterButtonInflated(Button button, @ColorInt int defaultButtonBackgroundColor) {
// Try to set default background
if (!applyDynamicColor) {
if (defaultButtonBackgroundColor != 0) {
FooterButtonStyleUtils.updateButtonBackground(button, defaultButtonBackgroundColor);
} else {
// TODO: get button background color from activity theme
}
}
buttonContainer.addView(button);
autoSetButtonBarVisibility();
}
private int getPartnerTheme(
FooterButton footerButton,
int defaultPartnerTheme,
PartnerConfig buttonBackgroundColorConfig) {
int overrideTheme = footerButton.getTheme();
// Set the default theme if theme is not set, or when running in setup flow.
if (footerButton.getTheme() == 0 || applyPartnerResources) {
overrideTheme = defaultPartnerTheme;
}
// TODO: Make sure customize attributes in theme can be applied during setup flow.
// If sets background color to full transparent, the button changes to colored borderless ink
// button style.
if (applyPartnerResources) {
int color = PartnerConfigHelper.get(context).getColor(context, buttonBackgroundColorConfig);
if (color == Color.TRANSPARENT) {
overrideTheme = R.style.SucPartnerCustomizationButton_Secondary;
} else {
overrideTheme = R.style.SucPartnerCustomizationButton_Primary;
}
}
return overrideTheme;
}
/** Returns the {@link LinearLayout} of button container. */
public LinearLayout getButtonContainer() {
return buttonContainer;
}
/** Returns the {@link FooterButton} of secondary button. */
public FooterButton getSecondaryButton() {
return secondaryButton;
}
/**
* Sets whether the footer bar should be removed when there are no footer buttons in the bar.
*
* @param value True if footer bar is gone, false otherwise.
*/
public void setRemoveFooterBarWhenEmpty(boolean value) {
removeFooterBarWhenEmpty = value;
autoSetButtonBarVisibility();
}
/**
* Checks the visibility state of footer buttons to set the visibility state of this footer bar
* automatically.
*/
private void autoSetButtonBarVisibility() {
Button primaryButton = getPrimaryButtonView();
Button secondaryButton = getSecondaryButtonView();
boolean primaryVisible = primaryButton != null && primaryButton.getVisibility() == View.VISIBLE;
boolean secondaryVisible =
secondaryButton != null && secondaryButton.getVisibility() == View.VISIBLE;
if (buttonContainer != null) {
buttonContainer.setVisibility(
primaryVisible || secondaryVisible
? View.VISIBLE
: removeFooterBarWhenEmpty ? View.GONE : View.INVISIBLE);
}
}
/** Returns the visibility status for this footer bar. */
@VisibleForTesting
public int getVisibility() {
return buttonContainer.getVisibility();
}
/**
* Returns the {@link Button} of secondary button.
*
* @apiNote It is not recommended to apply style to the view directly. The setup library will
* handle the button style. There is no guarantee that changes made directly to the button
* style will not cause unexpected behavior.
*/
public Button getSecondaryButtonView() {
return buttonContainer == null ? null : buttonContainer.findViewById(secondaryButtonId);
}
@VisibleForTesting
boolean isSecondaryButtonVisible() {
return getSecondaryButtonView() != null
&& getSecondaryButtonView().getVisibility() == View.VISIBLE;
}
private FooterActionButton inflateButton(
FooterButton footerButton, FooterButtonPartnerConfig footerButtonPartnerConfig) {
FooterActionButton button =
createThemedButton(context, footerButtonPartnerConfig.getPartnerTheme());
button.setId(View.generateViewId());
// apply initial configuration into button view.
button.setText(footerButton.getText());
button.setOnClickListener(footerButton);
button.setVisibility(footerButton.getVisibility());
button.setEnabled(footerButton.isEnabled());
button.setFooterButton(footerButton);
footerButton.setOnButtonEventListener(createButtonEventListener(button.getId()));
return button;
}
// TODO: Make sure customize attributes in theme can be applied during setup flow.
@TargetApi(VERSION_CODES.Q)
private void onFooterButtonApplyPartnerResource(
Button button, FooterButtonPartnerConfig footerButtonPartnerConfig) {
if (!applyPartnerResources) {
return;
}
FooterButtonStyleUtils.applyButtonPartnerResources(
context,
button,
applyDynamicColor,
/* isButtonIconAtEnd= */ (button.getId() == primaryButtonId),
footerButtonPartnerConfig);
if (!applyDynamicColor) {
// adjust text color based on enabled state
updateButtonTextColorWithStates(
button,
footerButtonPartnerConfig.getButtonTextColorConfig(),
footerButtonPartnerConfig.getButtonDisableTextColorConfig());
}
}
private void updateButtonTextColorWithStates(
Button button,
PartnerConfig buttonTextColorConfig,
PartnerConfig buttonTextDisabledColorConfig) {
if (button.isEnabled()) {
FooterButtonStyleUtils.updateButtonTextEnabledColorWithPartnerConfig(
context, button, buttonTextColorConfig);
} else {
FooterButtonStyleUtils.updateButtonTextDisabledColorWithPartnerConfig(
context, button, buttonTextDisabledColorConfig);
}
}
private static PartnerConfig getDrawablePartnerConfig(@ButtonType int buttonType) {
PartnerConfig result;
switch (buttonType) {
case ButtonType.ADD_ANOTHER:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_ADD_ANOTHER;
break;
case ButtonType.CANCEL:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CANCEL;
break;
case ButtonType.CLEAR:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_CLEAR;
break;
case ButtonType.DONE:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_DONE;
break;
case ButtonType.NEXT:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_NEXT;
break;
case ButtonType.OPT_IN:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_OPT_IN;
break;
case ButtonType.SKIP:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_SKIP;
break;
case ButtonType.STOP:
result = PartnerConfig.CONFIG_FOOTER_BUTTON_ICON_STOP;
break;
case ButtonType.OTHER:
default:
result = null;
break;
}
return result;
}
protected View inflateFooter(@LayoutRes int footer) {
LayoutInflater inflater =
LayoutInflater.from(
new ContextThemeWrapper(context, R.style.SucPartnerCustomizationButtonBar_Stackable));
footerStub.setLayoutInflater(inflater);
footerStub.setLayoutResource(footer);
return footerStub.inflate();
}
private void updateFooterBarPadding(
LinearLayout buttonContainer, int left, int top, int right, int bottom) {
if (buttonContainer == null) {
// Ignore action since buttonContainer is null
return;
}
buttonContainer.setPadding(left, top, right, bottom);
}
/** Returns the paddingTop of footer bar. */
@VisibleForTesting
int getPaddingTop() {
return (buttonContainer != null) ? buttonContainer.getPaddingTop() : footerStub.getPaddingTop();
}
/** Returns the paddingBottom of footer bar. */
@VisibleForTesting
int getPaddingBottom() {
return (buttonContainer != null)
? buttonContainer.getPaddingBottom()
: footerStub.getPaddingBottom();
}
/** Uses for notify mixin the view already attached to window. */
public void onAttachedToWindow() {
metrics.logPrimaryButtonInitialStateVisibility(
/* isVisible= */ isPrimaryButtonVisible(), /* isUsingXml= */ false);
metrics.logSecondaryButtonInitialStateVisibility(
/* isVisible= */ isSecondaryButtonVisible(), /* isUsingXml= */ false);
}
/** Uses for notify mixin the view already detached from window. */
public void onDetachedFromWindow() {
metrics.updateButtonVisibility(isPrimaryButtonVisible(), isSecondaryButtonVisible());
}
/**
* Assigns logging metrics to bundle for PartnerCustomizationLayout to log metrics to SetupWizard.
*/
@TargetApi(VERSION_CODES.Q)
public PersistableBundle getLoggingMetrics() {
return metrics.getMetrics();
}
}

View File

@@ -0,0 +1,431 @@
/*
* Copyright 2018 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 static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.TargetApi;
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 com.google.android.setupcompat.R;
import com.google.android.setupcompat.logging.CustomEvent;
import com.google.android.setupcompat.logging.LoggingObserver;
import com.google.android.setupcompat.logging.LoggingObserver.InteractionType;
import com.google.android.setupcompat.logging.LoggingObserver.SetupCompatUiEvent.ButtonInteractionEvent;
import java.lang.annotation.Retention;
import java.util.Locale;
/**
* Definition of a footer button. Clients can use this class to customize attributes like text,
* button type and click listener, and FooterBarMixin will inflate a corresponding Button view.
*/
public final class FooterButton implements OnClickListener {
private static final String KEY_BUTTON_ON_CLICK_COUNT = "_onClickCount";
private static final String KEY_BUTTON_TEXT = "_text";
private static final String KEY_BUTTON_TYPE = "_type";
@ButtonType private final int buttonType;
private CharSequence text;
private boolean enabled = true;
private int visibility = View.VISIBLE;
private int theme;
private OnClickListener onClickListener;
private OnClickListener onClickListenerWhenDisabled;
private OnButtonEventListener buttonListener;
private LoggingObserver loggingObserver;
private int clickCount = 0;
private Locale locale;
private int direction;
public FooterButton(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SucFooterButton);
text = a.getString(R.styleable.SucFooterButton_android_text);
onClickListener = null;
buttonType =
getButtonTypeValue(
a.getInt(R.styleable.SucFooterButton_sucButtonType, /* defValue= */ ButtonType.OTHER));
theme = a.getResourceId(R.styleable.SucFooterButton_android_theme, /* defValue= */ 0);
a.recycle();
}
/**
* Allows client customize text, click listener and theme for footer button before Button has been
* created. The {@link FooterBarMixin} will inflate a corresponding Button view.
*
* @param text The text for button.
* @param listener The listener for button.
* @param buttonType The type of button.
* @param theme The theme for button.
*/
private FooterButton(
CharSequence text,
@Nullable OnClickListener listener,
@ButtonType int buttonType,
@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. */
public CharSequence getText() {
return text;
}
/**
* Registers a callback to be invoked when this view of footer button is clicked.
*
* @param listener The callback that will run
*/
public void setOnClickListener(@Nullable OnClickListener listener) {
onClickListener = listener;
}
/** Returns an {@link OnClickListener} of this footer button. */
public OnClickListener getOnClickListenerWhenDisabled() {
return onClickListenerWhenDisabled;
}
/**
* Registers a callback to be invoked when footer button disabled and touch event has reacted.
*
* @param listener The callback that will run
*/
public void setOnClickListenerWhenDisabled(@Nullable OnClickListener listener) {
onClickListenerWhenDisabled = listener;
}
/** Returns the type of this footer button icon. */
@ButtonType
public int getButtonType() {
return buttonType;
}
/** Returns the theme of this footer button. */
@StyleRes
public int getTheme() {
return theme;
}
/**
* Sets the enabled state of this footer button.
*
* @param enabled True if this view is enabled, false otherwise.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
if (buttonListener != null) {
buttonListener.onEnabledChanged(enabled);
}
}
/** Returns the enabled status for this footer button. */
public boolean isEnabled() {
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.
*
* @param visibility one of {@link #VISIBLE}, {@link #INVISIBLE}, or {@link #GONE}.
*/
public void setVisibility(int visibility) {
this.visibility = visibility;
if (buttonListener != null) {
buttonListener.onVisibilityChanged(visibility);
}
}
/** Returns the visibility status for this footer button. */
public int getVisibility() {
return visibility;
}
/** Sets the text to be displayed using a string resource identifier. */
public void setText(Context context, @StringRes int resId) {
setText(context.getText(resId));
}
/** Sets the text to be displayed on footer button. */
public void setText(CharSequence text) {
this.text = text;
if (buttonListener != null) {
buttonListener.onTextChanged(text);
}
}
/** 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.
*
* @param listener The callback that will run
*/
void setOnButtonEventListener(@Nullable OnButtonEventListener listener) {
if (listener != null) {
buttonListener = listener;
} else {
throw new NullPointerException("Event listener of footer button may not be null.");
}
}
@Override
public void onClick(View v) {
if (onClickListener != null) {
clickCount++;
onClickListener.onClick(v);
if (loggingObserver != null) {
loggingObserver.log(new ButtonInteractionEvent(v, InteractionType.TAP));
}
}
}
void setLoggingObserver(LoggingObserver loggingObserver) {
this.loggingObserver = loggingObserver;
}
/** Interface definition for a callback to be invoked when footer button API has set. */
interface OnButtonEventListener {
void onEnabledChanged(boolean enabled);
void onVisibilityChanged(int visibility);
void onTextChanged(CharSequence text);
void onLocaleChanged(Locale locale);
void onDirectionChanged(int direction);
}
/** Maximum valid value of ButtonType */
private static final int MAX_BUTTON_TYPE = 8;
@Retention(SOURCE)
@IntDef({
ButtonType.OTHER,
ButtonType.ADD_ANOTHER,
ButtonType.CANCEL,
ButtonType.CLEAR,
ButtonType.DONE,
ButtonType.NEXT,
ButtonType.OPT_IN,
ButtonType.SKIP,
ButtonType.STOP
})
/**
* Types for footer button. The button appearance and behavior may change based on its type. In
* order to be backward compatible with application built with old version of setupcompat; each
* ButtonType should not be changed.
*/
public @interface ButtonType {
/** A type of button that doesn't fit into any other categories. */
int OTHER = 0;
/**
* A type of button that will set up additional elements of the ongoing setup step(s) when
* clicked.
*/
int ADD_ANOTHER = 1;
/** A type of button that will cancel the ongoing setup step(s) and exit setup when clicked. */
int CANCEL = 2;
/** A type of button that will clear the progress when clicked. (eg: clear PIN code) */
int CLEAR = 3;
/** A type of button that will exit the setup flow when clicked. */
int DONE = 4;
/** A type of button that will go to the next screen, or next step in the flow when clicked. */
int NEXT = 5;
/** A type of button to opt-in or agree to the features described in the current screen. */
int OPT_IN = 6;
/** A type of button that will skip the current step when clicked. */
int SKIP = 7;
/** A type of button that will stop the ongoing setup step(s) and skip forward when clicked. */
int STOP = 8;
}
private int getButtonTypeValue(int value) {
if (value >= 0 && value <= MAX_BUTTON_TYPE) {
return value;
} else {
throw new IllegalArgumentException("Not a ButtonType");
}
}
private String getButtonTypeName() {
switch (buttonType) {
case ButtonType.ADD_ANOTHER:
return "ADD_ANOTHER";
case ButtonType.CANCEL:
return "CANCEL";
case ButtonType.CLEAR:
return "CLEAR";
case ButtonType.DONE:
return "DONE";
case ButtonType.NEXT:
return "NEXT";
case ButtonType.OPT_IN:
return "OPT_IN";
case ButtonType.SKIP:
return "SKIP";
case ButtonType.STOP:
return "STOP";
case ButtonType.OTHER:
default:
return "OTHER";
}
}
/**
* Returns footer button related metrics bundle for PartnerCustomizationLayout to log to
* SetupWizard.
*/
@TargetApi(VERSION_CODES.Q)
public PersistableBundle getMetrics(String buttonName) {
PersistableBundle bundle = new PersistableBundle();
bundle.putString(
buttonName + KEY_BUTTON_TEXT, CustomEvent.trimsStringOverMaxLength(getText().toString()));
bundle.putString(buttonName + KEY_BUTTON_TYPE, getButtonTypeName());
bundle.putInt(buttonName + KEY_BUTTON_ON_CLICK_COUNT, clickCount);
return bundle;
}
/**
* Builder class for constructing {@code FooterButton} objects.
*
* <p>Allows client customize text, click listener and theme for footer button before Button has
* been created. The {@link FooterBarMixin} will inflate a corresponding Button view.
*
* <p>Example:
*
* <pre class="prettyprint">
* FooterButton primaryButton =
* new FooterButton.Builder(mContext)
* .setText(R.string.primary_button_label)
* .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;
public Builder(@NonNull Context context) {
this.context = context;
}
/** Sets the {@code text} of FooterButton. */
public Builder setText(String text) {
this.text = text;
return this;
}
/** Sets the {@code text} of FooterButton by resource. */
public Builder setText(@StringRes int text) {
this.text = context.getString(text);
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;
return this;
}
/** Sets the {@code buttonType} of FooterButton. */
public Builder setButtonType(@ButtonType int buttonType) {
this.buttonType = buttonType;
return this;
}
/** Sets the {@code theme} for applying FooterButton. */
public Builder setTheme(@StyleRes int theme) {
this.theme = theme;
return this;
}
public FooterButton build() {
return new FooterButton(text, onClickListener, buttonType, theme, locale, direction);
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* Copyright (C) 2018 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.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
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;
class FooterButtonInflater {
protected final Context context;
/**
* Creates a new inflater instance associated with a particular Resources bundle.
*
* @param context The Context using to get Resources and Generate FooterButton Object
*/
public FooterButtonInflater(@NonNull Context context) {
this.context = context;
}
public Resources getResources() {
return context.getResources();
}
/**
* Inflates a new hierarchy from the specified XML resource. Throws InflaterException if there is
* an error.
*
* @param resId ID for an XML resource to load (e.g. <code>R.xml.my_xml</code>)
* @return The root of the inflated hierarchy.
*/
public FooterButton inflate(int resId) {
XmlResourceParser parser = getResources().getXml(resId);
try {
return inflate(parser);
} finally {
parser.close();
}
}
/**
* Inflates a new hierarchy from the specified XML node. Throws InflaterException if there is an
* error.
*
* <p><em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance reasons, inflation
* relies heavily on pre-processing of XML files that is done at build time. Therefore, it is not
* currently possible to use inflater with an XmlPullParser over a plain XML file at runtime.
*
* @param parser XML dom node containing the description of the hierarchy.
* @return The root of the inflated hierarchy.
*/
private FooterButton inflate(XmlPullParser parser) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
FooterButton button;
try {
// Look for the root node.
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG
&& type != XmlPullParser.END_DOCUMENT) {
// continue
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription() + ": No start tag found!");
}
if (!parser.getName().equals("FooterButton")) {
throw new InflateException(parser.getPositionDescription() + ": not a FooterButton");
}
button = new FooterButton(context, attrs);
} catch (XmlPullParserException e) {
throw new InflateException(e.getMessage(), e);
} catch (IOException e) {
throw new InflateException(parser.getPositionDescription() + ": " + e.getMessage(), e);
}
return button;
}
}

View File

@@ -0,0 +1,495 @@
/*
* 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 static com.google.android.setupcompat.partnerconfig.PartnerConfigHelper.isFontWeightEnabled;
import android.annotation.SuppressLint;
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.view.ViewGroup;
import android.widget.Button;
import androidx.annotation.ColorInt;
import androidx.annotation.Nullable;
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;
import java.util.HashMap;
/** Utils for updating the button style. */
public class FooterButtonStyleUtils {
private static final float DEFAULT_DISABLED_ALPHA = 0.26f;
// android.graphics.fonts.FontStyle.FontStyle#FONT_WEIGHT_NORMAL
private static final int FONT_WEIGHT_NORMAL = 400;
private static final HashMap<Integer, ColorStateList> defaultTextColor = new HashMap<>();
/** 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)
.setButtonDisableTextColorConfig(
PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_DISABLED_TEXT_COLOR)
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_TEXT_COLOR)
.setMarginStartConfig(PartnerConfig.CONFIG_FOOTER_PRIMARY_BUTTON_MARGIN_START)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.setTextWeightConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_WEIGHT)
.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)
.setButtonDisableTextColorConfig(
PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_DISABLED_TEXT_COLOR)
.setButtonRadiusConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RADIUS)
.setButtonRippleColorAlphaConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_RIPPLE_COLOR_ALPHA)
.setTextColorConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_TEXT_COLOR)
.setMarginStartConfig(PartnerConfig.CONFIG_FOOTER_SECONDARY_BUTTON_MARGIN_START)
.setTextSizeConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_TEXT_SIZE)
.setButtonMinHeight(PartnerConfig.CONFIG_FOOTER_BUTTON_MIN_HEIGHT)
.setTextTypeFaceConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_FAMILY)
.setTextWeightConfig(PartnerConfig.CONFIG_FOOTER_BUTTON_FONT_WEIGHT)
.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) {
// Save default text color for the partner config disable button text color not available.
saveButtonDefaultTextColor(button);
// 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());
} else {
FooterButtonStyleUtils.updateButtonTextDisabledColorWithPartnerConfig(
context, button, footerButtonPartnerConfig.getButtonDisableTextColorConfig());
}
FooterButtonStyleUtils.updateButtonBackgroundWithPartnerConfig(
context,
button,
footerButtonPartnerConfig.getButtonBackgroundConfig(),
footerButtonPartnerConfig.getButtonDisableAlphaConfig(),
footerButtonPartnerConfig.getButtonDisableBackgroundConfig());
}
FooterButtonStyleUtils.updateButtonRippleColorWithPartnerConfig(
context,
button,
applyDynamicColor,
footerButtonPartnerConfig.getButtonTextColorConfig(),
footerButtonPartnerConfig.getButtonRippleColorAlphaConfig());
FooterButtonStyleUtils.updateButtonMarginStartWithPartnerConfig(
context, button, footerButtonPartnerConfig.getButtonMarginStartConfig());
FooterButtonStyleUtils.updateButtonTextSizeWithPartnerConfig(
context, button, footerButtonPartnerConfig.getButtonTextSizeConfig());
FooterButtonStyleUtils.updateButtonMinHeightWithPartnerConfig(
context, button, footerButtonPartnerConfig.getButtonMinHeightConfig());
FooterButtonStyleUtils.updateButtonTypeFaceWithPartnerConfig(
context,
button,
footerButtonPartnerConfig.getButtonTextTypeFaceConfig(),
footerButtonPartnerConfig.getButtonTextWeightConfig(),
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 updateButtonTextDisabledColorWithPartnerConfig(
Context context, Button button, PartnerConfig buttonDisableTextColorConfig) {
if (PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonDisableTextColorConfig)) {
@ColorInt
int color = PartnerConfigHelper.get(context).getColor(context, buttonDisableTextColorConfig);
updateButtonTextDisabledColor(button, color);
} else {
updateButtonTextDisableDefaultColor(button, getButtonDefaultTextCorlor(button));
}
}
static void updateButtonTextDisabledColor(Button button, @ColorInt int textColor) {
if (textColor != Color.TRANSPARENT) {
button.setTextColor(ColorStateList.valueOf(textColor));
}
}
static void updateButtonTextDisableDefaultColor(Button button, ColorStateList disabledTextColor) {
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};
int[] focusState = {android.R.attr.state_focused};
int argbColor = convertRgbToArgb(textColor, rippleAlpha);
// Set text color for ripple.
ColorStateList colorStateList =
new ColorStateList(
new int[][] {pressedState, focusState, StateSet.NOTHING},
new int[] {argbColor, argbColor, Color.TRANSPARENT});
rippleDrawable.setColor(colorStateList);
}
}
static void updateButtonMarginStartWithPartnerConfig(
Context context, Button button, PartnerConfig buttonMarginStartConfig) {
ViewGroup.LayoutParams lp = button.getLayoutParams();
boolean partnerConfigAvailable =
PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonMarginStartConfig);
if (partnerConfigAvailable && lp instanceof ViewGroup.MarginLayoutParams) {
final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp;
int startMargin =
(int) PartnerConfigHelper.get(context).getDimension(context, buttonMarginStartConfig);
mlp.setMargins(startMargin, mlp.topMargin, mlp.rightMargin, mlp.bottomMargin);
}
}
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);
}
}
}
@SuppressLint("NewApi") // Applying partner config should be guarded before Android S
static void updateButtonTypeFaceWithPartnerConfig(
Context context,
Button button,
PartnerConfig buttonTextTypeFaceConfig,
PartnerConfig buttonTextWeightConfig,
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;
int textWeightValue;
if (isFontWeightEnabled(context)
&& PartnerConfigHelper.get(context).isPartnerConfigAvailable(buttonTextWeightConfig)) {
textWeightValue =
PartnerConfigHelper.get(context)
.getInteger(context, buttonTextWeightConfig, FONT_WEIGHT_NORMAL);
Typeface fontFamily = Typeface.create(fontFamilyName, textStyleValue);
font = Typeface.create(fontFamily, textWeightValue, /* italic= */ false);
} else {
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: b/120488979 - 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;
}
button.setCompoundDrawablesRelative(iconStart, null, iconEnd, null);
}
static void updateButtonBackground(Button button, @ColorInt int color) {
button.getBackground().mutate().setColorFilter(color, Mode.SRC_ATOP);
}
private static void saveButtonDefaultTextColor(Button button) {
defaultTextColor.put(button.getId(), button.getTextColors());
}
private static ColorStateList getButtonDefaultTextCorlor(Button button) {
if (!defaultTextColor.containsKey(button.getId())) {
throw new IllegalStateException("There is no saved default color for button");
}
return defaultTextColor.get(button.getId());
}
static void clearSavedDefaultTextColor() {
defaultTextColor.clear();
}
/** Gets {@code GradientDrawable} from given {@code button}. */
@Nullable
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;
}
@Nullable
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() {}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 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;
/**
* Marker interface to indicate Mixin classes.
*
* @see com.google.android.setupcompat.internal.TemplateLayout#registerMixin(Class, Mixin)
* @see com.google.android.setupcompat.internal.TemplateLayout#getMixin(Class)
*/
public interface Mixin {}

View File

@@ -0,0 +1,178 @@
/*
* Copyright (C) 2018 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 static android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
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;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
import com.google.android.setupcompat.view.StatusBarBackgroundLayout;
/**
* A {@link Mixin} for setting and getting background color, and window compatible light/dark theme
* of status bar.
*/
public class StatusBarMixin implements Mixin {
private final PartnerCustomizationLayout partnerCustomizationLayout;
private StatusBarBackgroundLayout statusBarLayout;
private LinearLayout linearLayout;
private final View decorView;
/**
* Creates a mixin for managing status bar.
*
* @param partnerCustomizationLayout The layout this Mixin belongs to.
* @param window The window this activity of Mixin belongs to.
* @param attrs XML attributes given to the layout.
* @param defStyleAttr The default style attribute as given to the constructor of the layout.
*/
public StatusBarMixin(
@NonNull PartnerCustomizationLayout partnerCustomizationLayout,
@NonNull Window window,
@Nullable AttributeSet attrs,
@AttrRes int defStyleAttr) {
this.partnerCustomizationLayout = partnerCustomizationLayout;
View sucLayoutStatus = partnerCustomizationLayout.findManagedViewById(R.id.suc_layout_status);
if (sucLayoutStatus == null) {
throw new NullPointerException("sucLayoutStatus cannot be null in StatusBarMixin");
}
if (sucLayoutStatus instanceof StatusBarBackgroundLayout) {
statusBarLayout = (StatusBarBackgroundLayout) sucLayoutStatus;
} else {
linearLayout = (LinearLayout) sucLayoutStatus;
}
decorView = window.getDecorView();
// Support updating system status bar background color and is light system status bar from M.
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
// Override the color of status bar to transparent such that the color of
// StatusBarBackgroundLayout can be seen.
window.setStatusBarColor(Color.TRANSPARENT);
TypedArray a =
partnerCustomizationLayout
.getContext()
.obtainStyledAttributes(attrs, R.styleable.SucStatusBarMixin, defStyleAttr, 0);
setLightStatusBar(
a.getBoolean(R.styleable.SucStatusBarMixin_sucLightStatusBar, isLightStatusBar()));
setStatusBarBackground(a.getDrawable(R.styleable.SucStatusBarMixin_sucStatusBarBackground));
a.recycle();
}
}
/**
* Sets the background color of status bar. The color will be overridden by partner resource if
* the activity is running in setup wizard flow.
*
* @param color The background color of status bar.
*/
public void setStatusBarBackground(int color) {
setStatusBarBackground(new ColorDrawable(color));
}
/**
* Sets the background image of status bar. The drawable will be overridden by partner resource if
* the activity is running in setup wizard flow.
*
* @param background The drawable of status bar.
*/
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) {
linearLayout.setBackgroundDrawable(background);
} else {
statusBarLayout.setStatusBarBackground(background);
}
}
/** Returns the background of status bar. */
public Drawable getStatusBarBackground() {
if (statusBarLayout == null) {
return linearLayout.getBackground();
} else {
return statusBarLayout.getStatusBarBackground();
}
}
/**
* Sets the status bar to draw in a mode that is compatible with light or dark status bar
* backgrounds. The status bar drawing mode will be overridden by partner resource if the activity
* is running in setup wizard flow.
*
* @param isLight true means compatible with light theme, otherwise compatible with dark theme
*/
public void setLightStatusBar(boolean isLight) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
if (partnerCustomizationLayout.shouldApplyPartnerResource()) {
Context context = partnerCustomizationLayout.getContext();
isLight =
PartnerConfigHelper.get(context)
.getBoolean(context, PartnerConfig.CONFIG_LIGHT_STATUS_BAR, false);
}
if (isLight) {
decorView.setSystemUiVisibility(
decorView.getSystemUiVisibility() | SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
} else {
decorView.setSystemUiVisibility(
decorView.getSystemUiVisibility() & ~SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
}
}
}
/**
* Returns true if status bar icons should be drawn on light background, false if the icons should
* be light-on-dark.
*/
public boolean isLightStatusBar() {
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
return (decorView.getSystemUiVisibility() & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
== SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
}
return true;
}
}

View File

@@ -0,0 +1,268 @@
/*
* Copyright (C) 2018 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 static android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
import android.content.Context;
import android.content.res.TypedArray;
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 com.google.android.setupcompat.PartnerCustomizationLayout;
import com.google.android.setupcompat.R;
import com.google.android.setupcompat.internal.TemplateLayout;
import com.google.android.setupcompat.partnerconfig.PartnerConfig;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
import com.google.android.setupcompat.util.SystemBarHelper;
/**
* A {@link Mixin} for setting and getting background color and window compatible with light theme
* of system navigation bar.
*/
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;
/**
* Creates a mixin for managing the system navigation bar.
*
* @param layout The layout this Mixin belongs to.
* @param window The window this activity of Mixin belongs to.*
*/
public SystemNavBarMixin(@NonNull TemplateLayout layout, @Nullable Window window) {
this.templateLayout = layout;
this.windowOfActivity = window;
this.applyPartnerResources =
layout instanceof PartnerCustomizationLayout
&& ((PartnerCustomizationLayout) layout).shouldApplyPartnerResource();
this.useFullDynamicColor =
layout instanceof PartnerCustomizationLayout
&& ((PartnerCustomizationLayout) layout).useFullDynamicColor();
}
/**
* Creates a mixin for managing the system navigation bar.
*
* @param attrs XML attributes given to the layout.
* @param defStyleAttr The default style attribute as given to the constructor of the layout.
*/
public void applyPartnerCustomizations(@Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
// Support updating system navigation bar background color and is light system navigation bar
// from O.
if (Build.VERSION.SDK_INT >= VERSION_CODES.O_MR1) {
TypedArray a =
templateLayout
.getContext()
.obtainStyledAttributes(attrs, R.styleable.SucSystemNavBarMixin, defStyleAttr, 0);
sucSystemNavBarBackgroundColor =
a.getColor(R.styleable.SucSystemNavBarMixin_sucSystemNavBarBackgroundColor, 0);
setSystemNavBarBackground(sucSystemNavBarBackgroundColor);
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();
}
}
/**
* Sets the background color of navigation bar. The color will be overridden by partner resource
* if the activity is running in setup wizard flow.
*
* @param color The background color of navigation bar.
*/
public void setSystemNavBarBackground(int color) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && windowOfActivity != null) {
if (applyPartnerResources) {
// 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);
}
}
/** Returns the background color of navigation bar. */
public int getSystemNavBarBackground() {
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP && windowOfActivity != null) {
return windowOfActivity.getNavigationBarColor();
}
return Color.BLACK;
}
/**
* Sets the navigation bar to draw in a mode that is compatible with light or dark navigation bar
* backgrounds. The navigation bar drawing mode will be overridden by partner resource if the
* activity is running in setup wizard flow.
*
* @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) {
Context context = templateLayout.getContext();
isLight =
PartnerConfigHelper.get(context)
.getBoolean(context, PartnerConfig.CONFIG_LIGHT_NAVIGATION_BAR, false);
}
if (isLight) {
windowOfActivity
.getDecorView()
.setSystemUiVisibility(
windowOfActivity.getDecorView().getSystemUiVisibility()
| SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
} else {
windowOfActivity
.getDecorView()
.setSystemUiVisibility(
windowOfActivity.getDecorView().getSystemUiVisibility()
& ~SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR);
}
}
}
/**
* Returns true if the navigation bar icon should be drawn on light background, false if the icons
* should be drawn light-on-dark.
*/
public boolean isLightSystemNavBar() {
if (Build.VERSION.SDK_INT >= VERSION_CODES.O && windowOfActivity != null) {
return (windowOfActivity.getDecorView().getSystemUiVisibility()
& SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
== SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
return true;
}
/**
* 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
* android.app.Activity#getWindow()} to make the navigation and status bars follow the Setup
* Wizard style.
*
* <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
*/
public void hideSystemBars(final Window window) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
SystemBarHelper.addVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
SystemBarHelper.addImmersiveFlagsToDecorView(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
// Also set the navigation bar and status bar to transparent color. Note that this
// doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
window.setNavigationBarColor(Color.TRANSPARENT);
window.setStatusBarColor(Color.TRANSPARENT);
}
}
/**
* Reverts the actions of hideSystemBars. Note that this will remove the system UI visibility
* flags regardless of whether it is originally present. The status bar color is reset to
* transparent, thus it will show the status bar color set by StatusBarMixin.
*
* <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
*/
public void showSystemBars(final Window window, final Context context) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
SystemBarHelper.removeVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
SystemBarHelper.removeImmersiveFlagsFromDecorView(
window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
if (context != null) {
if (applyPartnerResources) {
int partnerNavigationBarColor =
PartnerConfigHelper.get(context)
.getColor(context, PartnerConfig.CONFIG_NAVIGATION_BAR_BG_COLOR);
window.setStatusBarColor(Color.TRANSPARENT);
window.setNavigationBarColor(partnerNavigationBarColor);
} else {
// noinspection AndroidLintInlinedApi
TypedArray typedArray =
context.obtainStyledAttributes(
new int[] {android.R.attr.statusBarColor, android.R.attr.navigationBarColor});
int statusBarColor = typedArray.getColor(0, 0);
int navigationBarColor = typedArray.getColor(1, 0);
if (templateLayout instanceof PartnerCustomizationLayout) {
if (VERSION.SDK_INT >= VERSION_CODES.M) {
statusBarColor = Color.TRANSPARENT;
}
if (VERSION.SDK_INT >= VERSION_CODES.O_MR1) {
navigationBarColor = sucSystemNavBarBackgroundColor;
}
}
window.setStatusBarColor(statusBarColor);
window.setNavigationBarColor(navigationBarColor);
typedArray.recycle();
}
}
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* 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;
import androidx.annotation.ChecksSdkIntAtLeast;
/**
* 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.isAtLeastR() suitable for use in Setup
*
* @return Whether the current OS version is higher or equal to R.
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.R)
public static boolean isAtLeastR() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
}
/**
* Implementation of BuildCompat.isAtLeastS() suitable for use in Setup
*
* @return Whether the current OS version is higher or equal to S.
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.S)
public static boolean isAtLeastS() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
}
/**
* Implementation of BuildCompat.isAtLeastT() suitable for use in Setup
*
* @return Whether the current OS version is higher or equal to T.
*/
@ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
public static boolean isAtLeastT() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
}
/**
* 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 =
* "UpsideDownCake", SDK_INT = 33)
* <li>For current Android release: when new API is finalized (CODENAME = "REL", SDK_INT = 34)
* <li>For next Android release (CODENAME = "VanillaIceCream", SDK_INT = 35+)
* </ul>
*
* <p>Note that Build.VERSION_CODES.T cannot be used here until final SDK is available in all
* 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 U.
*/
public static boolean isAtLeastU() {
return (Build.VERSION.CODENAME.equals("REL") && Build.VERSION.SDK_INT >= 34)
|| isAtLeastPreReleaseCodename("UpsideDownCake");
}
private static boolean isAtLeastPreReleaseCodename(String codename) {
// Special case "REL", which means the build is not a pre-release build.
if (Build.VERSION.CODENAME.equals("REL")) {
return false;
}
// Otherwise lexically compare them. Return true if the build codename is equal to or
// greater than the requested codename.
return Build.VERSION.CODENAME.compareTo(codename) >= 0;
}
private BuildCompatUtils() {}
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2024 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.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.view.WindowManager;
import android.view.WindowMetrics;
import androidx.annotation.LayoutRes;
import com.google.android.setupcompat.partnerconfig.PartnerConfig;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
/**
* A helper class to support force two pane feature on portrait orientation. This will inflate the
* layout from xml resource which concatenates with _two_pane suffix.
*/
public final class ForceTwoPaneHelper {
// Refer Support different screen sizes as guideline that any device that the width >= 600 will
// consider as large screen
// https://developer.android.com/guide/topics/large-screens/support-different-screen-sizes
private static final int DEFAULT_ADAPT_WINDOW_WIDTH = 600;
private static final Logger LOG = new Logger("ForceTwoPaneHelper");
/** A string to be a suffix of resource name which is associating to force two pane feature. */
public static final String FORCE_TWO_PANE_SUFFIX = "_two_pane";
/**
* Returns true to indicate the forced two pane feature is enabled, otherwise, returns false. This
* feature is supported from Sdk U while the feature enabled from SUW side.
*/
public static boolean isForceTwoPaneEnable(Context context) {
return Build.VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE
&& PartnerConfigHelper.isForceTwoPaneEnabled(context);
}
/**
* Returns true if satisfied 1) enable force two-pane feature, 2) portrait mode, 3) width >=
* setup_compat_two_pane_adapt_window_width, forced to show in two-pane style, otherwise, returns
* false.
*/
public static boolean shouldForceTwoPane(Context context) {
if (!isForceTwoPaneEnable(context)) {
return false;
}
if (context == null) {
return false;
}
WindowManager windowManager = context.getSystemService(WindowManager.class);
if (windowManager != null) {
WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics();
if (windowMetrics.getBounds().width() > windowMetrics.getBounds().height()) {
// Return false for portrait mode
return false;
}
int widthInDp = (int) (windowMetrics.getBounds().width() / windowMetrics.getDensity());
int adaptWindowWidth =
PartnerConfigHelper.get(context)
.getInteger(
context,
PartnerConfig.CONFIG_TWO_PANE_ADAPT_WINDOW_WIDTH,
DEFAULT_ADAPT_WINDOW_WIDTH);
return widthInDp >= adaptWindowWidth;
}
return false;
}
/**
* Returns a layout which is picking up from the layout resources with _two_pane suffix. Fallback
* to origin resource id if the layout resource not available. For example, pass an
* glif_sud_template resource id and it will return glif_sud_template_two_pane resource id if it
* available.
*/
@LayoutRes
@SuppressLint("DiscouragedApi")
public static int getForceTwoPaneStyleLayout(Context context, int template) {
if (!isForceTwoPaneEnable(context)) {
return template;
}
if (template == Resources.ID_NULL) {
return template;
}
try {
String layoutResName = context.getResources().getResourceEntryName(template);
int twoPaneLayoutId =
context
.getResources()
.getIdentifier(
layoutResName + FORCE_TWO_PANE_SUFFIX, "layout", context.getPackageName());
if (twoPaneLayoutId != Resources.ID_NULL) {
return twoPaneLayoutId;
}
} catch (NotFoundException ignore) {
LOG.w("Resource id 0x" + Integer.toHexString(template) + " is not found");
}
return template;
}
private ForceTwoPaneHelper() {}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright (C) 2019 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 java.util.Arrays;
/** The util for {@link java.util.Objects} method to suitable in all sdk version. */
public final class ObjectUtils {
private ObjectUtils() {}
/** Copied from {@link java.util.Objects#hash(Object...)} */
public static int hashCode(Object... args) {
return Arrays.hashCode(args);
}
/** Copied from {@link java.util.Objects#equals(Object, Object)} */
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (C) 2015 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 static android.app.Activity.RESULT_FIRST_USER;
/** Result codes for activities to return to Wizard Manager. */
public final class ResultCodes {
public static final int RESULT_SKIP = RESULT_FIRST_USER;
public static final int RESULT_RETRY = RESULT_FIRST_USER + 1;
public static final int RESULT_ACTIVITY_NOT_FOUND = RESULT_FIRST_USER + 2;
public static final int RESULT_LIFECYCLE_NOT_MATCHED = RESULT_FIRST_USER + 3;
public static final int RESULT_FLOW_NOT_MATCHED = RESULT_FIRST_USER + 4;
public static final int RESULT_FIRST_SETUP_USER = RESULT_FIRST_USER + 100;
private ResultCodes() {}
}

View File

@@ -0,0 +1,351 @@
/*
* Copyright (C) 2019 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.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Dialog;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Handler;
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
* systemUiVisibility flags to the given Window or View to make them follow the Setup Wizard style.
*
* <p>When the useImmersiveMode intent extra is true, a screen in Setup Wizard should hide the
* system bars using methods from this class. For Lollipop, {@link
* #hideSystemBars(android.view.Window)} will completely hide the system navigation bar and change
* the status bar to transparent, and layout the screen contents (usually the illustration) behind
* it.
*/
public final class 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;
@SuppressLint("InlinedApi")
public static final int DEFAULT_IMMERSIVE_FLAGS =
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
@SuppressLint("InlinedApi")
public static final int DIALOG_IMMERSIVE_FLAGS =
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
/**
* The maximum number of retries when peeking the decor view. When polling for the decor view,
* waiting it to be installed, set a maximum number of retries.
*/
private static final int PEEK_DECOR_VIEW_RETRIES = 3;
/** Convenience method to add a visibility flag in addition to the existing ones. */
public static void addVisibilityFlag(final View view, final int flag) {
final int vis = view.getSystemUiVisibility();
view.setSystemUiVisibility(vis | flag);
}
/** Convenience method to add a visibility flag in addition to the existing ones. */
public static void addVisibilityFlag(final Window window, final int flag) {
WindowManager.LayoutParams attrs = window.getAttributes();
attrs.systemUiVisibility |= flag;
window.setAttributes(attrs);
}
/**
* Convenience method to remove a visibility flag from the view, leaving other flags that are not
* specified intact.
*/
public static void removeVisibilityFlag(final View view, final int flag) {
final int vis = view.getSystemUiVisibility();
view.setSystemUiVisibility(vis & ~flag);
}
/**
* Convenience method to remove a visibility flag from the window, leaving other flags that are
* not specified intact.
*/
public static void removeVisibilityFlag(final Window window, final int flag) {
WindowManager.LayoutParams attrs = window.getAttributes();
attrs.systemUiVisibility &= ~flag;
window.setAttributes(attrs);
}
/**
* Add the specified immersive flags to the decor view of the window, because {@link
* View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} only takes effect when it is added to a view instead of
* the window.
*/
public static void addImmersiveFlagsToDecorView(final Window window, final int vis) {
getDecorView(
window,
new OnDecorViewInstalledListener() {
@Override
public void onDecorViewInstalled(View decorView) {
addVisibilityFlag(decorView, vis);
}
});
}
public static void removeImmersiveFlagsFromDecorView(final Window window, final int vis) {
getDecorView(
window,
new OnDecorViewInstalledListener() {
@Override
public void onDecorViewInstalled(View decorView) {
removeVisibilityFlag(decorView, vis);
}
});
}
/**
* Hide the navigation bar for a dialog.
*
* <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
*
* @deprecated If the layout is instance of TemplateLayout, please use
* SystemNavBarMixin.hideSystemBars.
*/
@Deprecated
public static void hideSystemBars(final Dialog dialog) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
final Window window = dialog.getWindow();
temporarilyDisableDialogFocus(window);
SystemBarHelper.addVisibilityFlag(window, SystemBarHelper.DIALOG_IMMERSIVE_FLAGS);
SystemBarHelper.addImmersiveFlagsToDecorView(window, SystemBarHelper.DIALOG_IMMERSIVE_FLAGS);
// Also set the navigation bar and status bar to transparent color. Note that this
// doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
window.setNavigationBarColor(0);
window.setStatusBarColor(0);
}
}
/**
* Hide 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
* android.app.Activity#getWindow()} to make the navigation and status bars follow the Setup
* Wizard style.
*
* <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
*
* @deprecated If the layout instance of TemplateLayout, please use
* SystemNavBarMixin.hideSystemBars.
*/
@Deprecated
public static void hideSystemBars(final Window window) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
SystemBarHelper.addVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
SystemBarHelper.addImmersiveFlagsToDecorView(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
// Also set the navigation bar and status bar to transparent color. Note that this
// doesn't work if android.R.boolean.config_enableTranslucentDecor is false.
window.setNavigationBarColor(0);
window.setStatusBarColor(0);
}
}
/**
* Revert the actions of hideSystemBars. Note that this will remove the system UI visibility flags
* regardless of whether it is originally present. You should also manually reset the navigation
* bar and status bar colors, as this method doesn't know what value to revert it to.
*
* @deprecated If the layout is instance of TemplateLayout, please use
* SystemNavBarMixin.showSystemBars.
*/
@Deprecated
public static void showSystemBars(final Window window, final Context context) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
SystemBarHelper.removeVisibilityFlag(window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
SystemBarHelper.removeImmersiveFlagsFromDecorView(
window, SystemBarHelper.DEFAULT_IMMERSIVE_FLAGS);
if (context != null) {
//noinspection AndroidLintInlinedApi
final TypedArray typedArray =
context.obtainStyledAttributes(
new int[] {android.R.attr.statusBarColor, android.R.attr.navigationBarColor});
final int statusBarColor = typedArray.getColor(0, 0);
final int navigationBarColor = typedArray.getColor(1, 0);
window.setStatusBarColor(statusBarColor);
window.setNavigationBarColor(navigationBarColor);
typedArray.recycle();
}
}
}
/**
* Sets whether the back button on the software navigation bar is visible. This only works if you
* have the STATUS_BAR permission. Otherwise framework will filter out this flag and this method
* call will not have any effect.
*
* <p>IMPORTANT: Do not assume that users have no way to go back when the back button is hidden.
* Many devices have physical back buttons, and accessibility services like TalkBack may have
* gestures mapped to back. Please use onBackPressed, onKeyDown, or other similar ways to make
* sure back button events are still handled (or ignored) properly.
*/
@RequiresPermission("android.permission.STATUS_BAR")
public static void setBackButtonVisible(final Window window, final boolean visible) {
if (visible) {
SystemBarHelper.removeVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
SystemBarHelper.removeImmersiveFlagsFromDecorView(window, STATUS_BAR_DISABLE_BACK);
} else {
SystemBarHelper.addVisibilityFlag(window, STATUS_BAR_DISABLE_BACK);
SystemBarHelper.addImmersiveFlagsToDecorView(window, STATUS_BAR_DISABLE_BACK);
}
}
/**
* Set a view to be resized when the keyboard is shown. This will set the bottom margin of the
* view to be immediately above the keyboard, and assumes that the view sits immediately above the
* navigation bar.
*
* <p>Note that you must set {@link android.R.attr#windowSoftInputMode} to {@code adjustResize}
* for this class to work. Otherwise window insets are not dispatched and this method will have no
* effect.
*
* <p>This will only take effect in versions Lollipop or above. Otherwise this is a no-op.
*
* @param view The view to be resized when the keyboard is shown.
*/
public static void setImeInsetView(final View view) {
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
view.setOnApplyWindowInsetsListener(new WindowInsetsListener());
}
}
/**
* Apply a hack to temporarily set the window to not focusable, so that the navigation bar will
* not show up during the transition.
*/
private static void temporarilyDisableDialogFocus(final Window window) {
window.setFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
// Add the SOFT_INPUT_IS_FORWARD_NAVIGATION_FLAG. This is normally done by the system when
// FLAG_NOT_FOCUSABLE is not set. Setting this flag allows IME to be shown automatically
// if the dialog has editable text fields.
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION);
new Handler()
.post(
new Runnable() {
@Override
public void run() {
window.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
}
});
}
@TargetApi(VERSION_CODES.LOLLIPOP)
private static class WindowInsetsListener implements View.OnApplyWindowInsetsListener {
private int bottomOffset;
private boolean hasCalculatedBottomOffset = false;
@Override
public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) {
if (!hasCalculatedBottomOffset) {
bottomOffset = getBottomDistance(view);
hasCalculatedBottomOffset = true;
}
int bottomInset = insets.getSystemWindowInsetBottom();
final int bottomMargin = Math.max(insets.getSystemWindowInsetBottom() - bottomOffset, 0);
final ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
// Check that we have enough space to apply the bottom margins before applying it.
// Otherwise the framework may think that the view is empty and exclude it from layout.
if (bottomMargin < lp.bottomMargin + view.getHeight()) {
lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, bottomMargin);
view.setLayoutParams(lp);
bottomInset = 0;
}
return insets.replaceSystemWindowInsets(
insets.getSystemWindowInsetLeft(),
insets.getSystemWindowInsetTop(),
insets.getSystemWindowInsetRight(),
bottomInset);
}
}
private static int getBottomDistance(View view) {
int[] coords = new int[2];
view.getLocationInWindow(coords);
return view.getRootView().getHeight() - coords[1] - view.getHeight();
}
private static void getDecorView(Window window, OnDecorViewInstalledListener callback) {
new DecorViewFinder().getDecorView(window, callback, PEEK_DECOR_VIEW_RETRIES);
}
private static class DecorViewFinder {
private final Handler handler = new Handler();
private Window window;
private int retries;
private OnDecorViewInstalledListener callback;
private final Runnable checkDecorViewRunnable =
new Runnable() {
@Override
public void run() {
// Use peekDecorView instead of getDecorView so that clients can still set window
// features after calling this method.
final View decorView = window.peekDecorView();
if (decorView != null) {
callback.onDecorViewInstalled(decorView);
} else {
retries--;
if (retries >= 0) {
// If the decor view is not installed yet, try again in the next loop.
handler.post(checkDecorViewRunnable);
} else {
LOG.e("Cannot get decor view of window: " + window);
}
}
}
};
public void getDecorView(Window window, OnDecorViewInstalledListener callback, int retries) {
this.window = window;
this.retries = retries;
this.callback = callback;
checkDecorViewRunnable.run();
}
}
private interface OnDecorViewInstalledListener {
void onDecorViewInstalled(View decorView);
}
private SystemBarHelper() {}
}

View File

@@ -0,0 +1,264 @@
/*
* Copyright (C) 2015 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.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.provider.Settings;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.errorprone.annotations.InlineMe;
import java.util.Arrays;
/**
* Helper to interact with Wizard Manager in setup wizard, which should be used when a screen is
* shown inside the setup flow. This includes things like parsing extras passed by Wizard Manager,
* and invoking Wizard Manager to start the next action.
*/
public final class WizardManagerHelper {
/** Enum for notifying an Activity that what SetupWizard flow is */
public enum SuwLifeCycleEnum {
UNKNOWN(0),
INITIALIZATION(1),
PREDEFERRED(2),
DEFERRED(3),
PORTAL(4),
RESTORE_ANYTIME(5);
public final int value;
SuwLifeCycleEnum(int value) {
this.value = value;
}
}
/** Extra for notifying an Activity that what SetupWizard flow is. */
public static final String EXTRA_SUW_LIFECYCLE = "suw_lifecycle";
@VisibleForTesting public static final String ACTION_NEXT = "com.android.wizard.NEXT";
@VisibleForTesting static final String EXTRA_WIZARD_BUNDLE = "wizardBundle";
private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode";
/** 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";
/** Extra for notifying an activity that was called from suggested action activity. */
public static final String EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW = "isSuwSuggestedActionFlow";
public static final String EXTRA_THEME = "theme";
public static final String EXTRA_USE_IMMERSIVE_MODE = "useImmersiveMode";
public static final String SETTINGS_GLOBAL_DEVICE_PROVISIONED = "device_provisioned";
public static final String SETTINGS_SECURE_USER_SETUP_COMPLETE = "user_setup_complete";
/**
* Gets an intent that will invoke the next step of setup wizard.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
* @param resultCode The result code of the step. See {@link ResultCodes}.
* @return A new intent that can be used with {@link Activity#startActivityForResult(Intent, int)}
* to start the next step of the setup flow.
*/
public static Intent getNextIntent(Intent originalIntent, int resultCode) {
return getNextIntent(originalIntent, resultCode, null);
}
/**
* Gets an intent that will invoke the next step of setup wizard.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
* @param resultCode The result code of the step. See {@link ResultCodes}.
* @param data An intent containing extra result data.
* @return A new intent that can be used with {@link Activity#startActivityForResult(Intent, int)}
* to start the next step of the setup flow.
*/
public static Intent getNextIntent(Intent originalIntent, int resultCode, Intent data) {
Intent intent = new Intent(ACTION_NEXT);
copyWizardManagerExtras(originalIntent, intent);
intent.putExtra(EXTRA_RESULT_CODE, resultCode);
if (data != null && data.getExtras() != null) {
intent.putExtras(data.getExtras());
}
intent.putExtra(EXTRA_THEME, originalIntent.getStringExtra(EXTRA_THEME));
return intent;
}
/**
* Copies the internal extras used by setup wizard from one intent to another. For low-level use
* only, such as when using {@link Intent#FLAG_ACTIVITY_FORWARD_RESULT} to relay to another
* intent.
*
* @param srcIntent Intent to get the wizard manager extras from.
* @param dstIntent Intent to copy the wizard manager extras to.
*/
public static void copyWizardManagerExtras(Intent srcIntent, Intent dstIntent) {
dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
for (String key :
Arrays.asList(
EXTRA_IS_FIRST_RUN,
EXTRA_IS_DEFERRED_SETUP,
EXTRA_IS_PRE_DEFERRED_SETUP,
EXTRA_IS_PORTAL_SETUP,
EXTRA_IS_SETUP_FLOW,
EXTRA_IS_SUW_SUGGESTED_ACTION_FLOW)) {
dstIntent.putExtra(key, srcIntent.getBooleanExtra(key, false));
}
// The TikTok code in Restore doesn't let us put serializable extras into intents.
dstIntent.putExtra(
EXTRA_SUW_LIFECYCLE,
srcIntent.getIntExtra(EXTRA_SUW_LIFECYCLE, SuwLifeCycleEnum.UNKNOWN.value));
dstIntent.putExtra(EXTRA_THEME, srcIntent.getStringExtra(EXTRA_THEME));
}
/**
* @deprecated Use {@link isInitialSetupWizard} instead.
*/
@InlineMe(
replacement = "intent.getBooleanExtra(WizardManagerHelper.EXTRA_IS_FIRST_RUN, false)",
imports = "com.google.android.setupcompat.util.WizardManagerHelper")
@Deprecated
public static boolean isSetupWizardIntent(Intent intent) {
return intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false);
}
/**
* Checks whether the current user has completed Setup Wizard. This is true if the current user
* has gone through Setup Wizard. The current user may or may not be the device owner and the
* device owner may have already completed setup wizard.
*
* @param context The context to retrieve the settings.
* @return true if the current user has completed Setup Wizard.
* @see #isDeviceProvisioned(Context)
*/
public static boolean isUserSetupComplete(Context context) {
return Settings.Secure.getInt(
context.getContentResolver(), SETTINGS_SECURE_USER_SETUP_COMPLETE, 0)
== 1;
}
/**
* Checks whether the device is provisioned. This means that the device has gone through Setup
* Wizard at least once. Note that the user can still be in Setup Wizard even if this is true, for
* a secondary user profile triggered through Settings > Add account.
*
* @param context The context to retrieve the settings.
* @return true if the device is provisioned.
* @see #isUserSetupComplete(Context)
*/
public static boolean isDeviceProvisioned(Context context) {
return Settings.Global.getInt(
context.getContentResolver(), SETTINGS_GLOBAL_DEVICE_PROVISIONED, 0)
== 1;
}
/**
* Checks whether an intent is running in the portal setup wizard flow. This API is supported
* since S.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
* @return true if the intent passed in was running in portal setup wizard.
*/
public static boolean isPortalSetupWizard(Intent originalIntent) {
return originalIntent != null && originalIntent.getBooleanExtra(EXTRA_IS_PORTAL_SETUP, false);
}
/**
* Checks whether an intent is running in the deferred setup wizard flow.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
* @return true if the intent passed in was running in deferred setup wizard.
*/
public static boolean isDeferredSetupWizard(Intent originalIntent) {
return originalIntent != null && originalIntent.getBooleanExtra(EXTRA_IS_DEFERRED_SETUP, false);
}
/**
* Checks whether an intent is running in "pre-deferred" setup wizard flow.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
* @return true if the intent passed in was running in "pre-deferred" setup wizard.
*/
public static boolean isPreDeferredSetupWizard(Intent originalIntent) {
return originalIntent != null
&& originalIntent.getBooleanExtra(EXTRA_IS_PRE_DEFERRED_SETUP, false);
}
/**
* Checks whether an intent is is running in the initial setup wizard flow.
*
* @param intent The intent to be checked, usually from {@link Activity#getIntent()}.
* @return true if the intent passed in was intended to be used with setup wizard.
*/
public static boolean isInitialSetupWizard(Intent intent) {
return intent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false);
}
/**
* Since Q, returns true if the intent passed in indicates that it is running in setup wizard
* flows, including initial, predeferred, deferred. Since S, it also supports portal setup.
*
* <p>Pre-Q, it is running in three setup wizard flows, including initial, predeferred, deferred
* setup.
*
* @param originalIntent The original intent that was used to start the step, usually via {@link
* Activity#getIntent()}.
*/
public static boolean isAnySetupWizard(@Nullable Intent originalIntent) {
if (originalIntent == null) {
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return originalIntent.getBooleanExtra(EXTRA_IS_SETUP_FLOW, false);
} else {
return isInitialSetupWizard(originalIntent)
|| isPreDeferredSetupWizard(originalIntent)
|| isDeferredSetupWizard(originalIntent);
}
}
private WizardManagerHelper() {}
}

View File

@@ -0,0 +1,220 @@
/*
* Copyright (C) 2017 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.view;
import android.content.Context;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.widget.LinearLayout;
import com.google.android.setupcompat.R;
import com.google.android.setupcompat.partnerconfig.PartnerConfigHelper;
import com.google.android.setupcompat.template.FooterActionButton;
import com.google.android.setupcompat.util.Logger;
import java.util.ArrayList;
import java.util.Collections;
/**
* An extension of LinearLayout that automatically switches to vertical orientation when it can't
* fit its child views horizontally.
*
* <p>Modified from {@code com.android.internal.widget.ButtonBarLayout}
*/
public class ButtonBarLayout extends LinearLayout {
private static final Logger LOG = new Logger(ButtonBarLayout.class);
private boolean stacked = false;
private int originalPaddingLeft;
private int originalPaddingRight;
public ButtonBarLayout(Context context) {
super(context);
}
public ButtonBarLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
setStacked(false);
boolean needsRemeasure = false;
int initialWidthMeasureSpec = widthMeasureSpec;
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
// Measure with WRAP_CONTENT, so that we can compare the measured size with the
// available size to see if we need to stack.
initialWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
// We'll need to remeasure again to fill excess space.
needsRemeasure = true;
}
super.onMeasure(initialWidthMeasureSpec, heightMeasureSpec);
final boolean childrenLargerThanContainer = (widthSize > 0) && (getMeasuredWidth() > widthSize);
if (!isFooterButtonsEvenlyWeighted(getContext()) && childrenLargerThanContainer) {
setStacked(true);
// Measure again in the new orientation.
needsRemeasure = true;
}
if (needsRemeasure) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private void setStacked(boolean stacked) {
if (this.stacked == stacked) {
return;
}
this.stacked = stacked;
boolean isUnstack = false;
int primaryStyleButtonCount = 0;
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
LayoutParams childParams = (LayoutParams) child.getLayoutParams();
if (stacked) {
child.setTag(R.id.suc_customization_original_weight, childParams.weight);
childParams.weight = 0;
childParams.leftMargin = 0;
} else {
Float weight = (Float) child.getTag(R.id.suc_customization_original_weight);
if (weight != null) {
childParams.weight = weight;
} else {
// If the tag in the child is gone, it will be unstack and the child in the container will
// be disorder.
isUnstack = true;
}
if (isPrimaryButtonStyle(child)) {
primaryStyleButtonCount++;
}
}
child.setLayoutParams(childParams);
}
setOrientation(stacked ? LinearLayout.VERTICAL : LinearLayout.HORIZONTAL);
if (isUnstack) {
LOG.w("Reorder the FooterActionButtons in the container");
ArrayList<View> childViewsInContainerInOrder =
getChildViewsInContainerInOrder(
childCount, /* isOnePrimaryButton= */ (primaryStyleButtonCount <= 1));
for (int i = 0; i < childCount; i++) {
View view = childViewsInContainerInOrder.get(i);
if (view != null) {
bringChildToFront(view);
}
}
} else {
for (int i = childCount - 1; i >= 0; i--) {
bringChildToFront(getChildAt(i));
}
}
if (stacked) {
// When stacked, the buttons need to be kept in the center of the button bar.
setHorizontalGravity(Gravity.CENTER);
// HACK: In the default button bar style, the left and right paddings are not
// balanced to compensate for different alignment for borderless (left) button and
// the raised (right) button. When it's stacked, we want the buttons to be centered,
// so we balance out the paddings here.
originalPaddingLeft = getPaddingLeft();
originalPaddingRight = getPaddingRight();
int paddingHorizontal = Math.max(originalPaddingLeft, originalPaddingRight);
setPadding(paddingHorizontal, getPaddingTop(), paddingHorizontal, getPaddingBottom());
} else {
setPadding(originalPaddingLeft, getPaddingTop(), originalPaddingRight, getPaddingBottom());
}
}
private boolean isPrimaryButtonStyle(View child) {
return child instanceof FooterActionButton
&& ((FooterActionButton) child).isPrimaryButtonStyle();
}
/**
* Return a array which store child views in the container and in the order (secondary button,
* space view, primary button), if only one primary button, the child views will replace null
* value in specific proper position, if there are two primary buttons, expected get the original
* child by the order (space view, secondary button, primary button), so insert the space view to
* the middle in the array.
*/
private ArrayList<View> getChildViewsInContainerInOrder(
int childCount, boolean isOnePrimaryButton) {
int childViewsInContainerCount = 3;
int secondaryButtonIndex = 0;
int spaceViewIndex = 1;
int primaryButtonIndex = 2;
ArrayList<View> childFooterButtons = new ArrayList<>();
if (isOnePrimaryButton) {
childFooterButtons.addAll(Collections.nCopies(childViewsInContainerCount, null));
}
for (int i = 0; i < childCount; i++) {
View childAt = getChildAt(i);
if (isOnePrimaryButton) {
if (isPrimaryButtonStyle(childAt)) {
childFooterButtons.set(primaryButtonIndex, childAt);
} else if (!(childAt instanceof FooterActionButton)) {
childFooterButtons.set(spaceViewIndex, childAt);
} else {
childFooterButtons.set(secondaryButtonIndex, childAt);
}
} else {
if (!(childAt instanceof FooterActionButton)) {
childFooterButtons.add(spaceViewIndex, childAt);
} else {
childFooterButtons.add(getChildAt(i));
}
}
}
return childFooterButtons;
}
private boolean isFooterButtonsEvenlyWeighted(Context context) {
int childCount = getChildCount();
int primaryButtonCount = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
if (child instanceof FooterActionButton) {
if (((FooterActionButton) child).isPrimaryButtonStyle()) {
primaryButtonCount += 1;
}
}
}
if (primaryButtonCount != 2) {
return false;
}
// TODO: Support neutral button style in glif layout for phone and tablet
if (context.getResources().getConfiguration().smallestScreenWidthDp >= 600
&& PartnerConfigHelper.shouldApplyExtendedPartnerConfig(context)) {
return true;
} else {
return false;
}
}
}

View File

@@ -0,0 +1,98 @@
/*
* Copyright (C) 2015 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.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.util.AttributeSet;
import android.view.WindowInsets;
import android.widget.FrameLayout;
/**
* A FrameLayout subclass that will responds to onApplyWindowInsets to draw a drawable in the top
* inset area, making a background effect for the navigation bar. To make use of this layout,
* specify the system UI visibility {@link android.view.View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN} and
* set specify fitsSystemWindows.
*
* <p>This view is a normal FrameLayout if either of those are not set, or if the platform version
* is lower than Lollipop.
*/
public class StatusBarBackgroundLayout extends FrameLayout {
private Drawable statusBarBackground;
private Object lastInsets; // Use generic Object type for compatibility
public StatusBarBackgroundLayout(Context context) {
super(context);
}
public StatusBarBackgroundLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(VERSION_CODES.HONEYCOMB)
public StatusBarBackgroundLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
if (lastInsets == null) {
requestApplyInsets();
}
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
if (lastInsets != null) {
final int insetTop = ((WindowInsets) lastInsets).getSystemWindowInsetTop();
if (insetTop > 0) {
statusBarBackground.setBounds(0, 0, getWidth(), insetTop);
statusBarBackground.draw(canvas);
}
}
}
}
public void setStatusBarBackground(Drawable background) {
statusBarBackground = background;
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
setWillNotDraw(background == null);
setFitsSystemWindows(background != null);
invalidate();
}
}
public Drawable getStatusBarBackground() {
return statusBarBackground;
}
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
lastInsets = insets;
return super.onApplyWindowInsets(insets);
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2017 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="?attr/sucFooterBarButtonHighlightAlpha"
android:color="?attr/sucFooterBarButtonColorControlHighlightRipple" />
</selector>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2018 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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suc_layout_status"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/suc_layout_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ViewStub
android:id="@+id/suc_layout_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2018 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.
-->
<com.google.android.setupcompat.view.StatusBarBackgroundLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suc_layout_status"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<FrameLayout
android:id="@+id/suc_layout_content"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<ViewStub
android:id="@+id/suc_layout_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</com.google.android.setupcompat.view.StatusBarBackgroundLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 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.
-->
<!-- A simple button to be inflated by the inflater. This allows creating AppCompatButton without
maintaining separate versions for compat and platform. -->
<com.google.android.setupcompat.template.FooterActionButton
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (C) 2017 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.
-->
<com.google.android.setupcompat.view.ButtonBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/suc_footer_button_bar"
style="@style/SucPartnerCustomizationButtonBar.Stackable"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

View File

@@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<resources>
<!-- Theme attributes -->
<attr name="sucLayoutTheme" format="reference" />
<declare-styleable name="SucTemplateLayout">
<attr name="android:layout" />
<attr name="sucContainer" format="reference" />
</declare-styleable>
<declare-styleable name="SucPartnerCustomizationLayout">
<attr name="sucLayoutFullscreen" format="boolean" />
<!-- When set to false, prevents the layout applying partner resource. This attribute is
particularly useful when the layout would like to apply their customized attributes.
This attribute will be ignored and use partner resource when inside setup wizard flow.
The default value is true. -->
<attr name="sucUsePartnerResource" format="boolean" />
<attr name="sucFullDynamicColor" format="boolean" />
</declare-styleable>
<!-- Status bar attributes; only takes effect on M or above -->
<declare-styleable name="SucStatusBarMixin">
<!-- The color for the status bar. For this to take effect,
"android:windowDrawsSystemBarBackgrounds" should be set to true and
"android:windowTranslucentStatus" should be set to false. Also,
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN should be set to true and
android:statusBarColor should be transparent. -->
<attr name="sucStatusBarBackground" format="color|reference" />
<attr name="sucLightStatusBar" format="boolean" />
</declare-styleable>
<!-- System navigation bar attributes; only takes effect on O_MR1 or above -->
<declare-styleable name="SucSystemNavBarMixin">
<!-- The color for the system navigation bar. For this to take effect,
"android:windowDrawsSystemBarBackgrounds" should be set to true and
"android:windowTranslucentNavigation" should be set to false. -->
<attr name="sucSystemNavBarBackgroundColor" format="color" />
<attr name="sucLightSystemNavBar" format="boolean" />
<!-- The color for the system navigation bar divider. For this to take effect,
"android:windowDrawsSystemBarBackgrounds" should be set to true and
"android:windowTranslucentNavigation" should be set to false. -->
<attr name="sucSystemNavBarDividerColor" format="color" />
</declare-styleable>
<!-- FooterButton attributes -->
<declare-styleable name="SucFooterButton">
<attr name="android:text" />
<attr name="android:theme" />
<!-- Next value: 9 -->
<attr name="sucButtonType">
<enum name="other" value="0" />
<enum name="add_another" value="1" />
<enum name="cancel" value="2" />
<enum name="clear" value="3" />
<enum name="done" value="4" />
<enum name="next" value="5" />
<enum name="opt_in" value="6" />
<enum name="skip" value="7" />
<enum name="stop" value="8" />
</attr>
<attr name="sucFooterButtonPaddingStart" format="dimension" />
<attr name="sucFooterButtonPaddingEnd" format="dimension" />
</declare-styleable>
<!-- Button of footer attributes -->
<declare-styleable name="SucFooterBarMixin">
<attr name="sucFooterBarButtonAllCaps" format="boolean" />
<attr name="sucFooterBarButtonAlignEnd" format="boolean" />
<attr name="sucFooterBarButtonCornerRadius" format="dimension" />
<attr name="sucFooterBarButtonFontFamily" format="string|reference" />
<attr name="sucFooterBarPaddingTop" format="dimension" />
<attr name="sucFooterBarPaddingBottom" format="dimension" />
<attr name="sucFooterBarPrimaryFooterBackground" format="color" />
<attr name="sucFooterBarPrimaryFooterButton" format="reference" />
<attr name="sucFooterBarSecondaryFooterBackground" format="color" />
<attr name="sucFooterBarSecondaryFooterButton" format="reference" />
<attr name="sucFooterBarButtonHighlightAlpha" format="float" />
<attr name="sucFooterBarButtonColorControlHighlight" format="color" />
<attr name="sucFooterBarButtonColorControlHighlightRipple" format="color" />
<attr name="sucFooterBarPaddingVertical" format="dimension" />
<attr name="sucFooterBarPaddingStart" format="dimension" />
<attr name="sucFooterBarPaddingEnd" format="dimension" />
<attr name="sucFooterBarMinHeight" format="dimension" />
</declare-styleable>
<declare-styleable name="SucHeaderMixin">
<attr name="sucHeaderText" format="string" localization="suggested" />
<attr name="sucHeaderTextColor" format="reference|color" />
<attr name="sucGlifHeaderMarginTop" format="dimension" />
<attr name="sucGlifHeaderMarginBottom" format="dimension" />
<attr name="sucGlifIconMarginTop" format="dimension" />
<attr name="sucHeaderContainerMarginBottom" format="dimension" />
</declare-styleable>
</resources>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<resources>
<!-- On versions < 23, we cannot reference other theme values in a color resource. Default to
the framework default of 12% black -->
<color name="suc_customization_button_highlight_ripple">#1f000000</color>
<color name="suc_customization_button_highlight_default">#ff1a73e8</color>
</resources>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<resources>
<!-- ID used with View#setTag to store the original weight on a ButtonBar -->
<item name="suc_customization_original_weight" type="id" />
</resources>

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (C) 2018 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.
-->
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Customization footer styles -->
<style name="SucPartnerCustomizationButtonBar.Stackable" parent="SucPartnerCustomizationButtonBar">
<item name="android:gravity">center</item>
</style>
<style name="SucPartnerCustomizationButtonBar">
<item name="android:baselineAligned">false</item>
<item name="android:clipChildren">false</item>
<item name="android:clipToPadding">false</item>
<item name="android:gravity">center_vertical</item>
<item name="android:minHeight">?attr/sucFooterBarMinHeight</item>
<item name="android:orientation">horizontal</item>
<item name="android:paddingTop">?attr/sucFooterBarPaddingVertical</item>
<item name="android:paddingBottom">?attr/sucFooterBarPaddingVertical</item>
<item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterBarPaddingEnd</item>
<item name="android:paddingLeft">?attr/sucFooterBarPaddingStart</item>
<item name="android:paddingRight">?attr/sucFooterBarPaddingEnd</item>
<item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterBarPaddingStart</item>
</style>
<style name="SucPartnerCustomizationButton.Primary" parent="android:Widget.Material.Button.Colored">
<!-- This style can be applied to a button either as a "style" in XML, or as a theme in
ContextThemeWrapper. These self-referencing attributes make sure this is applied as
both to the button. -->
<item name="android:buttonStyle">@style/SucPartnerCustomizationButton.Primary</item>
<item name="android:theme">@style/SucPartnerCustomizationButton.Primary</item>
<!-- Values used in styles -->
<item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/sucFooterBarButtonFontFamily</item>
<item name="android:paddingLeft">?attr/sucFooterButtonPaddingStart</item>
<item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterButtonPaddingStart</item>
<item name="android:paddingRight">?attr/sucFooterButtonPaddingEnd</item>
<item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterButtonPaddingEnd</item>
<item name="android:textAllCaps">?attr/sucFooterBarButtonAllCaps</item>
<item name="android:stateListAnimator" tools:ignore="NewApi">@null</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/sucFooterBarButtonCornerRadius</item>
</style>
<style name="SucPartnerCustomizationButton.Secondary" parent="android:Widget.Material.Button.Borderless.Colored">
<!-- This style can be applied to a button either as a "style" in XML, or as a theme in
ContextThemeWrapper. These self-referencing attributes make sure this is applied as
both to the button. -->
<item name="android:buttonStyle">@style/SucPartnerCustomizationButton.Secondary</item>
<item name="android:theme">@style/SucPartnerCustomizationButton.Secondary</item>
<!-- Values used in styles -->
<item name="android:fontFamily" tools:targetApi="jelly_bean">?attr/sucFooterBarButtonFontFamily</item>
<item name="android:minWidth">0dp</item>
<item name="android:paddingLeft">?attr/sucFooterButtonPaddingStart</item>
<item name="android:paddingStart" tools:ignore="NewApi">?attr/sucFooterButtonPaddingStart</item>
<item name="android:paddingRight">?attr/sucFooterButtonPaddingEnd</item>
<item name="android:paddingEnd" tools:ignore="NewApi">?attr/sucFooterButtonPaddingEnd</item>
<item name="android:textAllCaps">?attr/sucFooterBarButtonAllCaps</item>
<!-- Values used in themes -->
<item name="android:buttonCornerRadius" tools:ignore="NewApi">?attr/sucFooterBarButtonCornerRadius</item>
<item name="android:colorControlHighlight" tools:targetApi="lollipop">@color/suc_customization_button_highlight_ripple</item>
<item name="sucFooterBarButtonColorControlHighlight">@color/suc_customization_button_highlight_ripple</item>
</style>
</resources>