平台:Android7.1.1 高通MSM8937 骁龙430 红米手机

一、现象

频繁切换语言后快速设置中的运营商名称(中国移动,中国联通,中国电信)不更新或者更新不及时。

二、分析

快速设置中的运营商名称是在SystemUI中处理的,具体处理代码在MobileSignalController.java中

/**
     * Updates the network's name based on incoming spn and plmn.
     */
    void updateNetworkName(boolean showSpn, String spn, String dataSpn,
            boolean showPlmn, String plmn) {
        mLastShowSpn = showSpn;
        mLastSpn = spn;
        mLastDataSpn = dataSpn;
        mLastShowPlmn = showPlmn;
        mLastPlmn = plmn;
        if (CHATTY) {
            Log.d("CarrierLabel", "updateNetworkName showSpn=" + showSpn
                    + " spn=" + spn + " dataSpn=" + dataSpn
                    + " showPlmn=" + showPlmn + " plmn=" + plmn);
        }
        if (mConfig.showLocale) {
            if (showSpn && !TextUtils.isEmpty(spn)) {
                spn = getLocalString(spn);
            }
            if (showSpn && !TextUtils.isEmpty(dataSpn)) {
                dataSpn = getLocalString(dataSpn);
            }
            if (showPlmn && !TextUtils.isEmpty(plmn)) {
                plmn = getLocalString(plmn);
            }
        }
	}

    private String getLocalString(String originalString) {
        return android.util.NativeTextHelper.getLocalString(mContext, originalString,
                          com.android.internal.R.array.origin_carrier_names,
                          com.android.internal.R.array.locale_carrier_names);
    }


其中getLocalString方法就是从frameworks/base/core/res/res/values/config.xml配置文件中获取运营商姓名,我们来看一下config.xml中是如何配置的

<!-- Configuartion to support SIM contact batch operation.-->
    <bool name="config_sim_phonebook_batch_operation">true</bool>
    <string-array name="origin_carrier_names">
        <item>CHINA\u0020\u0020MOBILE</item>
        <item>CMCC</item>
        <item>CHN-UNICOM</item>
        <item>China Mobile</item>
        <item>China Unicom</item>
        <item>China Telecom</item>
        <item>CHN-CT</item>
        <item>中国移动</item>
        <item>中国联通</item>
        <item>中国电信</item>
        <item>中國移動</item>
        <item>中國聯通</item>
        <item>中國電信</item>
        <item>Searching for Service</item>
    </string-array>

    <string-array name="locale_carrier_names">
        <item>China_Mobile</item>
        <item>China_Mobile</item>
        <item>China_Unicom</item>
        <item>China_Mobile</item>
        <item>China_Unicom</item>
        <item>China_Telecom</item>
        <item>China_Telecom</item>
        <item>China_Mobile</item>
        <item>China_Unicom</item>
        <item>China_Telecom</item>
        <item>China_Mobile</item>
        <item>China_Unicom</item>
        <item>China_Telecom</item>
        <item>roamingTextSearching</item>
    </string-array>


可以看到在getLocalString方法中传入的origin_carrier_names和locale_carrier_names在配置文件中,

再来看一下具体是怎么获取到最终的string的,getLocalString方法调用了frameworks/base/core/java/android/util/NativeTextHelper.java中的getLocalString方法,最终实现代码如下:

/**
     * parse the string to current language.
     *
     * @param context base context of the application
     * @param originalString original string
     * @param defPackage the target package where the local language strings
     *            defined
     * @param originNamesId the id of the original string array.
     * @param localNamesId the id of the local string keys.
     * @return local language string
     */
    private static final String getLocalString(Context context, String originalString,
            String defPackage, int originNamesId, int localNamesId) {
        String[] origNames = context.getResources().getStringArray(originNamesId);
        String[] localNames = context.getResources().getStringArray(localNamesId);
        for (int i = 0; i < origNames.length; i++) {
            if (origNames[i].equalsIgnoreCase(originalString)) {
                return context.getString(context.getResources().getIdentifier(localNames[i],
                        "string", defPackage));
            }
        }
        return originalString;
    }



从代码中可以看出config.xml中的origin_carrier_names和locale_carrier_names中的item是一一对应的,获取string的逻辑就是根据传入的originalString获取origin_carrier_names中的item,再找到local_carrier_names对应的item,最终找到以locale_carrier_names中item为id所对应的string。

在MobileSignalController.java中的getLocalString方法打log发现最终原因是在切换第一语言时SystemUI所在Context的Configuration的Locale有时候会更新不及时,这样就导致了Context的语言环境有问题,如此获取的string自然是当前Context的语言环境,既然知道原因了,那么只需修复SystemUI Context的语言环境即可。

三、解决方案

