Android11 使用NTP同步时间

Android11 使用NTP同步时间

一、前言

前段时间遇到同样的版本和相关的产线的产品,存在有的设备可以同步时间,有的设备无法同步时间问题。
存在问题的设备不仅无法同步时间,而且无法使用浏览器搜索百度,但是可以ping同百度,
报错:system.err: Caused by: java.security.cert.CertPathValidatorException: timestamp check failed;
意外的是修改到当天的日期时间后是可以正常同步时间和正常浏览网页。
按照网上的做法:跳过SSL/TLS证书验证后,是可以正常同步时间的。

具体原因可能是:原生默认服务器 国内网络可能无法访问,可以用下阿里云的公网NTP获取时间。
跳过证书验证毕竟不是安全的行为,能直接访问是最好的。

在有问题的设备试了下,确实是阿里云的NTP网址是可以获取时间的,www.baidu.com是不行的。
并且测试了使用百度网址,修改到最新时间前3个月内和后面8个月内是可以获取到最新时间的。估计是百度服务器校验相关。

阿里云NTP网址(公网支持国内国外):
https://help.aliyun.com/document_detail/92704.html

如果需要研究系统NTP直接查看第三点的内容即可。

二、使用阿里云代码获取最新时间

代码参考:
https://blog.csdn.net/huang_cai_yuan/article/details/51689907

MainActivity.java

调用startCalibrateTime方法,创建一个线程获取时间,并且进行时间设置。


    private final int NTP_TIME_OUT_MILLISECOND = 3000;
    private final int NTP_TIMES = 20;

    private boolean isStopCalibrate = false;

    /**
     * ntp服务器地址集
     */
    private String[] ntpServerHost = new String[]{
            "ntp.aliyun.com",
            "ntp1.aliyun.com",
            "223.6.6.6",
            "www.alidns.com"
    };


    /**
     * 开始校准时间
     */
    public void startCalibrateTime() {
        new Thread() {
            @Override
            public void run() {
                int curTryTime = 0;
                while (!isStopCalibrate) {
                    for (int i = 0; i < ntpServerHost.length; i++) {
                        long time = getTimeFromNtpServer(ntpServerHost[i]);
                        SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd.HHmmss");
                        String datetime = df.format(time);
                        LogUtil.debug("i = " + i + ",time = " + time + ",datetime = " + datetime);
                        if (time != -1) {
                            int tryCount = 3;

                            while (tryCount > 0) {
                                tryCount--;
                                boolean isSetTimeSuccessful = setSysDateAndTime(MainActivity.this, time);
                                curTryTime++;
                                if (isSetTimeSuccessful) {
                                    tryCount = 0;
                                    isStopCalibrate = true;
                                    LogUtil.inform("set time successful");
                                } else {
                                    LogUtil.inform("set time failure");
                                }
                            }
                            break;
                        }
                    }
                    if (curTryTime >= NTP_TIMES) {
                        isStopCalibrate = true;
                        break;
                    }
                }
            }
        }.start();
    }


    /**
     * 从ntp服务器中获取时间
     *
     * @param ntpHost ntp服务器域名地址
     * @return 如果失败返回-1,否则返回当前的毫秒数
     */
    private long getTimeFromNtpServer(String ntpHost) {
        LogUtil.inform("get time from " + ntpHost);
        NtpClient client = new NtpClient();
        boolean isSuccessful = client.requestTime(ntpHost, NTP_TIME_OUT_MILLISECOND);
        if (isSuccessful) {
            return client.getNtpTime();
        }
        return -1;
    }

   //设置系统日期
    public boolean setSysDateAndTime(Context context, long time) {
        if (time / 1000 < Integer.MAX_VALUE) {
            ((AlarmManager) context.getSystemService(Context.ALARM_SERVICE)).setTime(time);
        }
        long timeLong = System.currentTimeMillis();
        LogUtil.debug("time = " + time + ",timeLong = " + timeLong);
        if (time - timeLong < 2000) {
            return true;
        } else {
            return false;
        }
    }

网络请求对象NtpClient :



package com.liwenzhi.synctimedemo;

import android.os.SystemClock;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

/**
 * ntp 网络请求数据对象
 */
public class NtpClient {

