Android 本地时间/时区自动更新 -- NITZ

        NITZ - Network Identity and Time Zone,网络标识和时区,是一种用于自动配置本地时间和日期的机制,同时也通过无线网向移动设备提供运营商信息。NITZ经常被用来自动更新移动电话的系统时钟,Android原有的更新机制就是采用NITZ方式,这是一种运营商的可选服务。其基本原理简单的来说,就是UI根据 Modem主动上报的时间信息,更新终端系统的时间及时区。


一、Framework 对Modem主动上报消息的处理及时间更新

1、RIL_UNSOL_NITZ_TIME_RECEIVED 主动上报及通知

RIL在收到Modem主动上报的RIL_UNSOL_NITZ_TIME_RECEIVED消息后,调用mNITZTimeRegistrant.notifyRegistrant 通知注册者进行时间更新处理。

frameworks/opt/telephony/src/java/com/android/internal/telephony/RIL.java

case RIL_UNSOL_NITZ_TIME_RECEIVED:
                if (RILJ_LOGD) unsljLogRet(response, ret);
                // has bonus long containing milliseconds since boot that the NITZ
                // time was received
                long nitzReceiveTime = p.readLong();
                Object[] result = new Object[2];
                result[0] = ret;
                result[1] = Long.valueOf(nitzReceiveTime);
                boolean ignoreNitz = SystemProperties.getBoolean(
                        TelephonyProperties.PROPERTY_IGNORE_NITZ, false);
                if (ignoreNitz) {
                    if (RILJ_LOGD) riljLog("ignoring UNSOL_NITZ_TIME_RECEIVED");
                } else {
                    if (mNITZTimeRegistrant != null) {
                        mNITZTimeRegistrant
                            .notifyRegistrant(new AsyncResult (null, result, null));  // 通知注册接收方
                    }
                    // in case NITZ time registrant isn't registered yet, or a new registrant
                    // registers later
                    mLastNITZTimeInfo = result;
                }
            break;

mNITZTimeRegistrant的注册监听方法:

@Override 
public void  setOnNITZTime(Handler h, int what, Object obj) {
        super.setOnNITZTime(h, what, obj);
        // Send the last NITZ time if we have it
        if (mLastNITZTimeInfo != null) {
            mNITZTimeRegistrant
                .notifyRegistrant(
                    new AsyncResult (null, mLastNITZTimeInfo, null));
        }
}

2、mNITZTimeRegistrant 注册及 EVENT_NITZ_TIME 接收处理

ServiceStateTracker在系统启动时,会调用setOnNITZTime将Tracher中的Handler与RIL中的上报消息绑定在一起,即收到上报消息,就回调Handler中的某些方法,以GsmServiceStateTracker 为例,代码分析如下:

frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

调用setOnNITZTime 进行mNITZTimeRegistrant 注册:

public GsmServiceStateTracker(GSMPhone phone) {
        super(phone, phone.mCi, new CellInfoGsm());
        mPhone = phone;
      ……
        mCi.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null);
        mCi.registerForRadioStateChanged(this, EVENT_RADIO_STATE_CHANGED, null);
        mCi.registerForVoiceNetworkStateChanged(this, EVENT_NETWORK_STATE_CHANGED, null);
        // 注册 NITZ 消息监听
        mCi.setOnNITZTime(this, EVENT_NITZ_TIME, null);	
     …….
}

接收到EVENT_NITZ_TIME后,调用 setTimeFromNITZString去设置时间和时区

case EVENT_NITZ_TIME:
        ar = (AsyncResult) msg.obj;
        String nitzString = (String)((Object[])ar.result)[0];
        long nitzReceiveTime = ((Long)((Object[])ar.result)[1]).longValue();
        setTimeFromNITZString(nitzString, nitzReceiveTime);
        break;

