Марти, серия еще доступна ?
Марти, серия еще доступна ?

Эта статья будет посвящена тому, как мы в команде 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)


  1. GDragon
    08.11.2022 00:31
    +1

    Всегда можно сделать фулл-бекап приложения =)
    В крайнем случае - системы.


  1. Alexmaru
    08.11.2022 00:46
    -2

    Записывайте показания других датчиков - например, барометра, уровня заряда батареи… Если человек пропутешествовал во времени назад, а данные сильно не совпадают с теми, что в журнале - пусть всё-таки соединится с интернетом.


  1. aamonster
    08.11.2022 01:51
    -1

    Как-то сложно... Не проще было по подозрительным событиям (перевод времени, перезагрузка) требовать синхронизации времени?

    Потом можно требования ослабить – например, давать некий период после перезагрузки/перевода, в течение которого будет работать без синхронизации (к примеру, если до события в запасе оставалось больше суток – то на сутки).


  1. werwolflg
    10.11.2022 03:35

    А если использовать NTIZ для получения времени? Вроде бы обходными путями к нему можно было достучаться.