    private static final int REFERENCE_TIME_OFFSET = 16;
    private static final int ORIGINATE_TIME_OFFSET = 24;
    private static final int RECEIVE_TIME_OFFSET = 32;
    private static final int TRANSMIT_TIME_OFFSET = 40;
    private static final int NTP_PACKET_SIZE = 48;

    private static final int NTP_PORT = 123;
    private static final int NTP_MODE_CLIENT = 3;
    private static final int NTP_VERSION = 3;
// Number of seconds between Jan 1, 1900 and Jan 1, 1970
    // 70 years plus 17 leap days
    private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
    // system time computed from NTP server response
    private long mNtpTime;
    // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
    private long mNtpTimeReference;
    // round trip time in milliseconds
    private long mRoundTripTime;


    /**
     * Sends an SNTP request to the given host and processes the response.
     *
     * @param host    host name of the server.
     * @param timeout network timeout in milliseconds.
     * @return true if the transaction was successful.
     */
    public boolean requestTime(String host, int timeout) {
        DatagramSocket socket = null;
        try {

            socket = new DatagramSocket();
            socket.setSoTimeout(timeout);
            InetAddress address = InetAddress.getByName(host);
            byte[] buffer = new byte[NTP_PACKET_SIZE];
            DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, NTP_PORT);

            buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);

            // get current time and write it to the request packet
            long requestTime = System.currentTimeMillis();
            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);
            long responseTicks = SystemClock.elapsedRealtime();
            long responseTime = requestTime + (responseTicks - requestTicks);

            // extract the results
            long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
            long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
            long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
            long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);

            long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime)) / 2;
            mNtpTime = responseTime + clockOffset;
            mNtpTimeReference = responseTicks;
            mRoundTripTime = roundTripTime;
        } catch (Exception e) {
            LogUtil.error("", e);
            return false;
        } finally {
            if (socket != null) {
                socket.close();
            }
        }

        return true;
    }

    public long getNtpTime() {
        return mNtpTime;
    }

    public long getNtpTimeReference() {
        return mNtpTimeReference;
    }

    public long getRoundTripTime() {
        return mRoundTripTime;
    }


    /**
     * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
     */
    private long read32(byte[] buffer, int offset) {
        byte b0 = buffer[offset];
        byte b1 = buffer[offset + 1];
        byte b2 = buffer[offset + 2];
        byte b3 = buffer[offset + 3];

        // convert signed bytes to unsigned values
        int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
        int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
        int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
        int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);

        return ((long) i0 << 24) + ((long) i1 << 16) + ((long) i2 << 8) + (long) i3;
    }

    /**
     * Reads the NTP time stamp at the given offset in the buffer and returns
     * it as a system time (milliseconds since January 1, 1970).
     */
    private long readTimeStamp(byte[] buffer, int offset) {
        long seconds = read32(buffer, offset);
        long fraction = read32(buffer, offset + 4);
        return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
    }

    /**
     * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
     * at the given offset in the buffer.
     */
    private void writeTimeStamp(byte[] buffer, int offset, long time) {
        long seconds = time / 1000L;
        long milliseconds = time - seconds * 1000L;
        seconds += OFFSET_1900_TO_1970;

        // write seconds in big endian format
        buffer[offset++] = (byte) (seconds >> 24);
        buffer[offset++] = (byte) (seconds >> 16);
        buffer[offset++] = (byte) (seconds >> 8);
        buffer[offset++] = (byte) (seconds >> 0);

        long fraction = milliseconds * 0x100000000L / 1000L;
        // write fraction in big endian format
        buffer[offset++] = (byte) (fraction >> 24);
        buffer[offset++] = (byte) (fraction >> 16);
        buffer[offset++] = (byte) (fraction >> 8);
        // low order bits should be random data
        buffer[offset++] = (byte) (Math.random() * 255.0);
    }
}



其他参考:
android设置系统时区和时间
https://www.csdn.net/tags/OtTaMgzsMjM2MS1ibG9n.html

Android App 设置系统时间,语言和时区、系统重启
https://blog.csdn.net/jdfkldjlkjdl/article/details/121020649

时区:
https://www.cnblogs.com/minimeta/p/16555015.html

ntp的配置:
https://blog.csdn.net/LoongEmbedded/article/details/111185359