setTimeFromNITZString 负责解析传过来字符串(nitzString)并进行时间和时区的设置

    /**
     * nitzReceiveTime is time_t that the NITZ time was posted
     */
    private void setTimeFromNITZString (String nitz, long nitzReceiveTime) {
        // "yy/mm/dd,hh:mm:ss(+/-)tz"
        // tz is in number of quarter-hours

        long start = SystemClock.elapsedRealtime();
        if (DBG) {log("NITZ: " + nitz + "," + nitzReceiveTime +
                        " start=" + start + " delay=" + (start - nitzReceiveTime));
        }

        // 解析从 modem 获取的时间字符串 nitzString
        try {
            /* NITZ time (hour:min:sec) will be in UTC but it supplies the timezone
             * offset as well (which we won't worry about until later) */
            Calendar c = Calendar.getInstance(TimeZone.getTimeZone("GMT"));

            c.clear();
            c.set(Calendar.DST_OFFSET, 0);

            String[] nitzSubs = nitz.split("[/:,+-]");

            int year = 2000 + Integer.parseInt(nitzSubs[0]);
            if (year > MAX_NITZ_YEAR) {
              if (DBG) loge("NITZ year: " + year + " exceeds limit, skip NITZ time update");
              return;
            }
            c.set(Calendar.YEAR, year);

            // month is 0 based!
            int month = Integer.parseInt(nitzSubs[1]) - 1;
            c.set(Calendar.MONTH, month);

            int date = Integer.parseInt(nitzSubs[2]);
            c.set(Calendar.DATE, date);

            int hour = Integer.parseInt(nitzSubs[3]);
            c.set(Calendar.HOUR, hour);

            int minute = Integer.parseInt(nitzSubs[4]);
            c.set(Calendar.MINUTE, minute);

            int second = Integer.parseInt(nitzSubs[5]);
            c.set(Calendar.SECOND, second);

            boolean sign = (nitz.indexOf('-') == -1);

            int tzOffset = Integer.parseInt(nitzSubs[6]);

            int dst = (nitzSubs.length >= 8 ) ? Integer.parseInt(nitzSubs[7])
                                              : 0;

            // The zone offset received from NITZ is for current local time,
            // so DST correction is already applied.  Don't add it again.
            //
            // tzOffset += dst * 4;
            //
            // We could unapply it if we wanted the raw offset.

            tzOffset = (sign ? 1 : -1) * tzOffset * 15 * 60 * 1000;

            TimeZone    zone = null;

            // As a special extension, the Android emulator appends the name of
            // the host computer's timezone to the nitz string. this is zoneinfo
            // timezone name of the form Area!Location or Area!Location!SubLocation
            // so we need to convert the ! into /
            if (nitzSubs.length >= 9) {
                String  tzname = nitzSubs[8].replace('!','/');
                zone = TimeZone.getTimeZone( tzname );
            }

            String iso = ((TelephonyManager) mPhone.getContext().
                    getSystemService(Context.TELEPHONY_SERVICE)).
                    getNetworkCountryIsoForPhone(mPhone.getPhoneId());

            if (zone == null) {

                if (mGotCountryCode) {
                    if (iso != null && iso.length() > 0) {
                        zone = TimeUtils.getTimeZone(tzOffset, dst != 0,
                                c.getTimeInMillis(),
                                iso);
                    } else {
                        // We don't have a valid iso country code.  This is
                        // most likely because we're on a test network that's
                        // using a bogus MCC (eg, "001"), so get a TimeZone
                        // based only on the NITZ parameters.
                        zone = getNitzTimeZone(tzOffset, (dst != 0), c.getTimeInMillis());
                    }
                }
            }

            if ((zone == null) || (mZoneOffset != tzOffset) || (mZoneDst != (dst != 0))){
                // We got the time before the country or the zone has changed
                // so we don't know how to identify the DST rules yet.  Save
                // the information and hope to fix it up later.

                mNeedFixZoneAfterNitz = true;   // 重要标记,用于SS变化时是否进行时区更新判断
                mZoneOffset  = tzOffset;
                mZoneDst     = dst != 0;
                mZoneTime    = c.getTimeInMillis();
            }

            if (zone != null) {
                if (getAutoTimeZone()) {
                    // 设置时区并发送广播
                     setAndBroadcastNetworkSetTimeZone(zone.getID());
                }
                // 保存当前设置时区值
                saveNitzTimeZone(zone.getID());
            }

            String ignore = SystemProperties.get("gsm.ignore-nitz");
            if (ignore != null && ignore.equals("yes")) {
                log("NITZ: Not setting clock because gsm.ignore-nitz is set");
                return;
            }

            try {
                mWakeLock.acquire();

                if (getAutoTime()) {
                    long millisSinceNitzReceived
                            = SystemClock.elapsedRealtime() - nitzReceiveTime;

                    if (millisSinceNitzReceived < 0) {
                        // Sanity check: something is wrong
                        if (DBG) {
                            log("NITZ: not setting time, clock has rolled "
                                            + "backwards since NITZ time was received, "
                                            + nitz);
                        }
                        return;
                    }

                    if (millisSinceNitzReceived > Integer.MAX_VALUE) {
                        // If the time is this far off, something is wrong > 24 days!
                        if (DBG) {
                            log("NITZ: not setting time, processing has taken "
                                        + (millisSinceNitzReceived / (1000 * 60 * 60 * 24))
                                        + " days");
                        }
                        return;
                    }

                    // Note: with range checks above, cast to int is safe
                    c.add(Calendar.MILLISECOND, (int)millisSinceNitzReceived);

                    if (DBG) {
                        log("NITZ: Setting time of day to " + c.getTime()
                            + " NITZ receive delay(ms): " + millisSinceNitzReceived
                            + " gained(ms): "
                            + (c.getTimeInMillis() - System.currentTimeMillis())
                            + " from " + nitz);
                    }
                    // 设置系统时间并发送广播
                    setAndBroadcastNetworkSetTime(c.getTimeInMillis());
                    Rlog.i(LOG_TAG, "NITZ: after Setting time of day");
                }
                // 保存当前设置 NITZ 时间值并存储到系统属性
                SystemProperties.set("gsm.nitz.time", String.valueOf(c.getTimeInMillis()));
                saveNitzTime(c.getTimeInMillis());
                if (VDBG) {
                    long end = SystemClock.elapsedRealtime();
                    log("NITZ: end=" + end + " dur=" + (end - start));
                }
                mNitzUpdatedTime = true;
            } finally {
                mWakeLock.release();
            }
        } catch (RuntimeException ex) {
            loge("NITZ: Parsing NITZ time " + nitz + " ex=" + ex);
        }
    }

