fix: 首次提交
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 */);
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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_]+");
|
||||
}
|
||||
@@ -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]+");
|
||||
}
|
||||
@@ -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()
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {}
|
||||
;
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> 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;
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user