Как обращаться с различиями базы данных JonesTime и Android в часовом поясе?

Я хочу продолжить обсуждение, которое я начал в сообществе RedDit Android Dev вчера с новым вопросом: как вы управляете обновленной базой данных часового пояса, поставляемой с вашим приложением, используя библиотеку JodaTime на устройстве с устаревшей информацией о часовом поясе?

Проблема

Конкретная проблема связана с конкретным часовым поясом «Европа / Калининград». И я могу воспроизвести проблему: на устройстве Android 4.4, если я вручную установил свой часовой пояс выше, вызов new DateTime() установит этот экземпляр DateTime на время за час до фактического времени, отображаемого в строке состояния телефона.

Я создал пример Activity чтобы проиллюстрировать проблему. На его onCreate() я вызываю следующее:

 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ResourceZoneInfoProvider.init(getApplicationContext()); ViewGroup v = (ViewGroup) findViewById(R.id.root); addTimeZoneInfo("America/New_York", v); addTimeZoneInfo("Europe/Paris", v); addTimeZoneInfo("Europe/Kaliningrad", v); } private void addTimeZoneInfo(String id, ViewGroup root) { AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); am.setTimeZone(id); //Joda does not update its time zone automatically when there is a system change DateTimeZone.setDefault(DateTimeZone.forID(id)); View v = getLayoutInflater().inflate(R.layout.info, root, false); TextView idInfo = (TextView) v.findViewById(R.id.id); idInfo.setText(id); TextView timezone = (TextView) v.findViewById(android.R.id.text1); timezone.setText("Time zone: " + TimeZone.getDefault().getDisplayName()); TextView jodaTime = (TextView) v.findViewById(android.R.id.text2); //Using the same pattern as Date() jodaTime.setText("Time now (Joda): " + new DateTime().toString("EEE MMM dd HH:mm:ss zzz yyyy")); TextView javaTime = (TextView) v.findViewById(R.id.time_java); javaTime.setText("Time now (Java): " + new Date().toString()); root.addView(v); } 

ResourceZoneInfoProvider.init() является частью библиотеки joda-time-android, и она предназначена для инициализации базы данных часовых поясов Joda. addTimeZoneInfo перезаписывает часовой пояс устройства и раздувает новое представление, где отображается обновленная информация о часовом поясе. Вот пример результата:

То же самое время в разных часовых поясах с использованием Java

Обратите внимание, что для «Калининграда» Android отображает его на «GMT + 3: 00», потому что это было до 26 октября 2014 года (см. Статью в Википедии ). Даже некоторые веб-сайты по-прежнему показывают этот часовой пояс как GMT + 3: 00 из-за того, насколько относительно недавнее это изменение. Правильно, однако, это «GMT + 2: 00», как показано JodaTime.

Недостатки возможных решений?

Это проблема, потому что, как бы я ни старался ее обойти, в конце концов, мне нужно отформатировать время, чтобы отобразить его пользователю в их часовом поясе. И когда я делаю это с использованием JodaTime, время будет неправильно отформатировано, потому что оно не соответствует ожидаемому времени отображения системы.

В качестве альтернативы предположим, что я обрабатываю все в UTC. Когда пользователь добавляет событие в календарь и выбирает время для напоминания, я могу установить его в UTC, сохранить его в db, как это и сделать с ним.

Тем не менее, мне нужно установить это напоминание с Android AlarmManager не в UTC, я конвертировал время, установленное пользователем, но в том, что касается времени, в течение которого они хотят, чтобы напоминание запускалось. Для этого требуется, чтобы информация о часовом поясе включалась в игру.

Например, если пользователь находится где-то в UTC + 1: 00, и он или она устанавливает напоминание в 9:00, я могу:

  • Создайте новый экземпляр DateTime установленный в 09:00 в часовом поясе пользователя, и сохраните его миллисекунды в db. Я также могу напрямую использовать те же миллисекунды с AlarmManager ;
  • Создайте новый экземпляр DateTime установленный в 09:00 в UTC, и сохраните его миллисекунды в db. Это лучше затрагивает несколько других вопросов, не связанных с этим вопросом. Но когда вы устанавливаете время с помощью AlarmManager , мне нужно вычислить его миллисекундное значение в 09:00 утра в часовом поясе пользователя;
  • Игнорировать полностью Joda DateTime и обрабатывать настройку напоминания с помощью Java Calendar . Это заставит мое приложение полагаться на устаревшую информацию о часовом поясе при отображении времени, но, по крайней мере, не будет несоответствий при планировании с помощью AlarmManager или отображении дат и времени.