后面研究发现,NtpClient的代码,在系统Framework中是的。
所以说系统是会自动同步时间的。但是为啥点击自动同步不能自动更新时间呢?下面是简单分析。

三、Android 系统NTP server分析

下面是简单的研究分析和涉及文件:

1、获取ntpServer网址类
frameworks\base\core\java\android\util\NtpTrustedTime.java
2、ntp默认网址配置
frameworks\base\core\res\res\values\config.xml
3、监听时间同步开关并进行时间设置管理
frameworks\base\services\core\java\com\android\server\NetworkTimeUpdateService.java

1、分析ntp网址的获取

NtpTrustedTime.java

    //获取最新时间方法
    public boolean forceRefresh() {
        synchronized (this) {
            NtpConnectionInfo connectionInfo = getNtpConnectionInfo();
            
//。。。网络等判断空情况

            final SntpClient client = new SntpClient();
            final String serverName = connectionInfo.getServer();
            final int timeoutMillis = connectionInfo.getTimeoutMillis();
        //(1)使用SntpClient 对象请求ntp网络获取时间
            if (client.requestTime(serverName, timeoutMillis, network)) {
                long ntpCertainty = client.getRoundTripTime() / 2;
        //(2)获取网络时间
                mTimeResult = new TimeResult(
                        client.getNtpTime(), client.getNtpTimeReference(), ntpCertainty);
                return true;
            } else {
                return false;
            }
        }
    }

    //获取ntp网址的方法
    private NtpConnectionInfo getNtpConnectionInfo() {
        final ContentResolver resolver = mContext.getContentResolver();

        final Resources res = mContext.getResources();
        //(1)从config配置中获取ntpServer网址配置
        final String defaultServer = res.getString(
            com.android.internal.R.string.config_ntpServer);
        //(2)从config配置中获取ntpTimeout网址连接超时时间长短配置
        final int defaultTimeoutMillis = res.getInteger(
            com.android.internal.R.integer.config_ntpTimeout);

        //(3)从Settings变量中获取NTP_SERVER网址配置
        final String secureServer = Settings.Global.getString(
            resolver, Settings.Global.NTP_SERVER);
        //(4)从Settings变量中获取ntpTimeout网址连接超时时间长短配置
        final int timeoutMillis = Settings.Global.getInt(
            resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis);
        //(5)最后判断如果Settings中未配置,则从config默认配置中获取
        final String server = secureServer != null ? secureServer : defaultServer;
        return TextUtils.isEmpty(server) ? null : new NtpConnectionInfo(server, timeoutMillis);
    }

所以ntp网址的获取的次序就是:
优先判断是否已在Settings.Global.NTP_SERVER变量中设置,如果没有就从config中获取。

2、定义默认ntp网址的文件

config.xml

    //(1)默认ntp网址,可以换成国家ntp网址:us.pool.ntp.org,获取阿里云公网:ntp.aliyun.com
    <!-- Remote server that can provide NTP responses. old ntp:time.android.com -->
    <string translatable="false" name="config_ntpServer">us.pool.ntp.org</string>
    //(2)ntp网址连接超时时间
    <!-- Try-again polling interval in milliseconds, in case the network request failed -->
    <integer name="config_ntpPollingIntervalShorter">60000</integer>

尝试ping time.android.com 确实是ping不通的。
使用公网ntp网络us.pool.ntp.org/阿里云ntp网络ntp.aliyun.com是可以ping通的。

所以这就是问题的根本原因。
所以解决这个问题只需要:

方式1:
修改config.xml的config_ntpServer为可用的ntp网址即可

方式二:
系统启动会校验是否存在:Settings.Global.NTP_SERVER属性
如果没有就收到设置可用的ntp网址。


3、监听时间同步开关并进行时间设置管理Service

下面是 NetworkTimeUpdateService.java 的代码简单分析

(1)自动同步事件监听

    //下面这段代码可以看到这里
    private static class AutoTimeSettingObserver extends ContentObserver {
        void observe() {
            ContentResolver resolver = mContext.getContentResolver();
            resolver.registerContentObserver(Settings.Global.getUriFor(Settings.Global.AUTO_TIME),
                    false, this);
        }

        @Override
        public void onChange(boolean selfChange) {
            if (isAutomaticTimeEnabled()) {
                mHandler.obtainMessage(mMsg).sendToTarget();
            }
        }

        /**
         * Checks if the user prefers to automatically set the time.
         */
        private boolean isAutomaticTimeEnabled() {
            ContentResolver resolver = mContext.getContentResolver();
            return Settings.Global.getInt(resolver, Settings.Global.AUTO_TIME, 0) != 0;
        }
    }

