• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app;
18 
19 import static android.app.ActivityThread.DEBUG_CONFIGURATION;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.ComponentCallbacks2;
24 import android.content.Context;
25 import android.content.pm.ActivityInfo;
26 import android.content.res.CompatibilityInfo;
27 import android.content.res.Configuration;
28 import android.content.res.Resources;
29 import android.graphics.Bitmap;
30 import android.graphics.Canvas;
31 import android.graphics.HardwareRenderer;
32 import android.inputmethodservice.InputMethodService;
33 import android.os.Build;
34 import android.os.LocaleList;
35 import android.os.Trace;
36 import android.util.DisplayMetrics;
37 import android.util.Log;
38 import android.util.Slog;
39 import android.view.ContextThemeWrapper;
40 import android.view.WindowManagerGlobal;
41 
42 import com.android.internal.annotations.GuardedBy;
43 
44 import java.util.ArrayList;
45 import java.util.Locale;
46 
47 /**
48  * A client side controller to handle process level configuration changes.
49  * @hide
50  */
51 class ConfigurationController {
52     private static final String TAG = "ConfigurationController";
53 
54     private final ActivityThreadInternal mActivityThread;
55 
56     private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
57 
58     @GuardedBy("mResourcesManager")
59     private @Nullable Configuration mPendingConfiguration;
60     private @Nullable Configuration mCompatConfiguration;
61     private @Nullable Configuration mConfiguration;
62 
ConfigurationController(@onNull ActivityThreadInternal activityThread)63     ConfigurationController(@NonNull ActivityThreadInternal activityThread) {
64         mActivityThread = activityThread;
65     }
66 
67     /** Update the pending configuration. */
updatePendingConfiguration(@onNull Configuration config)68     Configuration updatePendingConfiguration(@NonNull Configuration config) {
69         synchronized (mResourcesManager) {
70             if (mPendingConfiguration == null || mPendingConfiguration.isOtherSeqNewer(config)) {
71                 mPendingConfiguration = config;
72                 return mPendingConfiguration;
73             }
74         }
75         return null;
76     }
77 
78     /** Get the pending configuration. */
getPendingConfiguration(boolean clearPending)79     Configuration getPendingConfiguration(boolean clearPending) {
80         Configuration outConfig = null;
81         synchronized (mResourcesManager) {
82             if (mPendingConfiguration != null) {
83                 outConfig = mPendingConfiguration;
84                 if (clearPending) {
85                     mPendingConfiguration = null;
86                 }
87             }
88         }
89         return outConfig;
90     }
91 
92     /** Set the compatibility configuration. */
setCompatConfiguration(@onNull Configuration config)93     void setCompatConfiguration(@NonNull Configuration config) {
94         mCompatConfiguration = new Configuration(config);
95     }
96 
97     /** Get the compatibility configuration. */
getCompatConfiguration()98     Configuration getCompatConfiguration() {
99         return mCompatConfiguration;
100     }
101 
102     /** Apply the global compatibility configuration. */
applyCompatConfiguration()103     final Configuration applyCompatConfiguration() {
104         Configuration config = mConfiguration;
105         final int displayDensity = config.densityDpi;
106         if (mCompatConfiguration == null) {
107             mCompatConfiguration = new Configuration();
108         }
109         mCompatConfiguration.setTo(mConfiguration);
110         if (mResourcesManager.applyCompatConfiguration(displayDensity, mCompatConfiguration)) {
111             config = mCompatConfiguration;
112         }
113         return config;
114     }
115 
116     /** Set the configuration. */
setConfiguration(@onNull Configuration config)117     void setConfiguration(@NonNull Configuration config) {
118         mConfiguration = new Configuration(config);
119     }
120 
121     /** Get current configuration. */
getConfiguration()122     Configuration getConfiguration() {
123         return mConfiguration;
124     }
125 
126     /**
127      * Update the configuration to latest.
128      * @param config The new configuration.
129      */
handleConfigurationChanged(@onNull Configuration config)130     void handleConfigurationChanged(@NonNull Configuration config) {
131         if (mActivityThread.isCachedProcessState()) {
132             updatePendingConfiguration(config);
133             // If the process is in a cached state, delay the handling until the process is no
134             // longer cached.
135             return;
136         }
137         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged");
138         handleConfigurationChanged(config, null /* compat */);
139         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
140     }
141 
142     /**
143      * Update the configuration to latest.
144      * @param compat The new compatibility information.
145      */
handleConfigurationChanged(@onNull CompatibilityInfo compat)146     void handleConfigurationChanged(@NonNull CompatibilityInfo compat) {
147         handleConfigurationChanged(mConfiguration, compat);
148         WindowManagerGlobal.getInstance().reportNewConfiguration(mConfiguration);
149     }
150 
151     /**
152      * Update the configuration to latest.
153      * @param config The new configuration.
154      * @param compat The new compatibility information.
155      */
handleConfigurationChanged(@ullable Configuration config, @Nullable CompatibilityInfo compat)156     void handleConfigurationChanged(@Nullable Configuration config,
157             @Nullable CompatibilityInfo compat) {
158         int configDiff;
159         boolean equivalent;
160 
161         synchronized (mResourcesManager) {
162             final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme();
163             final Resources.Theme systemUiTheme = mActivityThread.getSystemUiContext().getTheme();
164             if (mPendingConfiguration != null) {
165                 if (!mPendingConfiguration.isOtherSeqNewer(config)) {
166                     config = mPendingConfiguration;
167                     updateDefaultDensity(config.densityDpi);
168                 }
169                 mPendingConfiguration = null;
170             }
171 
172             final boolean hasIme = mActivityThread.hasImeComponent();
173             if (config == null) {
174                 // TODO (b/135719017): Temporary log for debugging IME service.
175                 if (Build.IS_DEBUGGABLE && hasIme) {
176                     Log.w(TAG, "handleConfigurationChanged for IME app but config is null");
177                 }
178                 return;
179             }
180 
181             // This flag tracks whether the new configuration is fundamentally equivalent to the
182             // existing configuration. This is necessary to determine whether non-activity callbacks
183             // should receive notice when the only changes are related to non-public fields.
184             // We do not gate calling {@link #performActivityConfigurationChanged} based on this
185             // flag as that method uses the same check on the activity config override as well.
186             equivalent = mConfiguration != null && (0 == mConfiguration.diffPublicOnly(config));
187 
188             if (DEBUG_CONFIGURATION) {
189                 Slog.v(TAG, "Handle configuration changed: " + config);
190             }
191 
192             final Application app = mActivityThread.getApplication();
193             final Resources appResources = app.getResources();
194             if (appResources.hasOverrideDisplayAdjustments()) {
195                 // The value of Display#getRealSize will be adjusted by FixedRotationAdjustments,
196                 // but Display#getSize refers to DisplayAdjustments#mConfiguration. So the rotated
197                 // configuration also needs to set to the adjustments for consistency.
198                 appResources.getDisplayAdjustments().getConfiguration().updateFrom(config);
199             }
200             mResourcesManager.applyConfigurationToResources(config, compat,
201                     appResources.getDisplayAdjustments());
202             updateLocaleListFromAppContext(app.getApplicationContext());
203 
204             if (mConfiguration == null) {
205                 mConfiguration = new Configuration();
206             }
207             if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
208                 // TODO (b/135719017): Temporary log for debugging IME service.
209                 if (Build.IS_DEBUGGABLE && hasIme) {
210                     Log.w(TAG, "handleConfigurationChanged for IME app but config seq is obsolete "
211                             + ", config=" + config
212                             + ", mConfiguration=" + mConfiguration);
213                 }
214                 return;
215             }
216 
217             configDiff = mConfiguration.updateFrom(config);
218             config = applyCompatConfiguration();
219             HardwareRenderer.sendDeviceConfigurationForDebugging(config);
220 
221             if ((systemTheme.getChangingConfigurations() & configDiff) != 0) {
222                 systemTheme.rebase();
223             }
224 
225             if ((systemUiTheme.getChangingConfigurations() & configDiff) != 0) {
226                 systemUiTheme.rebase();
227             }
228         }
229 
230         final ArrayList<ComponentCallbacks2> callbacks =
231                 mActivityThread.collectComponentCallbacks(false /* includeActivities */);
232 
233         freeTextLayoutCachesIfNeeded(configDiff);
234 
235         if (callbacks != null) {
236             final int size = callbacks.size();
237             for (int i = 0; i < size; i++) {
238                 ComponentCallbacks2 cb = callbacks.get(i);
239                 if (!equivalent) {
240                     performConfigurationChanged(cb, config);
241                 } else {
242                     // TODO (b/135719017): Temporary log for debugging IME service.
243                     if (Build.IS_DEBUGGABLE && cb instanceof InputMethodService) {
244                         Log.w(TAG, "performConfigurationChanged didn't callback to IME "
245                                 + ", configDiff=" + configDiff
246                                 + ", mConfiguration=" + mConfiguration);
247                     }
248                 }
249             }
250         }
251     }
252 
253     /**
254      * Decides whether to update a component's configuration and whether to inform it.
255      * @param cb The component callback to notify of configuration change.
256      * @param newConfig The new configuration.
257      */
performConfigurationChanged(@onNull ComponentCallbacks2 cb, @NonNull Configuration newConfig)258     void performConfigurationChanged(@NonNull ComponentCallbacks2 cb,
259             @NonNull Configuration newConfig) {
260         // ContextThemeWrappers may override the configuration for that context. We must check and
261         // apply any overrides defined.
262         Configuration contextThemeWrapperOverrideConfig = null;
263         if (cb instanceof ContextThemeWrapper) {
264             final ContextThemeWrapper contextThemeWrapper = (ContextThemeWrapper) cb;
265             contextThemeWrapperOverrideConfig = contextThemeWrapper.getOverrideConfiguration();
266         }
267 
268         // Apply the ContextThemeWrapper override if necessary.
269         // NOTE: Make sure the configurations are not modified, as they are treated as immutable
270         // in many places.
271         final Configuration configToReport = createNewConfigAndUpdateIfNotNull(
272                 newConfig, contextThemeWrapperOverrideConfig);
273         cb.onConfigurationChanged(configToReport);
274     }
275 
276     /** Update default density. */
updateDefaultDensity(int densityDpi)277     void updateDefaultDensity(int densityDpi) {
278         if (!mActivityThread.isInDensityCompatMode()
279                 && densityDpi != Configuration.DENSITY_DPI_UNDEFINED
280                 && densityDpi != DisplayMetrics.DENSITY_DEVICE) {
281             DisplayMetrics.DENSITY_DEVICE = densityDpi;
282             Bitmap.setDefaultDensity(densityDpi);
283         }
284     }
285 
286     /** Get current default display dpi. This is only done to maintain @UnsupportedAppUsage. */
getCurDefaultDisplayDpi()287     int getCurDefaultDisplayDpi() {
288         return mConfiguration.densityDpi;
289     }
290 
291     /**
292      * The LocaleList set for the app's resources may have been shuffled so that the preferred
293      * Locale is at position 0. We must find the index of this preferred Locale in the
294      * original LocaleList.
295      */
updateLocaleListFromAppContext(@onNull Context context)296     void updateLocaleListFromAppContext(@NonNull Context context) {
297         final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0);
298         final LocaleList newLocaleList = mResourcesManager.getConfiguration().getLocales();
299         final int newLocaleListSize = newLocaleList.size();
300         for (int i = 0; i < newLocaleListSize; i++) {
301             if (bestLocale.equals(newLocaleList.get(i))) {
302                 LocaleList.setDefault(newLocaleList, i);
303                 return;
304             }
305         }
306 
307         // The app may have overridden the LocaleList with its own Locale
308         // (not present in the available list). Push the chosen Locale
309         // to the front of the list.
310         LocaleList.setDefault(new LocaleList(bestLocale, newLocaleList));
311     }
312 
313     /**
314      * Creates a new Configuration only if override would modify base. Otherwise returns base.
315      * @param base The base configuration.
316      * @param override The update to apply to the base configuration. Can be null.
317      * @return A Configuration representing base with override applied.
318      */
createNewConfigAndUpdateIfNotNull(@onNull Configuration base, @Nullable Configuration override)319     static Configuration createNewConfigAndUpdateIfNotNull(@NonNull Configuration base,
320             @Nullable Configuration override) {
321         if (override == null) {
322             return base;
323         }
324         Configuration newConfig = new Configuration(base);
325         newConfig.updateFrom(override);
326         return newConfig;
327     }
328 
329     /** Ask test layout engine to free its caches if there is a locale change. */
freeTextLayoutCachesIfNeeded(int configDiff)330     static void freeTextLayoutCachesIfNeeded(int configDiff) {
331         if (configDiff != 0) {
332             boolean hasLocaleConfigChange = ((configDiff & ActivityInfo.CONFIG_LOCALE) != 0);
333             if (hasLocaleConfigChange) {
334                 Canvas.freeTextLayoutCaches();
335                 if (DEBUG_CONFIGURATION) {
336                     Slog.v(TAG, "Cleared TextLayout Caches");
337                 }
338             }
339         }
340     }
341 }
342