blob: f79e0780ecae25c1f29ba73c3a778df5251c71b2 [file] [log] [blame]
/*
* 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 android.app;
import static android.app.ActivityThread.DEBUG_CONFIGURATION;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentCallbacks2;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.HardwareRenderer;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.LocaleList;
import android.os.Trace;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Slog;
import android.view.ContextThemeWrapper;
import android.view.WindowManagerGlobal;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
import java.util.Locale;
/**
* A client side controller to handle process level configuration changes.
* @hide
*/
class ConfigurationController {
private static final String TAG = "ConfigurationController";
private final ActivityThreadInternal mActivityThread;
private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
@GuardedBy("mResourcesManager")
private @Nullable Configuration mPendingConfiguration;
private @Nullable Configuration mCompatConfiguration;
private @Nullable Configuration mConfiguration;
ConfigurationController(@NonNull ActivityThreadInternal activityThread) {
mActivityThread = activityThread;
}
/** Update the pending configuration. */
Configuration updatePendingConfiguration(@NonNull Configuration config) {
synchronized (mResourcesManager) {
if (mPendingConfiguration == null || mPendingConfiguration.isOtherSeqNewer(config)) {
mPendingConfiguration = config;
return mPendingConfiguration;
}
}
return null;
}
/** Get the pending configuration. */
Configuration getPendingConfiguration(boolean clearPending) {
Configuration outConfig = null;
synchronized (mResourcesManager) {
if (mPendingConfiguration != null) {
outConfig = mPendingConfiguration;
if (clearPending) {
mPendingConfiguration = null;
}
}
}
return outConfig;
}
/** Set the compatibility configuration. */
void setCompatConfiguration(@NonNull Configuration config) {
mCompatConfiguration = new Configuration(config);
}
/** Get the compatibility configuration. */
Configuration getCompatConfiguration() {
return mCompatConfiguration;
}
/** Apply the global compatibility configuration. */
final Configuration applyCompatConfiguration() {
Configuration config = mConfiguration;
final int displayDensity = config.densityDpi;
if (mCompatConfiguration == null) {
mCompatConfiguration = new Configuration();
}
mCompatConfiguration.setTo(mConfiguration);
if (mResourcesManager.applyCompatConfiguration(displayDensity, mCompatConfiguration)) {
config = mCompatConfiguration;
}
return config;
}
/** Set the configuration. */
void setConfiguration(@NonNull Configuration config) {
mConfiguration = new Configuration(config);
}
/** Get current configuration. */
Configuration getConfiguration() {
return mConfiguration;
}
/**
* Update the configuration to latest.
* @param config The new configuration.
*/
void handleConfigurationChanged(@NonNull Configuration config) {
if (mActivityThread.isCachedProcessState()) {
updatePendingConfiguration(config);
// If the process is in a cached state, delay the handling until the process is no
// longer cached.
return;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged");
handleConfigurationChanged(config, null /* compat */);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
/**
* Update the configuration to latest.
* @param compat The new compatibility information.
*/
void handleConfigurationChanged(@NonNull CompatibilityInfo compat) {
handleConfigurationChanged(mConfiguration, compat);
WindowManagerGlobal.getInstance().reportNewConfiguration(mConfiguration);
}
/**
* Update the configuration to latest.
* @param config The new configuration.
* @param compat The new compatibility information.
*/
void handleConfigurationChanged(@Nullable Configuration config,
@Nullable CompatibilityInfo compat) {
int configDiff;
boolean equivalent;
synchronized (mResourcesManager) {
final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme();
final Resources.Theme systemUiTheme = mActivityThread.getSystemUiContext().getTheme();
if (mPendingConfiguration != null) {
if (!mPendingConfiguration.isOtherSeqNewer(config)) {
config = mPendingConfiguration;
updateDefaultDensity(config.densityDpi);
}
mPendingConfiguration = null;
}
final boolean hasIme = mActivityThread.hasImeComponent();
if (config == null) {
// TODO (b/135719017): Temporary log for debugging IME service.
if (Build.IS_DEBUGGABLE && hasIme) {
Log.w(TAG, "handleConfigurationChanged for IME app but config is null");
}
return;
}
// This flag tracks whether the new configuration is fundamentally equivalent to the
// existing configuration. This is necessary to determine whether non-activity callbacks
// should receive notice when the only changes are related to non-public fields.
// We do not gate calling {@link #performActivityConfigurationChanged} based on this
// flag as that method uses the same check on the activity config override as well.
equivalent = mConfiguration != null && (0 == mConfiguration.diffPublicOnly(config));
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Handle configuration changed: " + config);
}
final Application app = mActivityThread.getApplication();
final Resources appResources = app.getResources();
if (appResources.hasOverrideDisplayAdjustments()) {
// The value of Display#getRealSize will be adjusted by FixedRotationAdjustments,
// but Display#getSize refers to DisplayAdjustments#mConfiguration. So the rotated
// configuration also needs to set to the adjustments for consistency.
appResources.getDisplayAdjustments().getConfiguration().updateFrom(config);
}
mResourcesManager.applyConfigurationToResources(config, compat,
appResources.getDisplayAdjustments());
updateLocaleListFromAppContext(app.getApplicationContext());
if (mConfiguration == null) {
mConfiguration = new Configuration();
}
if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
// TODO (b/135719017): Temporary log for debugging IME service.
if (Build.IS_DEBUGGABLE && hasIme) {
Log.w(TAG, "handleConfigurationChanged for IME app but config seq is obsolete "
+ ", config=" + config
+ ", mConfiguration=" + mConfiguration);
}
return;
}
configDiff = mConfiguration.updateFrom(config);
config = applyCompatConfiguration();
HardwareRenderer.sendDeviceConfigurationForDebugging(config);
if ((systemTheme.getChangingConfigurations() & configDiff) != 0) {
systemTheme.rebase();
}
if ((systemUiTheme.getChangingConfigurations() & configDiff) != 0) {
systemUiTheme.rebase();
}
}
final ArrayList<ComponentCallbacks2> callbacks =
mActivityThread.collectComponentCallbacks(false /* includeActivities */);
freeTextLayoutCachesIfNeeded(configDiff);
if (callbacks != null) {
final int size = callbacks.size();
for (int i = 0; i < size; i++) {
ComponentCallbacks2 cb = callbacks.get(i);
if (!equivalent) {
performConfigurationChanged(cb, config);
} else {
// TODO (b/135719017): Temporary log for debugging IME service.
if (Build.IS_DEBUGGABLE && cb instanceof InputMethodService) {
Log.w(TAG, "performConfigurationChanged didn't callback to IME "
+ ", configDiff=" + configDiff
+ ", mConfiguration=" + mConfiguration);
}
}
}
}
}
/**
* Decides whether to update a component's configuration and whether to inform it.
* @param cb The component callback to notify of configuration change.
* @param newConfig The new configuration.
*/
void performConfigurationChanged(@NonNull ComponentCallbacks2 cb,
@NonNull Configuration newConfig) {
// ContextThemeWrappers may override the configuration for that context. We must check and
// apply any overrides defined.
Configuration contextThemeWrapperOverrideConfig = null;
if (cb instanceof ContextThemeWrapper) {
final ContextThemeWrapper contextThemeWrapper = (ContextThemeWrapper) cb;
contextThemeWrapperOverrideConfig = contextThemeWrapper.getOverrideConfiguration();
}
// Apply the ContextThemeWrapper override if necessary.
// NOTE: Make sure the configurations are not modified, as they are treated as immutable
// in many places.
final Configuration configToReport = createNewConfigAndUpdateIfNotNull(
newConfig, contextThemeWrapperOverrideConfig);
cb.onConfigurationChanged(configToReport);
}
/** Update default density. */
void updateDefaultDensity(int densityDpi) {
if (!mActivityThread.isInDensityCompatMode()
&& densityDpi != Configuration.DENSITY_DPI_UNDEFINED
&& densityDpi != DisplayMetrics.DENSITY_DEVICE) {
DisplayMetrics.DENSITY_DEVICE = densityDpi;
Bitmap.setDefaultDensity(densityDpi);
}
}
/** Get current default display dpi. This is only done to maintain @UnsupportedAppUsage. */
int getCurDefaultDisplayDpi() {
return mConfiguration.densityDpi;
}
/**
* The LocaleList set for the app's resources may have been shuffled so that the preferred
* Locale is at position 0. We must find the index of this preferred Locale in the
* original LocaleList.
*/
void updateLocaleListFromAppContext(@NonNull Context context) {
final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0);
final LocaleList newLocaleList = mResourcesManager.getConfiguration().getLocales();
final int newLocaleListSize = newLocaleList.size();
for (int i = 0; i < newLocaleListSize; i++) {
if (bestLocale.equals(newLocaleList.get(i))) {
LocaleList.setDefault(newLocaleList, i);
return;
}
}
// The app may have overridden the LocaleList with its own Locale
// (not present in the available list). Push the chosen Locale
// to the front of the list.
LocaleList.setDefault(new LocaleList(bestLocale, newLocaleList));
}
/**
* Creates a new Configuration only if override would modify base. Otherwise returns base.
* @param base The base configuration.
* @param override The update to apply to the base configuration. Can be null.
* @return A Configuration representing base with override applied.
*/
static Configuration createNewConfigAndUpdateIfNotNull(@NonNull Configuration base,
@Nullable Configuration override) {
if (override == null) {
return base;
}
Configuration newConfig = new Configuration(base);
newConfig.updateFrom(override);
return newConfig;
}
/** Ask test layout engine to free its caches if there is a locale change. */
static void freeTextLayoutCachesIfNeeded(int configDiff) {
if (configDiff != 0) {
boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0);
if (hasLocaleConfigChange) {
Canvas.freeTextLayoutCaches();
if (DEBUG_CONFIGURATION) {
Slog.v(TAG, "Cleared TextLayout Caches");
}
}
}
}
}