| /* |
| * 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 static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.app.servertransaction.ClientTransactionListenerController; |
| import android.content.ComponentCallbacks2; |
| import android.content.Context; |
| import android.content.res.CompatibilityInfo; |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.graphics.Bitmap; |
| import android.graphics.HardwareRenderer; |
| import android.os.LocaleList; |
| import android.os.Trace; |
| import android.util.DisplayMetrics; |
| 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) { |
| 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) { |
| final ClientTransactionListenerController controller = |
| ClientTransactionListenerController.getInstance(); |
| final Context contextToUpdate = ActivityThread.currentApplication(); |
| controller.onContextConfigurationPreChanged(contextToUpdate); |
| try { |
| handleConfigurationChangedInner(config, compat); |
| } finally { |
| controller.onContextConfigurationPostChanged(contextToUpdate); |
| } |
| } |
| |
| /** |
| * Update the configuration to latest. |
| * @param config The new configuration. |
| * @param compat The new compatibility information. |
| */ |
| private void handleConfigurationChangedInner(@Nullable Configuration config, |
| @Nullable CompatibilityInfo compat) { |
| int configDiff; |
| boolean equivalent; |
| |
| // Get theme outside of synchronization to avoid nested lock. |
| final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme(); |
| final ContextImpl systemUiContext = mActivityThread.getSystemUiContextNoCreate(); |
| final Resources.Theme systemUiTheme = |
| systemUiContext != null ? systemUiContext.getTheme() : null; |
| synchronized (mResourcesManager) { |
| if (mPendingConfiguration != null) { |
| if (!mPendingConfiguration.isOtherSeqNewer(config)) { |
| config = mPendingConfiguration; |
| updateDefaultDensity(config.densityDpi); |
| } |
| mPendingConfiguration = null; |
| } |
| |
| if (config == 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(); |
| mResourcesManager.applyConfigurationToResources(config, compat); |
| updateLocaleListFromAppContext(app.getApplicationContext()); |
| |
| if (mConfiguration == null) { |
| mConfiguration = new Configuration(); |
| } |
| if (!mConfiguration.isOtherSeqNewer(config) && compat == null) { |
| return; |
| } |
| |
| configDiff = mConfiguration.updateFrom(config); |
| config = applyCompatConfiguration(); |
| HardwareRenderer.sendDeviceConfigurationForDebugging(config); |
| |
| if ((systemTheme.getChangingConfigurations() & configDiff) != 0) { |
| systemTheme.rebase(); |
| } |
| |
| if (systemUiTheme != null |
| && (systemUiTheme.getChangingConfigurations() & configDiff) != 0) { |
| systemUiTheme.rebase(); |
| } |
| } |
| |
| final ArrayList<ComponentCallbacks2> callbacks = |
| mActivityThread.collectComponentCallbacks(false /* includeUiContexts */); |
| |
| 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); |
| } |
| } |
| } |
| } |
| |
| /** |
| * 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; |
| } |
| |
| } |