对于一个Launcher来说,时间基准是最为重要的,特别是我们这种没有系统顶部的通知栏的app,而且会经常断电重启的Android机器,Android自带的校时,由于某些原因,校时比较慢,需要在app上加上一个校时才能保证在有网的情况下第一时间校时。

在某些场景下,校时还是比较重要的例如

1
2
3
1.直播课的开始时间
2.课程表的定位
3.今天明天(这种对用户友好的日期方式)

相关类图

系统设置

系统设置

系统校时

系统校时

既然是校时,我先了解一下系统是怎么个校时流程,然后再动手写自己的校时

系统设置与时间相关部分

在Android系统自带的设置中有设置时间的功能,从这里出发是最好的

设置时间

Settings里包含所有Activity, 这里面所有的Activity又继承SettingsActivity;
在SettingsActivity里ENTRY_FRAGMENTS里全是Fragment的类名,可想而知Settings App里基本都是有Fragment组成,在这里面,DateTimeSettings.class.getName(),尤为耀眼,这不是我要找的么,至于,怎么把这个Fragment加载到Settings里,现在重点不在这。

SettingsFragment实现了TimePickerDialog.OnTimeSetListener, DatePickerDialog.OnDateSetListener
这两个就是设置时间和日期的回调。

在onResume()里添加了时间改变的接收器,接收器了更新相关时间的UI

1
2
3
4
5
6
// Register for time ticks and other reasons for time change
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_TIME_TICK);
filter.addAction(Intent.ACTION_TIME_CHANGED);
filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
getActivity().registerReceiver(mIntentReceiver, filter, null, null);

在onTimeSet和onDateSet里分别将设置好的时间封装进一个Calender里最后调用

1
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when);

然后mIntentReceiver会接收时间改变的广播,更UI

自动校时设置

这是我们后面app校时的一种方式,估计没人发现并用起来
设置自动校时在OnSharedPreferenceChangeListener的回调中完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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);
}
}

我们后面也可来个轮询设置自动校时,相当于调用系统自带的校时api校时,但是这种也是需要系统app的权限才可以

1
2
3
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
Settings.Global.putInt(
getContentResolver(), Settings.Global.AUTO_TIME_ZONE, autoZoneEnabled ? 1 : 0);

这是设置这条线走完

系统校时部分

ServerManager与校时相关的

Android的系统服务都是在Framework中的ServerManager中启动的
其中startOtherServices()负责我们所关注的NetworkTimeUpdateService的创建和运行;

ServerManager中的与NetworkTimeUpdateService相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void startOtherServices() {
...
NetworkTimeUpdateService networkTimeUpdater = null;
...
if (!disableNetwork && !disableNetworkTime) { // line 904
try {
Slog.i(TAG, "NetworkTimeUpdateService");
networkTimeUpdater = new NetworkTimeUpdateService(context);
} catch (Throwable e) {
reportWtf("starting NetworkTimeUpdate service", e);
}
}
...
final NetworkTimeUpdateService networkTimeUpdaterF = networkTimeUpdater; //line 1080
...
// We now tell the activity manager it is okay to run third party
// code. It will call back into us once it has gotten to the state
// where third party code can really run (but before it has actually
// started launching the initial applications), for us to complete our
// initialization.
mActivityManagerService.systemReady(new Runnable() {
@Override
public void run() {
...
try {
if (networkTimeUpdaterF != null) networkTimeUpdaterF.systemRunning(); // line 1174
} catch (Throwable e) {
reportWtf("Notifying NetworkTimeService running", e);
}
...
}
}
}

在startOtherServices中初始化了NetworkTimeUpdateService, 然后调用NetworkTimeUpdateService#systemRunning()

NetworkTimeUpdateService

NetworkTimeUpdateService初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public NetworkTimeUpdateService(Context context) {
mContext = context;
// 获得mCachedNtpTime
mTime = NtpTrustedTime.getInstance(context);
// 定时触发广播
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
Intent pollIntent = new Intent(ACTION_POLL, null);
// resetAlarm()递归用的
mPendingPollIntent = PendingIntent.getBroadcast(mContext, POLL_REQUEST, pollIntent, 0);

mPollingIntervalMs = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpPollingInterval);
mPollingIntervalShorterMs = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpPollingIntervalShorter);
mTryAgainTimesMax = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpRetry);
mTimeErrorThresholdMs = mContext.getResources().getInteger(
com.android.internal.R.integer.config_ntpThreshold);

