73

I have an old project that supports multi-languages. I want to upgrade support library and target platform, Before migrating to Androidx everything works fine but now change language not work!

I use this code to change default locale of App

private static Context updateResources(Context context, String language)
{
    Locale locale = new Locale(language);
    Locale.setDefault(locale);

    Configuration configuration = context.getResources().getConfiguration();
    configuration.setLocale(locale);

    return context.createConfigurationContext(configuration);
}

And call this method on each activity by override attachBaseContext like this:

@Override
protected void attachBaseContext(Context newBase)
{
    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
    String language = preferences.getString(SELECTED_LANGUAGE, "fa");
    super.attachBaseContext(updateResources(newBase, language));
}

I try other method to get string and I noticed that ‍‍‍‍getActivity().getBaseContext().getString work and getActivity().getString not work. Even the following code does not work and always show app_name vlaue in default resource string.xml.

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/app_name"/>

I share a sample code in https://github.com/Freydoonk/LanguageTest

Also getActivity()..getResources().getIdentifier not work and always return 0!

Fred
  • 3,265
  • 4
  • 31
  • 53

14 Answers14

127

UPDATE Aug 21 2020:

AppCompat 1.2.0 was finally released. If you're not using a ContextWrapper or ContextThemeWrapper at all, there should be nothing else to do and you should be able remove any workarounds you had from 1.1.0!

If you DO use a ContextWrapper or ContextThemeWrapper inside attachBaseContext, locale changes will break, because when you pass your wrapped context to super,

  1. the 1.2.0 AppCompatActivity makes internal calls which wrap your ContextWrapper in another ContextThemeWrapper,
  2. or if you use a ContextThemeWrapper, overrides its configuration to a blank one, similar to what happened back in 1.1.0.

But the solution is always the same. I've tried multiple other solutions for situation 2, but as pointed out by @Kreiri in the comments (thanks for your investigative help!), the AppCompatDelegateImpl always ended up stripping away the locale. The big obstacle is that, unlike in 1.1.0, applyOverrideConfiguration is called on your base context, not your host activity, so you can't just override that method in your activity and fix the locale as you could in 1.1.0. The only working solution I'm aware of is to reverse the wrapping by overriding getDelegate() to make sure your wrapping and/or locale override comes last. First, you add the class below:

Kotlin sample (please note that the class MUST be inside the androidx.appcompat.app package because the only existing AppCompatDelegate constructor is package private)

package androidx.appcompat.app

import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.util.AttributeSet
import android.view.MenuInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.Toolbar

class BaseContextWrappingDelegate(private val superDelegate: AppCompatDelegate) : AppCompatDelegate() {

    override fun getSupportActionBar() = superDelegate.supportActionBar

    override fun setSupportActionBar(toolbar: Toolbar?) = superDelegate.setSupportActionBar(toolbar)

    override fun getMenuInflater(): MenuInflater? = superDelegate.menuInflater

    override fun onCreate(savedInstanceState: Bundle?) {
        superDelegate.onCreate(savedInstanceState)
        removeActivityDelegate(superDelegate)
        addActiveDelegate(this)
    }

    override fun onPostCreate(savedInstanceState: Bundle?) = superDelegate.onPostCreate(savedInstanceState)

    override fun onConfigurationChanged(newConfig: Configuration?) = superDelegate.onConfigurationChanged(newConfig)

    override fun onStart() = superDelegate.onStart()

    override fun onStop() = superDelegate.onStop()

    override fun onPostResume() = superDelegate.onPostResume()

    override fun setTheme(themeResId: Int) = superDelegate.setTheme(themeResId)

    override fun <T : View?> findViewById(id: Int) = superDelegate.findViewById<T>(id)

    override fun setContentView(v: View?) = superDelegate.setContentView(v)

    override fun setContentView(resId: Int) = superDelegate.setContentView(resId)

    override fun setContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.setContentView(v, lp)

    override fun addContentView(v: View?, lp: ViewGroup.LayoutParams?) = superDelegate.addContentView(v, lp)

    override fun attachBaseContext2(context: Context) = wrap(superDelegate.attachBaseContext2(super.attachBaseContext2(context)))

    override fun setTitle(title: CharSequence?) = superDelegate.setTitle(title)

    override fun invalidateOptionsMenu() = superDelegate.invalidateOptionsMenu()