从代码中可以看出只有在数据库对自动同步网络时间/时区为勾选状态时,才会调用setAndBroadcastNetworkSetTime和 setAndBroadcastNetworkSetTimeZone设置当前NITZ解析的时间及时区,并发送广播进行最终的系统时间/时区维护。这里相关数据库的勾选状态获取方法如下,主要判断 Settings.Global.AUTO_TIME 及 Settings.Global.AUTO_TIME_ZONE 存储值

    private boolean getAutoTime() {
        try {
            return Settings.Global.getInt(mPhone.getContext().getContentResolver(),
                    Settings.Global.AUTO_TIME) > 0;
        } catch (SettingNotFoundException snfe) {
            return true;
        }
    }

    private boolean getAutoTimeZone() {
        try {
            return Settings.Global.getInt(mPhone.getContext().getContentResolver(),
                    Settings.Global.AUTO_TIME_ZONE) > 0;
        } catch (SettingNotFoundException snfe) {
            return true;
        }
    }

3、Modem主动上报消息跟新流程

    如上分析, framework 对 Modem 主动上报消息RIL_UNSOL_NITZ_TIME_RECEIVED 的处理流程及时间/时区更新逻辑,可简单总结流程如下。我们可以看到发送广播后,时间及时区的最终维护走到了 NetworkTimeUpdateService  中,具体该服务做了哪些处理,后面我们再对此作进一步解读。

Android 本地时间/时区自动更新 -- NITZ_第1张图片


二、UI层面时间更新的处理逻辑    

接着,我们再从用户主动选择自动更新的角度,继续分析代码。

1、点击自动更新数据库
Android手机的自动更新时间选项都设置在时间和日期选项卡下,正常用户主动点击勾选自动更新后,会通过修改数据库value 触发时间/时区的自动更新。在2.3中只有一个选项-同步,会同时同步时区和时间日期,4.0中把他们分成了两项,时区和日期时间能分别进行自动更新,其实原理都是一样,都是在数据库中设置对应值,详细如下。

packages/apps/Settings/src/com/android/settings/DateTimeSettings.java

    @Override
    public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
        if (key.equals(KEY_AUTO_TIME)) {
            boolean autoEnabled = preferences.getBoolean(key, true);
            Settings.Global.putInt(getContentResolver(), Settings.Global.AUTO_TIME,
                    autoEnabled ? 1 : 0);
            mTimePref.setEnabled(!autoEnabled);
            mDatePref.setEnabled(!autoEnabled);
        } else if (key.equals(KEY_AUTO_TIME_ZONE)) {
            boolean autoZoneEnabled = preferences.getBoolean(key, true);
            Settings.Global.putInt(
                    getContentResolver(), Settings.Global.AUTO_TIME_ZONE, autoZoneEnabled ? 1 : 0);
            mTimeZone.setEnabled(!autoZoneEnabled);
        }
    }

对于一些时间和时区共用一个控件的处理,通常会同时修改两个数据库,如

    @Override
    public boolean onPreferenceChange(Preference preference, Object newValue) {
        String key = preference.getKey();
        Log.d(TAG,"DateTimeSettings DateTimeSettings  key is :"+key+",value is:"+newValue);
        if (key.equals(KEY_AUTO_TIME_AND_ZONE)){ 
            boolean autoTimeZoneEnabled = (Boolean) newValue;
            Settings.Global.putInt(
                    getContentResolver(), Settings.Global.AUTO_TIME_ZONE, autoTimeZoneEnabled ? 1 : 0);
            mTimeZone.setEnabled(!autoTimeZoneEnabled);
            Settings.Global.putInt(getContentResolver(), Settings.Global.AUTO_TIME,
                    autoTimeZoneEnabled ? 1 : 0);
            mTimePref.setEnabled(!autoTimeZoneEnabled);
            mDatePref.setEnabled(!autoTimeZoneEnabled);        
            mDateTimePreference.setEnabled(!autoTimeZoneEnabled);
        } 
       .......
    }

2、数据库变化的监听处理

frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

追踪代码,发现GsmServiceStateTracker构造函数中注册了两个ContentObserver来监听数据库内容的变化

    public GsmServiceStateTracker(GSMPhone phone) {
        ……
        mCr = phone.getContext().getContentResolver();
        mCr.registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.AUTO_TIME), true,
                mAutoTimeObserver);
        mCr.registerContentObserver(
                Settings.Global.getUriFor(Settings.Global.AUTO_TIME_ZONE), true,
                mAutoTimeZoneObserver);
        .…..
}

