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