    override fun onDestroy() {
        superDelegate.onDestroy()
        removeActivityDelegate(this)
    }

    override fun getDrawerToggleDelegate() = superDelegate.drawerToggleDelegate

    override fun requestWindowFeature(featureId: Int) = superDelegate.requestWindowFeature(featureId)

    override fun hasWindowFeature(featureId: Int) = superDelegate.hasWindowFeature(featureId)

    override fun startSupportActionMode(callback: ActionMode.Callback) = superDelegate.startSupportActionMode(callback)

    override fun installViewFactory() = superDelegate.installViewFactory()

    override fun createView(parent: View?, name: String?, context: Context, attrs: AttributeSet): View? = superDelegate.createView(parent, name, context, attrs)

    override fun setHandleNativeActionModesEnabled(enabled: Boolean) {
        superDelegate.isHandleNativeActionModesEnabled = enabled
    }

    override fun isHandleNativeActionModesEnabled() = superDelegate.isHandleNativeActionModesEnabled

    override fun onSaveInstanceState(outState: Bundle?) = superDelegate.onSaveInstanceState(outState)

    override fun applyDayNight() = superDelegate.applyDayNight()

    override fun setLocalNightMode(mode: Int) {
        superDelegate.localNightMode = mode
    }

    override fun getLocalNightMode() = superDelegate.localNightMode

    private fun wrap(context: Context): Context {
        TODO("your wrapping implementation here")
    }
}

Then inside our base activity class you remove all your 1.1.0 workarounds and simply add this:

private var baseContextWrappingDelegate: AppCompatDelegate? = null

override fun getDelegate() = baseContextWrappingDelegate ?: BaseContextWrappingDelegate(super.getDelegate()).apply {
    baseContextWrappingDelegate = this
}

Depending on the ContextWrapper implementation you're using, configuration changes might break theming or locale changes. To fix that, additionally add this:

override fun createConfigurationContext(overrideConfiguration: Configuration) : Context {
    val context = super.createConfigurationContext(overrideConfiguration)
    TODO("your wrapping implementation here")
}

And you're good! You can expect Google to break this again in 1.3.0. I'll be there to fix it ... See you, space cowboy!

OLD ANSWER AND SOLUTION FOR APPCOMPAT 1.1.0:

Basically what's happening in the background is that while you've set the configuration correctly in attachBaseContext, the AppCompatDelegateImpl then goes and overrides the configuration to a completely fresh configuration without a locale:

 final Configuration conf = new Configuration();
 conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);

 try {
     ...
     ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
     handled = true;
 } catch (IllegalStateException e) {
     ...
 }

In an unreleased commit by Chris Banes this was actually fixed: The new configuration is a deep copy of the base context's configuration.

final Configuration conf = new Configuration(baseConfiguration);
conf.uiMode = newNightMode | (conf.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
try {
    ...
    ((android.view.ContextThemeWrapper) mHost).applyOverrideConfiguration(conf);
    handled = true;
} catch (IllegalStateException e) {
    ...
}

Until this is released, it's possible to do the exact same thing manually. To continue using version 1.1.0 add this below your attachBaseContext:

Kotlin solution

override fun applyOverrideConfiguration(overrideConfiguration: Configuration?) {
    if (overrideConfiguration != null) {
        val uiMode = overrideConfiguration.uiMode
        overrideConfiguration.setTo(baseContext.resources.configuration)
        overrideConfiguration.uiMode = uiMode
    }
    super.applyOverrideConfiguration(overrideConfiguration)
}

Java solution

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
    if (overrideConfiguration != null) {
        int uiMode = overrideConfiguration.uiMode;
        overrideConfiguration.setTo(getBaseContext().getResources().getConfiguration());
        overrideConfiguration.uiMode = uiMode;
    }
    super.applyOverrideConfiguration(overrideConfiguration);
}

This code does exactly the same what Configuration(baseConfiguration) does under the hood, but because we are doing it after the AppCompatDelegate has already set the correct uiMode, we have to make sure to take the overridden uiMode over to after we fix it so we don't lose the dark/light mode setting.

Please note that this only works by itself if you don't specify configChanges="uiMode" inside your manifest. If you do, then there's yet another bug: Inside onConfigurationChanged the newConfig.uiMode won't be set by AppCompatDelegateImpl's onConfigurationChanged. This can be fixed as well if you copy all the code AppCompatDelegateImpl uses to calculate the current night mode to your base activity code and then override it before the super.onConfigurationChanged call. In Kotlin it would look like this:

private var activityHandlesUiMode = false
private var activityHandlesUiModeChecked = false

private val isActivityManifestHandlingUiMode: Boolean
    get() {
        if (!activityHandlesUiModeChecked) {
            val pm = packageManager ?: return false
            activityHandlesUiMode = try {
                val info = pm.getActivityInfo(ComponentName(this, javaClass), 0)
                info.configChanges and ActivityInfo.CONFIG_UI_MODE != 0
            } catch (e: PackageManager.NameNotFoundException) {
                false
            }
        }
        activityHandlesUiModeChecked = true
        return activityHandlesUiMode
    }

override fun onConfigurationChanged(newConfig: Configuration) {
    if (isActivityManifestHandlingUiMode) {
        val nightMode = if (delegate.localNightMode != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED) 
            delegate.localNightMode
        else
            AppCompatDelegate.getDefaultNightMode()
        val configNightMode = when (nightMode) {
            AppCompatDelegate.MODE_NIGHT_YES -> Configuration.UI_MODE_NIGHT_YES
            AppCompatDelegate.MODE_NIGHT_NO -> Configuration.UI_MODE_NIGHT_NO
            else -> applicationContext.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
        }
        newConfig.uiMode = configNightMode or (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv())
    }
    super.onConfigurationChanged(newConfig)
}
0101100101
  • 5,560
  • 5
  • 30
  • 51
  • 1
    It worked perfect for me. Thanks! But, i didn't undestand how overriding "applyOverrideConfiguration" method solves the problem. Could you please explain it? – Burak Oct 23 '19 at 21:27
  • 1
    @Burak If you check the source code of ContextThemeWrapper you should be able to see that whatever is passed to super.applyOverrideConfiguration(overrideConfiguration) is stored and later used to fetch any kind of resources. That's just the way Google implemented the day/night theming. The base resource configuration doesn't have the correct uiMode setting, so ContextThemeWrapper overrides getResources() to return an instance where the configuration has it. – 0101100101 Oct 24 '19 at 00:02
  • This solution has problems if the application supports landscape display – mdtuyen Nov 28 '19 at 03:36
  • 1
    @phongvan what kind of problems? I've been using this for a long time and never had issues with landscape or switching between portrait/landscape. Pretty sure that if you're encountering any glitches that it's related to other bugs in version 1.1.0, and not my solution, because as explained in the answer, it's directly derived from Google's official fix. – 0101100101 Nov 28 '19 at 05:29
  • @0101100101 When I use this code I return to the screen, the orientation does not change at onconfigchange – mdtuyen Nov 28 '19 at 07:03
  • @phongvan I noticed that Google has been working a lot on the implementation and it's entirely possible that such bugs may occur. See the update in my answer. It also seems what you describe may occur completely randomly, sometimes it would work just fine and other times it would break. – 0101100101 Dec 15 '19 at 23:49
  • this `applyOverrideConfiguration` workaround works for me. Without that locale is not working on Android 6.0. I'm using the `androidx.appcompat:appcompat:1.1.0` – Deepak Goyal Dec 17 '19 at 08:36
  • I don't understand, its not working for me. I put applyOverrideConfiguration in my MainActivity. Then I have my updateResource method in my helper. Then on my UITest, Inside the @Before, i called : updateResources(InstrumentationRegistry.getInstrumentation().context, "EN") but not working, any help please? NB : version 'androidx.appcompat:appcompat:1.1.0' – Tai Nguyen Dec 24 '19 at 12:42
  • 1
    @TaiNguyen what's your updateResource method, what does it do? Just to point out, if you're using ContextWrappers in attachBaseContext, there are more code changes needed to make my solution work, because ContextWrappers might have their own logic, for example storing stale Resources and Configuration instances. – 0101100101 Dec 27 '19 at 00:17
  • For me this additional step (as per comment just above) was necessary... do not pass a `ContextWrapper` to `super.attachBaseContext()` but instead just pass a `Context`, i.e. not wrapped. That in combination with the use of `applyOverrideConfiguration()` seems to work for me. Thanks. But **why** does Google make it so **hard** to do something so **simple**??!! – drmrbrewer May 05 '20 at 07:51
  • UPDATE.... aargh, no it's still not working consistently. Still getting the default (system) language being used sometimes. Android is a nightmare. After backing out of my Activity, if I open it again soon after, the default (system) language is used. If I open the "recent apps" listing and Close All, then open the Activity, the correct (manual override) language is used. It's like some default (system) config is still being used. – drmrbrewer May 05 '20 at 08:22
  • further UPDATE @0101100101... doing what you suggest in this answer, **plus also** downgrading to `appcompat 1.1.0` seems to work better for me... was using `appcompat 1.2.0-beta01`. My concern is that `appcompat` is progressing in a direction which will make this answer redundant... any idea where to report these issues with `appcompat`? – drmrbrewer May 05 '20 at 09:12
  • final UPDATE... found something that works better for me... see my answer here: https://stackoverflow.com/a/61643167/4070848 – drmrbrewer May 06 '20 at 19:22
  • @drmrbrewer "My concern is that appcompat is progressing in a direction which will make this answer redundant" yes, that was pretty much expected. See my latest update to this answer. – 0101100101 Aug 10 '20 at 03:41
  • Using ContextThemeWrapper instead of ContextWrapper does not help. – Kreiri Aug 18 '20 at 09:57
  • @Kreiri can you try if it works by also adding the `applyOverrideConfiguration` solution in addition to that? I just edited my answer to suggest this. I noticed the AppCompatDelegateImpl code is still breaking the configuration, it's ridiculous. Let me know how it goes. – 0101100101 Aug 19 '20 at 02:29
  • @0101100101 I couldn't get it work. `applyOverrideConfiguration` doesn't get called with 1.2.0 in my project... – Kreiri Aug 19 '20 at 10:25
  • @Kreiri I see, the implementation actually doesn't call `applyOverrideConfiguration` on the activity anymore, only on the base context. See updated answer and solution: Just call `getResources()` before you call `super.attachBaseContext`. No need to override `applyOverrideConfiguration`. – 0101100101 Aug 20 '20 at 02:56
  • @0101100101 Even with calling getResources() on new context before passing it into super.attachBaseContext, locale in activity does not change. `attachBaseContext2` always returns context with system locale. – Kreiri Aug 20 '20 at 05:27
  • @Kreiri you're right, I just checked and Google's implementation *still* manages to strip away the locale, sigh ... Try removing all workarounds and switch to the `BaseContextWrappingDelegate` solution, independent of whether you're using a theme or normal wrapper. You'll just need to move your wrapping and locale change from your activity to inside that class. – 0101100101 Aug 21 '20 at 04:33
  • Yes from 1.2.0, `applyOverrideConfiguration` is not getting called. You have to call this function manually from `attachBaseContext` – Deepak Goyal Oct 13 '20 at 06:49
  • 2
    In case it might be of help to anyone, [here](https://github.com/CNugteren/NLWeer/pull/20/files) is a full diff of a working locale change implementation based on combining this answer with [another one](https://stackoverflow.com/a/56630533) using an AppCompatActivity activity and library version 1.2.0. – CNugteren Oct 20 '20 at 14:32
  • thanks for the update of appCompat 1.2.0, love you. – Youtoo Nov 17 '20 at 10:14
  • Finally found an answer that works for me, I've been working on this for days and was not able to get it to work; this is great. Thank you very much for this excellent answer – M. Abdel-Ghani Dec 14 '20 at 17:09
  • 1
    This works with AppCompat 1.3.0-beta01 also! – Claus Holst Feb 17 '21 at 11:45
19

Finally, I find the problem in my app. When migrating the project to Androidx dependencies of my project changed like this:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.1.0-alpha04'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0-alpha02'
} 

