Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3dbe4cd5 authored by Chilun's avatar Chilun
Browse files

Introduce ConfigurationController

Move configuration related logic to ConfigurationController.

Bug: 127877792
Test: atest WmTests
Change-Id: I311339da5ff5d451a4e6f5131f0e6ec3533ac763
parent 66c9982c
Loading
Loading
Loading
Loading
+86 −254

File changed.

Preview size limit exceeded, changes collapsed.

+42 −0
Original line number Diff line number Diff line
/*
 * 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 android.content.ComponentCallbacks2;

import java.util.ArrayList;

/**
 * ActivityThread internal interface.
 * It is a subset of ActivityThread and used for communicating with
 * {@link ConfigurationController}.
 */
interface ActivityThreadInternal {
    ContextImpl getSystemContext();

    ContextImpl getSystemUiContext();

    boolean isInDensityCompatMode();

    boolean hasImeComponent();

    boolean isCachedProcessState();

    Application getApplication();

    ArrayList<ComponentCallbacks2> collectComponentCallbacks(boolean includeActivities);
}
+343 −0
Original line number Diff line number Diff line
/*
 * 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.applyCompatConfigurationLocked(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;

        final Resources.Theme systemTheme = mActivityThread.getSystemContext().getTheme();
        final Resources.Theme systemUiTheme = mActivityThread.getSystemUiContext().getTheme();

        synchronized (mResourcesManager) {
            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.applyConfigurationToResourcesLocked(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");
                }
            }
        }
    }
}