1. 程式人生 > >Android中的時間自動更新

Android中的時間自動更新

最近幾天,一直糾結於android的時間的自動更新,先簡要說下android自己原有的更新機制,android原有的更新機制很簡單,是採用NITZ(Network identity and Time Zone)的方式更新的,這應該是一種運營商的可選服務,簡單的來說,就是運營商通知CP主動上報時間資訊,CP上報後上層更新相應的時間。CDMA制式估計上報時間比較頻繁,更新比較給力,因此CDMA制式的時間只能自動更新,而不會讓使用者手動設定的(原因僅僅為個人猜測)。WCDMA和GSM的就比較悲催了,如果你等主動上報的更新訊息,可能得等N長時間還是不能更新。

但是,也不是說沒有辦法讓時間自動更新,採用SNTP方式的話,我們也能自動更新時間,但是,採用SNTP的話,是不能更新時區的。

下面以4.0的程式碼為例(2.3的基本類似且更簡單,並且2.3中沒有自帶SNTP的更新方式)來分析下android的自動更新時間原理。

1.選中自動更新

其實就是在資料庫中設定了一個值,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_DATE_FORMAT)) {
            String format = preferences.getString(key,
                    getResources().getString(R.string.default_date_format));
            Settings.System.putString(getContentResolver(),
                    Settings.System.DATE_FORMAT, format);
            updateTimeAndDateDisplay(getActivity());
        } else if (key.equals(KEY_AUTO_TIME)) {
            boolean autoEnabled = preferences.getBoolean(key, true);
            Settings.System.putInt(getContentResolver(), Settings.System.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.System.putInt(
                    getContentResolver(), Settings.System.AUTO_TIME_ZONE, autoZoneEnabled ? 1 : 0);
            mTimeZone.setEnabled(!autoZoneEnabled);
        }
    }

KEY_AUTO_TIME就是更新時間的CheckBoxPreference,下面的KEY_AUTO_TIME_ZONE就是更新時區的KEY_AUTO_TIME_ZONE,這兩個操作僅僅只是設定了兩個值下去而已,那麼真正幹事情的地方在哪兒呢?

2.真正做事的地方

我們選中CheckBoxPreference,僅僅只是更改了資料庫的值,但是時間和時區還是得更新的,那麼更新它們的地方在哪兒呢?在GsmServiceStateTracker.java中。

程式碼路徑為framworks/base/telephony/java/com/android/internal/telephony/gsm/GsmServiceStateTracker.java

裡面有兩個ContentObserver來監聽資料庫內容的變化,讓我們來看下這兩個ContentObserver的定義。

        cr = phone.getContext().getContentResolver();
        cr.registerContentObserver(
                Settings.System.getUriFor(Settings.System.AUTO_TIME), true,
                mAutoTimeObserver);
        cr.registerContentObserver(
                Settings.System.getUriFor(Settings.System.AUTO_TIME_ZONE), true,
                mAutoTimeZoneObserver);

可以很明顯的看到兩個監聽的和我們設定的URI是一樣的。那我們再來看下這兩個ContentObserver幹了啥事

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

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

OK,真相出來了,正是他們更新了時間和時區。好的,下一步,我們繼續分析時間和時區究竟是如何更新的。

3.更新時間和時區

先來分析更新時間。可能大家用過4.0的非CDMA制式手機就會有這樣的感覺,選擇自動更新時間,很快就自動更新了,選擇自動更新時區,發現有時候過了很久很久都沒有更新。OK,讓我們先來分析下,時間是怎麼自動更新的。

來分析下revertToNitzTime()函式。

    private void revertToNitzTime() {
        if (Settings.System.getInt(phone.getContext().getContentResolver(),
                Settings.System.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));
        }
    }
 

這個函式做的事情好簡單啊,就是判斷了下有沒有選中自動更新啊,沒有,那麼返回。有,那再繼續判斷,mSavedTime和mSavedAtTime為不為0啊(這兩個變數後面再講),都不為0,那麼就要發廣播了。

    private void setAndBroadcastNetworkSetTime(long time) {
        SystemClock.setCurrentTimeMillis(time);
        Intent intent = new Intent(TelephonyIntents.ACTION_NETWORK_SET_TIME);
        intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
        intent.putExtra("time", time);
        phone.getContext().sendStickyBroadcast(intent);
    }

