10 December 2014

Ведение независимого времени на android девайсе

Блог компании ЕТранспорт corporate blogDevelopment of mobile applicationsDevelopment for Android
Здравствуйте!

В один прекрасный день приходит ко мне менеждер и говорит: «Можем ли мы запретить пользователю менять время на телефоне?». И конечно же ответ мой был нет, но это не решало задачу. Необходимо было искать выход из ситуации.
Критерии для решения были следующими:
  • должно работать без частых синхронизаций с сервером, например, достаточно взять время раз в месяц.
  • должно быть устойчиво к переводу времени назад/вперед/смене часового пояса
  • работать при перезагрузке устройства/неожиданном завершении/ вытаскивании батареи
  • не отклоняться от эталонного времени на слишком большие значения, в моем случае было 5 минут.
  • если все же удалось обмануть, то отслеживать этот момент


Мы сели, подумали, и нашелся другой приемлемый вариант — вести свое с блэкджеком и ... независимое от девайса время.



disclaimer
Данное решение не гарантирует точности до миллисекунд. Допускается погрешность 1-4 минуты.
Не защищено от взлома (обхода) особо продвинутыми юзерами. Если уж на то пошло, ломается все. Рассчитано на среднестатистического обывателя.


Итак, начнем.
Для начала создадим класс, который будет отвечать за хранение времени. В качестве места я выбрал SharedPreferences.
Т.к. тут делаются банальные вещи, то спрячу в спойлер, чтобы не мозолило глаза.
класс SettingsMaster
class SettingsMaster
{
    private static final String FILE_SETTINGS = "prop";
    private static final String LOCAL_TIME = "LOCAL_TIME";
    private static final String SYSTEM_TIME = "SYSTEM_TIME";
    private static final String FLASH_BACK = "FLASH_BACK";


    private static SharedPreferences getPreference(final Context context)
    {
        return context.getSharedPreferences(FILE_SETTINGS, Context.MODE_PRIVATE);
    }

    private static SharedPreferences.Editor getEditor(final Context context)
    {
        return getPreference(context).edit(); 
    }

    public static void setTime(final Context context, final long mls)
    {
        getEditor(context).putLong(LOCAL_TIME, mls).apply();
    }

    
    public static long getTime(final Context context)
    {
        return getPreference(context).getLong(LOCAL_TIME, 0);
    }

    public static void setSystemTime(final Context context, final long mls)
    {
        getEditor(context).putLong(SYSTEM_TIME, mls).apply();
    }

    
    public static long getSystemTime(final Context context)
    {
        return getPreference(context).getLong(SYSTEM_TIME, 0);
    }


    public static void setFlashBack(final Context context, final boolean isFlashback)
    {
        getEditor(context).putBoolean(FLASH_BACK, isFlashback).apply();
    }

    public static boolean isFlashBack(final Context context)
    {
        return getPreference(context).getBoolean(FLASH_BACK, false);
    }
}



Далее будет класс, который предоставляет основное api. Он сохранит и отдаст время, сам запустит таймер, который будет обновлять время.
Тоже все довольно обыденно. Единственное, что тут интересно: при установке серверного времени мы должны сначала остановить таймер, сохранить новое серверное время, а затем вновь запустить.
класс IndependentTimeHelper
public class IndependentTimeHelper
{
    public static void setServerTime(final Context context, final long serverTime)
    {
        stopTimer(context);
        SettingsMaster.setTime(context, serverTime);
        SettingsMaster.setFlashBack(context, false);
        SettingsMaster.setSystemTime(context,System.currentTimeMillis());
        startTimer(context);
    }

   
    static void startTimer(final Context context)
    {
        final Intent intent = new Intent(context, TimeReceiver.class);
        intent.setAction(TimeReceiver.ACTION_TO_UPDATE_TIME);

        if (PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_NO_CREATE) == null)
        {
            final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
            final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + TimeReceiver.TIME_PERIOD, TimeReceiver.TIME_PERIOD, pendingIntent);
        }
    }

    
    static void stopTimer(final Context context)
    {
        final Intent intent = new Intent(context, TimeReceiver.class);
        intent.setAction(TimeReceiver.ACTION_TO_UPDATE_TIME);
        final PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_NO_CREATE);
        if (pendingIntent != null)
        {
            final AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
            alarmManager.cancel(pendingIntent);
            pendingIntent.cancel();
        }
    }
    
    public static long getTime(final Context context)
    {
        if (SettingsMaster.isFlashBack(context))
            return -1;

        return SettingsMaster.getTime(context);
    }

}