As it is seen, version of androidx.appcompat:appcompat is 1.1.0-alpha03 when I changed it to the latest stable version, 1.0.2, my problem is resolved and the change language working properly.

I find the latest stable version of appcompat library in Maven Repository. I also change other libraries to the latest stable version.

Now my app dependencies section is like bellow:

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'com.google.android.material:material:1.0.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
Fred
  • 3,265
  • 4
  • 31
  • 53
  • 1
    I had a similar issue. I already had stable `1.0.2` for `appcompat`, but my problem was in the design support library or `com.google.android.material:material:1.1.0-alpha06`. So when I switched to the stable `1.0.0`, it's fixed. – Abir Hasan May 15 '19 at 13:53
13

There is an issue in new app compat libraries related to night mode that is causing to override the configuration on android 21 to 25. This can be fixed by applying your configuration when this public function is called:

public void applyOverrideConfiguration(Configuration overrideConfiguration

For me, this little trick has worked by copying the settings from the overridden configuration to my configuration but you can do whatever you want. It is better to reapply your language logic to the new configuration to minimize errors

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration) {
    if (Build.VERSION.SDK_INT >= 21&& Build.VERSION.SDK_INT <= 25) {
        //Use you logic to update overrideConfiguration locale
        Locale locale = getLocale()//your own implementation here;
        overrideConfiguration.setLocale(locale);
    }
    super.applyOverrideConfiguration(overrideConfiguration);
}
user2342558
  • 4,717
  • 5
  • 27
  • 50
