言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Activity#getTitle()はActivity/Applicationのresourcesを使わない

このエントリは先日参加した「まったりAndroid Framework Code Reading #4」の成果です٩( 'ω’ )و

mandroidfcr.connpass.com

知りたかったこと

AppCompatActivity#setSupportActionBar()した時に、Frameworkのバグと思しき挙動を見つけてしまいました。
setするToolBarにtitleを設定していない場合、ActionBarのタイトルにアプリで設定したlocaleではなくdevice defaultのlocaleが適用されてしまうのです:;(∩´﹏`∩);:
回避策としてToolBarにapplicationのcontextからとってきたstringをsetすればいいんですけど、せっかくなので Frameworkがタイトルに表示するstringをどのように取得するのか知りたくてFrameworkのコードを読んでみました٩( 'ω’ )و

わかったこと

  • setSupportActionBar()でsetするToolbarにtitleが設定されていればそれを、設定されていなかったらAppCompatDelegateImplV7のContextからgetTitle()したものが使われる
  • AppCompatDelegateImplV7のContextはAppCompatActivity自身
  • しかしgetTitle()の時に使われるresourcesはAppCompatActivity自身のものではなく、ActivityThreadがActivityをattachする時に使うresources
  • 上記resourcesはApplicationPackageManager#getResourcesForApplication() で取得したもので、これがdevice defaultのlocaleになっている
  • device defaultなlocaleになる理由は、引数のoverride configurationにapplicationのresourcesに設定されたconfigurationではなくnullを設定しているから。

ActivityThread.java

   1844     Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
   1845             String[] libDirs, int displayId, LoadedApk pkgInfo) {
   1846         return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
// ★最後から3番目の引数がoverride configurationだが、ここでnullを指定しているのでdevice defaultの物が設定される
   1847                 displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader()); 
   1848     }

ざっくりまとめると以上ですが、コードを読んだメモを置いておきます。

ToolBarについて

AppCompatActivity.java

    public void setSupportActionBar(@Nullable Toolbar toolbar) {
        getDelegate().setSupportActionBar(toolbar);
    }

AppCompatDelegateImplV7.java

    189     @Override
    190     public void setSupportActionBar(Toolbar toolbar) {

    212         if (toolbar != null) {
    213             final ToolbarActionBar tbab = new ToolbarActionBar(toolbar,
    214                     ((Activity) mContext).getTitle(), mAppCompatWindowCallback);
    215             mActionBar = tbab;

ToolbarActionBar.java

     73     public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) {
     74         mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false);
     75         mWindowCallback = new ToolbarCallbackWrapper(callback);
     76         mDecorToolbar.setWindowCallback(mWindowCallback);
     77         toolbar.setOnMenuItemClickListener(mMenuClicker);
     78         mDecorToolbar.setWindowTitle(title);
     79     }

ToolbarWidgetWrapper.java

     90     public ToolbarWidgetWrapper(Toolbar toolbar, boolean style) {
     91         this(toolbar, style, R.string.abc_action_bar_up_description,
     92                 R.drawable.abc_ic_ab_back_material);
     93     }
     95     public ToolbarWidgetWrapper(Toolbar toolbar, boolean style,
     96             int defaultNavigationContentDescription, int defaultNavigationIcon) {
     97         mToolbar = toolbar;
     98         mTitle = toolbar.getTitle();
     99         mSubtitle = toolbar.getSubtitle();
    100         mTitleSet = mTitle != null;

    105         if (style) {
    106             final CharSequence title = a.getText(R.styleable.ActionBar_title);
    107             if (!TextUtils.isEmpty(title)) {
    108                 setTitle(title);
    109             }
    237     @Override
    238     public void setWindowTitle(CharSequence title) {
    239         // "Real" title always trumps window title.
    240         if (!mTitleSet) {
    241             setTitleInt(title);
    242         }
    243     }

AppCompatDelegateImplBase.java

     67     AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) {
     68         mContext = context;

AppCompatDelegate.java

    185     private static AppCompatDelegate create(Context context, Window window,
    186             AppCompatCallback callback) {
    187         final int sdk = Build.VERSION.SDK_INT;
    188         if (BuildCompat.isAtLeastN()) {
    189             return new AppCompatDelegateImplN(context, window, callback);
    190         } else if (sdk >= 23) {
    191             return new AppCompatDelegateImplV23(context, window, callback);
    192         } else if (sdk >= 14) {
    193             return new AppCompatDelegateImplV14(context, window, callback);
    194         } else if (sdk >= 11) {
    195             return new AppCompatDelegateImplV11(context, window, callback);
    196         } else {
    197             return new AppCompatDelegateImplV7(context, window, callback);
    198         }
    199     }

AppCompatActivity.java

    509     public AppCompatDelegate getDelegate() {
    510         if (mDelegate == null) {
    511             mDelegate = AppCompatDelegate.create(this, this);
    512         }
    513         return mDelegate;
    514     }

getTitle()で取得するStringについて

Activity.java

   5635     public final CharSequence getTitle() {
   5636         return mTitle;
   5637     }
   6593     final void attach(Context context, ActivityThread aThread,
   6594             Instrumentation instr, IBinder token, int ident,
   6595             Application application, Intent intent, ActivityInfo info,
   6596             CharSequence title, Activity parent, String id,
   6597             NonConfigurationInstances lastNonConfigurationInstances,
   6598             Configuration config, String referrer, IVoiceInteractor voiceInteractor,
   6599             Window window) {

   6626         mTitle = title;

ActivityThread.java

   2514     private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

   2567                 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());

   2580                 activity.attach(appContext, this, getInstrumentation(), r.token,
   2581                         r.ident, app, r.intent, r.activityInfo, title, r.parent,
   2582                         r.embeddedID, r.lastNonConfigurationInstances, config,
   2583                         r.referrer, r.voiceInteractor, window);

ComponentInfo.java

     92     @Override public CharSequence loadLabel(PackageManager pm) {
     93         if (nonLocalizedLabel != null) {
     94             return nonLocalizedLabel;
     95         }
     96         ApplicationInfo ai = applicationInfo;
     97         CharSequence label;
     98         if (labelRes != 0) {
     99             label = pm.getText(packageName, labelRes, ai);
    100             if (label != null) {
    101                 return label;
    102             }
    103         }
    104         if (ai.nonLocalizedLabel != null) {
    105             return ai.nonLocalizedLabel;
    106         }
    107         if (ai.labelRes != 0) {
    108             label = pm.getText(packageName, ai.labelRes, ai);
    109             if (label != null) {
    110                 return label;
    111             }
    112         }
    113         return name;
    114     }

ApplicationPackageManager.java

   1491     @Override
   1492     public CharSequence getText(String packageName, @StringRes int resid,
   1493                                 ApplicationInfo appInfo) {
   1494         ResourceName name = new ResourceName(packageName, resid);
   1495         CharSequence text = getCachedString(name);
   1496         if (text != null) {
   1497             return text;
   1498         }
   1499         if (appInfo == null) {
   1500             try {
   1501                 appInfo = getApplicationInfo(packageName, sDefaultFlags);
   1502             } catch (NameNotFoundException e) {
   1503                 return null;
   1504             }
   1505         }
   1506         try {
   1507             Resources r = getResourcesForApplication(appInfo);
   1508             text = r.getText(resid);
   1509             putCachedString(name, text);
   1510             return text;
   1241     @Override
   1242     public Resources getResourcesForApplication(@NonNull ApplicationInfo app)
   1243             throws NameNotFoundException {
   1244         if (app.packageName.equals("system")) {
   1245             return mContext.mMainThread.getSystemContext().getResources();
   1246         }
   1247         final boolean sameUid = (app.uid == Process.myUid());
   1248         try {
   1249             return mContext.mMainThread.getTopLevelResources(
   1250                     sameUid ? app.sourceDir : app.publicSourceDir,
   1251                     sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
   1252                     app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
   1253                     mContext.mPackageInfo);
   1254         } catch (Resources.NotFoundException cause) {
   1255             final NameNotFoundException ex =
   1256                     new NameNotFoundException("Unable to open " + app.publicSourceDir);
   1257             ex.initCause(cause);
   1258             throw ex;
   1259         }
   1260     }

ActivityThread.java

   1844     Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
   1845             String[] libDirs, int displayId, LoadedApk pkgInfo) {
   1846         return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
   1847                 displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
   1848     }

ResourcesManager.java

    625     public @NonNull Resources getResources(@Nullable IBinder activityToken,
    626             @Nullable String resDir,
    627             @Nullable String[] splitResDirs,
    628             @Nullable String[] overlayDirs,
    629             @Nullable String[] libDirs,
    630             int displayId,
    631             @Nullable Configuration overrideConfig,
    632             @NonNull CompatibilityInfo compatInfo,
    633             @Nullable ClassLoader classLoader) {
    634         try {
    635             Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
    636             final ResourcesKey key = new ResourcesKey(
    637                     resDir,
    638                     splitResDirs,
    639                     overlayDirs,
    640                     libDirs,
    641                     displayId,
    642                     overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
    643                     compatInfo);
    644             classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
    645             return getOrCreateResources(activityToken, key, classLoader);
    646         } finally {
    647             Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    648         }
    649     }
    517     private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
    518             @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    519         synchronized (this) {

    553             } else {
    554                 // Clean up any dead references so they don't pile up.
    555                 ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
    556 
    557                 // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
    558                 ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
    559                 if (resourcesImpl != null) {
    560                     if (DEBUG) {
    561                         Slog.d(TAG, "- using existing impl=" + resourcesImpl);
    562                     }
    563                     return getOrCreateResourcesLocked(classLoader, resourcesImpl);
    564                 }
    565 
    566                 // We will create the ResourcesImpl object outside of holding this lock.
    567             }
    568         }
    569 
    570         // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
    571         ResourcesImpl resourcesImpl = createResourcesImpl(key);