修复语言环境的方法就是获取当前系统的语言,然后使用Configuration.setLocale或者Configuration.setLocales方法设置进去,再更新SystemUI的Configuration,而系统切换语言是在frameworks/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.
     */
    public static void updateLocales(LocaleList locales) {
        try {
            final IActivityManager am = ActivityManagerNative.getDefault();
            final Configuration config = am.getConfiguration();

            config.setLocales(locales);
            config.userSetLocale = true;

            am.updatePersistentConfiguration(config);
            // Trigger the dirty bit for the Settings Provider.
            BackupManager.dataChanged("com.android.providers.settings");
        } catch (RemoteException e) {
            // Intentionally left blank
        }
    }

切换语言后会setLocales(locales),然后调用updatePersistentConfiguration(config)方法,最终会调用到AMS的updateConfigurationLocked方法,

/**
     * Do either or both things: (1) change the current configuration, and (2)
     * make sure the given activity is running with the (now) current
     * configuration.  Returns true if the activity has been left running, or
     * false if <var>starting</var> is being destroyed to match the new
     * configuration.
     *
     * @param userId is only used when persistent parameter is set to true to persist configuration
     *               for that particular user
     */
    private boolean updateConfigurationLocked(Configuration values, ActivityRecord starting,
            boolean initLocale, boolean persistent, int userId, boolean deferResume) {
        int changes = 0;

        if (mWindowManager != null) {
            mWindowManager.deferSurfaceLayout();
        }
        if (values != null) {
            Configuration newConfig = new Configuration(mConfiguration);
            changes = newConfig.updateFrom(values);
            if (changes != 0) {
                if (DEBUG_SWITCH || DEBUG_CONFIGURATION) Slog.i(TAG_CONFIGURATION,
                        "Updating configuration to: " + values);

                EventLog.writeEvent(EventLogTags.CONFIGURATION_CHANGED, changes);

                if (!initLocale && !values.getLocales().isEmpty() && values.userSetLocale) {
                    final LocaleList locales = values.getLocales();
                    int bestLocaleIndex = 0;
                    if (locales.size() > 1) {
                        if (mSupportedSystemLocales == null) {
                            mSupportedSystemLocales =
                                    Resources.getSystem().getAssets().getLocales();
                        }
                        bestLocaleIndex = Math.max(0,
                                locales.getFirstMatchIndex(mSupportedSystemLocales));
                    }
                    SystemProperties.set("persist.sys.locale",
                            locales.get(bestLocaleIndex).toLanguageTag());
                    LocaleList.setDefault(locales, bestLocaleIndex);
                    mHandler.sendMessage(mHandler.obtainMessage(SEND_LOCALE_TO_MOUNT_DAEMON_MSG,
                            locales.get(bestLocaleIndex)));
                }

                mConfigurationSeq++;
                if (mConfigurationSeq <= 0) {
                    mConfigurationSeq = 1;
                }
                newConfig.seq = mConfigurationSeq;
                mConfiguration = newConfig;
                Slog.i(TAG, "Config changes=" + Integer.toHexString(changes) + " " + newConfig);
                mUsageStatsService.reportConfigurationChange(newConfig,
                        mUserController.getCurrentUserIdLocked());
                //mUsageStatsService.noteStartConfig(newConfig);

                final Configuration configCopy = new Configuration(mConfiguration);

                // TODO: If our config changes, should we auto dismiss any currently
                // showing dialogs?
                mShowDialogs = shouldShowDialogs(newConfig, mInVrMode);

                AttributeCache ac = AttributeCache.instance();
                if (ac != null) {
                    ac.updateConfiguration(configCopy);
                }

                // Make sure all resources in our process are updated
                // right now, so that anyone who is going to retrieve
                // resource values after we return will be sure to get
                // the new ones.  This is especially important during
                // boot, where the first config change needs to guarantee
                // all resources have that config before following boot
                // code is executed.
                mSystemThread.applyConfigurationToResources(configCopy);

                if (persistent && Settings.System.hasInterestingConfigurationChanges(changes)) {
                    Message msg = mHandler.obtainMessage(UPDATE_CONFIGURATION_MSG);
                    msg.obj = new Configuration(configCopy);
                    msg.arg1 = userId;
                    mHandler.sendMessage(msg);
                }

                final boolean isDensityChange = (changes & ActivityInfo.CONFIG_DENSITY) != 0;
                if (isDensityChange) {
                    // Reset the unsupported display size dialog.
                    mUiHandler.sendEmptyMessage(SHOW_UNSUPPORTED_DISPLAY_SIZE_DIALOG_MSG);

                    killAllBackgroundProcessesExcept(Build.VERSION_CODES.N,
                            ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
                }

                for (int i=mLruProcesses.size()-1; i>=0; i--) {
                    ProcessRecord app = mLruProcesses.get(i);
                    try {
                        if (app.thread != null) {
                            if (DEBUG_CONFIGURATION) Slog.v(TAG_CONFIGURATION, "Sending to proc "
                                    + app.processName + " new config " + mConfiguration);
                            app.thread.scheduleConfigurationChanged(configCopy);
                        }
                    } catch (Exception e) {
                    }
                }
                Intent intent = new Intent(Intent.ACTION_CONFIGURATION_CHANGED);
                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY
                        | Intent.FLAG_RECEIVER_REPLACE_PENDING
                        | Intent.FLAG_RECEIVER_FOREGROUND);
                broadcastIntentLocked(null, null, intent, null, null, 0, null, null,
                        null, AppOpsManager.OP_NONE, null, false, false,
                        MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
                if ((changes&ActivityInfo.CONFIG_LOCALE) != 0) {
                    intent = new Intent(Intent.ACTION_LOCALE_CHANGED);
                    intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
	            if (initLocale || !mProcessesReady) {
                        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
                    }
                    broadcastIntentLocked(null, null, intent,
                            null, null, 0, null, null, null, AppOpsManager.OP_NONE,
                            null, false, false, MY_PID, Process.SYSTEM_UID, UserHandle.USER_ALL);
                }
            }
            // Update the configuration with WM first and check if any of the stacks need to be
            // resized due to the configuration change. If so, resize the stacks now and do any
            // relaunches if necessary. This way we don't need to relaunch again below in
            // ensureActivityConfigurationLocked().
            if (mWindowManager != null) {
                final int[] resizedStacks = mWindowManager.setNewConfiguration(mConfiguration);
                if (resizedStacks != null) {
                    for (int stackId : resizedStacks) {
                        final Rect newBounds = mWindowManager.getBoundsForNewConfiguration(stackId);
                        mStackSupervisor.resizeStackLocked(
                                stackId, newBounds, null, null, false, false, deferResume);
                    }
                }
            }
        }

        boolean kept = true;
        final ActivityStack mainStack = mStackSupervisor.getFocusedStack();
        // mainStack is null during startup.
        if (mainStack != null) {
            if (changes != 0 && starting == null) {
                // If the configuration changed, and the caller is not already
                // in the process of starting an activity, then find the top
                // activity to check if its configuration needs to change.
                starting = mainStack.topRunningActivityLocked();
            }

            if (starting != null) {
                kept = mainStack.ensureActivityConfigurationLocked(starting, changes, false);
                // And we need to make sure at this point that all other activities
                // are made visible with the correct configuration.
                mStackSupervisor.ensureActivitiesVisibleLocked(starting, changes,
                        !PRESERVE_WINDOWS);
            }
        }
        if (mWindowManager != null) {
            mWindowManager.continueSurfaceLayout();
        }
        return kept;
    }



在其中会设置一个系统属性persist.sys.locale,这个属性就是当前的系统首选语言了,既然如此,直接在MobileSignalController.java中获取这个系统属性,然后把值设置为SystemUI所在Context的Locale即可,具体修改代码如下:

import java.util.Locale;

    private Context mContextNew;
    private String getLocalString(String originalString) {
        String[] str = SystemProperties.get("persist.sys.locale").split("-");
        if (str.length == 1) {
            mContext.getResources().getConfiguration().setLocale(new Locale(str[0]));
        } else if (str.length > 1) {
            mContext.getResources().getConfiguration().setLocale(new Locale(str[0], str[1]));
        }
        mContextNew = mContext.createConfigurationContext(mContext.getResources().getConfiguration());
        return android.util.NativeTextHelper.getLocalString(mContextNew, originalString,
                          com.android.internal.R.array.origin_carrier_names,
                          com.android.internal.R.array.locale_carrier_names);
    }




也可以改成:

import java.util.Locale;

    private String getLocalString(String originalString) {
        String[] str = SystemProperties.get("persist.sys.locale").split("-");
        if (str.length == 1) {
            mContext.getResources().getConfiguration().setLocale(new Locale(str[0]));
        } else if (str.length > 1) {
            mContext.getResources().getConfiguration().setLocale(new Locale(str[0], str[1]));
        }
        mContext.getResources().updateConfiguration(mContext.getResources().getConfiguration(), mContext.getResources().getDisplayMetrics());
        return android.util.NativeTextHelper.getLocalString(mContext, originalString,
                          com.android.internal.R.array.origin_carrier_names,
                          com.android.internal.R.array.locale_carrier_names);
    }



但是这种方法官方注明updateConfiguration方法已经过时了,所以尽量少用,记得之前Android M的时候SystemUI没有权限获取系统属性?如果无法获取,那么倒也还有折中方案获取系统语言,

mContext.getResources().getConfiguration().setLocales(Resources.getSystem().getConfiguration().getLocales());

不过这种方式也不是一定保险,因为切换语言后会发送ACTION为LOCALE_CHANGED的广播,其他进程在收到广播后会更新Configuration,发送广播和设置是异步操作的,这也是为什么SystemUI的Context有时候Configuration会没有更新,如果Resources.getSystem().getConfiguration().getLocales()所得到的语言也是没有更新过的,那么SystemUI语言当然也会不准确。

网上还有说用Locale.getDefault()或者LocaleList.getDefault()方法获取的,其中LocaleList.getDefault()方法获取的并不是当前系统所设置顺序的语言,这个方法的注释中有说明,至于Locale.getDefault()这个没有深究,因为现在官方推荐的是使用LocaleList类。

如果实在不行,还可以用反射机制获取系统语言。