Activity#getTitle()はActivity/Applicationのresourcesを使わない
このエントリは先日参加した「まったりAndroid Framework Code Reading #4」の成果です٩( 'ω’ )و
知りたかったこと
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を設定しているから。
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);