如何更改 attach() 中的活动标题

How to change activities title in attach()

我想 运行 具有不同语言环境的参数化 Instrumentation 测试 运行 使用所有支持语言的相同测试。

观察到的行为是 activity 将具有第一个测试 运行 的 本地化标题,每个后续 运行 也将具有该标题。因此,无论我的 phone 使用哪种语言,第一个 参数化测试 运行 的标题都将被正确本地化 ,并且对于接下来的每个测试都是相同的.

虽然覆盖语言环境本身适用于任何资源,但它 仅对 活动标题 有效一次 如果 [=13] =].

Activity 似乎在 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),但我不希望仅出于测试目的更改活动。

问题: 如何更改 activity 的标题 第一次测试 运行?如上所述,这似乎被缓存并且没有正确更新,并且杀死应用程序以重新启动它将使仪器测试失败。

测试设置

整个测试项目可以是 found here on GitHubLocalization.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();
}

什么不起作用:

我显然尝试在目标上下文、应用程序上下文和 activity 上以相同的方式设置语言环境(无论如何都可能为时已晚)。

我看到 attachInstrumentation 调用,但只是创建一个新应用程序并尝试启动 activity 也不会本地化标题。

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);

我们发现 activity 标题是在 onAttach() 中设置的,标题由 Activity 经理提供。因此,我认为您需要改为更改系统区域设置。

为此,测试可以在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. 运行 测试

标题字符串以静态 sStringCache.

缓存在包管理器 ApplicationPackageManager

虽然有一种方法 static void configurationChanged() 可以清除缓存,但似乎不会在手动更改时调用它。因此,在第一次调用后出现了错误本地化 activity 标题的问题。

解决这个问题的方法是使用反射来加载 class 并自己调用方法。 这有点脏,因为它正在访问私有方法,但它有效。

// 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);

或者,您可以在另一个 non-public API 上使用 public 方法,这反过来也会调用上述方法。 仍然很脏,但没有调用私有方法。

您似乎可以使用此方法省略 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);