接着再看看这两个 ContentObserver,我们发现两个关于NITZ的revert函数 ,到底是不是它们更新了时间/时区呢

    private ContentObserver mAutoTimeObserver = new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            Rlog.i("GsmServiceStateTracker", "Auto time state changed");
            revertToNitzTime();
        }
    };

    private ContentObserver mAutoTimeZoneObserver = new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            Rlog.i("GsmServiceStateTracker", "Auto time zone state changed");
            revertToNitzTimeZone();
        }
};

接着看代码,终于发现了我们熟悉的调用 setAndBroadcastNetworkSetTime 和 setAndBroadcastNetworkSetTimeZone,至此,我们也就和上节的讨论关联到了一起

    private void revertToNitzTime() {
        if (Settings.Global.getInt(mPhone.getContext().getContentResolver(),
                Settings.Global.AUTO_TIME, 0) == 0) {
            return;
        }
        if (DBG) {
            log("Reverting to NITZ Time: mSavedTime=" + mSavedTime
                + " mSavedAtTime=" + mSavedAtTime);
        }
        if (mSavedTime != 0 && mSavedAtTime != 0) {
            setAndBroadcastNetworkSetTime(mSavedTime
                    + (SystemClock.elapsedRealtime() - mSavedAtTime));
        }
    }

    private void revertToNitzTimeZone() {
        if (Settings.Global.getInt(mPhone.getContext().getContentResolver(),
                Settings.Global.AUTO_TIME_ZONE, 0) == 0) {
            return;
        }
        if (DBG) log("Reverting to NITZ TimeZone: tz='" + mSavedTimeZone);
        if (mSavedTimeZone != null) {
            setAndBroadcastNetworkSetTimeZone(mSavedTimeZone);
        }
    }

仔细看下代码,我们发现这里有几个关键值 mSavedTime、 mSavedAtTime 和 mSavedTimeZone 影响着上述两个调用的执行,那么他们究竟从哪里来的呢?追踪一下,如下两个函数进行了设置,具体调用在上节(Framework 对Modem主动上报消息的处理及时间更新)中与 setAndBroadcastNetworkSetTime 、setAndBroadcastNetworkSetTimeZone 有同步处理

    private void saveNitzTimeZone(String zoneId) {
        mSavedTimeZone = zoneId;
    }

    private void saveNitzTime(long time) {
        mSavedTime = time;
        mSavedAtTime = SystemClock.elapsedRealtime();
    }

既然找到了这些值的赋值处,是不是又有一个疑问呢?显然这里只有进行过 NITZ 时间设置才会调用 setAndBroadcastNetworkSetTime 、setAndBroadcastNetworkSetTimeZone 设置时间和时区,并发出广播进行最终维护。那么,如果从未进行过 NITZ 时间设置呢?显然这和我们实际遇到的情况是不一样的,那么必然还有其他的监听处理,带着这个问题我们继续看下最终维护时间的 NetworkTimeUpdateService


三、最终的时间维护服务 NetworkTimeUpdateService

从上面两节,发现他们时间设置最终都调到了setAndBroadcastNetworkSetTime 、setAndBroadcastNetworkSetTimeZone 进行时间与时区的更新维护,那么这两个函数到底做了什么呢,下面我们具体看下代码

    /**
     * Set the timezone and send out a sticky broadcast so the system can
     * determine if the timezone was set by the carrier.
     *
     * @param zoneId timezone set by carrier
     */
    private void setAndBroadcastNetworkSetTimeZone(String zoneId) {
        if (DBG) log("setAndBroadcastNetworkSetTimeZone: setTimeZone=" + zoneId);
        AlarmManager alarm =
            (AlarmManager) mPhone.getContext().getSystemService(Context.ALARM_SERVICE);
       // 设置时区
        alarm.setTimeZone(zoneId);
        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE);
        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
        intent.putExtra("time-zone", zoneId);
       // 发送广播
        mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
        if (DBG) {
            log("setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast zoneId=" +
                zoneId);
        }
    }

    /**
     * Set the time and Send out a sticky broadcast so the system can determine
     * if the time was set by the carrier.
     *
     * @param time time set by network
     */
    private void setAndBroadcastNetworkSetTime(long time) {
        if (DBG) log("setAndBroadcastNetworkSetTime: time=" + time + "ms");
       // 设置系统时间
        SystemClock.setCurrentTimeMillis(time);
        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
        intent.putExtra("time", time);
       // 发送广播
        mPhone.getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
    }

找到相应的 Receiver ,这里只对mNitzTimeSetTime、mNitzZoneSetTime两个变量进行了赋值,那么这样做的目的是什么呢?