傳送廣播如上,找到廣播接收的地方NetworkTimeUpdateService.java

路徑如下:frameworks/base/services/java/com/android/server/NetworkTimeUpdateService.java

找到它的receiver,驚訝的發現,什麼事都沒幹,就是賦值了兩個變數。

        @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();
            }
        }

真正更新時間的地方在哪兒?

答案還是在NetworkTimeupdateService.java中,它也註冊了ContentObserver。

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


SettingsObserver就是一個ContentObserver,具體的程式碼我就不貼出來了,很簡單,大家可以自己去看。

好的,繼續分析更改時間的地方,找到handleMessage裡的回撥函式,onPollNetworkTime(),這個函式很長,我就簡單的貼出部分關鍵程式碼

        // If NITZ time was received less than POLLING_INTERVAL_MS time ago,
        // no need to sync to NTP.
        if (mNitzTimeSetTime != NOT_SET && refTime - mNitzTimeSetTime < POLLING_INTERVAL_MS) {
            resetAlarm(POLLING_INTERVAL_MS);
            return;
        }
.......
            if (mTime.getCacheAge() < POLLING_INTERVAL_MS) {
                final long ntp = mTime.currentTimeMillis();
.....
}


OK,看註釋,看到我們更新的NITZ時間不為NOT_SET(-1),且更新的時間小於POLLING_INTERVAL_MS,那麼就直接更新NITZ的時間,否則用SNTP更新時間。那麼,NITZ的時間是從何而來的呢,我們進一步分析。

4.為什麼不能更新時區,卻能更新時間

看看NITZ的時間究竟從何而來,找到廣播的傳送處。GsmServiceStateTracker中,我們傳送的是mSavedTime
                    + (SystemClock.elapsedRealtime() - mSavedAtTime)。

看看這兩個變數在哪裡賦值,在setTimeFromNITZString()中,那麼我們往上追蹤就會發現,這個函式是由RIL_UNSOL_NITZ_TIME_RECEIVED這個主動上報的訊息激發的,至此,我們終於找到了時間更新原理。先看有沒有RIL的主動上報,如果有,那麼就用這種主動上報的NITZ時間更新,如果沒有,那麼就選擇用SNTP更新時間。

那麼,時區為什麼不能自動更新呢,那是因為,如果沒有RIL的主動上報,時區就沒有了初始值的,而SNTP不能更新時區。所以,時區只能用NITZ更新。

我們來用程式碼驗證下。

先來看發廣播的地方,廣播仍然發到了NetworkTimeUpdateService,它是更改了mNitzZoneSetTime,但是,我們驚奇的發現,這個變數值一點作用都沒有,而且在mNitzZoneSetTime中,我們也沒有自動更新時區的監聽。因此,時區完全被我們拋棄了。。。

那在GsmServiceStateTracker中呢,看看RIL主動上報幹了些啥事

            String iso = SystemProperties.get(TelephonyProperties.PROPERTY_OPERATOR_ISO_COUNTRY);

            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) {
                // We got the time before the country, so we don't know
                // how to identify the DST rules yet.  Save the information
                // and hope to fix it up later.

                mNeedFixZone = true;
                mZoneOffset  = tzOffset;
                mZoneDst     = dst != 0;
                mZoneTime    = c.getTimeInMillis();
            }

            if (zone != null) {
                if (getAutoTimeZone()) {
                    setAndBroadcastNetworkSetTimeZone(zone.getID());
                }
                saveNitzTimeZone(zone.getID());
            }

利用得到的國家碼和偏移值算出時區,而這個算出來的時區會儲存在變數中,當你選擇自動更新的時候,會把這個變數賦值給上層完成更新。好了,大體上更新時間和時區就是這樣,還有部分是天線狀態改變的時候會觸發時間和時區的更改,這裡就不討論了,大家有興趣的可以看下,程式碼同樣在GsmServiceStateTracker中。