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