frameworks/base/services/core/java/com/android/server/NetworkTimeUpdateService.java

    private BroadcastReceiver mNitzReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (TelephonyIntents.ACTION_NETWORK_SET_TIME.equals(action)) {
                mNitzTimeSetTime = SystemClock.elapsedRealtime();
            } else if (TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE.equals(action)) {
                mNitzZoneSetTime = SystemClock.elapsedRealtime();
            }
        }
};

继续查找这两个赋值的使用,我们发现其使用场景为 onPollNetworkTimeUnderWakeLock <- onPollNetworkTime <- handleMessage (EVENT_AUTO_TIME_CHANGED)。看到这里是不是有种似曾相识的感觉呢?对的,正如你所想的,NetworkTimeUpdateService 同样注册了对数据库 Settings.Global.AUTO_TIME 的监听SettingsObserver,在数据库变化时通过EVENT_AUTO_TIME_CHANGED 回调来进行最终时间的维护,这也解释了上节中我们的疑问。

mSettingsObserver = new SettingsObserver(mHandler, EVENT_AUTO_TIME_CHANGED);
mSettingsObserver.observe(mContext);

同样的,onPollNetworkTime只有在设置自动更新时间打开的情况下才会调用onPollNetworkTimeUnderWakeLock 进行时间的最终维护,简单看下代码,该函数主要是用在NITZ 没更新时间的情况下,通过 NTP 服务器来完成时间的同步

    private void onPollNetworkTimeUnderWakeLock(int event) {
        final long refTime = SystemClock.elapsedRealtime();

        // If NITZ time was received less than mPollingIntervalMs time ago,
        // no need to sync to NTP.
        if (mNitzTimeSetTime != NOT_SET && refTime - mNitzTimeSetTime < mPollingIntervalMs) {
            if (DBG) Log.i(TAG, "onPollNetworkTime nitz used mNitzTimeSetTime = " + mNitzTimeSetTime + "; mNitzTimeSetTime = " +mNitzTimeSetTime + "; refTime = " +refTime + " resetAlarm 1 ...");
            resetAlarm(mPollingIntervalMs);
            return;
        }
        final long currentTime = System.currentTimeMillis();
        if (DBG) Log.i(TAG, "onPollNetworkTime after nitz logic System time = " + currentTime + ", mLastNtpFetchTime = " +mLastNtpFetchTime +", refTime = " +refTime
                 +", mLastNtpFetchTime = " +mLastNtpFetchTime + ", mPollingIntervalMs = " +mPollingIntervalMs);
        // Get the NTP time
        if (mLastNtpFetchTime == NOT_SET || refTime >= mLastNtpFetchTime + mPollingIntervalMs
                || event == EVENT_AUTO_TIME_CHANGED) {
            if (DBG) Log.i(TAG, "Before Ntp fetch");

            // force refresh NTP cache when outdated
            if (mTime.getCacheAge() >= mPollingIntervalMs && isNetworkOk()) {
                if (DBG) Log.i(TAG, "onPollNetworkTime force refresh NTP cache when outdated ...");
                //mTime.forceRefresh();
                int index = mTryAgainCounter % mNtpServers.size();
                if (DBG) Log.i(TAG, "mTryAgainCounter = " + mTryAgainCounter + ";mNtpServers.size() = " + mNtpServers.size() + ";index = " + index + ";mNtpServers = " + mNtpServers.get(index));
                if (mTime instanceof NtpTrustedTime)
                {
                    if (DBG) Log.i(TAG, "onPollNetworkTime start foceRefresh ...");
                    ((NtpTrustedTime) mTime).setServer(mNtpServers.get(index));
                    mTime.forceRefresh();
                    ((NtpTrustedTime) mTime).setServer(mDefaultServer);
                    if (DBG) Log.i(TAG, "onPollNetworkTime after foceRefresh ...");
                }
                else
                {
                    if (DBG) Log.i(TAG, "onPollNetworkTime other TrustedTime instance ...");
                    mTime.forceRefresh();
                }
            }

            // only update when NTP time is fresh
            if (mTime.getCacheAge() < mPollingIntervalMs) {
                if (DBG) Log.i(TAG, "onPollNetworkTime only update when NTP time is fresh ...");
                final long ntp = mTime.currentTimeMillis();
                mTryAgainCounter = 0;
                if (DBG) Log.i(TAG, "onPollNetworkTime ntp = " + ntp + ", currentTime = " +currentTime + ", mLastNtpFetchTime = "+mLastNtpFetchTime);

                // If the clock is more than N seconds off or this is the first time it's been
                // fetched since boot, set the current time.
                if (Math.abs(ntp - currentTime) > mTimeErrorThresholdMs
                        || mLastNtpFetchTime == NOT_SET) {
                    // Set the system time
                    if (DBG && mLastNtpFetchTime == NOT_SET
                            && Math.abs(ntp - currentTime) <= mTimeErrorThresholdMs) {
                        Log.i(TAG, "For initial setup, rtc = " + currentTime);
                    }
                    if (DBG) Log.i(TAG, "Ntp time to be set = " + ntp);
                    // Make sure we don't overflow, since it's going to be converted to an int
                    if (ntp / 1000 < Integer.MAX_VALUE) {
                        if (DBG) Log.i(TAG, "onPollNetworkTime ******** SystemClock.setCurrentTimeMillis(ntp) ********");
                        SystemClock.setCurrentTimeMillis(ntp);
                    }
                } else {
                    if (DBG) Log.i(TAG, "Ntp time is close enough = " + ntp);
                }
                mLastNtpFetchTime = SystemClock.elapsedRealtime();
            } else {
                // Try again shortly
                if (DBG) Log.i(TAG, "onPollNetworkTime NTP time is not fresh... mTryAgainCounter = " + mTryAgainCounter + " mTryAgainTimesMax=" + mTryAgainTimesMax);
                mTryAgainCounter++;
                if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
                    if (DBG) Log.i(TAG, "onPollNetworkTime resetAlarm short ...");
                    resetAlarm(mPollingIntervalShorterMs);
                } else {
                    if (DBG) Log.i(TAG, "onPollNetworkTime clear counter resetAlarm max ...");
                    // Try much later
                    mTryAgainCounter = 0;
                    resetAlarm(mPollingIntervalMs);
                }
                return;
            }
        }
         if (DBG) Log.i(TAG, "onPollNetworkTime final resetAlarm ...");
        resetAlarm(mPollingIntervalMs);
    }