Jack Lebbos
  • 237
  • 3
  • 10
  • this code change makes crash in multiple places on some devices `Attempt to invoke virtual method 'java.lang.String java.util.Locale.toString()' on a null object reference android.widget.Editor$TextActionModeCallback.populateMenuWithItemsExt`. It is better adviced to downgrade or follow @0101100101's answer.. – sanjeev Nov 08 '19 at 09:11
  • getLocale and setLocale should be on [Android N/24 or above](https://stackoverflow.com/a/40704077/2199894) – Rahmat Ihsan May 27 '20 at 01:51
10

I am using "androidx.appcompat:appcompat:1.3.0-alpha01" but I suppose it will also work on Version 1.2.0.
The following code is based on Android Code Search.

import android.content.Context
import android.content.res.Configuration
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import java.util.*

open class MyBaseActivity :AppCompatActivity(){

    override fun attachBaseContext(newBase: Context?) {
        super.attachBaseContext(newBase)
        val config = Configuration()
        applyOverrideConfiguration(config)
    }

    override fun applyOverrideConfiguration(newConfig: Configuration) {
        super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig))
    }

    open fun updateConfigurationIfSupported(config: Configuration): Configuration? {
        // Configuration.getLocales is added after 24 and Configuration.locale is deprecated in 24
        if (Build.VERSION.SDK_INT >= 24) {
            if (!config.locales.isEmpty) {
                return config
            }
        } else {
            if (config.locale != null) {
                return config
            }
        }
        // Please Get your language code from some storage like shared preferences
        val languageCode = "fa"
        val locale = Locale(languageCode)
        if (locale != null) {
            // Configuration.setLocale is added after 17 and Configuration.locale is deprecated
            // after 24
            if (Build.VERSION.SDK_INT >= 17) {
                config.setLocale(locale)
            } else {
                config.locale = locale
            }
        }
        return config
    }
}
Ehsan Shadi
  • 521
  • 5
  • 16
  • 1
    I think this is the most simple answer that solves my issue, thank! @EhsanShadi I'll just wait for 1.3.0 updates to what will happen then, :) – Ric17101 Nov 04 '20 at 09:09
  • How's this one works? creating new configuration overrides all the pre defined configurations and breaks the ui. – dor506 Nov 12 '20 at 08:31
  • @dor506 what kind of UI breakages are you experiencing? This seems to be the only solution that has worked for me androidx.appcompat:appcompat:1.3.0-alpha03 but would be happy to know some of the caveat to be prepared for release. Thanks – saintjab Nov 16 '20 at 16:15
  • @saintjab Take a look at 0101100101 latest update (aug 20), which actually does work while updating the current configuration (context.getResources().getConfiguration()), instead of create new configuration in every activity. – dor506 Nov 16 '20 at 21:38
9

Late answer but I thought might be helpful. As of androidx.appcompat:appcompat:1.2.0-beta01 The solution of 0101100101 overriding applyOverrideConfiguration no longer works on me. Instead, in then overriden attacheBaseContext, you have to call the applyOverrideConfiguration() without overriding it.

override fun attachBaseContext(newBase: Context) {
    val newContext = LocaleHelper.getUpdatedContext(newBase)
    super.attachBaseContext(newContext)
    if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && Build.VERSION.SDK_INT <= Build.VERSION_CODES.N_MR1){
        applyOverrideConfiguration(newContext.resources.configuration)
    }
}

It's just a shame that his solution only works on 1.1.0. Base on my research this should have been officially fix. It's just weird that this bug still here. I know I use the beta but for someone who wants to use the latest, this solution for me is working. Tested on emulator api level 21-25. Above that api level, you don't have to worry about it.

