http://habrahabr.ru/company/etransport/blog/245477/
Здравствуйте!
В один прекрасный день приходит ко мне менеждер и говорит: «Можем ли мы запретить пользователю менять время на телефоне?». И конечно же ответ мой был нет, но это не решало задачу. Необходимо было искать выход из ситуации.
Критерии для решения были следующими:
- должно работать без частых синхронизаций с сервером, например, достаточно взять время раз в месяц.
- должно быть устойчиво к переводу времени назад/вперед/смене часового пояса
- работать при перезагрузке устройства/неожиданном завершении/ вытаскивании батареи
- не отклоняться от эталонного времени на слишком большие значения, в моем случае было 5 минут.
- если все же удалось обмануть, то отслеживать этот момент
Мы сели, подумали, и нашелся другой приемлемый вариант — вести свое
с блэкджеком и ... независимое от девайса время.
disclaimerДанное решение не гарантирует точности до миллисекунд. Допускается погрешность 1-4 минуты.
Не защищено от взлома (обхода) особо продвинутыми юзерами. Если уж на то пошло, ломается все. Рассчитано на среднестатистического обывателя.
Итак, начнем.
Для начала создадим класс, который будет отвечать за хранение времени. В качестве места я выбрал SharedPreferences.
Т.к. тут делаются банальные вещи, то спрячу в спойлер, чтобы не мозолило глаза.
класс SettingsMasterclass 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. Он сохранит и отдаст время, сам запустит таймер, который будет обновлять время.
Тоже все довольно обыденно. Единственное, что тут интересно: при установке серверного времени мы должны сначала остановить таймер, сохранить новое серверное время, а затем вновь запустить.
класс IndependentTimeHelperpublic 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. менеджер доволен.