需求:在客戶端安裝后一段時候后做某些不可描述的操作,普通情況下是通過手機時間來判斷的,導致審核人員通過修改手機時間就可以觸發到這些操作,所以需要使用網絡時間來校準,很多程度上就能避免被"強制"觸發的可能,類似的需求也應該還有很多的
因為系統本來就提供自動確定時間的功能,所以首先想到的是看看系統的實現,果然網上也能找到不少資料
系統的實現是通過 NetworkTimeUpdateService
這個類來實現的
Monitors the network time and updates the system time if it is out of sync and there hasn't been any NITZ update from the carrier recently. If looking up the network time fails for some reason, it tries a few times with a short interval and then resets to checking on longer intervals. If the user enables AUTO_TIME, it will check immediately for the network time, if NITZ wasn't available.
上面是 NetworkTimeUpdateService
類的描述,就是用來保持系統時間和網絡時間的同步的,并且有更新失敗重試的機制,如果用戶打開自動更新時間,優先會使用 NITZ(Network Identity and Time Zone)
機制來更新時間,而后才是 NTP(Network Time Protocol)
NITZ 和 NTP
NITZ
(網絡標識和時區),是一種用于自動配置本地的時間和日期的機制,需要運營商支持,可從運營商獲取時間和時區具體信息NTP
(網絡時間協議),用來同步網絡中各個計算機的時間的協議。在手機中,NTP
更新時間的方式是通過網絡向特定服務器獲取時間信息(不包含時區信息)
通過 NITZ
來更新時間依賴運營商,這樣會比較被動,你不知道什么時候才會收到更新,且涉及到驅動層,個人能力問題,這里就不分析,但根據 NetworkTimeUpdateService
可以看出其實通過監聽 TelephonyIntents.ACTION_NETWORK_SET_TIME
和 TelephonyIntents.ACTION_NETWORK_SET_TIMEZONE
廣播來監聽來自運營商的推送的
NTP 確定系統時間
NetworkTimeUpdateService#SettingsObserver
類是 ContentObserver
,用來監聽自動更新時間這個配置的打開
private static class SettingsObserver extends ContentObserver {
//...
//NetworkTimeUpdateService 啟動的時候調用
void observe(Context context) {
ContentResolver resolver = context.getContentResolver();
resolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.AUTO_TIME), false, this);
}
@Override
public void onChange(boolean selfChange) {
mHandler.obtainMessage(mMsg).sendToTarget(); //配置改變觸發
}
}
最后交由 NetworkTimeUpdateService#MyHandler
來處理
private class MyHandler extends Handler {
//...
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case EVENT_AUTO_TIME_CHANGED:
case EVENT_POLL_NETWORK_TIME:
case EVENT_NETWORK_CHANGED:
onPollNetworkTime(msg.what);
break;
}
}
}
一直跟蹤
private void onPollNetworkTime(int event) {
if (!isAutomaticTimeRequested()) return;
//...
onPollNetworkTimeUnderWakeLock(event);
//...
}
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) {
resetAlarm(mPollingIntervalMs);
return;
}
final long currentTime = System.currentTimeMillis();
if (DBG) Log.d(TAG, "System time = " + currentTime);
// Get the NTP time
if (mLastNtpFetchTime == NOT_SET || refTime >= mLastNtpFetchTime + mPollingIntervalMs
|| event == EVENT_AUTO_TIME_CHANGED) {
if (DBG) Log.d(TAG, "Before Ntp fetch");
// force refresh NTP cache when outdated
if (mTime.getCacheAge() >= mPollingIntervalMs) {
mTime.forceRefresh();
}
//...
}
resetAlarm(mPollingIntervalMs);
}
其中 mTime.forceRefresh
正式強制使用 NTP
來更新時間,mTime
是 NtpTrustedTime
類型的單例對象,其記錄了提供 NTP
的主機地址和超時時長,我看的是 25 版本的代碼,主機是 2.android.pool.ntp.org
public static synchronized NtpTrustedTime getInstance(Context context) {
if (sSingleton == null) {
final Resources res = context.getResources();
final ContentResolver resolver = context.getContentResolver();
final String defaultServer = res.getString(com.android.internal.R.string.config_ntpServer); //2.android.pool.ntp.org
final long defaultTimeout = res.getInteger( com.android.internal.R.integer.config_ntpTimeout);//5000
final String secureServer = Settings.Global.getString( resolver, Settings.Global.NTP_SERVER);
final long timeout = Settings.Global.getLong(resolver, Settings.Global.NTP_TIMEOUT, defaultTimeout); //5000
final String server = secureServer != null ? secureServer : defaultServer;
sSingleton = new NtpTrustedTime(server, timeout);
sContext = context;
}
return sSingleton;
}
繼續看它怎樣更新時間
NtpTrustedTime.java
@Override
public boolean forceRefresh() {
//...
final SntpClient client = new SntpClient();
if (client.requestTime(mServer, (int) mTimeout)) {
mHasCache = true;
mCachedNtpTime = client.getNtpTime();
mCachedNtpElapsedRealtime = client.getNtpTimeReference();
mCachedNtpCertainty = client.getRoundTripTime() / 2;
return true;
} else {
return false;
}
}
這里出現了一個 SntpClient
類,并調用 SntpClient#requestTime
方法,應該就獲取網絡時間的具體方法
public boolean requestTime(String host, int timeout) {
InetAddress address = null;
//..
address = InetAddress.getByName(host);
//...
return requestTime(address, NTP_PORT, timeout);
}
public boolean requestTime(InetAddress address, int port, int timeout) {
DatagramSocket socket = null;
try {
socket = new DatagramSocket();
socket.setSoTimeout(timeout);
byte[] buffer = new byte[NTP_PACKET_SIZE];
DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, port);
// set mode = 3 (client) and version = 3
// mode is in low 3 bits of first byte
// version is in bits 3-5 of first byte
buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
// get current time and write it to the request packet
final long requestTime = System.currentTimeMillis();
final long requestTicks = SystemClock.elapsedRealtime();
writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
socket.send(request);
// read the response
DatagramPacket response = new DatagramPacket(buffer, buffer.length);
socket.receive(response);
final long responseTicks = SystemClock.elapsedRealtime();
final long responseTime = requestTime + (responseTicks - requestTicks);
// extract the results
final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
final byte mode = (byte) (buffer[0] & 0x7);
final int stratum = (int) (buffer[1] & 0xff);
final long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
final long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
final long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
/* do sanity check according to RFC */
// TODO: validate originateTime == requestTime.
checkValidServerReply(leap, mode, stratum, transmitTime);
long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2;
// save our results - use the times on this side of the network latency
// (response rather than request time)
mNtpTime = responseTime + clockOffset;
mNtpTimeReference = responseTicks;
mRoundTripTime = roundTripTime;
} catch (Exception e) {
if (DBG) Log.d(TAG, "request time failed: " + e);
return false;
} finally {
if (socket != null) {
socket.close();
}
}
return true;
}
其通過 DatagramPacket
來實現 UDP
傳輸的,獲取響應結果并解析,最后怎么計算得到網絡時間這部分就不講了
其實這個流程下來還是比較簡單的,以下是這過程的流程圖
最后
根據系統根據 NTP
獲取時間流程,實現自己的網絡時間獲取也就不難了,主要是 SntpClient
和 NtpTrustedTime
這兩個類來實現的,但是是 @hide
的,所以可以考慮直接拷貝代碼或者通過反射的形式來實現即可,為了方便我使用反射的方式,代碼以上傳到了 gist
上,NTUSUtils.java