CodeAndWave
  • 1,940
  • 3
  • 23
  • 32
  • 1
    this is the only solution that worked for me for androidx.appcompat...1.2.0 otherwise locale change was not happening in some phones (samsung s5 in my case) Thanks ! – syed_noorullah May 08 '21 at 21:14
5

Finally i got solution for locate, In my case actually issue was with bundle apk because it split the locate files. In bundle apk by default all splits will be generated. but within the android block of your build.gradle file you are able to declare which splits will be generated.

bundle {
            language {
                // Specifies that the app bundle should not support
                // configuration APKs for language resources. These
                // resources are instead packaged with each base and
                // dynamic feature APK.
                enableSplit = false
            }
        }

After adding this code to android block of build.gradle file my issue get resolved.

Mansukh Ahir
  • 3,833
  • 5
  • 38
  • 61
  • 1
    I was having the same issue. When I used to check before publishing on play store, everything worked and after publishing the locale change did not work. I applied this solution and now works flawlessly. Thanks – Sunny Feb 28 '20 at 02:25
  • 1
    I had the same problem. Whenever I install debug version it was working but when I release bundle had the issue and tried alot of solution but this is the only one that worked. Thanks. – Yonatan Dawit May 03 '20 at 18:04
  • This! It makes so much sense. One of the testing phones never displayed the selected language from the app and we wondered why it only happens when it's downloaded from Google Play. I verified it working by adding the system language -> clear data -> restarting the app. Thank you! – Saehun Sean Oh Apr 19 '21 at 21:45
5

The androidx.appcompat:appcompat:1.1.0 bug can also be resolved by simply calling getResources() in Activity.applyOverrideConfiguration()

@Override public void
applyOverrideConfiguration(Configuration cfgOverride)
{
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
      Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    // add this to fix androidx.appcompat:appcompat 1.1.0 bug
    // which happens on Android 6.x ~ 7.x
    getResources();
  }

  super.applyOverrideConfiguration(cfgOverride);
}
Sam Lu
  • 3,188
  • 1
  • 25
  • 36
3

Try something like this:

public class MyActivity extends AppCompatActivity {
    public static final float CUSTOM_FONT_SCALE = 4.24f;
    public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(useCustomConfig(newBase));
    }

    private Context useCustomConfig(Context context) {
        Locale.setDefault(CUSTOM_LOCALE);
        if (Build.VERSION.SDK_INT >= 17) {
            Configuration config = new Configuration();
            config.fontScale = CUSTOM_FONT_SCALE;
            config.setLocale(CUSTOM_LOCALE);
            return context.createConfigurationContext(config);
        } else {
            Resources res = context.getResources();
            Configuration config = new Configuration(res.getConfiguration());
            config.fontScale = CUSTOM_FONT_SCALE;
            config.locale = CUSTOM_LOCALE;
            res.updateConfiguration(config, res.getDisplayMetrics());
            return context;
        }
    }
}   

Sources: issuetracker comment and the first sample linked from the issuetracker comment.

Whilst the above is working fine for me, another option from the second sample linked from the issuetracker comment is as follows (I haven't personally tried this out):

@RequiresApi(17)
public class MyActivity extends AppCompatActivity {
    public static final float CUSTOM_FONT_SCALE = 4.24f;
    public static final Locale CUSTOM_LOCALE = Locale.CANADA_FRENCH; // or whatever

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);

        Configuration config = new Configuration();
        config.fontScale = CUSTOM_FONT_SCALE;
        applyOverrideConfiguration(config);
    }

    @Override
    public void applyOverrideConfiguration(Configuration newConfig) {
        super.applyOverrideConfiguration(updateConfigurationIfSupported(newConfig));
    }

    private Configuration updateConfigurationIfSupported(Configuration config) {
        if (Build.VERSION.SDK_INT >= 24) {
            if (!config.getLocales().isEmpty()) {
                return config;
            }
        } else {
            if (config.locale != null) {
                return config;
            }
        }

        Locale locale = CUSTOM_LOCALE;
        if (locale != null) {
            if (Build.VERSION.SDK_INT >= 17) {
                config.setLocale(locale);
            } else {
                config.locale = locale;
            }
        }
        return config;
    }
}
drmrbrewer
  • 9,593
  • 16
  • 68
  • 156
3

Now the language does not change with these libraries: androidx.appcompat:appcompat:1.1.0, androidx.appcompat:appcompat:1.2.0