Перейдем к интересному. Вся основная логика пришлась на ресивер.
Ресивер подписан на три события: стартовать при загрузке, стартовать при выключении, стартовать при обновление времени.

Что должно происходить при обновлении времени — ясно, должно инкрементиться время.
    private void incrementTimeAndSaveSystemTime(final Context context)
    {
        final long localTime = SettingsMaster.getTime(context) + TIME_PERIOD;
        SettingsMaster.setTime(context, localTime);
        SettingsMaster.setSystemTime(context, System.currentTimeMillis());
    }

Значение для TIME_PERIOD было выбрано 30 секунд. И нет, это не влияет на батарею. Приложение, в котором это работает, всегда установлено на моем устройстве, и все клёво.

Следующий шаг — запоминать системное время, чтобы мы могли знать примерное время, которое устройство было выключено.
if (action.equals(Intent.ACTION_SHUTDOWN))
       SettingsMaster.setSystemTime(context, System.currentTimeMillis());


И, наконец, самое важное — вычисление времени, которое девайс находился в выключенном состоянии.
Сначала получим последнее сохраненное время системы
final long systemTime = SettingsMaster.getSystemTime(context);

и вычислим время в выключенном состоянии
final long offTime = System.currentTimeMillis() - systemTime;

если оно меньше или равно нулю, значит, мы наткнулись на перевод времени назад. Перевод вперед нас не особо интересовал, да и отследить его весьма трудно.
if (offTime <= 0)
    SettingsMaster.setFlashBack(context, true);


добавляем его к текущему и запускаем таймер
final long localTime = SettingsMaster.getTime(context);
final long newLocalTime = localTime + offTime;
SettingsMaster.setTime(context, newLocalTime);
IndependentTimeHelper.startTimer(context);

полный код ресивера
public class TimeReceiver extends BroadcastReceiver
{
    public static final String ACTION_TO_UPDATE_TIME = "com.useit.independenttime.ACTION_TO_UPDATE_TIME";
    public static final long TIME_PERIOD = 30 * 1000;

    @Override
    public void onReceive(Context context, Intent intent)
    {
        if (SettingsMaster.getTime(context) <= 0)
        {
            IndependentTimeHelper.stopTimer(context);
            return;
        }

        final String action = intent.getAction();
        if (action.equals(Intent.ACTION_BOOT_COMPLETED))
            startReceiverAfterBootComplete(context);

        if (action.equals(Intent.ACTION_SHUTDOWN))
            SettingsMaster.setSystemTime(context, System.currentTimeMillis());

        if (action.equals(ACTION_TO_UPDATE_TIME))
            incrementTimeAndSaveSystemTime(context);
    }


    private void startReceiverAfterBootComplete(final Context context)
    {
        final long systemTime = SettingsMaster.getSystemTime(context);
        if (systemTime > 0)
        {
            final long offTime = System.currentTimeMillis() - systemTime;

            if (offTime <= 0)
                SettingsMaster.setFlashBack(context, true);

            final long localTime = SettingsMaster.getTime(context);
            final long newLocalTime = localTime + offTime;
            SettingsMaster.setTime(context, newLocalTime);
            IndependentTimeHelper.startTimer(context);
        }

    }


    private void incrementTimeAndSaveSystemTime(final Context context)
    {
        final long localTime = SettingsMaster.getTime(context) + TIME_PERIOD;
        SettingsMaster.setTime(context, localTime);
        SettingsMaster.setSystemTime(context, System.currentTimeMillis());
    }
}



Вот и все. Готово.
Не забываем про добавление разрешения в манифест
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />


Исходники и пример

Итогом стала работающая система ведения времени на устройстве. Да, она не идеальна, но поставленную задачу решает хорошо.

PS. менеджер доволен.
Tags:android developmentandroidindependent timeкостыливелосипеды
Hubs: Блог компании ЕТранспорт corporate blog Development of mobile applications Development for Android
+6
14.9k 78
Comments 25