Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change activities title in attach()

I want to run a parameterized Instrumentation Test featuring different locales to run the same test with all supported languages.

The observed behavior is that the activity will have the localized title of the first test run also for every following run. So no matter which language my phone is in, the title will be correctly localized for the first parameterized test run, and still be the same for every following one.

While overwriting locales itself works for any resources, it will work only once for the activities title if set by the AndroidManifest.xml.

Activities seem to get their title set once in attach, and whatever is calling attach seems to be caching the title in the locale the app was first launched in.

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

Since the resources always get correctly localized, a workaround would be to call setTitle(R.string.title) or just getActionBar().setTitle(R.string.setTitle), but I would like not to change the activities solely for testing purposes.

Question: How can I change the title the activity gets launched with after the first test run? As mentioned above, this seems to get cached and not properly updated, and killing the app to restart it will fail the instrumentation test.

Test setup

The whole test project can be found here on GitHub (Localization.java contains the currently failing unit tests with the issue described here) and is using a Parameterized Unit Test in in conjunction with UIAutomator.

The goal is to take a batch of screenshots without knowing too much about the app itself (UIAutomator), and the app not having to be modified for the test either.

Changing the locale:

I'm successfully changing the locale before every test, and my texts get correctly displayed by doing the following, also I have multiple assertions in place making sure that the resources are in fact the right locale.

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

What doesn't work:

I obviously tried setting the locale in the same way on the target context, the application context, and the activity (which would probably be too late anyways).

I see that attach gets called from Instrumentation, but just creating a new App and trying to launch the activity will not localize the title either.

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);
like image 494
David Medenjak Avatar asked Jan 22 '16 18:01

David Medenjak


2 Answers

The title string gets cached within the package manager ApplicationPackageManager in a static sStringCache.

While there is a method static void configurationChanged() which clears the cache, it does not seem to get invoked on manual changes. Hence the decribed problem with the wrongly localized activity title after the first invocation.

The solution to this is made possible using reflection to load the class and invoke the method oneself. This is kinda dirty since it is accessing a private method, but it works.

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

Alternatively you can use public methods on another non-public API which in turn will also invoke the above method. Still dirty but not invoking private methods.

It seems like you can omit resources.updateConfiguration(...); by using this method though since it will also take care of that.

// 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);
like image 179
David Medenjak Avatar answered Sep 20 '22 20:09

David Medenjak


We found out that the activity title is set in onAttach() with the title provided by the Activity manager. Hence, I think you need to change the system locale instead.

To do so, the test can use reflection on ActivityManagerNative to update the configuration:

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

To do so, give permission to your app (this permission is signed with debug keys, it's not sufficient to add it in the AndroidManifest)

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

I have tested this solution and can confirm the locale is now correctly changed, and the test passes — makeScreenshot is quite flaky, but thats' for another day.

You will need to change the workflow of your software factory:

  1. Update the app apk and test apk
  2. Give android.permission.CHANGE_CONFIGURATION to the test apk
  3. Run the tests
like image 35
rds Avatar answered Sep 21 '22 20:09

rds