The problem was only solved in this library: androidx.appcompat:appcompat:1.3.0-rc01

Tim Kruichkov
  • 1,363
  • 13
  • 18
  • Hey is it good to use 1.3.0-rc01 version instead of 1.2.0 in production level release? – Coder Apr 14 '21 at 08:05
  • yes, it's good. Otherwise, 1.2.0 not working at all. In my case there was troubles with Application context, but all was good with Activity context. – Tim Kruichkov Apr 14 '21 at 12:51
1

Now there is a newer version that also works:

implementation 'androidx.appcompat:appcompat:1.1.0-alpha04'

As @Fred mentioned appcompat:1.1.0-alpha03 has a glitch although it's not mentioned on their release versions log

Choletski
  • 6,451
  • 6
  • 40
  • 58
  • I test `androidx.appcompat:appcompat:1.1.0-alpha04`, It has this problem yet. – Fred Apr 04 '19 at 20:28
  • for my production app works just fine, I use a custom implementation for `ContextWrapper` within `attachBaseContext(Context newBase)` for each activity – Choletski Apr 05 '19 at 06:58
0

Had the same bug on androidx.appcompat:appcompat:1.1.0. Switched to androidx.appcompat:appcompat:1.1.0-rc01 and now langs change on Android 5-6.

Anas Mehar
  • 2,549
  • 11
  • 24
Spinner
  • 29
  • 4
0

Answer from @0101100101 worked for me.

Only that I used

@Override
public void applyOverrideConfiguration(Configuration overrideConfiguration)
{
  if (overrideConfiguration != null) {
    int uiMode = overrideConfiguration.uiMode;
    overrideConfiguration.setTo(getResources().getConfiguration());
    overrideConfiguration.uiMode = uiMode;
  }
  super.applyOverrideConfiguration(overrideConfiguration);
}

so only getResources() instead of getBaseContext().getResources().

In my case I have extended ContextWrapper with overridden getResources(). But after applyOverrideConfiguration is called I can't access my custom getResources. I get standard ones instead.

If I use the code above everything works fine.

Brontes
  • 104
  • 5
0

Solution:

In the App level Gradle, I included the following code within the android division,

bundle {
    language {
        // Specifies that the app bundle should not support
        // configuration APKs for language resources. These
        // resources are instead packaged with each base and
        // dynamic feature APK.
        enableSplit = false
    }
}

https://medium.com/dwarsoft/how-to-provide-languages-dynamically-using-app-bundle-567d2ec32be6

sweetnandha cse
  • 349
  • 4
  • 7
-2

1. Method you may use in attachBaseContext()

private void setLanguage(Context mContext, String localeName) {
        Locale myLocale = new Locale(localeName);
        Resources res = mContext.getResources();
        DisplayMetrics dm = res.getDisplayMetrics();
        Configuration conf = res.getConfiguration();
        conf.locale = myLocale;
        res.updateConfiguration(conf, dm);
    }

2. Override in activities

@Override
protected void attachBaseContext(Context newBase) {
    setLanguage(newBase, "your language");
    super.attachBaseContext(newBase);
}

NB: This is working fine for me after I recreate the activity

  • Where you call 'recreate();` – Fred Mar 20 '19 at 17:28
  • Suppose, you use a button click to change language. In that case, when you click the button, put your language in shared Preferences and recreate your activity. Get language string from shared Preferences when necessary. If you require more detail, I'll share code example. – mustafiz012 Mar 20 '19 at 17:42
  • I do this, I persist selected language in shared Preferences and recreate the app, I get selected language from shared Preferences and pass to `setLanguage(newBase, myPpersistedLanguage);` in `attachBaseContext` but this is not work. I edit an complete `attachBaseContext` in sample code in my question to show this. – Fred Mar 20 '19 at 17:52
  • I think your strings.xml file aren't configured as it requires. If you please tell me, do you have alternative strings.xml file?? For example: strings.xml, strings.xml (fa) – mustafiz012 Mar 20 '19 at 18:02
  • I add a screenshot of my resource in my project – Fred Mar 20 '19 at 18:10
  • Then I'm gonna share my working code example. Please allow me some time. – mustafiz012 Mar 20 '19 at 18:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/190386/discussion-between-mustafiz012-and-fred). – mustafiz012 Mar 20 '19 at 18:32