After the project is transformed by Androidx, multi-language switching will be invalid. Here is a summary.

When quoting the androidx. Appcompat: appcompat: 1.1.0, BaseActivity implement the following methods:

@ Override public void applyOverrideConfiguration (Configuration overrideConfiguration) {/ / compatible androidX if change language in some parts of the cell phone failure problem (overrideConfiguration ! = null) { int uiMode = overrideConfiguration.uiMode; overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration()); overrideConfiguration.uiMode = uiMode; } super.applyOverrideConfiguration(overrideConfiguration); }Copy the code

When quoting the androidx. Appcompat: appcompat: 1.2.0, BaseActivity implement the following methods:

Compatactivity: // BaseActivity inherits AppCompatActivity. // Fix compatCompat 1.2+. The custom ContextThemeWrapper @ Override protected void attachBaseContext (Context newBase) {if (shouldSupportMultiLanguage ())  { Integer language = SpUtil.getInt(newBase, Cons.SP_KEY_OF_CHOOSED_LANGUAGE, -1); Context context = LanguageUtil.attachBaseContext(newBase, language); final Configuration configuration = context.getResources().getConfiguration(); / / here is ContextThemeWrapper androidx. Appcompat. View under the package / / you can also use the android. The ContextThemeWrapper, But using the object only with the lowest to the API 17. / / so use androidx appcompat. The ContextThemeWrapper worry final ContextThemeWrapper wrappedContext = new  ContextThemeWrapper(context, R.style.Theme_AppCompat_Empty) { @Override public void applyOverrideConfiguration(Configuration overrideConfiguration) {  if (overrideConfiguration ! = null) { overrideConfiguration.setTo(configuration); } super.applyOverrideConfiguration(overrideConfiguration); }}; super.attachBaseContext(wrappedContext); } else { super.attachBaseContext(newBase); Public static Context attachBaseContext(Context Context, attachBaseContext, attachBaseContext, attachBaseContext) Integer language) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return createConfigurationResources(context, language); } else { applyLanguage(context, language); return context; } } public static void applyLanguage(Context context, Integer newLanguage) { Resources resources = context.getResources(); Configuration configuration = resources.getConfiguration(); Locale locale = getSupportLanguage(newLanguage); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { DisplayMetrics dm = resources.getDisplayMetrics(); // apply locale configuration.setLocale(locale); resources.updateConfiguration(configuration, dm); } else { // updateConfiguration DisplayMetrics dm = resources.getDisplayMetrics(); configuration.locale = locale; resources.updateConfiguration(configuration, dm); } } @TargetApi(Build.VERSION_CODES.N) private static Context createConfigurationResources(Context context, Integer language) { Resources resources = context.getResources(); final Configuration configuration = resources.getConfiguration(); final DisplayMetrics dm = resources.getDisplayMetrics(); Locale locale; If (language < 0) {/ / if you don't specify the language used system preferred language locale = getSystemPreferredLanguage (); } else {// Specify the language to use the specified language, not use the preferred language locale = getSupportLanguage(language); } configuration.setLocale(locale); resources.updateConfiguration(configuration, dm); return context; RequiresApi(API = build.version_codes.n) public static Locale getSystemPreferredLanguage() { Locale locale; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { locale = LocaleList.getDefault().get(0); } else { locale = Locale.getDefault(); } return locale; } // getSupportLanguage is a custom Locale, depending on your requirementsCopy the code

Why does this problem occur after an upgrade?

According to the logic of previous versions, the multi-language implementation is mainly achieved by overwriting attachBaseContext of AppCompatActivit, so this method is the starting point.

// implement in AppCompatActivit @override protected void attachBaseContext(Context newBase) { super.attachBaseContext(getDelegate().attachBaseContext2(newBase)); }Copy the code

You can see that AppCompatActivit does some processing on the newBase that we pass in through the proxy. Look at the getDelegate() method, which creates an AppCompatDelegate.

/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
    if (mDelegate == null) {
        mDelegate = AppCompatDelegate.create(this, this);
    }
    return mDelegate;
} 
Copy the code

Find the AppCompatDelegate implementation class AppCompatDelegateImpl and navigate to the attachBaseContext2(Context) method.

// AppCompatDelegateImpl
...
 
/**
* Flag indicating whether we can return a different context from attachBaseContext().
* Unfortunately, doing so breaks Robolectric tests, so we skip night mode application there.
*/
private static final boolean sCanReturnDifferentContext =
            !"robolectric".equals(Build.FINGERPRINT);
 
/**
 * Flag indicating whether ContextThemeWrapper.applyOverrideConfiguration() is available.
 */
