Эта статья будет посвящена тому, как мы в команде PREMIER, пытались установить актуальность загруженного контента и что из этого вышло. Возможно публикация будет полезна тем, кто решил следить за переводами времени устройства в условиях отсутствующего соединения.
Предыстория
По договоренности с правообладателем все скачанные фильмы и сериалы могут быть доступны пользователю только в течение 14 дней. Поэтому мы должны ограничивать пользователя в доступе к просмотру загрузок по истечении этого срока. Время окончания доступности для серии рассчитывается в момент ее загрузки и проверяется с помощью времени на устройстве.
Но может возникнуть ситуация, когда пользователь умышлено отключит интернет и, убрав последнюю возможность синхронизировать время с сервером, переведет время на устройстве назад, продлив тем самым для себя возможность просмотра контента. Чтобы избежать проблем с правообладателями и не ограничивать пользователей, которые по каким-то причинам перевели время вперед, уменьшив для себя время доступности контента, было принято решение пробовать отслеживать реальное оставшееся время для загрузок.
Засучив рукава
Изначально была идея использовать библиотеку AndroidTrueTime, но из-за того, что в ней используется NTP(Network Time Protocol), а нам требовалось работать в оффлайне, ее пришлось откинуть. Также была мысль использовать системное время из линукс (Hardware Time), который бы не изменялся после перевода времени, но для его использования нужно чтобы у пользователя был установлен BusyBox или что-то подобное.
Изменения времени решили устанавливать самостоятельно по системному уведомлению приходящему из BroadcastReceiver’a. Так как Android не присылает никакой информации о том, на сколько было изменено время на девайсе, а просто шлет уведомление о событии, то появилась потребности рассчитывать разницу между старым и новым временем устройства.
Для этого воспользовались связкой системных переменных:
System.currentTimeMillis() // возвращает текущее время время в миллисекундах
SystemClock.elapsedRealtime() // возвращает время от старта системы в миллисекундах
Разница между ними:
private fun currentTimestamp(): Long {
return System.currentTimeMillis() - SystemClock.elapsedRealtime()
}
Это позволило получить неизменяемый timestamp, который бы отличался только при изменении времени на устройстве и при перезагрузке устройства, но об этом позже.
Таким образом, для того чтобы узнать насколько изменилось время, мы находим разницу:
val timeChangeInMillis = savedTimestamp - currentTimestamp
savedTimestamp
устанавливается методом currentTimestamp()
при первом запуске приложения и служит базовой точкой отсчета при вычислениях разницы времени.
Изменение во времени сохраняем в переменную timeChangingDelta
.
Вывод актуального времени стал выглядеть так:
val timeChangingDelta = preferences.getLong(KEY_LAST_DIFFERENCE, NO_TIME)
val timestamp = System.currentTimeMillis() + timeChangingDelta
Непреодолимые трудности
Но что делать, если устройство было перезагружено, ведь в этот момент elapsedRealtime сбрасывается к 0, ломая наши вычисления? Правильно, дополнительно сохранять его в переменную elapsingTimeChangingDelta
. Перезагрузку устройства можно определить по признаку того, что сохраненное значение станет больше реального(т.к. реальное сбросится) или как аналог можно воспользоваться Settings.Global.BOOT_COUNT
, которая возвращает количество перезагрузок устройства.
val wasDeviceRebooted = Settings.Global.BOOT_COUNT > savedBootCount
Если девайс был перезагружен, то вычисляем изменение elapsedRealtime
и суммируем с предыдущими изменениями:
val elapsedTimeDelta = preferences.getLong(KEY_ELAPSED_TIME, NO_TIME) - SystemClock.elapsedRealtime()
preferences.edit {
val previousSum = getLong(KEY_ELAPSING_DELTA_SUM, NO_TIME)
putLong(KEY_ELAPSING_DELTA_SUM, previousSum + elapsedTimeDelta)
}
Метод получения времени стал выглядеть так:
fun timestamp(): Long {
when {
hasTimestamps() -> updateTimestamp()
else -> initializeTimestamps()
}
val timeChangingDelta = preferences.getLong(KEY_LAST_DIFFERENCE, NO_TIME)
val elapsingTimeChangingDelta = preferences.getLong(KEY_ELAPSING_DELTA_SUM, NO_TIME)
return System.currentTimeMillis() + (timeChangingDelta + elapsingTimeChangingDelta)
}
Сохраненное значение elapsedRealtime
суммируется к нашей timeChangingDelta
, и таким образом мы находим сдвиг времени.
Еще одним препятствием оказалось то, что событие о переводе времени приходит с задержкой или, в зависимости от ОС девайса, вовсе не приходит при закрытом приложении. Все вычисления потребовалось выполнять уже в момент отображения контента, что могло произойти с большой задержкой после перевода времени или перезагрузки.
Сматывая удочки
Стало ясно, что в текущем виде без дополнительной синхронизации устанавливать актуальное время не удастся.
В итоге, подход не справился с задачей в полной мере и от него пришлось отказаться. Пользователь все равно сможет обойти защиту с помощью перезагрузки и перевода времени. Но решение может оказаться для кого-то полезным в случае изменений времени в рамках одного цикла перезагрузки девайса.
Проблему пришлось решить другим способом, сохранив последнее время открытия серии в lastOpenTimestamp
и при обнаружении, что время девайса меньше сохраненного, блокировать доступ к серии. (currentTimestamp < lastOpenTimestamp
) . Это не решило всех проблем, но позволило защитить контент.
Комментарии (4)
Alexmaru
08.11.2022 00:46-2Записывайте показания других датчиков - например, барометра, уровня заряда батареи… Если человек пропутешествовал во времени назад, а данные сильно не совпадают с теми, что в журнале - пусть всё-таки соединится с интернетом.
aamonster
08.11.2022 01:51-1Как-то сложно... Не проще было по подозрительным событиям (перевод времени, перезагрузка) требовать синхронизации времени?
Потом можно требования ослабить – например, давать некий период после перезагрузки/перевода, в течение которого будет работать без синхронизации (к примеру, если до события в запасе оставалось больше суток – то на сутки).
werwolflg
10.11.2022 03:35А если использовать NTIZ для получения времени? Вроде бы обходными путями к нему можно было достучаться.
GDragon
Всегда можно сделать фулл-бекап приложения =)
В крайнем случае - системы.