Android 6.0拨号界面号码格式化

时间:2021-11-07 19:11:19

需求及分析

客户需求:
Android 6.0拨号界面号码格式化
使用hierarchyviewer工具可以发现这个界面对应的activity是DialtactsActivity.

通过搜索拨号盘的source id(dialpad_view)找到dialpad_fragment.xml,从而找到DialpadFragment.java。

最后在Dialpad_view.xml里面自定义了一个EditText类来容纳拨号的内容:

<view class="com.android.phone.common.dialpad.DigitsEditText"
            xmlns:ex="http://schemas.android.com/apk/res-auto"
            android:id="@+id/digits"

public class DigitsEditText extends ResizingTextEditText

所以,主要的函数逻辑在DialpadFragment.java里面。

线索

在DialpadFragment里面全局搜索mDigits。在初始化函数onCreateView里面,对mDigits注册了对输入内容变化的监听器。

mDigits.addTextChangedListener(this);
PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(getActivity(), mDigits);

在PhoneNumberFormatter中会启动一个TextWatcherLoadAsyncTask来另起一个线程来进行数字的分割。


//PhoneNumberFormatter.java
public static final void setPhoneNumberFormattingTextWatcher(Context context, TextView textView) {
        new TextWatcherLoadAsyncTask(GeoUtil.getCurrentCountryIso(context), textView).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null);
    }

注意,使用国家ISO码以及mDigits这个TextView来初始化这个对象。这样在该对象中就可以直接操作TextView的内容了。而在这个AsyncTask中会实例化一个PhoneNumberFormattingTextWatcher对象来对变化的数字进行格式化。

最后的数字格式处理是在这个TextWatcher的afterTextChanged中进行处理。

//PhoneNumberFormattingTextWatcher.java
public synchronized void afterTextChanged(Editable s) {
    String formatted = reformat(s, Selection.getSelectionEnd(s));
    if (formatted != null) {
        int rememberedPos = mFormatter.getRememberedPosition();
        s.replace(0, s.length(), formatted, 0, formatted.length()); 
    }
}

针对通过Intent,启动拨号界面显示的号码,我们在DialpadFragment中可以找到fillDigitsIfNecessary。这个函数中,该函数接收通过intent调用发送过来的号码,并且调用下面的setFormattedDigits来格式化这个号码,并且显示在mDigits这个TextView中。

/** DialpadFragment.java * Sets formatted digits to digits field. */
    private void setFormattedDigits(String data, String normalizedNumber) {
        // strip the non-dialable numbers out of the data string.
        String dialString = PhoneNumberUtils.extractNetworkPortion(data);
        dialString =

                PhoneNumberUtils.formatNumber(dialString, normalizedNumber, mCurrentCountryIso);
        if (!TextUtils.isEmpty(dialString)) {
            Editable digits = mDigits.getText();
            digits.replace(0, digits.length(), dialString);
            // for some reason this isn't getting called in the digits.replace call above..
            // but in any case, this will make sure the background drawable looks right
            afterTextChanged(digits);
        }
    }

这两个的共同点是都使用了CountryIso这个参数。所以数字是按照不同的国家,显示不同的样式的。

这两个分支中CountryIso都是通过下面的代码获取的:

mCurrentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
//packages/apps/contactcommon/src/com/android/contacts/common/location/CountryDetector.java
public String getCurrentCountryIso() {
        String result = null;
        if (isNetworkCountryCodeAvailable()) {
            result = getNetworkBasedCountryIso();
            Log.w(TAG, "getNetworkBasedCountryIso." + result);
        }

        if (TextUtils.isEmpty(result)) {
            result = getLocationBasedCountryIso();
            Log.w(TAG, "getLocationBasedCountryIso." + result);
        }

        if (TextUtils.isEmpty(result)) {
            result = getSimBasedCountryIso();
            Log.w(TAG, "getSimBasedCountryIso." + result);
        }

        if (TextUtils.isEmpty(result)) {
            result = getLocaleBasedCountryIso();
            Log.w(TAG, "getLocaleBasedCountryIso." + result);
        }
        if (TextUtils.isEmpty(result)) {
            result = DEFAULT_COUNTRY_ISO;
        }
        Log.w(TAG, "DEFAULT_COUNTRY_ISO." + result);


        //for debug
        result = "BR";
        return result.toUpperCase(Locale.US);
    }