mWakeLock = ((PowerManager) context.getSystemService(Context.POWER_SERVICE)).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, TAG);
}

注册广播接收器,初始化Handler,开始校时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/** Initialize the receivers and initiate the first NTP request */
public void systemRunning() {
// 时间变化后,缓存上次的时间
registerForTelephonyIntents();
// 定时接收广播
registerForAlarms();
// 网络变化时,发送消息到 mHandler,发送消息
registerForConnectivityIntents();

HandlerThread thread = new HandlerThread(TAG);
thread.start();
mHandler = new MyHandler(thread.getLooper());
// Check the network time on the new thread
mHandler.obtainMessage(EVENT_POLL_NETWORK_TIME).sendToTarget();

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

校时轮询

处理状态变化后的时间轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/** Handler to do the network accesses on */
private class MyHandler extends Handler {

public MyHandler(Looper l) {
super(l);
}

@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 Automatic time is not set, don't bother.
if (!isAutomaticTimeRequested()) return;
mWakeLock.acquire();
try {
onPollNetworkTimeUnderWakeLock(event);
} finally {
mWakeLock.release();
}
}

onPollNetworkTimeUnderWakeLock这是校时的内心代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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();
}

// only update when NTP time is fresh
if (mTime.getCacheAge() < mPollingIntervalMs) {
final long ntp = mTime.currentTimeMillis();
mTryAgainCounter = 0;
// 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.d(TAG, "For initial setup, rtc = " + currentTime);
}
if (DBG) Log.d(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) {
SystemClock.setCurrentTimeMillis(ntp);
}
} else {
if (DBG) Log.d(TAG, "Ntp time is close enough = " + ntp);
}
mLastNtpFetchTime = SystemClock.elapsedRealtime();
} else {
// Try again shortly
mTryAgainCounter++;
if (mTryAgainTimesMax < 0 || mTryAgainCounter <= mTryAgainTimesMax) {
resetAlarm(mPollingIntervalShorterMs);
} else {
// Try much later
mTryAgainCounter = 0;
resetAlarm(mPollingIntervalMs);
}
return;
}
}
resetAlarm(mPollingIntervalMs);
}

执行逻辑

如果已经设置过了,并且和前一次设置的时间小于轮询时间,
下一个轮询时间后再来看,
如果上次刷新时间和调用此方法的时间差大于了轮询时间,
说明上次轮询超时,强制更新。
没有设置时间||当前启动时间大于上次轮询时间+轮询间隔,或者用户改变了自动校时
上次tcp请求到调用getCacheAge()之间的时间间隔>轮询的时间间隔
直接强制更新
小于的是时候
ntp 获得时间和当前时间大于容错时间的时候||或者时间没设置的时候
设置时间
同时页跳出了一只定时轮询的循环
剩下的是每次都会执行
重试次数++;
检验时间计数
有没有超过最大重试次数的时候
轮询时间设置短点
超过了
重置重试次数
并且请下次轮询时间增大
都有没满足,下次再说

系统校时总结

写法的巧妙之处
在于,两个不同的轮询间隔mPollingIntervalShorterMs和mPollingIntervalMs
第一次,强制更新后但并不会设置时间,而是等到mPollingIntervalShorterMs下次拿设置时间,
而下次来设置时间的时候,一般网络情况较好的时候,是能把时间获取得到
将时间保存在mTime中的mCachedNtpTime中,同时记录了获取时间时的开机时间

下次调用的时候,mTime.getCacheAge()基本都会小于mPollingIntervalMs
这是时候就可以得到ntp时间了(mTime.currentTimeMillis();)这个时间是由上次ntp获取的时间+当前开机时间-上次获取ntp时间时的开机时间
再判断断一下是否需要设置时间。
这是后就跳出轮询了。