从上面代码可以看出Service中是有对Settings.Global.AUTO_TIME属性进行监听的,
并且监听到打开状态会做相应的操作,比如从网络同步时间。

(2)网络变化监听

    private class NetworkTimeUpdateCallback extends NetworkCallback {
        @Override
        public void onAvailable(Network network) {
            Log.d(TAG, String.format("New default network %s; checking time.", network));
            mDefaultNetwork = network;
            // Running on mHandler so invoke directly.
            onPollNetworkTime(EVENT_NETWORK_CHANGED);
        }

        @Override
        public void onLost(Network network) {
            if (network.equals(mDefaultNetwork)) mDefaultNetwork = null;
        }
    }

这里可以看到是判断网络变化为可用onAvailable时会进行对应处理。
具体监听都是在Service创建的时候监听,这里不进行展开描述。

(3)handler处理

    private static final int EVENT_AUTO_TIME_ENABLED = 1; //监听设置同步时间
    private static final int EVENT_POLL_NETWORK_TIME = 2; //系统启动首次初始化
    private static final int EVENT_NETWORK_CHANGED = 3; //监听网络改变

    private class MyHandler extends Handler {

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

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case EVENT_AUTO_TIME_ENABLED:
                case EVENT_POLL_NETWORK_TIME:
                case EVENT_NETWORK_CHANGED:
                    onPollNetworkTime(msg.what);
                    break;
            }
        }
    }

其实不管是什么消息,这里都是进行调用了更新时间的方法onPollNetworkTime。

(4)onPollNetworkTime 方法

下面简单跟一下 onPollNetworkTime 方法流程

    private void onPollNetworkTime(int event) {
        // If we don't have any default network, don't bother.
        if (mDefaultNetwork == null) return;
        mWakeLock.acquire();
        try {
            //关键流程(1)
            onPollNetworkTimeUnderWakeLock(event);
        } finally {
            mWakeLock.release();
        }
    }

    //关键流程 <1> 方法
    private void onPollNetworkTimeUnderWakeLock(int event) {
        // Force an NTP fix when outdated
        NtpTrustedTime.TimeResult cachedNtpResult = mTime.getCachedTimeResult();
        if (cachedNtpResult == null || cachedNtpResult.getAgeMillis() >= mPollingIntervalMs) {
            if (DBG) Log.d(TAG, "Stale NTP fix; forcing refresh");
            mTime.forceRefresh();//关键流程 <2> 获取到网络时间
            cachedNtpResult = mTime.getCachedTimeResult();
        }
        if (cachedNtpResult != null && cachedNtpResult.getAgeMillis() < mPollingIntervalMs) {
            // Obtained fresh fix; schedule next normal update
            //关键流程<3>真正就行时间同步
            resetAlarm(mPollingIntervalMs);//同步闹钟时间,不知道会不会同步系统数据!
            //自己添加的同步时间和同步RTC
            if (isAutomaticTimeRequested()) {
                updateSystemClock(event); 
            }
        }
        。。。//有多次重连机制,不进行展开描述!
    }
    

而关键流程<2>又回到了最开始的时候的NtpTrustedTime.java ntp获取网络时间的逻辑。

所以上面就是从各个状态到获取网络时间的完整流程。

(5)根据需求获取到最新时间后的具体操作

    private void updateSystemClock(int event) {
        //判断是否勾选了"同步时间",触发的时间更新
        //如果不是"同步时间"触发的,判断时间差距大的就同步一下,否则不用管(其实没啥用)
        final boolean forceUpdate = (event == EVENT_AUTO_TIME_ENABLED);
        if (!forceUpdate) {
            if (getNitzAge() < mPollingIntervalMs) {
                if (DBG) Log.d(TAG, "Ignoring NTP update due to recent NITZ");
                return;
            }

            final long skew = Math.abs(mTime.currentTimeMillis() - System.currentTimeMillis());
            if (skew < mTimeErrorThresholdMs) {
                if (DBG) Log.d(TAG, "Ignoring NTP update due to low skew");
                return;
            }
        }

        SystemClock.setCurrentTimeMillis(mTime.currentTimeMillis());//关键流程 同步系统时间
        //这是自己加的代码方法,一般系统需要同步设置RTC时间,可以在这里进行同步
        updateRtcTime();
    }