四、ServiceState 注册状态变化时触发的时间/时区更新

frameworks/opt/telephony/src/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

对于SS 从非注册状态变成注册状态过程,pollStateDone 会发起时间/时区的新一轮更新处理。大概总结下,在保证当前获取的 operatorNumeric != null情况下,进行时间/时区更新的场景主要分如下几类:
1、    根据解析出的 mcc 获取有效国家码(ios)
2、    插卡且mcc 变化
3、    mNeedFixZoneAfterNitz = true,即解析出时间信息时国家码和时区还没变化(具体参考setTimeFromNITZString)

    protected void pollStateDone() {
         .......
        boolean hasRegistered =
            mSS.getVoiceRegState() != ServiceState.STATE_IN_SERVICE
            && mNewSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE;

        boolean hasChanged = !mNewSS.equals(mSS);

        if (hasRegistered) {
            mNetworkAttachedRegistrants.notifyRegistrants();

            if (DBG) {
                log("pollStateDone: registering current mNitzUpdatedTime=" +
                        mNitzUpdatedTime + " changing to false");
            }
            mNitzUpdatedTime = false;
        }

        if (hasChanged) {
            String operatorNumeric;

            updateSpnDisplay();

            tm.setNetworkOperatorNameForPhone(mPhone.getPhoneId(), mSS.getOperatorAlphaLong());

            String prevOperatorNumeric = tm.getNetworkOperatorForPhone(mPhone.getPhoneId());
            operatorNumeric = mSS.getOperatorNumeric();
            tm.setNetworkOperatorNumericForPhone(mPhone.getPhoneId(), operatorNumeric);
            updateCarrierMccMncConfiguration(operatorNumeric,
                    prevOperatorNumeric, mPhone.getContext());
            if (operatorNumeric == null) {
                if (DBG) log("operatorNumeric is null");
                tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), "");
                mGotCountryCode = false;
                mNitzUpdatedTime = false;
            } else {
                String iso = "";
                String mcc = "";
                try{
                    mcc = operatorNumeric.substring(0, 3);
                    iso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
                } catch ( NumberFormatException ex){
                    loge("pollStateDone: countryCodeForMcc error" + ex);
                } catch ( StringIndexOutOfBoundsException ex) {
                    loge("pollStateDone: countryCodeForMcc error" + ex);
                }

                tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), iso);
                mGotCountryCode = true;

                TimeZone zone = null;

                if (!mNitzUpdatedTime && !mcc.equals("000") && !TextUtils.isEmpty(iso)) {

                    // Test both paths if ignore nitz is true
                    boolean testOneUniqueOffsetPath = SystemProperties.getBoolean(
                                TelephonyProperties.PROPERTY_IGNORE_NITZ, false) &&
                                    ((SystemClock.uptimeMillis() & 1) == 0);

                    ArrayList uniqueZones = TimeUtils.getTimeZonesWithUniqueOffsets(iso);
                    if ((uniqueZones.size() == 1) || testOneUniqueOffsetPath) {
                        zone = uniqueZones.get(0);
                        if (DBG) {
                           log("pollStateDone: no nitz but one TZ for iso-cc=" + iso +
                                   " with zone.getID=" + zone.getID() +
                                   " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath);
                        }

                        if (getAutoTimeZone()) {
                            setAndBroadcastNetworkSetTimeZone(zone.getID());
                        }
                        saveNitzTimeZone(zone.getID());
                    } else {
                        if (DBG) {
                            log("pollStateDone: there are " + uniqueZones.size() +
                                " unique offsets for iso-cc='" + iso +
                                " testOneUniqueOffsetPath=" + testOneUniqueOffsetPath +
                                "', do nothing");
                        }
                    }
                }

                if (shouldFixTimeZoneNow(mPhone, operatorNumeric, prevOperatorNumeric,
                        mNeedFixZoneAfterNitz)) {
                    // If the offset is (0, false) and the timezone property
                    // is set, use the timezone property rather than
                    // GMT.
                    String zoneName = SystemProperties.get(TIMEZONE_PROPERTY);
                    if (DBG) {
                        log("pollStateDone: fix time zone zoneName='" + zoneName +
                            "' mZoneOffset=" + mZoneOffset + " mZoneDst=" + mZoneDst +
                            " iso-cc='" + iso +
                            "' iso-cc-idx=" + Arrays.binarySearch(GMT_COUNTRY_CODES, iso));
                    }

                    if (zone != null) {
                        log("pollStateDone: zone="+zone);
                    } else
                    if ("".equals(iso) && mNeedFixZoneAfterNitz) {
                        // Country code not found.  This is likely a test network.
                        // Get a TimeZone based only on the NITZ parameters (best guess).
                        zone = getNitzTimeZone(mZoneOffset, mZoneDst, mZoneTime);
                        if (DBG) log("pollStateDone: using NITZ TimeZone");
                    } else
                    // "(mZoneOffset == 0) && (mZoneDst == false) &&
                    //  (Arrays.binarySearch(GMT_COUNTRY_CODES, iso) < 0)"
                    // means that we received a NITZ string telling
                    // it is  GMTin+0 w/ DST time zone
                    // BUT iso tells is NOT, e.g, a wrong NITZ reporting
                    // local time w/ 0 offset.
                    if ((mZoneOffset == 0) && (mZoneDst == false) &&
                        (zoneName != null) && (zoneName.length() > 0) &&
                        (Arrays.binarySearch(GMT_COUNTRY_CODES, iso) < 0)) {
                        zone = TimeZone.getDefault();
                        if (mNeedFixZoneAfterNitz) {
                            // For wrong NITZ reporting local time w/ 0 offset,
                            // need adjust time to reflect default timezone setting
                            long ctm = System.currentTimeMillis();
                            long tzOffset = zone.getOffset(ctm);
                            if (DBG) {
                                log("pollStateDone: tzOffset=" + tzOffset + " ltod=" +
                                        TimeUtils.logTimeOfDay(ctm));
                            }
                            if (getAutoTime()) {
                                long adj = ctm - tzOffset;
                                if (DBG) log("pollStateDone: adj ltod=" +
                                        TimeUtils.logTimeOfDay(adj));
                                setAndBroadcastNetworkSetTime(adj);
                            } else {
                                // Adjust the saved NITZ time to account for tzOffset.
                                mSavedTime = mSavedTime - tzOffset;
                            }
                        }
                        if (DBG) log("pollStateDone: using default TimeZone");
                    } else {
                        zone = TimeUtils.getTimeZone(mZoneOffset, mZoneDst, mZoneTime, iso);
                        if (DBG) log("pollStateDone: using getTimeZone(off, dst, time, iso)");
                    }

                    mNeedFixZoneAfterNitz = false;

                    if (zone != null) {
                        log("pollStateDone: zone != null zone.getID=" + zone.getID());
                        if (getAutoTimeZone()) {
                            setAndBroadcastNetworkSetTimeZone(zone.getID());
                        }
                        saveNitzTimeZone(zone.getID());
                    } else {
                        log("pollStateDone: zone == null");
                    }
                }
            }
            .......
    }


