Android N изменяет язык программным путем

Я нашел действительно странную ошибку, которая воспроизводится только на устройствах Android N.

В гастролях моего приложения есть возможность изменить язык. Вот код, который его изменяет.

 public void update(Locale locale) {

    Locale.setDefault(locale);

    Configuration configuration = res.getConfiguration();

    if (BuildUtils.isAtLeast24Api()) {
        LocaleList localeList = new LocaleList(locale);

        LocaleList.setDefault(localeList);
        configuration.setLocales(localeList);
        configuration.setLocale(locale);

    } else if (BuildUtils.isAtLeast17Api()){
        configuration.setLocale(locale);

    } else {
        configuration.locale = locale;
    }

    res.updateConfiguration(configuration, res.getDisplayMetrics());
}

Этот код отлично работает в действии моего тура (с вызовом recreate()), но во всех последующих действиях все ресурсы String ошибочны. Вращение экрана исправляет его. Что я могу сделать с этой проблемой? Должен ли я менять локаль для Android N по-другому или это просто системная ошибка?

P.S. Вот что я нашел. При первом запуске MainActivity (который после моего тура) Locale.getDefault() правильный, но ресурсы неправильные. Но в других действиях это дает мне неправильную локаль и неправильные ресурсы из этой локали. После поворота экрана (или, возможно, другого изменения конфигурации) Locale.getDefault() будет правильным.

Ответ 1

Хорошо. Наконец мне удалось найти решение.

Во-первых, вы должны знать, что в API 25 Resources.updateConfiguration(...) устарела. Так что вместо этого вы можете сделать что-то вроде этого:

1) Вам нужно создать свой собственный ContextWrapper, который будет переопределять все параметры конфигурации в baseContext. Например, это мой ContextWrapper, который корректно меняет локаль. Обратите внимание на метод context.createConfigurationContext(configuration).

public class ContextWrapper extends android.content.ContextWrapper {

    public ContextWrapper(Context base) {
        super(base);
    }

    public static ContextWrapper wrap(Context context, Locale newLocale) {
        Resources res = context.getResources();
        Configuration configuration = res.getConfiguration();

        if (BuildUtils.isAtLeast24Api()) {
            configuration.setLocale(newLocale);

            LocaleList localeList = new LocaleList(newLocale);
            LocaleList.setDefault(localeList);
            configuration.setLocales(localeList);

            context = context.createConfigurationContext(configuration);

        } else if (BuildUtils.isAtLeast17Api()) {
            configuration.setLocale(newLocale);
            context = context.createConfigurationContext(configuration);

        } else {
            configuration.locale = newLocale;
            res.updateConfiguration(configuration, res.getDisplayMetrics());
        }

        return new ContextWrapper(context);
    }
}

2) Вот что вы должны сделать в своей BaseActivity:

@Override
protected void attachBaseContext(Context newBase) {

    Locale newLocale;
    // .. create or get your new Locale object here.

    Context context = ContextWrapper.wrap(newBase, newLocale);
    super.attachBaseContext(context);
}

Замечания:

Не забудьте воссоздать свою деятельность, если вы хотите где-нибудь изменить локаль в своем приложении. Вы можете переопределить любую конфигурацию, которую хотите, используя это решение.

Ответ 2

Вдохновленный различными кодами (например, нашей командой Stackoverflow (выкрикивая людей)), я создал гораздо более простую версию. Расширение ContextWrapper.

Сначала допустим, что у вас есть 2 кнопки для 2 языков: EN и KH. В onClick для кнопок сохраните код языка в SharedPreferences, затем вызовите метод recreate().

Пример:

@Override
public void onClick(View v) {
    switch(v.getId()) {
        case R.id.btn_lang_en:
            //save "en" to SharedPref here
            break;
        case R.id.btn_lang_kh:
            //save "kh" to SharedPref here
            break;

        default:
        break;
    }
    getActivity().recreate();
}

Затем создайте статический метод, который возвращает ContextWrapper, возможно, в классе Utils (потому что то, что я сделал, lul).

public static ContextWrapper changeLang(Context context, String lang_code){
    Locale sysLocale;

    Resources rs = context.getResources();
    Configuration config = rs.getConfiguration();

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        sysLocale = config.getLocales().get(0);
    } else {
        sysLocale = config.locale;
    }
    if (!lang_code.equals("") && !sysLocale.getLanguage().equals(lang_code)) {
        Locale locale = new Locale(lang_code);
        Locale.setDefault(locale);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            config.setLocale(locale);
        } else {
            config.locale = locale;
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            context = context.createConfigurationContext(config);
        } else {
            context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
        }
    }

    return new ContextWrapper(context);
}

Наконец, загрузите код языка из SharedPreferences в метод attachBaseContext(Context newBase) ВСЕХ ACTIVITY.

@Override
protected void attachBaseContext(Context newBase) {
    String lang_code = "en"; //load it from SharedPref
    Context context = Utils.changeLang(newBase, lang_code);
    super.attachBaseContext(context);
}

БОНУС: Чтобы сохранить пот ладони на клавиатуре, я создал класс LangSupportBaseActivity который расширяет Activity и использует там последний кусок кода. А у меня все остальные действия расширяет LangSupportBaseActivity.

Пример:

public class LangSupportBaseActivity extends Activity{
    ...blab blab blab so on and so forth lines of neccessary code

    @Override
    protected void attachBaseContext(Context newBase) {
        String lang_code = "en"; //load it from SharedPref
        Context context = Utils.changeLang(newBase, lang_code);
        super.attachBaseContext(context);
    }
}

public class HomeActivity extends LangSupportBaseActivity{
    ...blab blab blab
}

Ответ 3

Приведенные выше ответы поставили меня на правильный путь, но оставили несколько вопросов

  1. На Android 7 и 9 я мог бы с радостью перейти на любой язык, кроме приложения по умолчанию. Когда я переключился обратно на язык приложения по умолчанию, на нем показывался последний выбранный язык, что неудивительно, поскольку это переопределило значение по умолчанию (хотя интересно, что это не проблема для Android 8!).
  2. Для языков RTL он не обновлял макеты до RTL

Чтобы исправить первый элемент, я сохранил локаль по умолчанию при запуске приложения.

Примечание Если для языка по умолчанию установлено значение "en", то для языков "enGB" или "enUS" оба должны соответствовать языку по умолчанию (если вы не предоставите для них отдельную локализацию). Аналогично, в приведенном ниже примере, если языком пользователя является arEG (арабский Египет), тогда defLanguage должен быть "ar", а не "arEG"

private Locale defLocale = Locale.getDefault();
private Locale locale = Locale.getDefault();
public static myApplication myApp;
public static Resources res;
private static String defLanguage = Locale.getDefault().getLanguage() + Locale.getDefault().getCountry();
private static sLanguage = "en";
private static final Set<String> SUPPORTEDLANGUAGES = new HashSet<>(Arrays.asList(new String[]{"en", "ar", "arEG"})); 

@Override
protected void attachBaseContext(Context base) {
  if (myApp == null) myApp = this;
  if (base == null) super.attachBaseContext(this);
  else super.attachBaseContext(setLocale(base));
}

@Override
public void onCreate() {
  myApp = this;

  if (!SUPPORTEDLANGUAGES.contains(test)) {
    // The default locale (eg enUS) is not in the supported list - lets see if the language is
    if (SUPPORTEDLANGUAGES.contains(defLanguage.substring(0,2))) {
      defLanguage = defLanguage.substring(0,2);
    }
  }
}

private static void setLanguage(String sLang) {
  Configuration baseCfg = myApp.getBaseContext().getResources().getConfiguration();
  if ( sLang.length() > 2 ) {
    String s[] = sLang.split("_");
    myApp.locale = new Locale(s[0],s[1]);
    sLanguage = s[0] + s[1];
  }
  else {
    myApp.locale = new Locale(sLang);
    sLanguage = sLang;
  }
}

public static Context setLocale(Context ctx) {
  Locale.setDefault(myApp.locale);
  Resources tempRes = ctx.getResources();
  Configuration config = tempRes.getConfiguration();

  if (Build.VERSION.SDK_INT >= 24) {
    // If changing to the app default language, set locale to the default locale
    if (sLanguage.equals(myApp.defLanguage)) {
      config.setLocale(myApp.defLocale);
      // restored the default locale as well
      Locale.setDefault(myApp.defLocale);
    }
    else config.setLocale(myApp.locale);

    ctx = ctx.createConfigurationContext(config);

    // update the resources object to point to the current localisation
    res = ctx.getResources();
  } else {
    config.locale = myApp.locale;
    tempRes.updateConfiguration(config, tempRes.getDisplayMetrics());
  }

  return ctx;
}

Чтобы устранить проблемы с RTL, я расширил AppCompatActivity в соответствии с комментариями фрагментов в этом ответе.

public class myCompatActivity extends AppCompatActivity {
  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(myApplication.setLocale(base));
  }

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (Build.VERSION.SDK_INT >= 17) {
      getWindow().getDecorView().setLayoutDirection(myApplication.isRTL() ?
              View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
    }
  }
}

Ответ 4

Вот тестовый код

Но вы должны следовать правильному шагу, иначе это не сработает Посмотрите

Ответ 5

Начиная с Android 7. 0+ некоторые части моего приложения больше не меняли свой язык. Даже с новыми методами, предложенными выше. Мне помогло обновление как приложения, так и контекста активности. Вот пример Kotlin переопределений подкласса Activity:

private fun setApplicationLanguage(newLanguage: String) {
    val activityRes = resources
    val activityConf = activityRes.configuration
    val newLocale = Locale(newLanguage)
    activityConf.setLocale(newLocale)
    activityRes.updateConfiguration(activityConf, activityRes.displayMetrics)

    val applicationRes = applicationContext.resources
    val applicationConf = applicationRes.configuration
    applicationConf.setLocale(newLocale)
    applicationRes.updateConfiguration(applicationConf,
            applicationRes.displayMetrics)
}

override fun attachBaseContext(newBase: Context?) {
    super.attachBaseContext(newBase)

    setApplicationLanguage("fa");
}

Примечание: updateConfiguration устарела, но в любом случае createConfigurationContext для каждой операции оставил некоторые строки без изменений.

Ответ 6

Представленные решения работают хорошо, но если вы планируете поддерживать дневную/ночную смену тем во время выполнения, вам также необходимо выполнить этот обходной путь.