Android
March 19

Немного о часах и 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), где каждый уровень определяет степень удалённости от эталонного источника времени.

Иерархия NTP системы

Clock drift

Возможно, вы замечали, что после длительного отключения смартфона его время может отличаться от других устройств.

Это явление называется clock drift – расхождение системных часов устройства с точным временем из-за аппаратных или программных факторов.
Для исправления данной проблемы производится синхронизация с эталонными источниками времени при помощи NTP.

Графический пример Clock drift

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!🤘

Полезные ссылки:


Telegram-канал Теплица

Обратная связь