blob: e87d9983ef526c9aff3365ed49c916dccb23048b [file] [log] [blame]
/*
* 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.partnerconfig;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.database.ContentObserver;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.window.embedding.ActivityEmbeddingController;
import com.google.android.setupcompat.partnerconfig.PartnerConfig.ResourceType;
import com.google.android.setupcompat.util.BuildCompatUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.Objects;
/** The helper reads and caches the partner configurations from SUW. */
public class PartnerConfigHelper {
private static final String TAG = PartnerConfigHelper.class.getSimpleName();
public static final String SUW_AUTHORITY = "com.google.android.setupwizard.partner";
@VisibleForTesting public static final String SUW_GET_PARTNER_CONFIG_METHOD = "getOverlayConfig";
@VisibleForTesting public static final String KEY_FALLBACK_CONFIG = "fallbackConfig";
@VisibleForTesting
public static final String IS_SUW_DAY_NIGHT_ENABLED_METHOD = "isSuwDayNightEnabled";
@VisibleForTesting
public static final String IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD =
"isExtendedPartnerConfigEnabled";
@VisibleForTesting
public static final String IS_MATERIAL_YOU_STYLE_ENABLED_METHOD = "IsMaterialYouStyleEnabled";
@VisibleForTesting
public static final String IS_DYNAMIC_COLOR_ENABLED_METHOD = "isDynamicColorEnabled";
@VisibleForTesting
public static final String IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD = "isFullDynamicColorEnabled";
@VisibleForTesting
public static final String IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD = "isNeutralButtonStyleEnabled";
@VisibleForTesting
public static final String IS_FONT_WEIGHT_ENABLED_METHOD = "isFontWeightEnabled";
@VisibleForTesting
public static final String IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD =
"isEmbeddedActivityOnePaneEnabled";
@VisibleForTesting
public static final String GET_SUW_DEFAULT_THEME_STRING_METHOD = "suwDefaultThemeString";
@VisibleForTesting public static final String SUW_PACKAGE_NAME = "com.google.android.setupwizard";
@VisibleForTesting public static final String MATERIAL_YOU_RESOURCE_SUFFIX = "_material_you";
@VisibleForTesting
public static final String EMBEDDED_ACTIVITY_RESOURCE_SUFFIX = "_embedded_activity";
@VisibleForTesting static Bundle suwDayNightEnabledBundle = null;
@VisibleForTesting public static Bundle applyExtendedPartnerConfigBundle = null;
@VisibleForTesting public static Bundle applyMaterialYouConfigBundle = null;
@VisibleForTesting public static Bundle applyDynamicColorBundle = null;
@VisibleForTesting public static Bundle applyFullDynamicColorBundle = null;
@VisibleForTesting public static Bundle applyNeutralButtonStyleBundle = null;
@VisibleForTesting public static Bundle applyFontWeightBundle = null;
@VisibleForTesting public static Bundle applyEmbeddedActivityOnePaneBundle = null;
@VisibleForTesting public static Bundle suwDefaultThemeBundle = null;
private static PartnerConfigHelper instance = null;
@VisibleForTesting Bundle resultBundle = null;
@VisibleForTesting
final EnumMap<PartnerConfig, Object> partnerResourceCache = new EnumMap<>(PartnerConfig.class);
private static ContentObserver contentObserver;
private static int savedConfigUiMode;
private static boolean savedConfigEmbeddedActivityMode;
@VisibleForTesting static Bundle applyTransitionBundle = null;
@VisibleForTesting public static int savedOrientation = Configuration.ORIENTATION_PORTRAIT;
/** The method name to get if transition settings is set from client. */
public static final String APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD =
"applyGlifThemeControlledTransition";
/**
* When testing related to fake PartnerConfigHelper instance, should sync the following saved
* config with testing environment.
*/
@VisibleForTesting public static int savedScreenHeight = Configuration.SCREEN_HEIGHT_DP_UNDEFINED;
@VisibleForTesting public static int savedScreenWidth = Configuration.SCREEN_WIDTH_DP_UNDEFINED;
public static synchronized PartnerConfigHelper get(@NonNull Context context) {
if (!isValidInstance(context)) {
instance = new PartnerConfigHelper(context);
}
return instance;
}
private static boolean isValidInstance(@NonNull Context context) {
Configuration currentConfig = context.getResources().getConfiguration();
if (instance == null) {
savedConfigEmbeddedActivityMode =
isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU();
savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
savedOrientation = currentConfig.orientation;
savedScreenWidth = currentConfig.screenWidthDp;
savedScreenHeight = currentConfig.screenHeightDp;
return false;
} else {
boolean uiModeChanged =
isSetupWizardDayNightEnabled(context)
&& (currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) != savedConfigUiMode;
boolean embeddedActivityModeChanged =
isEmbeddedActivityOnePaneEnabled(context) && BuildCompatUtils.isAtLeastU();
if (uiModeChanged
|| embeddedActivityModeChanged != savedConfigEmbeddedActivityMode
|| currentConfig.orientation != savedOrientation
|| currentConfig.screenWidthDp != savedScreenWidth
|| currentConfig.screenHeightDp != savedScreenHeight) {
savedConfigUiMode = currentConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK;
savedOrientation = currentConfig.orientation;
savedScreenHeight = currentConfig.screenHeightDp;
savedScreenWidth = currentConfig.screenWidthDp;
resetInstance();
return false;
}
}
return true;
}
private PartnerConfigHelper(Context context) {
getPartnerConfigBundle(context);
registerContentObserver(context);
}
/**
* Returns whether partner customized config values are available. This is true if setup wizard's
* content provider returns us a non-empty bundle, even if all the values are default, and none
* are customized by the overlay APK.
*/
public boolean isAvailable() {
return resultBundle != null && !resultBundle.isEmpty();
}
/**
* Returns whether the given {@code resourceConfig} are available. This is true if setup wizard's
* content provider returns us a non-empty bundle, and this result bundle includes the given
* {@code resourceConfig} even if all the values are default, and none are customized by the
* overlay APK.
*/
public boolean isPartnerConfigAvailable(PartnerConfig resourceConfig) {
return isAvailable() && resultBundle.containsKey(resourceConfig.getResourceName());
}
/**
* Returns the color of given {@code resourceConfig}, or 0 if the given {@code resourceConfig} is
* not found. If the {@code ResourceType} of the given {@code resourceConfig} is not color,
* IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@link PartnerConfig} of target resource
*/
@ColorInt
public int getColor(@NonNull Context context, PartnerConfig resourceConfig) {
if (resourceConfig.getResourceType() != ResourceType.COLOR) {
throw new IllegalArgumentException("Not a color resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (int) partnerResourceCache.get(resourceConfig);
}
int result = 0;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
// for @null
TypedValue outValue = new TypedValue();
resource.getValue(resId, outValue, true);
if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
return result;
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.M) {
result = resource.getColor(resId, null);
} else {
result = resource.getColor(resId);
}
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException exception) {
// fall through
}
return result;
}
/**
* Returns the {@code Drawable} of given {@code resourceConfig}, or {@code null} if the given
* {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
* resourceConfig} is not drawable, IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
*/
@Nullable
public Drawable getDrawable(@NonNull Context context, PartnerConfig resourceConfig) {
if (resourceConfig.getResourceType() != ResourceType.DRAWABLE) {
throw new IllegalArgumentException("Not a drawable resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (Drawable) partnerResourceCache.get(resourceConfig);
}
Drawable result = null;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
// for @null
TypedValue outValue = new TypedValue();
resource.getValue(resId, outValue, true);
if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
return result;
}
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
result = resource.getDrawable(resId, null);
} else {
result = resource.getDrawable(resId);
}
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException | NotFoundException exception) {
// fall through
}
return result;
}
/**
* Returns the string of the given {@code resourceConfig}, or {@code null} if the given {@code
* resourceConfig} is not found. If the {@code ResourceType} of the given {@code resourceConfig}
* is not string, IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
*/
@Nullable
public String getString(@NonNull Context context, PartnerConfig resourceConfig) {
if (resourceConfig.getResourceType() != ResourceType.STRING) {
throw new IllegalArgumentException("Not a string resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (String) partnerResourceCache.get(resourceConfig);
}
String result = null;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getString(resId);
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException exception) {
// fall through
}
return result;
}
/**
* Returns the string array of the given {@code resourceConfig}, or {@code null} if the given
* {@code resourceConfig} is not found. If the {@code ResourceType} of the given {@code
* resourceConfig} is not string, IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
*/
@NonNull
public List<String> getStringArray(@NonNull Context context, PartnerConfig resourceConfig) {
if (resourceConfig.getResourceType() != ResourceType.STRING_ARRAY) {
throw new IllegalArgumentException("Not a string array resource");
}
String[] result;
List<String> listResult = new ArrayList<>();
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getStringArray(resId);
Collections.addAll(listResult, result);
} catch (NullPointerException exception) {
// fall through
}
return listResult;
}
/**
* Returns the boolean of given {@code resourceConfig}, or {@code defaultValue} if the given
* {@code resourceName} is not found. If the {@code ResourceType} of the given {@code
* resourceConfig} is not boolean, IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
* @param defaultValue The default value
*/
public boolean getBoolean(
@NonNull Context context, PartnerConfig resourceConfig, boolean defaultValue) {
if (resourceConfig.getResourceType() != ResourceType.BOOL) {
throw new IllegalArgumentException("Not a bool resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (boolean) partnerResourceCache.get(resourceConfig);
}
boolean result = defaultValue;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getBoolean(resId);
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException | NotFoundException exception) {
// fall through
}
return result;
}
/**
* Returns the dimension of given {@code resourceConfig}. The default return value is 0.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
*/
public float getDimension(@NonNull Context context, PartnerConfig resourceConfig) {
return getDimension(context, resourceConfig, 0);
}
/**
* Returns the dimension of given {@code resourceConfig}. If the given {@code resourceConfig} is
* not found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
* resourceConfig} is not dimension, will throw IllegalArgumentException.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
* @param defaultValue The default value
*/
public float getDimension(
@NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
if (resourceConfig.getResourceType() != ResourceType.DIMENSION) {
throw new IllegalArgumentException("Not a dimension resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return getDimensionFromTypedValue(
context, (TypedValue) partnerResourceCache.get(resourceConfig));
}
float result = defaultValue;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getDimension(resId);
TypedValue value = getTypedValueFromResource(resource, resId, TypedValue.TYPE_DIMENSION);
partnerResourceCache.put(resourceConfig, value);
result =
getDimensionFromTypedValue(
context, (TypedValue) partnerResourceCache.get(resourceConfig));
} catch (NullPointerException | NotFoundException exception) {
// fall through
}
return result;
}
/**
* Returns the float of given {@code resourceConfig}. The default return value is 0.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
*/
public float getFraction(@NonNull Context context, PartnerConfig resourceConfig) {
return getFraction(context, resourceConfig, 0.0f);
}
/**
* Returns the float of given {@code resourceConfig}. If the given {@code resourceConfig} not
* found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
* resourceConfig} is not fraction, will throw IllegalArgumentException.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
* @param defaultValue The default value
*/
public float getFraction(
@NonNull Context context, PartnerConfig resourceConfig, float defaultValue) {
if (resourceConfig.getResourceType() != ResourceType.FRACTION) {
throw new IllegalArgumentException("Not a fraction resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (float) partnerResourceCache.get(resourceConfig);
}
float result = defaultValue;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getFraction(resId, 1, 1);
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException | NotFoundException exception) {
// fall through
}
return result;
}
/**
* Returns the integer of given {@code resourceConfig}. If the given {@code resourceConfig} is not
* found, will return {@code defaultValue}. If the {@code ResourceType} of given {@code
* resourceConfig} is not dimension, will throw IllegalArgumentException.
*
* @param context The context of client activity
* @param resourceConfig The {@code PartnerConfig} of target resource
* @param defaultValue The default value
*/
public int getInteger(@NonNull Context context, PartnerConfig resourceConfig, int defaultValue) {
if (resourceConfig.getResourceType() != ResourceType.INTEGER) {
throw new IllegalArgumentException("Not a integer resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (int) partnerResourceCache.get(resourceConfig);
}
int result = defaultValue;
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
result = resource.getInteger(resId);
partnerResourceCache.put(resourceConfig, result);
} catch (NullPointerException | NotFoundException exception) {
// fall through
}
return result;
}
/**
* Returns the {@link ResourceEntry} of given {@code resourceConfig}, or {@code null} if the given
* {@code resourceConfig} is not found. If the {@link ResourceType} of the given {@code
* resourceConfig} is not illustration, IllegalArgumentException will be thrown.
*
* @param context The context of client activity
* @param resourceConfig The {@link PartnerConfig} of target resource
*/
@Nullable
public ResourceEntry getIllustrationResourceEntry(
@NonNull Context context, PartnerConfig resourceConfig) {
if (resourceConfig.getResourceType() != ResourceType.ILLUSTRATION) {
throw new IllegalArgumentException("Not a illustration resource");
}
if (partnerResourceCache.containsKey(resourceConfig)) {
return (ResourceEntry) partnerResourceCache.get(resourceConfig);
}
try {
ResourceEntry resourceEntry =
getResourceEntryFromKey(context, resourceConfig.getResourceName());
Resources resource = resourceEntry.getResources();
int resId = resourceEntry.getResourceId();
// TODO: The illustration resource entry validation should validate is it a video
// resource or not?
// for @null
TypedValue outValue = new TypedValue();
resource.getValue(resId, outValue, true);
if (outValue.type == TypedValue.TYPE_REFERENCE && outValue.data == 0) {
return null;
}
partnerResourceCache.put(resourceConfig, resourceEntry);
return resourceEntry;
} catch (NullPointerException exception) {
// fall through
}
return null;
}
private void getPartnerConfigBundle(Context context) {
if (resultBundle == null || resultBundle.isEmpty()) {
try {
resultBundle =
context
.getContentResolver()
.call(
getContentUri(),
SUW_GET_PARTNER_CONFIG_METHOD,
/* arg= */ null,
/* extras= */ null);
partnerResourceCache.clear();
Log.i(
TAG, "PartnerConfigsBundle=" + (resultBundle != null ? resultBundle.size() : "(null)"));
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "Fail to get config from suw provider");
}
}
}
@Nullable
@VisibleForTesting
ResourceEntry getResourceEntryFromKey(Context context, String resourceName) {
Bundle resourceEntryBundle = resultBundle.getBundle(resourceName);
Bundle fallbackBundle = resultBundle.getBundle(KEY_FALLBACK_CONFIG);
if (fallbackBundle != null) {
resourceEntryBundle.putBundle(KEY_FALLBACK_CONFIG, fallbackBundle.getBundle(resourceName));
}
ResourceEntry resourceEntry = ResourceEntry.fromBundle(context, resourceEntryBundle);
if (BuildCompatUtils.isAtLeastU() && isActivityEmbedded(context)) {
resourceEntry = adjustEmbeddedActivityResourceEntryDefaultValue(context, resourceEntry);
} else if (BuildCompatUtils.isAtLeastT() && shouldApplyMaterialYouStyle(context)) {
resourceEntry = adjustMaterialYouResourceEntryDefaultValue(context, resourceEntry);
}
return adjustResourceEntryDayNightMode(context, resourceEntry);
}
@VisibleForTesting
boolean isActivityEmbedded(Context context) {
Activity activity;
try {
activity = lookupActivityFromContext(context);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Not a Activity instance in parent tree");
return false;
}
return isEmbeddedActivityOnePaneEnabled(context)
&& ActivityEmbeddingController.getInstance(activity).isActivityEmbedded(activity);
}
public static Activity lookupActivityFromContext(Context context) {
if (context instanceof Activity) {
return (Activity) context;
} else if (context instanceof ContextWrapper) {
return lookupActivityFromContext(((ContextWrapper) context).getBaseContext());
} else {
throw new IllegalArgumentException("Cannot find instance of Activity in parent tree");
}
}
/**
* Force to day mode if setup wizard does not support day/night mode and current system is in
* night mode.
*/
private static ResourceEntry adjustResourceEntryDayNightMode(
Context context, ResourceEntry resourceEntry) {
Resources resource = resourceEntry.getResources();
Configuration configuration = resource.getConfiguration();
if (!isSetupWizardDayNightEnabled(context) && Util.isNightMode(configuration)) {
if (resourceEntry == null) {
Log.w(TAG, "resourceEntry is null, skip to force day mode.");
return resourceEntry;
}
configuration.uiMode =
Configuration.UI_MODE_NIGHT_NO
| (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
resource.updateConfiguration(configuration, resource.getDisplayMetrics());
}
return resourceEntry;
}
// Check the MNStyle flag and replace the inputResourceEntry.resourceName &
// inputResourceEntry.resourceId after T, that means if using Gliv4 before S, will always use
// glifv3 resources.
ResourceEntry adjustMaterialYouResourceEntryDefaultValue(
Context context, ResourceEntry inputResourceEntry) {
// If not overlay resource
try {
if (Objects.equals(inputResourceEntry.getPackageName(), SUW_PACKAGE_NAME)) {
String resourceTypeName =
inputResourceEntry
.getResources()
.getResourceTypeName(inputResourceEntry.getResourceId());
// try to update resourceName & resourceId
String materialYouResourceName =
inputResourceEntry.getResourceName().concat(MATERIAL_YOU_RESOURCE_SUFFIX);
int materialYouResourceId =
inputResourceEntry
.getResources()
.getIdentifier(
materialYouResourceName, resourceTypeName, inputResourceEntry.getPackageName());
if (materialYouResourceId != 0) {
Log.i(TAG, "use material you resource:" + materialYouResourceName);
return new ResourceEntry(
inputResourceEntry.getPackageName(),
materialYouResourceName,
materialYouResourceId,
inputResourceEntry.getResources());
}
}
} catch (NotFoundException ex) {
// fall through
}
return inputResourceEntry;
}
// Check the embedded activity flag and replace the inputResourceEntry.resourceName &
// inputResourceEntry.resourceId, and try to find the embedded resource from the different
// package.
ResourceEntry adjustEmbeddedActivityResourceEntryDefaultValue(
Context context, ResourceEntry inputResourceEntry) {
// If not overlay resource
try {
String resourceTypeName =
inputResourceEntry.getResources().getResourceTypeName(inputResourceEntry.getResourceId());
// For the first time to get embedded activity resource id, it may get from setup wizard
// package or Overlay package.
String embeddedActivityResourceName =
inputResourceEntry.getResourceName().concat(EMBEDDED_ACTIVITY_RESOURCE_SUFFIX);
int embeddedActivityResourceId =
inputResourceEntry
.getResources()
.getIdentifier(
embeddedActivityResourceName,
resourceTypeName,
inputResourceEntry.getPackageName());
if (embeddedActivityResourceId != 0) {
Log.i(TAG, "use embedded activity resource:" + embeddedActivityResourceName);
return new ResourceEntry(
inputResourceEntry.getPackageName(),
embeddedActivityResourceName,
embeddedActivityResourceId,
inputResourceEntry.getResources());
} else {
// If resource id is not available from the Overlay package, try to get it from setup wizard
// package.
PackageManager manager = context.getPackageManager();
Resources resources = manager.getResourcesForApplication(SUW_PACKAGE_NAME);
embeddedActivityResourceId =
resources.getIdentifier(
embeddedActivityResourceName, resourceTypeName, SUW_PACKAGE_NAME);
if (embeddedActivityResourceId != 0) {
return new ResourceEntry(
SUW_PACKAGE_NAME,
embeddedActivityResourceName,
embeddedActivityResourceId,
resources);
}
}
} catch (NotFoundException | NameNotFoundException ex) {
// fall through
}
return inputResourceEntry;
}
@VisibleForTesting
public static synchronized void resetInstance() {
instance = null;
suwDayNightEnabledBundle = null;
applyExtendedPartnerConfigBundle = null;
applyMaterialYouConfigBundle = null;
applyDynamicColorBundle = null;
applyFullDynamicColorBundle = null;
applyNeutralButtonStyleBundle = null;
applyEmbeddedActivityOnePaneBundle = null;
suwDefaultThemeBundle = null;
applyTransitionBundle = null;
}
/**
* Checks whether SetupWizard supports the DayNight theme during setup flow; if return false setup
* flow should force to light theme.
*
* <p>Returns true if the setupwizard is listening to system DayNight theme setting.
*/
public static boolean isSetupWizardDayNightEnabled(@NonNull Context context) {
if (suwDayNightEnabledBundle == null) {
try {
suwDayNightEnabledBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_SUW_DAY_NIGHT_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "SetupWizard DayNight supporting status unknown; return as false.");
suwDayNightEnabledBundle = null;
return false;
}
}
return (suwDayNightEnabledBundle != null
&& suwDayNightEnabledBundle.getBoolean(IS_SUW_DAY_NIGHT_ENABLED_METHOD, false));
}
/** Returns true if the SetupWizard supports the extended partner configs during setup flow. */
public static boolean shouldApplyExtendedPartnerConfig(@NonNull Context context) {
if (applyExtendedPartnerConfigBundle == null) {
try {
applyExtendedPartnerConfigBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(
TAG,
"SetupWizard extended partner configs supporting status unknown; return as false.");
applyExtendedPartnerConfigBundle = null;
return false;
}
}
return (applyExtendedPartnerConfigBundle != null
&& applyExtendedPartnerConfigBundle.getBoolean(
IS_EXTENDED_PARTNER_CONFIG_ENABLED_METHOD, false));
}
/**
* Returns true if the SetupWizard is flow enabled "Material You(Glifv4)" style, or the result of
* shouldApplyExtendedPartnerConfig() in SDK S as fallback.
*/
public static boolean shouldApplyMaterialYouStyle(@NonNull Context context) {
if (applyMaterialYouConfigBundle == null || applyMaterialYouConfigBundle.isEmpty()) {
try {
applyMaterialYouConfigBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_MATERIAL_YOU_STYLE_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
// The suw version did not support the flag yet, fallback to
// shouldApplyExtendedPartnerConfig() for SDK S.
if (applyMaterialYouConfigBundle != null
&& applyMaterialYouConfigBundle.isEmpty()
&& !BuildCompatUtils.isAtLeastT()) {
return shouldApplyExtendedPartnerConfig(context);
}
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "SetupWizard Material You configs supporting status unknown; return as false.");
applyMaterialYouConfigBundle = null;
return false;
}
}
return (applyMaterialYouConfigBundle != null
&& applyMaterialYouConfigBundle.getBoolean(IS_MATERIAL_YOU_STYLE_ENABLED_METHOD, false));
}
/**
* Returns default glif theme name string from setupwizard, or if the setupwizard has not
* supported this api, return a null string.
*/
@Nullable
public static String getSuwDefaultThemeString(@NonNull Context context) {
if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
try {
suwDefaultThemeBundle =
context
.getContentResolver()
.call(
getContentUri(),
GET_SUW_DEFAULT_THEME_STRING_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "SetupWizard default theme status unknown; return as null.");
suwDefaultThemeBundle = null;
return null;
}
}
if (suwDefaultThemeBundle == null || suwDefaultThemeBundle.isEmpty()) {
return null;
}
return suwDefaultThemeBundle.getString(GET_SUW_DEFAULT_THEME_STRING_METHOD);
}
/** Returns true if the SetupWizard supports the dynamic color during setup flow. */
public static boolean isSetupWizardDynamicColorEnabled(@NonNull Context context) {
if (applyDynamicColorBundle == null) {
try {
applyDynamicColorBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_DYNAMIC_COLOR_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "SetupWizard dynamic color supporting status unknown; return as false.");
applyDynamicColorBundle = null;
return false;
}
}
return (applyDynamicColorBundle != null
&& applyDynamicColorBundle.getBoolean(IS_DYNAMIC_COLOR_ENABLED_METHOD, false));
}
/** Returns {@code true} if the SetupWizard supports the full dynamic color during setup flow. */
public static boolean isSetupWizardFullDynamicColorEnabled(@NonNull Context context) {
if (applyFullDynamicColorBundle == null) {
try {
applyFullDynamicColorBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "SetupWizard full dynamic color supporting status unknown; return as false.");
applyFullDynamicColorBundle = null;
return false;
}
}
return (applyFullDynamicColorBundle != null
&& applyFullDynamicColorBundle.getBoolean(IS_FULL_DYNAMIC_COLOR_ENABLED_METHOD, false));
}
/** Returns true if the SetupWizard supports the one-pane embedded activity during setup flow. */
public static boolean isEmbeddedActivityOnePaneEnabled(@NonNull Context context) {
if (applyEmbeddedActivityOnePaneBundle == null) {
try {
applyEmbeddedActivityOnePaneBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(
TAG,
"SetupWizard one-pane support in embedded activity status unknown; return as false.");
applyEmbeddedActivityOnePaneBundle = null;
return false;
}
}
return (applyEmbeddedActivityOnePaneBundle != null
&& applyEmbeddedActivityOnePaneBundle.getBoolean(
IS_EMBEDDED_ACTIVITY_ONE_PANE_ENABLED_METHOD, false));
}
/** Returns true if the SetupWizard supports the neutral button style during setup flow. */
public static boolean isNeutralButtonStyleEnabled(@NonNull Context context) {
if (applyNeutralButtonStyleBundle == null) {
try {
applyNeutralButtonStyleBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "Neutral button style supporting status unknown; return as false.");
applyNeutralButtonStyleBundle = null;
return false;
}
}
return (applyNeutralButtonStyleBundle != null
&& applyNeutralButtonStyleBundle.getBoolean(IS_NEUTRAL_BUTTON_STYLE_ENABLED_METHOD, false));
}
/** Returns true if the SetupWizard supports the font weight customization during setup flow. */
public static boolean isFontWeightEnabled(@NonNull Context context) {
if (applyFontWeightBundle == null) {
try {
applyFontWeightBundle =
context
.getContentResolver()
.call(
getContentUri(),
IS_FONT_WEIGHT_ENABLED_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(TAG, "Font weight supporting status unknown; return as false.");
applyFontWeightBundle = null;
return false;
}
}
return (applyFontWeightBundle != null
&& applyFontWeightBundle.getBoolean(IS_FONT_WEIGHT_ENABLED_METHOD, true));
}
/**
* Returns the system property to indicate the transition settings is set by Glif theme rather
* than the client.
*/
public static boolean isGlifThemeControlledTransitionApplied(@NonNull Context context) {
if (applyTransitionBundle == null
|| applyTransitionBundle.isEmpty()) {
try {
applyTransitionBundle =
context
.getContentResolver()
.call(
getContentUri(),
APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD,
/* arg= */ null,
/* extras= */ null);
} catch (IllegalArgumentException | SecurityException exception) {
Log.w(
TAG,
"applyGlifThemeControlledTransition unknown; return applyGlifThemeControlledTransition"
+ " as default value");
}
}
if (applyTransitionBundle != null
&& !applyTransitionBundle.isEmpty()) {
return applyTransitionBundle.getBoolean(
APPLY_GLIF_THEME_CONTROLLED_TRANSITION_METHOD, true);
}
return true;
}
@VisibleForTesting
static Uri getContentUri() {
return new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(SUW_AUTHORITY)
.build();
}
private static TypedValue getTypedValueFromResource(Resources resource, int resId, int type) {
TypedValue value = new TypedValue();
resource.getValue(resId, value, true);
if (value.type != type) {
throw new NotFoundException(
"Resource ID #0x"
+ Integer.toHexString(resId)
+ " type #0x"
+ Integer.toHexString(value.type)
+ " is not valid");
}
return value;
}
private static float getDimensionFromTypedValue(Context context, TypedValue value) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
return value.getDimension(displayMetrics);
}
private static void registerContentObserver(Context context) {
if (isSetupWizardDayNightEnabled(context)) {
if (contentObserver != null) {
unregisterContentObserver(context);
}
Uri contentUri = getContentUri();
try {
contentObserver =
new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
resetInstance();
}
};
context
.getContentResolver()
.registerContentObserver(contentUri, /* notifyForDescendants= */ true, contentObserver);
} catch (SecurityException | NullPointerException | IllegalArgumentException e) {
Log.w(TAG, "Failed to register content observer for " + contentUri + ": " + e);
}
}
}
private static void unregisterContentObserver(Context context) {
try {
context.getContentResolver().unregisterContentObserver(contentObserver);
contentObserver = null;
} catch (SecurityException | NullPointerException | IllegalArgumentException e) {
Log.w(TAG, "Failed to unregister content observer: " + e);
}
}
}