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中。