Как изменить название деятельности в attach()

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

Наблюдаемое поведение заключается в том, что активность будет иметь локализованное название первого тестового запуска также и для каждого последующего запуска. Таким образом, независимо от того, на каком языке работает мой телефон, название будет корректно локализовано для первого запуска параметризованного теста и будет одинаковым для каждого последующего.

Хотя перезапись локалей сама по себе работает для любых ресурсов, она будет работать только один раз для заголовка действий, если это установлено параметром AndroidManifest.xml.

Действия, кажется, получают свой заголовок один раз в attach, и все, что вызывает присоединение, похоже, кэширует заголовок в локали, в которой приложение было впервые запущено.

final void attach(Context context, ActivityThread aThread,
        Instrumentation instr, IBinder token, int ident,
        Application application, Intent intent, ActivityInfo info,
   ---> CharSequence title, Activity parent, String id,
        NonConfigurationInstances lastNonConfigurationInstances,
        Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
    attachBaseContext(context);

Поскольку ресурсы всегда правильно локализуются, обходным путем будет вызов setTitle(R.string.title) или просто getActionBar().setTitle(R.string.setTitle), но я бы не хотел менять действия исключительно в целях тестирования.

Вопрос. Как изменить заголовок, с которым запускается активность после первого запуска теста? Как упоминалось выше, похоже, что это кешируется и не обновляется должным образом, и уничтожение приложения для его перезапуска не пройдет инструментальный тест.

Испытательная установка

Весь тестовый проект можно найти здесь, на GitHub (Localization.java содержит неудачные модульные тесты с проблемой описан здесь) и использует параметризованный модульный тест в сочетании с UIAutomator.

Цель – сделать серию снимков экрана, не зная слишком много о самом приложении (UIAutomator), а также о том, что приложение не нужно модифицировать для теста.

Изменение локали:

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

public LocalizationTest(Locale locale) {
    mLocale = locale;
    Configuration config = new Configuration();
    Locale.setDefault(mLocale);
    config.setLocale(mLocale);

    Resources resources = InstrumentationRegistry.getTargetContext().getResources();
    resources.updateConfiguration(config, resources.getDisplayMetrics());

    resources.flushLayoutCache();
}

Что не работает:

Я, очевидно, пытался установить локаль таким же образом для целевого контекста, контекста приложения и активности (что, вероятно, в любом случае было бы слишком поздно).

Я вижу, что attach вызывается из Instrumentation, но простое создание нового приложения и попытка запуска активности также не локализуют заголовок.

Intent intent = context.getPackageManager().getLaunchIntentForPackage(BuildConfig.APPLICATION_ID);
context = InstrumentationRegistry.getInstrumentation().newApplication(App.class,
                InstrumentationRegistry.getTargetContext());
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);

person David Medenjak    schedule 22.01.2016    source источник


Ответы (2)


Строка заголовка кэшируется в диспетчере пакетов ApplicationPackageManager в статическом файле sStringCache.

Хотя существует метод static void configurationChanged(), который очищает кеш, похоже, он не вызывается при ручных изменениях. Отсюда и описанная проблема с неправильно локализованным названием активности после первого вызова.

Решение этой проблемы стало возможным благодаря использованию рефлексии для загрузки класса и самостоятельного вызова метода. Это немного грязно, так как он обращается к частному методу, но это работает.

// as before
Configuration config = new Configuration();
Locale.setDefault(mLocale);
config.setLocale(mLocale);

Resources resources = context.getResources();
resources.updateConfiguration(config, resources.getDisplayMetrics());

// CLEAR the cache!
Method method = getClass().getClassLoader()
        .loadClass("android.app.ApplicationPackageManager")
        .getDeclaredMethod("configurationChanged");
method.setAccessible(true);
method.invoke(null);

В качестве альтернативы вы можете использовать общедоступные методы в другом непубличном API, который, в свою очередь, также будет вызывать вышеуказанный метод. Все еще грязно, но не вызывает частные методы.

Похоже, что вы можете опустить resources.updateConfiguration(...);, используя этот метод, поскольку он также позаботится об этом.

// Clear the cache. 
Object thread = getClass().getClassLoader()
        .loadClass("android.app.ActivityThread")
        .getMethod("currentActivityThread")
        .invoke(null);
Method method = getClass().getClassLoader()
        .loadClass("android.app.ActivityThread")
        .getMethod("applyConfigurationToResources", Configuration.class);
method.invoke(thread, config);
person David Medenjak    schedule 31.01.2016

Мы обнаружили, что заголовок активности установлен в onAttach() с заголовком, предоставленным менеджером активности. Следовательно, я думаю, вам нужно изменить языковой стандарт системы.

Для этого тест может использовать отражение на ActivityManagerNative для обновления конфигурации:

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public Localization(Locale locale) throws ClassNotFoundException, NoSuchMethodException,
        InvocationTargetException, IllegalAccessException {
    Context context = InstrumentationRegistry.getTargetContext();
    log(context.toString());
    log(context.getApplicationContext().toString());

    mLocale = locale;

    Class<?> amClass = Class.forName("android.app.ActivityManagerNative");
    Method getDefaultMethod = amClass.getDeclaredMethod("getDefault");
    Object iActivityManager = getDefaultMethod.invoke(null /* static method */);
    Method updateConfigurationMethod =
            amClass.getMethod("updateConfiguration", Configuration.class);
    Configuration configuration = new Configuration(context
            .getResources().getConfiguration());
    configuration.locale = locale;
    updateConfigurationMethod.invoke(iActivityManager, configuration);
}

Для этого дайте разрешение вашему приложению (это разрешение подписано ключами отладки, недостаточно добавить его в AndroidManifest)

adb shell pm grant at.bleeding182.testing.instrumentationtest android.permission.CHANGE_CONFIGURATION

Я протестировал это решение и могу подтвердить, что локаль теперь правильно изменена, и тест проходит — makeScreenshot довольно ненадежно, но это в другой раз.

Вам нужно будет изменить рабочий процесс вашей фабрики программного обеспечения:

  1. Обновите apk приложения и протестируйте apk
  2. Дайте android.permission.CHANGE_CONFIGURATION тестовому apk
  3. Запустите тесты
person rds    schedule 31.01.2016
comment
Полностью измененный ответ, основанный на том, что вы используете UIAutomator. Это может относиться и к тестам на эспрессо. - person rds; 31.01.2016
comment
Большое спасибо за усилия, я с радостью награждаю вас наградой. Ваш подход помог мне найти решение, не требуя дополнительного разрешения - person David Medenjak; 31.01.2016
comment
Спасибо за щедрость, ваше решение действительно чище. - person rds; 01.02.2016