没有满足条件就会resetAlarm(),在mPollingIntervalMs后在registerForAlarms注册的广播,
发送消息到MyHandler, 最后还是到onPollNetworkTimeUnderWakeLock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void registerForAlarms() {
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
mHandler.obtainMessage(EVENT_POLL_NETWORK_TIME).sendToTarget();
}
}, new IntentFilter(ACTION_POLL));
}

/**
* Cancel old alarm and starts a new one for the specified interval.
*
* @param interval when to trigger the alarm, starting from now.
*/
private void resetAlarm(long interval) {
mAlarmManager.cancel(mPendingPollIntent);
long now = SystemClock.elapsedRealtime();
long next = now + interval;
mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, next, mPendingPollIntent);
}

对于我们app来说应该怎么弄

设置时间目前我们有4种方式

SystemClock.setCurrentTimeMillis(ntp);
((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(when);
用shell命令来弄
轮询的方式设置为自动校时(这是方式还没试过,完全依赖系统还是比较靠谱的)
Settings.Global.putInt(getContentResolver(), Settings.Global.AUTO_TIME_ZONE, 1);
需要在mainfest.xml添加写入系统设置的权限(这个很没节操)

1
2
3
<!--写系统设置校时用的设置为自动校时-->
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"/>
<uses-permission android:name="android.permission.READ_SECURE_SETTINGS"/>

1, 2 两种需要系统签名,而3.三种需要有root权限。如果这两种都没有,抱歉臣妾做不到
还有一种是轮询设置自动校时,这种不道德,但是也是被逼的😆

我们的app没有系统签名,有root权限,比较奇葩

第4种是需要系统级app的权限。

校时的实现
当然我们这种网络校时有中不好的情况,
就是我们的校时先于系统的校时,这时候会触发两次时间改变的广播

时间改变后,重新提交一下fragment(简单粗暴我喜欢)

校时的services;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
public class SyncTimeService extends Service {
private static final String TAG = SyncTimeService.class.getSimpleName();
private Timer mTimer;
public SyncTimeService() {
}

@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {

return super.onStartCommand(intent, flags, startId);
}

@Override
public void onCreate() {
super.onCreate();
if (mTimer == null){
mTimer = new Timer();
} else {
mTimer.cancel();
mTimer = null;
mTimer = new Timer();
}
mTimer.schedule(new TimerTask() {
@Override
public void run() {
try {
URLConnection uc = new URL("http://www.baidu.com").openConnection();
uc.connect();
calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
DebugLogger.d(TAG, "没处理的网络时间:"+new Date(uc.getDate()));
calendar.setTimeInMillis(uc.getDate());
// 百度是GMT 0:00时区,需要转到GMT +8:00时区
calendar.setTimeZone(new SimpleTimeZone(8, "GMT"));
data = calendar.getTimeInMillis();
DebugLogger.d(TAG, "得到的网络时间:"+new Date(calendar.getTimeInMillis()).toString());
DebugLogger.d(TAG, "得到的系统时间:"+new Date(Calendar.getInstance().getTimeInMillis()));
// 年份相同直接停止
if (year == calendar.get(Calendar.YEAR)){
DebugLogger.d(TAG,"系统的网络校时成功或者已经校对过时间了");
stopSelf();
return;
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd.HHmmss");
Date date = new Date(data);
String format = simpleDateFormat.format(date);
String shellString = "date -s " +format;
// 设置时区,只能是这个问题了,这个很快就闪过去了,暂时没找到合适的原因
shellString += "&& setprop persist.sys.timezone \"Asia/Shanghai\"";
RootCmd.execRootCmd(shellString);
DebugLogger.d(TAG,"网络校时成功");
DebugLogger.d(TAG, "执行的shell命令:"+shellString);
DebugLogger.d(TAG,"校时过后的时区:"+ TimeZone.getDefault().getDisplayName());
} catch (Exception e) {
e.printStackTrace();
DebugLogger.d("SyncTimeService","网络校时失败");
}
}
}, 0,5000);
}

@Override
public void onDestroy() {
super.onDestroy();
if (mTimer != null){
mTimer.cancel();
}
}
}

一般都是系统校时快于我们手动执行校时。
参考

https://blog.csdn.net/firedancer0089/article/details/59526369