¿Cómo manejar las diferencias de la base de datos de horarios de JodaTime y Android?

Quiero ampliar una discusión que comencé ayer en la comunidad de desarrolladores de Android de Reddit con una nueva pregunta: ¿Cómo gestionar una base de datos actualizada de zona horaria enviada con su aplicación mediante la biblioteca JodaTime en un dispositivo que ha superado la información de la zona horaria?

El problema

La cuestión específica se refiere a una zona horaria determinada, "Europa / Kaliningrado". Y puedo reproducir el problema: En un dispositivo Android 4.4, si establezco manualmente su zona horaria en el anterior, al llamar a new DateTime() establecerá esta instancia de DateTime a un tiempo una hora antes de la hora actual que se muestra en la barra de estado del teléfono.

Creé una muestra Activity para ilustrar el problema. En su onCreate() llamo a lo siguiente:

 @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() es parte de la biblioteca joda-time-android y está destinado a inicializar la base de datos de zona horaria de Joda. addTimeZoneInfo sobrescribe la zona horaria del dispositivo e infla una nueva vista donde se muestra la información de zona horaria actualizada. He aquí un ejemplo de resultado:

Al mismo tiempo en diferentes zonas horarias utilizando Java

Observe cómo para "Kaliningrad", Android los mapas él a "GMT + 3: 00" porque ése era el caso hasta el 26 de octubre de 2014 (véase el artículo de Wikipedia ). Incluso algunos sitios web siguen mostrando esta zona horaria como GMT + 3: 00 debido a lo relativamente reciente que es este cambio. Lo correcto, sin embargo, es "GMT + 2: 00", como muestra JodaTime.

Soluciones erróneas posibles?

Esto es un problema porque no importa cómo trato de eludirlo, al final, tengo que dar formato al tiempo para mostrarlo al usuario en su zona horaria. Y cuando lo hago usando JodaTime, el tiempo se formateará incorrectamente porque no coincidirá con el tiempo esperado que el sistema está mostrando.

Alternativamente, supongamos que manejar todo en UTC. Cuando el usuario está agregando un evento en el calendario y elige un tiempo para el recordatorio, puedo configurarlo en UTC, almacenarlo en el db así y hacerlo con él.

Sin embargo, necesito fijar ese recordatorio con AlarmManager de Android no en la hora de UTC que convertí el tiempo fijado por el usuario sino en el relativo al tiempo que desean que el recordatorio accione. Esto requiere que la información de zona horaria entre en juego.

Por ejemplo, si el usuario está en algún lugar en UTC + 1: 00 y él o ella establece un recordatorio para las 9:00 am, puedo:

  • Cree una nueva instancia de DateTime establecida para las 09:00 am en el huso horario del usuario y guarde sus milisegundos en el db. También puedo usar directamente los mismos milisegundos con AlarmManager ;
  • Cree una nueva instancia de DateTime establecida para 09:00 am en UTC y almacene sus milisegundos en el db. Esto aborda mejor algunas otras cuestiones que no están relacionadas exactamente con esta pregunta. Pero al configurar la hora con el AlarmManager , necesito calcular su valor de milisegundos para las 09:00 am en la zona horaria del usuario;
  • Ignora completamente Joda DateTime y maneja la configuración del recordatorio usando el Calendar de Java. Esto hará que mi aplicación dependa de información de zona horaria obsoleta al mostrar el tiempo, pero al menos no habrá inconsistencias al programar con el AlarmManager o mostrar las fechas y horas.

¿Qué me estoy perdiendo?

Puedo estar más pensando esto y me temo que podría estar perdiendo algo obvio. ¿Estoy? ¿Hay alguna manera que podría seguir usando JodaTime en Android corto de añadir mi propia gestión de la zona horaria a la aplicación y despreciar completamente todas las funciones integradas de formato de Android?

Creo que las otras respuestas están perdiendo el punto. Sí, cuando persiste la información de tiempo, debe considerar cuidadosamente sus casos de uso para decidir la mejor forma de hacerlo. Pero incluso si lo hubieras hecho, el problema que plantea esta pregunta persistiría.

Considere la aplicación de reloj despertador de Android, que tiene su código fuente libremente disponible . Si observa su clase AlarmInstance , así es como se modeliza en la base de datos:

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

Y para saber cuándo una instancia de alarma debe disparar, llama a 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; } 

Observe cómo una AlarmInstance almacena la hora exacta en que debe disparar, independientemente de la zona horaria. Esto asegura que cada vez que llame a getAlarmTime() obtenga la hora correcta para disparar en la zona horaria del usuario. El problema aquí es si la zona horaria no se actualiza, getAlarmTime() no puede obtener cambios de hora correctos, por ejemplo, cuando se inicia DST.

JodaTime es útil en este escenario porque se incluye con su propia base de datos de zona horaria. Puede considerar otras bibliotecas de fecha y hora tales como date4j para la conveniencia de mejores cálculos de fecha de manipulación, pero éstos típicamente no manejan sus propios datos de zona horaria.

Pero tener tus propios datos de zona horaria introduce una restricción a tu aplicación: no puedes confiar más en la zona horaria de Android. Esto significa que no puede utilizar su clase de Calendar o sus funciones de formato. JodaTime proporciona funciones de formato, así, utilizarlos. Si debe convertir a Calendar , en lugar de utilizar el método toCalendar() , cree uno similar al getAlarmTime() anterior en el que pasa el tiempo exacto que desea.

Alternativamente, podría comprobar si hay una diferencia de zona horaria y advierta al usuario como Matt Johnson sugirió en su comentario . Si decides seguir usando las funciones de Android y Joda, estoy de acuerdo con él:

Sí – con dos fuentes de verdad, si están fuera de sincronía, habrá desajustes. Compruebe las versiones, muestre una advertencia, pida ser puesto al día, etc. No hay probablemente mucho más que usted puede hacer que eso.

Excepto que hay una cosa más que puedes hacer: Puedes cambiar la zona horaria de Android tú mismo. Probablemente deberías avisar al usuario antes de hacerlo, pero entonces podrías obligar a Android a usar la misma zona horaria que Joda:

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

Después de comprobar, si no es lo mismo, puede cambiar la zona horaria de Android con una zona "falsa" que cree desde el desplazamiento de la información de zona horaria correcta de 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()); } 

Recuerde que necesitará el <uses-permission android:name="android.permission.SET_TIME_ZONE"/> para eso.

Finalmente, cambiar la zona horaria cambiará la hora actual del sistema. Lamentablemente, sólo las aplicaciones del sistema pueden establecer el tiempo para que lo mejor que puede hacer es abrir la configuración de fecha y hora para el usuario y pedirle que lo cambie manualmente a la correcta:

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

También tendrá que agregar más controles para asegurarse de que la zona horaria se actualiza cuando comienza y termina DST. Como has dicho, agregarás tu propia gestión de zona horaria, pero es la única manera de asegurar la coherencia entre las dos bases de datos de zona horaria.

Lo estás haciendo mal. Si el usuario agrega una entrada de calendario para las 9 am del próximo año, significa 9 am. No importa si la base de datos de zona horaria en el dispositivo del usuario cambia o el gobierno decide alterar el inicio o final del horario de verano. Lo que importa es lo que dicen los relojes en la pared. Debe almacenar "9 am" en su base de datos.

Esto es generalmente un problema imposible. Nunca puede almacenar el tiempo delta (por ejemplo, ms después de la época) para un evento futuro a una hora determinada del reloj de pared, y estar seguro de que permanecerá correcto. Considere que por cualquier tiempo arbitrario, una nación puede firmar una ley que el tiempo de ahorro de luz diurna entra en vigor 30 minutos antes de ese tiempo, y el tiempo se salta adelante por 1 hora. Una solución que puede mitigar este problema es, en lugar de pedirle al teléfono que se despierte a una hora específica, se despierta periódicamente, compruebe la hora actual y, a continuación, compruebe si se debe activar cualquier recordatorio. Esto se puede hacer de manera eficiente ajustando el tiempo de despertar para reflejar una idea aproximada de cuándo se establece el siguiente recordatorio. Por ejemplo, si el siguiente recordatorio no es para 1 año, despierte después de 360 ​​días y empiece a despertar con más frecuencia hasta que esté muy cerca de la hora del recordatorio.

Puesto que usted está usando AlarmManager , y parece que requiere que usted fije el tiempo en UTC, entonces usted es correcto en que usted necesitará proyectar su tiempo al UTC para programarlo.

Pero usted no tiene que persistir de esa manera. Por ejemplo, si tiene un evento diario que se repite, guarde la hora local del día en que debe desencadenarse el evento. Si el suceso debe producirse en una zona horaria específica (en lugar de la zona horaria actual del dispositivo), guarde esa ID de zona horaria también.

Proyecte la hora local a la hora UTC utilizando JodaTime – luego pase ese valor a AlarmManager .

Periódicamente (o al menos, cada vez que aplique una actualización a los datos de JodaTime), vuelva a evaluar las horas UTC planificadas. Cancelar y restablecer los eventos según sea necesario.

Tenga cuidado con los horarios programados durante una transición de horario de verano. Es posible que tenga un horario local que no sea válido en un día determinado (que probablemente debería ser avanzado), o puede tener una hora local que es ambigua en un día específico (que probablemente debería elegir la primera de las dos instancias).

En última instancia, creo que su preocupación es que sería posible que los datos de Joda Time sean más precisos que los datos del dispositivo. Por lo tanto, una alarma podría apagarse a la hora local correcta, pero podría no coincidir con la hora que se muestra en el dispositivo. Estoy de acuerdo en que podría ser desconcertante para el usuario final, pero probablemente estarán agradecidos de haber hecho lo correcto. No creo que haya mucho que puedas hacer al respecto de todos modos.

Una cosa que podría considerar es comprobar TimeUtils.getTimeZoneDatabaseVersion() y compararlo con el número de versión de los datos cargados en Joda Time. Si están fuera de sincronización, puede presentar un mensaje de advertencia a su usuario para actualizar su aplicación (cuando esté atrasado) o actualizar el tzdata de su dispositivo (cuando el dispositivo esté detrás).

Una búsqueda rápida encontró estas instrucciones y esta aplicación para actualizar el tzdata en Android. (No las he probado yo mismo.)

  • Gradle duplicar archivos durante el embalaje - messages.properties de JodaTime
  • Convertir milisegundo a Joda Fecha Hora o para zona 0000
  • Intervalo de tiempo límite en el selector de tiempo
  • Recursos de la biblioteca con Robolectric 3 - JodaTime
  • Android: Proguard no compiló con el archivo jar de Joda Time
  • Anular el primer día de la semana en joda?
  • Pruebas unitarias y joda-time-android
  • JodaTime - Compruebe si LocalTime es después de ahora y ahora antes de otro LocalTime
  • JodaTime no reconoce algunas zonas horarias de Android
  • Encuentre el día y la hora restante usando jodatime
  • Añadiendo el tiempo de Joda
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.