Что мне не хватает?

Возможно, я об этом думаю, и я боюсь, что у меня может быть что-то очевидное. Я? Можно ли каким-либо образом использовать JodaTime на Android, не добавляя мое собственное управление часовым поясом в приложение и полностью игнорируя все встроенные функции форматирования Android?

Solutions Collecting From Web of "Как обращаться с различиями базы данных JonesTime и Android в часовом поясе?"

Я думаю, что другие ответы не имеют смысла. Да, когда вы сохраняете информацию о времени, вы должны тщательно изучить свои варианты использования, чтобы решить, как это сделать. Но даже если бы вы это сделали, проблема, поставленная этим вопросом, по-прежнему сохраняется.

Рассмотрим приложение Android для будильника, которое имеет свободный исходный код. Если вы посмотрите на свой класс AlarmInstance , то, как он моделируется в базе данных:

 private static final String[] QUERY_COLUMNS = { _ID, YEAR, MONTH, DAY, HOUR, MINUTES, LABEL, VIBRATE, RINGTONE, ALARM_ID, ALARM_STATE }; 

И чтобы узнать, когда должен срабатывать аварийный экземпляр, вы вызываете getAlarmTime() :

 /** * Return the time when a alarm should fire. * * @return the time */ public Calendar getAlarmTime() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.YEAR, mYear); calendar.set(Calendar.MONTH, mMonth); calendar.set(Calendar.DAY_OF_MONTH, mDay); calendar.set(Calendar.HOUR_OF_DAY, mHour); calendar.set(Calendar.MINUTE, mMinute); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MILLISECOND, 0); return calendar; } 

Обратите внимание, что AlarmInstance сохраняет точное время, которое должно срабатывать, независимо от часового пояса. Это гарантирует, что каждый раз, когда вы вызываете getAlarmTime() вы получаете правильное время для getAlarmTime() в часовом поясе пользователя. Проблема здесь в том, что часовой пояс не обновляется, getAlarmTime() не может получить правильные изменения времени, например, когда начинается DST.

JodaTime пригодится в этом сценарии, потому что он поставляется со своей собственной базой данных часовых поясов. Вы можете подумать о других библиотеках времени, таких как date4j, для удобства вычислений даты обработки данных, но они обычно не обрабатывают свои собственные данные часового пояса.

Но наличие ваших собственных данных часового пояса приводит к ограничению вашего приложения: вы больше не можете полагаться на часовой пояс Android. Это означает, что вы не можете использовать свой класс Calendar или его функции форматирования. JodaTime также предоставляет функции форматирования, использует их. Если вы должны преобразовать в Calendar , вместо того, чтобы использовать метод toCalendar() , создайте одно из них, похожее на getAlarmTime() выше, где вы передадите точное время, которое вы хотите.

В качестве альтернативы вы можете проверить, существует ли несоответствие часового пояса и предупредить пользователя, как Мэтт Джонсон в своем комментарии . Если вы решите использовать функции Android и Joda, я согласен с ним:

Да – с двумя источниками правды, если они не синхронизированы, будут несоответствия. Проверьте версии, покажите предупреждение, попросите обновить и т. Д. Вероятно, вы не можете сделать больше, чем можете.

Кроме того, вы можете сделать еще одну вещь: вы можете самостоятельно изменить часовой пояс Android. Вероятно, вы должны предупредить пользователя, прежде чем делать это, но затем вы можете заставить Android использовать одно и то же смещение часового пояса как Joda's:

 public static boolean isSameOffset() { long now = System.currentTimeMillis(); return DateTimeZone.getDefault().getOffset(now) == TimeZone.getDefault().getOffset(now); } 