private static final boolean sCanApplyOverrideConfiguration = Build.VERSION.SDK_INT >= 17;
 
...
 
@NonNull
@Override
@CallSuper
public Context attachBaseContext2(@NonNull final Context baseContext) {
    mBaseContextAttached = true;
    // This is a tricky method. Here are some things to avoid:
    // 1. Don't modify the configuration of the Application context. All changes should remain
    //    local to the Activity to avoid conflicting with other Activities and internal logic.
    // 2. Don't use createConfigurationContext() with Robolectric because Robolectric relies on
    //    method overrides.
    // 3. Don't use createConfigurationContext() unless you're able to retain the base context's
    //    theme stack. Not the last theme applied -- the entire stack of applied themes.
    final int modeToApply = mapNightMode(baseContext, calculateNightMode());
    // If the base context is a ContextThemeWrapper (thus not an Application context)
    // and nobody's touched its Resources yet, we can shortcut and directly apply our
    // override configuration.
    if (sCanApplyOverrideConfiguration
            && baseContext instanceof android.view.ContextThemeWrapper) {
        // api>=17 并且通过attachBaseContext传递的对象需要是android.view.ContextThemeWrapper
        // 上面的解决方案就是让AppCompatActivity中attachBaseContext方法代理程序进入此段代码
        // 来达到返回自定义的ContextThemeWrapper,然后覆写applyOverrideConfiguration来实现
        // 修改Configuration中local的功能
        final Configuration config = createOverrideConfigurationForDayNight(
                baseContext, modeToApply, null);
        if (DEBUG) {
            Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                    config.toString()));
        }
        try {
            ContextThemeWrapperCompatApi17Impl.applyOverrideConfiguration(
                    (android.view.ContextThemeWrapper) baseContext, config);
            return baseContext;
        } catch (IllegalStateException e) {
            if (DEBUG) {
                Log.d(TAG, "Failed to apply configuration to base context", e);
            }
        }
    }
    // Again, but using the AppCompat version of ContextThemeWrapper.
    if (baseContext instanceof ContextThemeWrapper) {
        // 通过attachBaseContext传递的对象需要是androidx.appcompat.view.ContextThemeWrapper
        // 上面的解决方案就是让AppCompatActivity中attachBaseContext方法代理程序进入此段代码
        // 来达到返回自定义的ContextThemeWrapper,然后覆写applyOverrideConfiguration来实现
        // 修改Configuration中local的功能
        final Configuration config = createOverrideConfigurationForDayNight(
                baseContext, modeToApply, null);
        if (DEBUG) {
            Log.d(TAG, String.format("Attempting to apply config to base context: %s",
                    config.toString()));
        }
        try {
            ((ContextThemeWrapper) baseContext).applyOverrideConfiguration(config);
            return baseContext;
        } catch (IllegalStateException e) {
            if (DEBUG) {
                Log.d(TAG, "Failed to apply configuration to base context", e);
            }
        }
    }
    // We can't apply the configuration directly to the existing base context, so we need to
    // wrap it. We can't create a new configuration context since the app may rely on method
    // overrides or a specific theme -- neither of which are preserved when creating a
    // configuration context. Instead, we'll make a best-effort at wrapping the context and
    // rebasing the original theme.
    if (!sCanReturnDifferentContext) {
        return super.attachBaseContext2(baseContext);
    }
    // We can't trust the application resources returned from the base context, since they
    // may have been altered by the caller, so instead we'll obtain them directly from the
    // Package Manager.
    final Configuration appConfig;
    try {
        appConfig = baseContext.getPackageManager().getResourcesForApplication(
                baseContext.getApplicationInfo()).getConfiguration();
    } catch (PackageManager.NameNotFoundException e) {
        throw new RuntimeException("Application failed to obtain resources from itself", e);
    }
    // The caller may have directly modified the base configuration, so we'll defensively
    // re-structure their changes as a configuration overlay and merge them with our own
    // night mode changes. Diffing against the application configuration reveals any changes.
    final Configuration baseConfig = baseContext.getResources().getConfiguration();
    final Configuration configOverlay;
    if (!appConfig.equals(baseConfig)) {
        configOverlay = generateConfigDelta(appConfig, baseConfig);
        if (DEBUG) {
            Log.d(TAG,
                    "Application config (" + appConfig + ") does not match base config ("
                            + baseConfig + "), using base overlay: " + configOverlay);
        }
    } else {
        configOverlay = null;
        if (DEBUG) {
            Log.d(TAG, "Application config (" + appConfig + ") matches base context "
                    + "config, using empty base overlay");
        }
    }
    final Configuration config = createOverrideConfigurationForDayNight(
            baseContext, modeToApply, configOverlay);
    if (DEBUG) {
        Log.d(TAG, String.format("Applying night mode using ContextThemeWrapper and "
                + "applyOverrideConfiguration(). Config: %s", config.toString()));
    }
    // Next, we'll wrap the base context to ensure any method overrides or themes are left
    // intact. Since ThemeOverlay.AppCompat theme is empty, we'll get the base context's theme.
    // 如果没有通过attachBaseContext传递自定义ContextThemeWrapper,那么最终AppCompatActivity
    // 得到的Context对象为此处new的ContextThemeWrapper,所以我们无法覆写applyOverrideConfiguration
    // 最终也正是因为包裹了这一层,导致我们获取的Resources是此处ContextThemeWrapper的Resources,
    // 而非我们修改语言后的Resources对象
    final ContextThemeWrapper wrappedContext = new ContextThemeWrapper(baseContext,
            R.style.Theme_AppCompat_Empty);
    wrappedContext.applyOverrideConfiguration(config);
    // Check whether the base context has an explicit theme or is able to obtain one
    // from its outer context. If it throws an NPE because we're at an invalid point in app
    // initialization, we don't need to worry about rebasing under the new configuration.
    boolean needsThemeRebase;
    try {
        needsThemeRebase = baseContext.getTheme() != null;
    } catch (NullPointerException e) {
        needsThemeRebase = false;
    }
    if (needsThemeRebase) {
        // Attempt to rebase the old theme within the new configuration. This will only
        // work on SDK 23 and up, but it's unlikely that we're keeping the base theme
        // anyway so maybe nobody will notice. Note that calling getTheme() will clone
        // the base context's theme into the wrapped context's theme.
        ResourcesCompat.ThemeCompat.rebase(wrappedContext.getTheme());
    }
    return super.attachBaseContext2(wrappedContext);
}
Copy the code