下面看下 关键 函数 shouldFixTimeZoneNow

protected boolean shouldFixTimeZoneNow(PhoneBase phoneBase, String operatorNumeric,
            String prevOperatorNumeric, boolean needToFixTimeZone) {
        // Return false if the mcc isn't valid as we don't know where we are.
        // Return true if we have an IccCard and the mcc changed or we
        // need to fix it because when the NITZ time came in we didn't
        // know the country code.

        // If mcc is invalid then we'll return false
        int mcc;
        try {
            mcc = Integer.parseInt(operatorNumeric.substring(0, 3));
        } catch (Exception e) {
            if (DBG) {
                log("shouldFixTimeZoneNow: no mcc, operatorNumeric=" + operatorNumeric +
                        " retVal=false");
            }
            return false;
        }

        // If prevMcc is invalid will make it different from mcc
        // so we'll return true if the card exists.
        int prevMcc;
        try {
            prevMcc = Integer.parseInt(prevOperatorNumeric.substring(0, 3));
        } catch (Exception e) {
            prevMcc = mcc + 1;
        }

        // Determine if the Icc card exists
        boolean iccCardExist = false;
        if (mUiccApplcation != null) {
            iccCardExist = mUiccApplcation.getState() != AppState.APPSTATE_UNKNOWN;
        }

        // Determine retVal
        boolean retVal = ((iccCardExist && (mcc != prevMcc)) || needToFixTimeZone);
        if (DBG) {
            long ctm = System.currentTimeMillis();
            log("shouldFixTimeZoneNow: retVal=" + retVal +
                    " iccCardExist=" + iccCardExist +
                    " operatorNumeric=" + operatorNumeric + " mcc=" + mcc +
                    " prevOperatorNumeric=" + prevOperatorNumeric + " prevMcc=" + prevMcc +
                    " needToFixTimeZone=" + needToFixTimeZone +
                    " ltod=" + TimeUtils.logTimeOfDay(ctm));
        }
        return retVal;
    }

