美文网首页
Android Settings(设置)语言的切换和添加原理流程

Android Settings(设置)语言的切换和添加原理流程

作者: 龙之叶 | 来源:发表于2023-05-20 11:02 被阅读0次

    Settings 部分

    切换流程

    首先是语言和输入的设置界面
    src/com/android/settings/language/LanguageAndInputSettings.java

    ......
    // 注释1_1:加载了language_and_input 这个布局文件
    @Override
    protected int getPreferenceScreenResId() {
       return R.xml.language_and_input;
    }
    ......
    
    

    然后看下language_and_input.xml这个布局文件

    <PreferenceScreen
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:settings="http://schemas.android.com/apk/res-auto"
        android:title="@string/language_settings">
        <!-- 注释1_2: 这里的 phone_language 即对应着 界面上的语言选项 
                     下面的 keyboards_category 则对应着键盘的设置选项
                     注意:这里的 fragment 属性设置的是com.android.settings.localepicker.LocaleListEditor
                     即我们点开语言选项,即由这个页面来实现-->
        <Preference
            android:key="phone_language"
            android:title="@string/phone_language"
            android:icon="@drawable/ic_translate_24dp"
            android:fragment="com.android.settings.localepicker.LocaleListEditor" />
    
        <PreferenceCategory
            android:key="keyboards_category"
            android:title="@string/keyboard_and_input_methods_category">
            <Preference
                android:key="virtual_keyboard_pref"
                android:title="@string/virtual_keyboard_category"
                android:fragment="com.android.settings.inputmethod.VirtualKeyboardFragment"
                settings:keywords="@string/keywords_virtual_keyboard"/>
            <Preference
                android:key="physical_keyboard_pref"
                android:title="@string/physical_keyboard_title"
                android:summary="@string/summary_placeholder"
                android:fragment="com.android.settings.inputmethod.PhysicalKeyboardFragment"/>
        </PreferenceCategory>
        ......
    
    

    另外,Settings里的界面基本都是Preference(界面xml) 和 xxxController(数据逻辑管理) ,语言的controller 是 PhoneLanguagePreferenceController ,具体这里不再详细展开。

    多语言的切换和添加页面:
    src/com/android/settings/localepicker/LocaleListEditor.java

    ......
          @Override
       public void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setHasOptionsMenu(true);
       // 注释1_4: 这里是已经添加列表数据的初始化,获取和填充 这里的 LocaleStore getUserLocaleList()
       // 所涉及的 LocaleList LocalePiker 都是frameworks层的实现,后面会说到
          LocaleStore.fillCache(this.getContext());
          final List<LocaleStore.LocaleInfo> feedsList = getUserLocaleList();
          mAdapter = new LocaleDragAndDropAdapter(this.getContext(), feedsList);
       }
       
       @Override
       public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstState) {
          final View result = super.onCreateView(inflater, container, savedInstState);
          //注释1_4: 这里加载了布局 locale_order_list.xml 比较简单 一个列表 和 一个添加按钮
          final View myLayout = inflater.inflate(R.layout.locale_order_list, (ViewGroup) result);
    
          configureDragAndDrop(myLayout);
          return result;
       }
    
    ......
       // 注释1_5: 这里的方法,对应上面注释1_4。即已添加的语言列表数据
       private List<LocaleStore.LocaleInfo> getUserLocaleList() {
          final List<LocaleStore.LocaleInfo> result = new ArrayList<>();
          final LocaleList localeList = LocalePicker.getLocales();
          for (int i = 0; i < localeList.size(); i++) {
             Locale locale = localeList.get(i);
             result.add(LocaleStore.getLocaleInfo(locale));
          }
          return result;
       }
    
    private void configureDragAndDrop(View view) {
          final RecyclerView list = view.findViewById(R.id.dragList);
          final LocaleLinearLayoutManager llm = new LocaleLinearLayoutManager(getContext(), mAdapter);
          llm.setAutoMeasureEnabled(true);
          list.setLayoutManager(llm);
    
          list.setHasFixedSize(true);
          mAdapter.setRecyclerView(list);
          list.setAdapter(mAdapter);
    
          mAddLanguage = view.findViewById(R.id.add_language);
          // 注释1_6: 这里是添加按钮的监听
          mAddLanguage.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                   FeatureFactory.getFactory(getContext()).getMetricsFeatureProvider()
                         .logSettingsTileClick(INDEX_KEY_ADD_LANGUAGE, getMetricsCategory());
    
                   final Intent intent = new Intent(getActivity(),
                         LocalePickerWithRegionActivity.class);
                   startActivityForResult(intent, REQUEST_LOCALE_PICKER);
             }
       });
       
       ......
    

    已添加语言的由前面分析可以知道,布局locale_order_list.xml里面是由

    自定义RecyclerView列表:src/com/android/settings/localepicker/LocaleRecyclerView.java

    数据适配器:src/com/android/settings/localepicker/LocaleDragAndDropAdapter.java

    来组合实现的,先看下

    LocaleDragAndDropAdapter.java

    ......
          public void doTheUpdate() {
          int count = mFeedItemList.size();
          final Locale[] newList = new Locale[count];
    
          for (int i = 0; i < count; i++) {
             final LocaleStore.LocaleInfo li = mFeedItemList.get(i);
             newList[i] = li.getLocale();
          }
    
          final LocaleList ll = new LocaleList(newList);
          // 注释1_7: 前面都是做准备工作,这里调用这个做更实质的处理
          updateLocalesWhenAnimationStops(ll);
       }
       
       private LocaleList mLocalesToSetNext = null;
       private LocaleList mLocalesSetLast = null;
       
       public void updateLocalesWhenAnimationStops(final LocaleList localeList) {
          if (localeList.equals(mLocalesToSetNext)) {
             return;
          }
    
          // This will only update the Settings application to make things feel more responsive,
          // the system will be updated later, when animation stopped.
          LocaleList.setDefault(localeList);
    
          mLocalesToSetNext = localeList;
          final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator();
          itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() {
             @Override
             public void onAnimationsFinished() {
                   if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) {
                      // All animations finished, but the locale list did not change
                      return;
                   }
                // 注释1_8: 当已添加的语言数目发生了改变,则调用frameworks 层的 
                // LocalePicker的 updateLocales 方法处理,具体后面看
                   LocalePicker.updateLocales(mLocalesToSetNext);
                   mLocalesSetLast = mLocalesToSetNext;
                   new ShortcutsUpdateTask(mContext).execute();
    
                   mLocalesToSetNext = null;
    
                   mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault());
             }
          });
       }
    

    初看可能有点疑惑,这个doTheUpdate方法 在哪里调用和触发的呢?

    LocaleRecyclerView.java

    @Override
    public boolean onTouchEvent(MotionEvent e) {
       if (e.getAction() == MotionEvent.ACTION_UP || e.getAction() == MotionEvent.ACTION_CANCEL) {
          LocaleDragAndDropAdapter adapter = (LocaleDragAndDropAdapter) this.getAdapter();
          if (adapter != null) {
                // 注释1_9: 这里一目了然,当列表的触摸事件手指离开的时候,便会触发这个更新
                adapter.doTheUpdate();
          }
       }
       return super.onTouchEvent(e);
    }
    

    关于Settings 切换已选语言的处理流程基本就这些了,然后说下添加的流程。

    添加流程

    前面的介绍在 注释1_6 处,LocaleListEditor.java的添加语言的按钮,点击监听事件里是流程的入口,会跳转

    src/com/android/settings/localepicker/LocalePickerWithRegionActivity.java

    @Override
    public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       getActionBar().setDisplayHomeAsUpEnabled(true);
    // 注释1_10: 这里的页面以及逻辑实现都交给了 LocalePickerWithRegion ,
    // 这个类的实现也是在frameworks 层
       final LocalePickerWithRegion selector = LocalePickerWithRegion.createLanguagePicker(
                this, LocalePickerWithRegionActivity.this, false /* translate only */);
       getFragmentManager()
                .beginTransaction()
                .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                .replace(android.R.id.content, selector)
                .addToBackStack(PARENT_FRAGMENT_NAME)
                .commit();
    }
    

    所以,添加的流程,Settings 的添加流程到这里也就结束了

    Frameworks 部分

    切换流程

    由前面分析可知:切换语言Settings 处理跳转到 framework 的入口是LocalePickerupdateLocales方法

    base/core/java/com/android/internal/app/LocalePicker.java

    ......
    
       /**
       * Requests the system to update the list of system locales.
       * Note that the system looks halted for a while during the Locale migration,
       * so the caller need to take care of it.
       */
       @UnsupportedAppUsage
       public static void updateLocales(LocaleList locales) {
          if (locales != null) {
             locales = removeExcludedLocales(locales);
          }
          // Note: the empty list case is covered by Configuration.setLocales().
    
          try {
             final IActivityManager am = ActivityManager.getService();
             final Configuration config = am.getConfiguration();
          // 注释2_1:这里对切换语言后的数据封装到Configuration里,用于后面流程处理
             config.setLocales(locales);
             config.userSetLocale = true;
          // 注释2_2: 这里通过ActivityManager的一个Binder服务,调用
          // updatePersistentConfigurationWithAttribution 继续处理  
             am.updatePersistentConfigurationWithAttribution(config,
                      ActivityThread.currentOpPackageName(), null);
             // Trigger the dirty bit for the Settings Provider.
             BackupManager.dataChanged("com.android.providers.settings");
          } catch (RemoteException e) {
             // Intentionally left blank
          }
       }
       
    ......
    

    这里是通过 Binder 获取 ActivityManager 的一个服务代理对象,来处理 实现方法是 updatePersistentConfigurationWithAttribution
    这里ActivityManager的Binder 实际处理对象是:
    base/services/core/java/com/android/server/am/ActivityManagerService.java

    @Override
    public void updatePersistentConfiguration(Configuration values) {
       updatePersistentConfigurationWithAttribution(values,
                Settings.getPackageNameForUid(mContext, Binder.getCallingUid()), null);
    }
    
    @Override
    public void updatePersistentConfigurationWithAttribution(Configuration values,
          String callingPackage, String callingAttributionTag) {
       enforceCallingPermission(CHANGE_CONFIGURATION, "updatePersistentConfiguration()");
       enforceWriteSettingsPermission("updatePersistentConfiguration()", callingPackage,
                callingAttributionTag);
       if (values == null) {
          throw new NullPointerException("Configuration must not be null");
       }
    
       int userId = UserHandle.getCallingUserId();
    // 注释2_3:可以看到,这里流程将处理方法又传递给了 mActivityTaskManager 实例的updatePersistentConfiguration 方法
       mActivityTaskManager.updatePersistentConfiguration(values, userId);
    }
    

    上面的mActivityTaskManager的实例即 ActivityTaskManagerService.java,来看看内部实现

    base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
    通过内部层层调用,以及一些判断条件的筛选,最后会执行到(方法内部处理逻辑很多,挑重点看下):

    /** Update default (global) configuration and notify listeners about changes. */
    int updateGlobalConfigurationLocked(@NonNull Configuration values, boolean initLocale,
          boolean persistent, int userId) {
          
       ......
       // 注释2_4:这里对切换语言后封装的Configuration做后续处理的预检查和判断
          if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {
          final LocaleList locales = values.getLocales();
          int bestLocaleIndex = 0;
          if (locales.size() > 1) {
                if (mSupportedSystemLocales == null) {
                // 注释2_5:这里是获取系统资源配置支持的语言,也就是说实际能真的支持的语言
                   mSupportedSystemLocales = Resources.getSystem().getAssets().getLocales();
                }
                // 注释2_6:这里是通过方法计算得到在所有支持语言列表里 最匹配的语言的所在列表的 索引
                // 具体算法可getFirstMatchIndex 一路点进去看
                bestLocaleIndex = Math.max(0, locales.getFirstMatchIndex(mSupportedSystemLocales));
          }
          // 注释2_7:这里是修改系统属性值,即 当前系统的默认语言
          SystemProperties.set("persist.sys.locale",
                   locales.get(bestLocaleIndex).toLanguageTag());
          LocaleList.setDefault(locales, bestLocaleIndex);
       // 注释2_8:这里是将切换语言的动作通过 handler - message 的形式分发出去,以通知系统各个地方刷新
          final Message m = PooledLambda.obtainMessage(
                   ActivityTaskManagerService::sendLocaleToMountDaemonMsg, this,
                   locales.get(bestLocaleIndex));
          mH.sendMessage(m);
       }
       ......      
    }
    

    切换语言的流程,简单的流程就到这里,其余就不在详细展开了

    添加流程

    由前面分析可知,添加流程由 Settings 的 LocalePickerWithRegionActivity.java到 frameworks 的 LocalePickerWithRegion.java
    base/core/java/com/android/internal/app/LocalePickerWithRegion.java

    ......
    
       @Override
       public void onListItemClick(ListView l, View v, int position, long id) {
          final LocaleStore.LocaleInfo locale =
                   (LocaleStore.LocaleInfo) getListAdapter().getItem(position);
    
          if (locale.getParent() != null) {
             if (mListener != null) {
                   mListener.onLocaleSelected(locale);
             }
             returnToParentFrame();
          } else {
          // 注释2_9:这里是语言列表的点击事件,即代表这开启了被点击语言的添加流程
          // 这里是 用到本类的 createCountryPicker 方法 
             LocalePickerWithRegion selector = LocalePickerWithRegion.createCountryPicker(
                      getContext(), mListener, locale, mTranslatedOnly /* translate only */);
             if (selector != null) {
                   getFragmentManager().beginTransaction()
                         .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN)
                         .replace(getId(), selector).addToBackStack(null)
                         .commit();
             } else {
                   returnToParentFrame();
             }
          }
       }
    ......
    
    // 注释2_10_1:看上一步,调用的是这个4个参数的方法
    private static LocalePickerWithRegion createCountryPicker(Context context,
             LocaleSelectedListener listener, LocaleStore.LocaleInfo parent,
             boolean translatedOnly) {   
          LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
          // 注释2_10_2:重要是这一步,调用了localePicker.setListener,localePicker是LocalePickerWithRegion
          // 实例化的对象,于是将流程传递给了本类的setListener方法
          boolean shouldShowTheList = localePicker.setListener(context, listener, parent,
                   translatedOnly);
          return shouldShowTheList ? localePicker : null;
       }
    
       public static LocalePickerWithRegion createLanguagePicker(Context context,
             LocaleSelectedListener listener, boolean translatedOnly) {
          LocalePickerWithRegion localePicker = new LocalePickerWithRegion();
          localePicker.setListener(context, listener, /* parent */ null, translatedOnly);
          return localePicker;
       }
    
    ......
    
    private boolean setListener(Context context, LocaleSelectedListener listener,
             LocaleStore.LocaleInfo parent, boolean translatedOnly) {
          this.mParentLocale = parent;
          this.mListener = listener;
          this.mTranslatedOnly = translatedOnly;
          setRetainInstance(true);
       // 注释2_11_1:这部分的逻辑是 获取已经添加的语言列表,作为需要忽略的部分,毕竟已经添加的,还能再添加就不合理了
          final HashSet<String> langTagsToIgnore = new HashSet<>();
          if (!translatedOnly) {
             final LocaleList userLocales = LocalePicker.getLocales();
             final String[] langTags = userLocales.toLanguageTags().split(",");
             Collections.addAll(langTagsToIgnore, langTags);
          }
       // 注释2_11_2:这里做了选择的语言不为空的判断后,就将逻辑流程 传递给了 LocaleStore.java 
       // 的getLevelLocales方法,从而获得一个新的已添加语言列表
          if (parent != null) {
             mLocaleList = LocaleStore.getLevelLocales(context,
                      langTagsToIgnore, parent, translatedOnly);
             if (mLocaleList.size() <= 1) {
                   if (listener != null && (mLocaleList.size() == 1)) {
                      listener.onLocaleSelected(mLocaleList.iterator().next());
                   }
                   return false;
             }
          } else {
             mLocaleList = LocaleStore.getLevelLocales(context, langTagsToIgnore,
                      null /* no parent */, translatedOnly);
          }
    
          return true;
    

    由于上面的添加流程,已经走完,转向了LocaleStore.java 这里,那就看看

    base/core/java/com/android/internal/app/LocaleStore.java

    ......
    
       @UnsupportedAppUsage
       public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
             LocaleInfo parent, boolean translatedOnly) {
          // 注释2_12_1:这个方法很关键,后面逻辑处理保存的数据,需要这里先做预处理的
          fillCache(context);
          String parentId = parent == null ? null : parent.getId();
    
          HashSet<LocaleInfo> result = new HashSet<>();
          for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
             int level = getLevel(ignorables, li, translatedOnly);
             if (level == 2) {
                   if (parent != null) { // region selection
                      if (parentId.equals(li.getParent().toLanguageTag())) {
                         result.add(li);
                      }
                   } else { // language selection
                      if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
                         result.add(li);
                      } else {
                         result.add(getLocaleInfo(li.getParent()));
                      }
                   }
             }
          }
          return result;
       }
       
       ......// fillCache方法内部逻辑不少,只看下核心部分
       
       @UnsupportedAppUsage
       public static void fillCache(Context context) {
          if (sFullyInitialized) {
             return;
          }
          ......
          // 注释2_12_2:LocalePicker.getSupportedLocales(context) 这个方法很关键 ,他是整个数据处理的来源
       for (String localeId : LocalePicker.getSupportedLocales(context)) {
             if (localeId.isEmpty()) {
                   throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
             }
             LocaleInfo li = new LocaleInfo(localeId);
    
             if (LocaleList.isPseudoLocale(li.getLocale())) {
                   if (isInDeveloperMode) {
                      li.setTranslated(true);
                      li.mIsPseudo = true;
                      li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
                   } else {
                      // Do not display pseudolocales unless in development mode.
                      continue;
                   }
             }
    
             if (simCountries.contains(li.getLocale().getCountry())) {
                   li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
             }
                // 注释2_12_3: 这也是2_12_1处后续逻辑处理 sLocaleCache的加载的地方
             sLocaleCache.put(li.getId(), li);
             final Locale parent = li.getParent();
             if (parent != null) {
                   String parentId = parent.toLanguageTag();
                   if (!sLocaleCache.containsKey(parentId)) {
                      sLocaleCache.put(parentId, new LocaleInfo(parent));
                   }
             }
          }
          ......
       }
    

    看到上一步,其实数据源来自LocalePicker.getSupportedLocales(context),那就看看
    base/core/java/com/android/internal/app/LocalePicker.java

    ......
    // 注释2_13_1:这个方法也很关键,这是获取系统资源配置支持的语言列表
    public static String[] getSystemAssetLocales() {
          return Resources.getSystem().getAssets().getLocales();
       }
    
       public static String[] getSupportedLocales(Context context) {
       // 注释2_13:好了,找到这里,似乎真相大白了,所有的语言列表,都是从这个supported_locales 资源数组获取的
       // 资源位置:base/core/res/res/values/locale_config.xml
          String[] allLocales = context.getResources().getStringArray(R.array.supported_locales);
          Predicate<String> localeFilter = getLocaleFilter();
          if (localeFilter == null) {
             return allLocales;
          }
    
          List<String> result = new ArrayList<>(allLocales.length);
          for (String locale : allLocales) {
             if (localeFilter.test(locale)) {
                   result.add(locale);
             }
          }
          int localeCount = result.size();
          return (localeCount == allLocales.length) ? allLocales
                   : result.toArray(new String[localeCount]);
       }
       
    ......
    

    好了分析到这里,加载的流程也基本结束了。

    最后总结

    • R.array.supported_locales 获取到的语言列表 是config 里的xml 配置,如果要对系统做语言支持上的变动,可以改这里
    • Resources.getSystem().getAssets().getLocales()才是系统真正可以支持的语言列表,ActivityTaskManagerService.java的updateGlobalConfigurationLocked 方法里判断选择的语言是不是可用,也是用的这个资源作为判断依据
    • R.array.supported_locales里的语言配置,可能Resources.getSystem().getAssets().getLocales()不能全部支持,这就会导致我们添加了某个语言,切换到它,但是却没效果,所以有个更好的优化方案这边分享一个:

    修改 base/core/java/com/android/internal/app/LocalePicker.java 的 getSupportedLocales 方法

    public static String[] getSupportedLocales(Context context) {
       String[] allLocales = context.getResources().getStringArray(R.array.supported_locales);
       // 设置切换语言不支持的问题__配置可支持的语言筛掉没有系统资源配置的
       /* 
       Predicate<String> localeFilter = getLocaleFilter();
       if (localeFilter == null) {
          return allLocales;
       }
    
       List<String> result = new ArrayList<>(allLocales.length);
       for (String locale : allLocales) {
          if (localeFilter.test(locale)) {
                result.add(locale);
          }
       }
       */
       Predicate<String> localeFilter = getLocaleFilter();
       List<String> result = new ArrayList<>(allLocales.length);
       String[] sysAssetLocales = getSystemAssetLocales();
       for(String locale : allLocales){
          if(!isCongenericLocales(sysAssetLocales,locale)){
                // 本地资源配没有有配置直接跳过继续检查下一个
                continue;
          }
          if(localeFilter == null){
                result.add(locale);
          }else{
                if (localeFilter.test(locale)) {
                   result.add(locale);
                }
          }
       }
    
       int localeCount = result.size();
       return (localeCount == allLocales.length) ? allLocales
                : result.toArray(new String[localeCount]);
    }
    
    // 设置切换语言不支持的问题__筛查系统没有配置资源的语言
    private static boolean isCongenericLocales(String[] sysAssetLocales, String xmlLocales){
       boolean result = false;
       try{
          if(xmlLocales !=null){
                String[] xmlPartHead = xmlLocales.split("-",2);
                for(String assetLocales : sysAssetLocales){
                   String[] sysPartHead = assetLocales.split("-",2);
                   if(xmlPartHead[0].equals(sysPartHead[0])){
                      result = true;
                      break;
                   }
                }
          }
       }catch(Exception e){
          Log.e(TAG, "Failed to deal sysAssetLocales and xmlLocales compare!", e);
       }
       return result;
    }
    

    相关文章

      网友评论

          本文标题:Android Settings(设置)语言的切换和添加原理流程

          本文链接:https://www.haomeiwen.com/subject/sgqmjdtx.html