The code analysis

Through the analysis of the above code, it can be found that, If the BaseActivity overlays the object passed by attachBaseContext(Context) that is not ContextThemeWrapper, the system will create a ContextThemeWrapper of its own. And then wrap the Context that we passed around as an argument. The system creates a ContextThemeWrapper Resource that does not use the Resource in our Context. Instead, it defines a separate Resource of its own. The Configuration in the separate Resource is in the current language of the system, such as China, which is Chinese, rather than the language we set.

Finally we switch languages and getResources in different languages via getResources(), so let’s look at AppCompatActivit to getResources.

// AppCompatActivit
@Override
public Resources getResources() {
    if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
        mResources = new VectorEnabledTintResources(this, super.getResources());
    }
    return mResources == null ? super.getResources() : mResources;
} 
Copy the code

If mResources == null in AppCompatActivit, then super.getResources() will be called. The object returned by this method is ContextThemeWrapper (below the Android. view package, which is obtained when no custom ContextThemeWrapper is passed, or if a custom object is passed, it is ContextImpl). During the testing phase, I found that AppCompatActivit doesn’t hold Resources and ends up getting Resources from the parent class context (ContextThemeWrapper or ContextImpl).

// android.view.ContextThemeWrapper
 
@Override
public Resources getResources() {
    return getResourcesInternal();
}
private Resources getResourcesInternal() {
    if (mResources == null) {
        if (mOverrideConfiguration == null) {
            mResources = super.getResources();
        } else {
            final Context resContext = createConfigurationContext(mOverrideConfiguration);
            mResources = resContext.getResources();
        }
    }
    return mResources;
}
Copy the code

conclusion

Combined with the code above, you can see why a multilanguage switch is invalid if attachBaseContext2(Context) returns the Context from AppCompatDelegateImpl by wrapping only one layer of the Context we passed. Since Resources in the ContextThemeWrapper object returned by attachBaseContext2(Context) are not null, Resources does not apply to the Resources Configuration object in the Context we set, so the multilingual setting is invalid.

To clearly understand the Resources acquisition process, Debug is recommended. Debug shows that AppCompatActivit and its parent class hold the Resources structure if a custom ContextThemeWrapper is not passed:

AppCompatActivit -mResources(null) -mBase (AppCompatActivit parent (super) context, ContextThemeWrapper) -mResources(not null) -mResourcesImpl -mConfigration(The current system language, -mBase (ContextThemeWrapper parent context) -mBase (ContextThemeWrapper parent context) ContextImpl) -mresources (not null) -mresourcesImpl -mConfigration(the language we configure) Upload custom ContextThemeWrapper to appactivit structure: AppCompatActivit -mResources(null) -mBase (AppCompatActivit parent (super) context, ContextImpl) -mResources(not null) -mResourcesImpl -mConfigration(our configured language)Copy the code