执行了SystemClock.setCurrentTimeMillis 方法后,后面各应用获取时间就是设置的时间了。

系统有啥获取时间的方法:

//1、Calendar获取时间值
        Calendar calendar = Calendar.getInstance();
        int year = calendar.get(Calendar.YEAR);
        int month = calendar.get(Calendar.MONTH);
        int day = calendar.get(Calendar.DAY_OF_MONTH);
        int hour = calendar.get(Calendar.HOUR_OF_DAY);
        int minute = calendar.get(Calendar.MINUTE);
        int sec = calendar.get(Calendar.SECOND);

//2、System.currentTimeMillis()获取系统时间搓


RTC是啥?RTC是硬件芯片记录时间的程序。

RTC 官方解释:
Real-time clock,中文名称:实时时钟,是指可以像时钟一様输出实际时间的电子设备,
一般会是集成电路,因此也称为时钟芯片。

网上 NetworkTimeUpdateService 相关分析:
https://www.cnblogs.com/zhangming-blog/articles/6215312.html

四、之前写的一个同步时间Demo

之前以为要ntp时间同步自己实现,后面发现系统已经存在,所以这个demo没有太多的使用价值。
还是有一定的分析和研究价值的demo代码,供大家参考。

界面效果:

在这里插入图片描述
这里是在模拟器上运行,所以第一点ping 百度网站没返回数据,真机联网是会返回数据的。

包含内容:


1、判断网络是否可用
2、ping www.baidu.com网络并返回网络情况
3、使用最原始的URLConnection方式情况网络获取百度网址的时间
4、参考系统NTP源码使用DatagramSocket获取网络时间
5、查看获取网络时间需要的时间

demo代码:
https://download.csdn.net/download/wenzhi20102321/86806264

五、总结一下 Android ntp时间同步

Android 系统是有ntp相关服务 NetworkTimeUpdateService。

但是 NetworkTimeUpdateService 并不是单纯的Service,
而是一个继承了extends Binder的BinderService,具体怎么使用不展开描述。

这个服务在SystemServer起来的时候,
伴随OtherService那些服务一起起来,
并且后续在相关时机会做一些同步时间。

其实NetworkTimeUpdateService的代码并不是很完善的,
比如同步SystemClock和硬件RTC时间都是要自己添加代码实现的。

如果不想在Framework的Service中操作这些时间同步,
在自己的应用中创建Service也是可以进行同步时间的操作,
需要的广播,状态监听等可以参考NetworkTimeUpdateService的实现。

六、NTP相关问题解决思路

1、设备联网后点击"同步时间",但是时间未同步

(1)判断设备是否联网成功,直接ping网址或者使用里面浏览器浏览网页查看
(2)判断ntp网址是否有效
手动设置一个能正常ping通的ntp网址
settings put global ntp_server us.pool.ntp.org //设置ntp网址
settings get global ntp_server //查询ntp网址,默认为null
需要重启后ntpService才会使用新网址请求时间。

2、同一个软件在不同的设备上的能同步时间,有的设备不能同步时间

这个问题之前还报了原厂,后面发现是未使用ntp网址的导致,
之前在应用中使用百度网址获取网上时间,
需要把系统时间修改到最近时间才能请求网络获取到到数据,这个估计和百度服务器校验相关
能够成功是有时间范围要求的,往前早三个月内,和网后10个月内,都是可以获取到最新时间的,
有效时间范围外会有CA证书校验失败的错误:
System.err: Caused by: java.security.cert.CertPathValidatorException: timestamp check failed
无法获取到最新时间。
所以使用ntp网址更新时间时没有这个问题的。

共勉:曾经错过的东西,不一定能追回到最初,但是能尽力挽回,也不枉初心。

猜你喜欢

转载自blog.csdn.net/wenzhi20102321/article/details/127482145