Немного о часах и TrustedTime API
Недавно Google представил новое API, которое позволяет получать текущее реальное время напрямую с NTP
-серверов Google, не используя системные часы устройства. В этой статье я хочу не просто показать, как его подключить, но и подробнее рассказать, что такое NTP
, как работает этот протокол и что такое timestamp
и clock drift
.
NTP
Network Time Protocol (NTP)
— это сетевой протокол, предназначенный для синхронизации времени между устройствами.
Он работает по модели клиент-сервер: NTP
-клиенты запрашивают актуальное время у NTP
-серверов, которые получают его от высокоточных источников, таких как атомные часы или GPS
. Затем сервер передаёт точные данные клиенту, который обновляет своё время.
Для передачи используется протокол User Datagram Protocol (UDP)
, который обеспечивает быструю отправку данных без установления соединения. Соответственно, запрос на получение точного времени можно отправить через стандартные IP
-сети.
Вся система NTP
организована в иерархическую структуру с уровнями (stratum
), где каждый уровень определяет степень удалённости от эталонного источника времени.
Clock drift
Возможно, вы замечали, что после длительного отключения смартфона его время может отличаться от других устройств.
Это явление называется clock drift
– расхождение системных часов устройства с точным временем из-за аппаратных или программных факторов.
Для исправления данной проблемы производится синхронизация с эталонными источниками времени при помощи NTP
.
Timestamp
При разработке функционала, где мы реализуем отображение времени заказа или операции, мы часто работаем с timestamp
(временной меткой).
Timestamp
— это числовое значение, указывающее на конкретный момент времени, зафиксированный с определенной точностью.
Данное значение обычно является количеством секунд или миллисекунд, прошедших с определенной даты отсчёта. Чаще всего в работе встречается Unix Timestamp
, где отсчёт начинается с 1 января 1970 года 00:00:00 UTC.
Timestamp может иметь различную точность: до секунды, миллисекунды, микросекунды или наносекунды. Поэтому, важно заранее обсудить точность с backend-разработчиком. Например, если вы разрабатываете чат поддержки и имеете разную точность с сервером, то могут встречаться ситуации с дублированием информации или некорректной сортировкой.
Для чего нужен TrustedTime API
Локальное время подвержено изменениям: в стране могут ввести или отменить перевод на летнее время, изменить часовой пояс для какой-то местности (или вовсе объединить в один часовой пояс всю страну).
Для владельцев устройств с Android версией ниже 10 такие изменения могут стать серьезной проблемой. До Android 10 обновления данных о часовых поясах зависели от системных обновлений, и чем старше устройство, тем ниже вероятность получения актуальной информации. В некоторых случаях, пользователи могут обновить часовые зоны вручную, но для этого часто требуется ROOT-доступ.
В итоге многие пользователи начинают вручную корректировать системное время, что делает его ненадёжным источником.
TrustedTime API позволяет приложениям получать точное и защищенное время (Unix Timestamp
), даже если системные часы устройства не являются достоверными.
Подключение TrustedTime API
Ниже будет приведён упрощённый вариант реализации логики.
Для начала необходимо добавить зависимость в build.gradle
:
dependencies { implementation("com.google.android.gms:play-services-time:{version}") }
Далее, можно создать интерфейс и его реализацию, которые позволят инкапсулировать логику создания TrustedTimeClient
и получения времени:
interface TimeService { suspend fun getCurrentTimeMillisOrNull(): Long? } class DefaultTimeService(private val context: Context) : TimeService { @Volatile private var trustedTimeClient: TrustedTimeClient? = null override suspend fun getCurrentTimeMillisOrNull(): Long? { val client = trustedTimeClient ?: createClient() return client.computeCurrentUnixEpochMillis() } private suspend fun createClient(): TrustedTimeClient { return suspendCancellableCoroutine { continuation -> TrustedTime.createClient(context) .addOnSuccessListener { client -> trustedTimeClient = client continuation.resume(client) } .addOnFailureListener { exception -> continuation.resumeWithException(exception) } } } }
Я использовала Koin при создании примера кода, поэтому файл DI может выглядеть следующим образом:
val appModule = module { single<TimeService> { DefaultTimeService( context = androidContext(), ) } viewModel { MainViewModel( timeService = get(), ) } }
Класс Application
в таком случае может выглядеть так:
class MyApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MyApplication) modules(appModule) } } }
Не забываем указать Application
в AndroidManifest
:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <application android:name=".MyApplication" //...
Теперь можно подключать сервис и использовать его. Например, так:
class MainViewModel( private val timeService: TimeService, ) : ViewModel() { private val _currentTime = MutableStateFlow<Long?>(null) val currentTime: StateFlow<Long?> = _currentTime.asStateFlow() fun updateCurrentTime() { viewModelScope.launch { _currentTime.update { timeService.getCurrentTimeMillisOrNull() } } } }
Работа на устройствах без GMS
Если ваше приложение распространяется в Huawei AppGallery и может использоваться на устройствах без GMS, то стоит учитывать, что TrustedTime API не будет работать на таких устройствах.
При попытке создать TrustedTimeClient
вы получите исключение ApiException
с кодом 17
. Я рекомендую добавить метод проверки наличия GMS на устройстве:
private fun Context.isGooglePlayServicesAvailable(): Boolean { return GoogleApiAvailability .getInstance() .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS }
и вызывать его в методах создания клиента и получение времени.
Какие ещё методы есть у TrustedTimeClient
К сожалению, доступных методов на момент написания статьи не так много. Нам доступны:
getLatestTimeSignal()
— возвращает последний полученныйTimeSignal
илиnull
, если внешний сигнал времени отсутствует. Позволяет получить информацию об ошибках в измерении времени и проследить погрешность;computeCurrentUnixEpochMillis()
— данный метод рассматривался ранее в статье. Является заменойSystem.currentTimeMillis()
, но без влияния системных часов устройства. Возвращаетзначение в миллисекундах
с начала Unix-эпохи, основываясь на доверенном внешнем источнике, или возвращаетnull
, если сигнал недоступен;computeCurrentInstant()
— вычисляетInstant
на основе последнего точного временного сигнала от внешнего источника или возвращаетnull
, если сигнал недоступен. Является заменой дляInstant.now()
без влияния системных часов устройства;addTimeSignalListener(listener: OnNewTimeSignalAvailableListener)
— метод для регистрации слушателя сигналов времени. После регистрации слушатель сразу получает последний сигнал времени, если он доступен. Стоит отметить, что повторная регистрация слушателя не будет выполняться, если слушатель в клиенте уже зарегистрирован;removeTimeSignalListener(listener: OnNewTimeSignalAvailableListener)
— метод для удаления ранее зарегистрированного слушателя. В случае, если слушатель зарегистрирован не был, то вызов метода не порождает исключений;dispose()
— позволят освободить ресурсы клиента, когда он становится ненужным и отменяет регистрацию всех слушателей.
На этом наш пост подошёл к концу. Good coding and happy day!🤘