五、案例分析

[系统设置][必现]关闭自动确定时区,更改时区后,重启手机,开启自动确定时区,时区同步错误

【原因分析】:原生的设计逻辑是只有在网络发生变化时才可以自动调整正确的时区,但是本问题的出现时用户在网络稳定后操作时区开关,由于此时网络不发生变化,因此导致时区无法调整正确,具体日志如下

//开始的时候,注册到网络上,但是因为自动对时区是关闭的, 导致系统默认没有更新时区,使用了默认时区
03-09 22:35:10.392 3569 3569 D GsmSST : [GsmSST0] pollStateDone: registering current mNitzUpdatedTime=false changing to false
03-09 22:35:10.418 3569 3569 D GsmSST : [GsmSST0] pollStateDone: fix time zone zoneName='America/Sao_Paulo' mZoneOffset=0 mZoneDst=false iso-cc='cn' iso-cc-idx=-3
03-09 22:35:10.418 3569 3569 D GsmSST : [GsmSST0] pollStateDone: using default TimeZone
03-09 22:35:10.418 3569 3569 D GsmSST : [GsmSST0] pollStateDone: zone != null zone.getID=America/Sao_Paulo

//用户操作自动对时区的开关为开,但是由于网络状态没有发生变化,导致时区还是默认值
03-09 22:35:28.578 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:28.579 3569 3569 D GsmSST : [GsmSST1] Reverting to NITZ TimeZone: tz='null
03-09 22:35:28.579 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:28.579 3569 3569 D GsmSST : [GsmSST0] Reverting to NITZ TimeZone: tz='America/Sao_Paulo

//用户操作自动对时区的开关为关
03-09 22:35:29.790 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:29.791 3569 3569 I GsmServiceStateTracker: Auto time zone state changed

//用户操作自动对时区的开关为开,但是由于网络状态没有发生变化,导致时区还是默认值
03-09 22:35:35.086 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:35.087 3569 3569 D GsmSST : [GsmSST1] Reverting to NITZ TimeZone: tz='null
03-09 22:35:35.087 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:35.087 3569 3569 D GsmSST : [GsmSST0] Reverting to NITZ TimeZone: tz='America/Sao_Paulo

//用户操作自动对时区的开关为关
03-09 22:35:38.159 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:38.161 3569 3569 I GsmServiceStateTracker: Auto time zone state changed

//用户操作自动对时区的开关为开,但是由于网络状态没有发生变化,导致时区还是默认值
03-09 23:35:43.070 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 23:35:43.071 3569 3569 D GsmSST : [GsmSST1] Reverting to NITZ TimeZone: tz='null
03-09 23:35:43.071 3569 3569 I GsmServiceStateTracker: Auto time zone state changed
03-09 22:35:43.080 3569 3569 D GsmSST : [GsmSST0] Reverting to NITZ TimeZone: tz='America/Sao_Paulo

【解决方案】:在网络注册成功后,将通过网络MCC查询出来的时区记忆下来,等待后续时区开关动作时使用


PS:NITZ 与 NTP 小结

现在Android通过网络同步时间有两种方式:NITZ和NTP,它们使用的条件不同,可以获取的信息也不一样;勾选自动同步功能后,手机首先会尝试NITZ方式,若获取时间失败,则使用NTP方式
1.NITZ(network identity and time zone)同步时间

NITZ是一种GSM/WCDMA基地台方式,必须插入SIM卡,且需要operator支持;可以提供时间和时区信息

中国大陆运营商基本是不支持的

2.NTP(network time protocol)同步时间

NTP在无SIM卡或operator不支持NITZ时使用,单纯通过网络(GPRS/WIFI)获取时间,只提供时间信息,没有时区信息(因此在不支持NITZ的地区,自动获取时区功能实际上是无效的)

NTP还有一种缓存机制:当前成功获取的时间会保存下来,当用户下次开启自动更新时间功能时会结合手机clock来进行时间更新。这也是没有任何网络时手机却能自动更新时间的原因。

此外,因为NTP是通过对时的server获取时间,当同步时间失败时,可以检查一下对时的server是否有效,并替换为其他server试一下。



你可能感兴趣的:(那些事儿)