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 import static android.window.ConfigurationHelper.freeTextLayoutCachesIfNeeded;
21 
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.ComponentCallbacks2;
25 import android.content.Context;
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.HardwareRenderer;
31 import android.os.LocaleList;
32 import android.os.Trace;
33 import android.util.DisplayMetrics;
34 import android.util.Slog;
35 import android.view.ContextThemeWrapper;
36 import android.view.WindowManagerGlobal;
37 
38 import com.android.internal.annotations.GuardedBy;
39 
40 import java.util.ArrayList;
41 import java.util.Locale;
42 
43 /**
44  * A client side controller to handle process level configuration changes.
45  * @hide
46  */
47 class ConfigurationController {
48     private static final String TAG = "ConfigurationController";
49 
50     private final ActivityThreadInternal mActivityThread;
51 
52     private final ResourcesManager mResourcesManager = ResourcesManager.getInstance();
53 
54     @GuardedBy("mResourcesManager")
55     private @Nullable Configuration mPendingConfiguration;
56     private @Nullable Configuration mCompatConfiguration;
57     private @Nullable Configuration mConfiguration;
58 
ConfigurationController(@onNull ActivityThreadInternal activityThread)59     ConfigurationController(@NonNull ActivityThreadInternal activityThread) {
60         mActivityThread = activityThread;
61     }
62 
63     /** Update the pending configuration. */
updatePendingConfiguration(@onNull Configuration config)64     Configuration updatePendingConfiguration(@NonNull Configuration config) {
65         synchronized (mResourcesManager) {
66             if (mPendingConfiguration == null || mPendingConfiguration.isOtherSeqNewer(config)) {
67                 mPendingConfiguration = config;
68                 return mPendingConfiguration;
69             }
70         }
71         return null;
72     }
73 
74     /** Get the pending configuration. */
getPendingConfiguration(boolean clearPending)75     Configuration getPendingConfiguration(boolean clearPending) {
76         Configuration outConfig = null;
77         synchronized (mResourcesManager) {
78             if (mPendingConfiguration != null) {
79                 outConfig = mPendingConfiguration;
80                 if (clearPending) {
81                     mPendingConfiguration = null;
82                 }
83             }
84         }
85         return outConfig;
86     }
87 
88     /** Set the compatibility configuration. */
setCompatConfiguration(@onNull Configuration config)89     void setCompatConfiguration(@NonNull Configuration config) {
90         mCompatConfiguration = new Configuration(config);
91     }
92 
93     /** Get the compatibility configuration. */
getCompatConfiguration()94     Configuration getCompatConfiguration() {
95         return mCompatConfiguration;
96     }
97 
98     /** Apply the global compatibility configuration. */
applyCompatConfiguration()99     final Configuration applyCompatConfiguration() {
100         Configuration config = mConfiguration;
101         final int displayDensity = config.densityDpi;
102         if (mCompatConfiguration == null) {
103             mCompatConfiguration = new Configuration();
104         }
105         mCompatConfiguration.setTo(mConfiguration);
106         if (mResourcesManager.applyCompatConfiguration(displayDensity, mCompatConfiguration)) {
107             config = mCompatConfiguration;
108         }
109         return config;
110     }
111 
112     /** Set the configuration. */
setConfiguration(@onNull Configuration config)113     void setConfiguration(@NonNull Configuration config) {
114         mConfiguration = new Configuration(config);
115     }
116 
117     /** Get current configuration. */
getConfiguration()118     Configuration getConfiguration() {
119         return mConfiguration;
120     }
121 
122     /**
123      * Update the configuration to latest.
124      * @param config The new configuration.
125      */
handleConfigurationChanged(@onNull Configuration config)126     void handleConfigurationChanged(@NonNull Configuration config) {
127         if (mActivityThread.isCachedProcessState()) {
128             updatePendingConfiguration(config);
129             // If the process is in a cached state, delay the handling until the process is no
130             // longer cached.
131             return;
132         }
133         Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "configChanged");
134         handleConfigurationChanged(config, null /* compat */);
135         Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
136     }
137 
138     /**
139      * Update the configuration to latest.
140      * @param compat The new compatibility information.
141      */
handleConfigurationChanged(@onNull CompatibilityInfo compat)142     void handleConfigurationChanged(@NonNull CompatibilityInfo compat) {
143         handleConfigurationChanged(mConfiguration, compat);
144         WindowManagerGlobal.getInstance().reportNewConfiguration(mConfiguration);
145     }
146 
147     /**
148      * Update the configuration to latest.
149      * @param config The new configuration.
150      * @param compat The new compatibility information.
151      */
handleConfigurationChanged(@ullable Configuration config, @Nullable CompatibilityInfo compat)152     void handleConfigurationChanged(@Nullable Configuration config,
153             @Nullable CompatibilityInfo compat) {
154         int configDiff;
155         boolean equivalent;
156 
157         // Get theme outside of synchronization to avoid nested lock.
158         final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme();
159         final ContextImpl systemUiContext = mActivityThread.getSystemUiContextNoCreate();
160         final Resources.Theme systemUiTheme =
161                 systemUiContext != null ? systemUiContext.getTheme() : null;
162         synchronized (mResourcesManager) {
163             if (mPendingConfiguration != null) {
164                 if (!mPendingConfiguration.isOtherSeqNewer(config)) {
165                     config = mPendingConfiguration;
166                     updateDefaultDensity(config.densityDpi);
167                 }
168                 mPendingConfiguration = null;
169             }
170 
171             if (config == null) {
172                 return;
173             }
174 
175             // This flag tracks whether the new configuration is fundamentally equivalent to the
176             // existing configuration. This is necessary to determine whether non-activity callbacks
177             // should receive notice when the only changes are related to non-public fields.
178             // We do not gate calling {@link #performActivityConfigurationChanged} based on this
179             // flag as that method uses the same check on the activity config override as well.
180             equivalent = mConfiguration != null && (0 == mConfiguration.diffPublicOnly(config));
181 
182             if (DEBUG_CONFIGURATION) {
183                 Slog.v(TAG, "Handle configuration changed: " + config);
184             }
185 
186             final Application app = mActivityThread.getApplication();
187             final Resources appResources = app.getResources();
188             if (appResources.hasOverrideDisplayAdjustments()) {
189                 // The value of Display#getRealSize will be adjusted by FixedRotationAdjustments,
190                 // but Display#getSize refers to DisplayAdjustments#mConfiguration. So the rotated
191                 // configuration also needs to set to the adjustments for consistency.
192                 appResources.getDisplayAdjustments().getConfiguration().updateFrom(config);
193             }
194             mResourcesManager.applyConfigurationToResources(config, compat,
195                     appResources.getDisplayAdjustments());
196             updateLocaleListFromAppContext(app.getApplicationContext());
197 
198             if (mConfiguration == null) {
199                 mConfiguration = new Configuration();
200             }
201             if (!mConfiguration.isOtherSeqNewer(config) && compat == null) {
202                 return;
203             }
204 
205             configDiff = mConfiguration.updateFrom(config);
206             config = applyCompatConfiguration();
207             HardwareRenderer.sendDeviceConfigurationForDebugging(config);
208 
209             if ((systemTheme.getChangingConfigurations() & configDiff) != 0) {
210                 systemTheme.rebase();
211             }
212 
213             if (systemUiTheme != null
214                     && (systemUiTheme.getChangingConfigurations() & configDiff) != 0) {
215                 systemUiTheme.rebase();
216             }
217         }
218 
219         final ArrayList<ComponentCallbacks2> callbacks =
220                 mActivityThread.collectComponentCallbacks(false /* includeUiContexts */);
221 
222         freeTextLayoutCachesIfNeeded(configDiff);
223 
224         if (callbacks != null) {
225             final int size = callbacks.size();
226             for (int i = 0; i < size; i++) {
227                 ComponentCallbacks2 cb = callbacks.get(i);
228                 if (!equivalent) {
229                     performConfigurationChanged(cb, config);
230                 }
231             }
232         }
233     }
234 
235     /**
236      * Decides whether to update a component's configuration and whether to inform it.
237      * @param cb The component callback to notify of configuration change.
238      * @param newConfig The new configuration.
239      */
performConfigurationChanged(@onNull ComponentCallbacks2 cb, @NonNull Configuration newConfig)240     void performConfigurationChanged(@NonNull ComponentCallbacks2 cb,
241             @NonNull Configuration newConfig) {
242         // ContextThemeWrappers may override the configuration for that context. We must check and
243         // apply any overrides defined.
244         Configuration contextThemeWrapperOverrideConfig = null;
245         if (cb instanceof ContextThemeWrapper) {
246             final ContextThemeWrapper contextThemeWrapper = (ContextThemeWrapper) cb;
247             contextThemeWrapperOverrideConfig = contextThemeWrapper.getOverrideConfiguration();
248         }
249 
250         // Apply the ContextThemeWrapper override if necessary.
251         // NOTE: Make sure the configurations are not modified, as they are treated as immutable
252         // in many places.
253         final Configuration configToReport = createNewConfigAndUpdateIfNotNull(
254                 newConfig, contextThemeWrapperOverrideConfig);
255         cb.onConfigurationChanged(configToReport);
256     }
257 
258     /** Update default density. */
updateDefaultDensity(int densityDpi)259     void updateDefaultDensity(int densityDpi) {
260         if (!mActivityThread.isInDensityCompatMode()
261                 && densityDpi != Configuration.DENSITY_DPI_UNDEFINED
262                 && densityDpi != DisplayMetrics.DENSITY_DEVICE) {
263             DisplayMetrics.DENSITY_DEVICE = densityDpi;
264             Bitmap.setDefaultDensity(densityDpi);
265         }
266     }
267 
268     /** Get current default display dpi. This is only done to maintain @UnsupportedAppUsage. */
getCurDefaultDisplayDpi()269     int getCurDefaultDisplayDpi() {
270         return mConfiguration.densityDpi;
271     }
272 
273     /**
274      * The LocaleList set for the app's resources may have been shuffled so that the preferred
275      * Locale is at position 0. We must find the index of this preferred Locale in the
276      * original LocaleList.
277      */
updateLocaleListFromAppContext(@onNull Context context)278     void updateLocaleListFromAppContext(@NonNull Context context) {
279         final Locale bestLocale = context.getResources().getConfiguration().getLocales().get(0);
280         final LocaleList newLocaleList = mResourcesManager.getConfiguration().getLocales();
281         final int newLocaleListSize = newLocaleList.size();
282         for (int i = 0; i < newLocaleListSize; i++) {
283             if (bestLocale.equals(newLocaleList.get(i))) {
284                 LocaleList.setDefault(newLocaleList, i);
285                 return;
286             }
287         }
288 
289         // The app may have overridden the LocaleList with its own Locale
290         // (not present in the available list). Push the chosen Locale
291         // to the front of the list.
292         LocaleList.setDefault(new LocaleList(bestLocale, newLocaleList));
293     }
294 
295     /**
296      * Creates a new Configuration only if override would modify base. Otherwise returns base.
297      * @param base The base configuration.
298      * @param override The update to apply to the base configuration. Can be null.
299      * @return A Configuration representing base with override applied.
300      */
createNewConfigAndUpdateIfNotNull(@onNull Configuration base, @Nullable Configuration override)301     static Configuration createNewConfigAndUpdateIfNotNull(@NonNull Configuration base,
302             @Nullable Configuration override) {
303         if (override == null) {
304             return base;
305         }
306         Configuration newConfig = new Configuration(base);
307         newConfig.updateFrom(override);
308         return newConfig;
309     }
310 
311 }
312