这个值是由很多条件判断来决定的。先判断当前连接上的网络所属的国家。如果为空,判断当前的物理位置所在的国家。如果依然为空,获取Sim卡所属的国家。依然为空,判断当前选择的语言Locale所属的国家。否则就默认使用DEFAULT_COUNTRY_ISO = “US”作为国家码。

注意,上面的Log打印语句都是添加用来验证国家码是否会影响分割方法的。

修改之后输出结果为:

W/CountryDetector( 3948): getLocationBasedCountryIso.null
W/CountryDetector( 3948): getSimBasedCountryIso.
W/CountryDetector( 3948): getLocaleBasedCountryIso.US
W/CountryDetector( 3948): DEFAULT_COUNTRY_ISO.US

说明Location和Sim都是为null,而LocaleBaseCountryIso则为US。

getLocaleBasedCountryIso()这个函数还有点意思,是通过属性中的值类判定用户属于哪个locale的:


//Locale.java
public static Locale getDefaultLocaleFromSystemProperties() {
        final String languageTag = System.getProperty("user.locale", "");
        final Locale defaultLocale;
        if (!languageTag.isEmpty()) {
            defaultLocale = Locale.forLanguageTag(languageTag);
        } else {
            String language = System.getProperty("user.language", "en");
            String region = System.getProperty("user.region", "US");
            String variant = System.getProperty("user.variant", "");
            defaultLocale = new Locale(language, region, variant);
        }
        return defaultLocale;
    }

然后通过defaultLocale.getCountry()来获取国家。第二个,也就是region实际上就是country的值。

查一下“String region = System.getProperty(“user.region”, “US”);”这个值是怎么获取的(其实也就是systemProperties.getProperty(name, defaultValue))。

在liccore/luni/src/main/java/java/lang/System.java里面有如下语句:

static {
        in = new BufferedInputStream(new FileInputStream(FileDescriptor.in));
        unchangeableSystemProperties = initUnchangeableSystemProperties();
        systemProperties = createSystemProperties();//会将unchangeableSystemProperties的内容添加到systemProperties里面
        addLegacyLocaleSystemProperties();
    }

在addLegacyLocaleSystemProperties里面,会根据user.locale的值来设定一些值,如下:

private static void addLegacyLocaleSystemProperties() {
    final String locale = getProperty("user.locale", "");
    if (!locale.isEmpty()) {
        Locale l = Locale.forLanguageTag(locale);
        setUnchangeableSystemProperty("user.language", l.getLanguage());
        setUnchangeableSystemProperty("user.region", l.getCountry());
        setUnchangeableSystemProperty("user.variant", l.getVariant());
    } else {
        // If "user.locale" isn't set we fall back to our old defaults of
        // language="en" and region="US" (if unset) and don't attempt to set it.
        // The Locale class will fall back to using user.language and user.region if unset.
        final String language = getProperty("user.language", "");
        final String region = getProperty("user.region", "");
        if (language.isEmpty()) {            setUnchangeableSystemProperty("user.language", "en");
        }
        if (region.isEmpty()) {
            setUnchangeableSystemProperty("user.region", "US");
        }}}

其实就是根据user.locale的值来设定user.language和user.region的值。

总结

通过上面的分析,我们知道拨号界面数字的分割是按照当前网络所属的国家来显示。如果针对特定国家的项目,可以通过设定user.locale来改变拨号界面数字的分割方式,以适应相关的客户需求。这个分割方式都是google提供的原生方式,推荐不要修改。当然如果客户有特殊需求,那么就需要修改PhoneNumberFormattingTextWatcher里面afterTextChanged函数中的reformat方法,来满足客户需求。