После проверки, если это не то же самое, вы можете изменить часовой пояс Android с помощью «поддельной» зоны, которую вы создаете, из смещения правильной информации о часовом поясе Joda:

 public static void updateTimeZone(Context c) { TimeZone tz = DateTimeZone.forOffsetMillis(DateTimeZone.getDefault().getOffset(System.currentTimeMillis())).toTimeZone(); AlarmManager mgr = (AlarmManager) c.getSystemService(Context.ALARM_SERVICE); mgr.setTimeZone(tz.getID()); } 

Помните, что для этого вам понадобится <uses-permission android:name="android.permission.SET_TIME_ZONE"/> .

Наконец, изменение часового пояса изменит текущее время системы. К сожалению, только системные приложения могут установить время, поэтому лучше всего вы можете открыть настройки даты для пользователя и предложить ему / ей изменить его вручную до правильного:

 startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS)); 

Вам также нужно будет добавить еще несколько элементов управления, чтобы убедиться, что часовой пояс обновляется, когда начинается и заканчивается летнее время. Как вы сказали, вы будете добавлять свое собственное управление часовым поясом, но это единственный способ обеспечить согласованность между двумя базами данных часовых поясов.

Ты делаешь это неправильно. Если пользователь добавляет запись календаря в 9 утра следующего года, он означает 9 утра. Не имеет значения, изменится ли база данных часового пояса в устройстве пользователя или правительство решит изменить начало или конец летнего времени. Важно то, что часы говорят на стене. Вам необходимо сохранить «9 утра» в вашей базе данных.

Это, как правило, невозможная проблема. Вы никогда не сможете хранить дельта-время (например, ms после эпохи) для будущего события в заданное время настенных часов и быть уверены, что он останется верным. Учтите, что в любое произвольное время нация может подписать закон о том, что переход на летнее время вступает в силу за 30 минут до этого времени, а время проскакивает вперед на 1 час. Решение, которое может смягчить эту проблему, заключается в том, чтобы вместо того, чтобы просить телефон просыпаться в определенное время, периодически просыпаться, проверять текущее время и затем проверять, должны ли активироваться какие-либо напоминания. Это можно сделать эффективно, регулируя время пробуждения, чтобы отобразить приблизительное представление о том, когда установлено следующее напоминание. Например, если следующее напоминание не на 1 год, просыпайтесь через 360 дней и начинайте просыпаться чаще, пока вы не приблизитесь ко времени напоминания.

Поскольку вы используете AlarmManager , и, похоже, для этого требуется установить время в формате UTC, то вы правы в том, что вам нужно проецировать свое время на UTC, чтобы запланировать его.

Но вам не нужно упорствовать в этом. Например, если у вас есть повторяющееся ежедневное мероприятие, тогда сохраните местное время дня, в котором должно произойти событие. Если событие должно происходить в определенном часовом поясе (а не в текущем часовом поясе устройства), тогда также сохраняйте этот идентификатор часового пояса.

Запрограммируйте время по времени UTC с использованием JodaTime – затем передайте это значение AlarmManager .

Периодически (или, как минимум, всякий раз, когда вы применяете обновление к данным JodaTime), переоцените запланированное время UTC. Отмените и восстановите события по мере необходимости.

Следите за временем, запланированным во время перехода на летнее время. У вас может быть местное время, которое недействительно в определенный день (который, вероятно, должен быть расширен), или у вас может быть местное время, которое неоднозначно в определенный день (который вы, вероятно, должны выбрать первый из двух экземпляров).

В конечном счете, я думаю, что ваша забота заключается в том, что данные Joda Time будут более точными, чем данные устройства. Таким образом, будильник может погаснуть в нужное местное время, но может не совпадать с показанным на устройстве временем. Я согласен, что это может быть озадачивающим конечного пользователя, но они, вероятно, будут благодарны за то, что вы поступили правильно. Я не думаю, что вы все равно можете сделать это.

Одна вещь, которую вы можете рассмотреть, – проверить TimeUtils.getTimeZoneDatabaseVersion() и сравнить ее с номером версии данных, загруженных в Joda Time. Если они не синхронизированы, вы можете предоставить вашему пользователю предупреждающее сообщение либо о том, чтобы обновить приложение (когда вы находитесь за ним), либо обновить tzdata своего устройства (когда устройство позади).

Быстрый поиск нашел эти инструкции и это приложение для обновления tzdata на Android. (Я сам их не тестировал).