如何处理JodaTime和Android的时区数据库差异?

Any*_*324 5 timezone datetime android date jodatime

我想在昨天开始讨论我在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);
}
Run Code Online (Sandbox Code Playgroud)

ResourceZoneInfoProvider.init()joda-time-android库的一部分,它用于初始化Joda的时区数据库.addTimeZoneInfo覆盖设备的时区并为显示更新的时区信息的新视图充气.以下是结果示例:

同时在使用Java的不同时区

请注意,对于"加里宁格勒",Android将其映射到"GMT + 3:00",因为直到2014年10月26日才是这种情况(参见维基百科文章).甚至一些网站仍然将这个时区显示为格林尼治标准时间+3:00,因为这种变化相对较近.然而,正确的是JodaTime显示的"GMT + 2:00".

有缺陷的解决方案?

这是一个问题,因为无论我如何绕过它,最后,我必须格式化时间,以便在时区中向用户显示它.当我使用JodaTime执行此操作时,时间将被错误地格式化,因为它将与系统显示的预期时间不匹配.

或者,假设我处理UTC中的所有内容.当用户在日历中添加事件并为提醒选择时间时,我可以将其设置为UTC,将其存储在数据库中并完成它.

但是,我需要使用Android设置提醒,AlarmManager而不是在UTC时我转换了用户设置的时间,而是相对于他们希望提醒触发的时间.这需要时区信息发挥作用.

例如,如果用户在UTC + 1:00的某个位置并且他或她在上午9:00设置提醒,我可以:

  • DateTime在用户的时区上午09:00 创建一个新实例集,并将其毫秒存储在数据库中.我也可以直接使用相同的毫秒AlarmManager;
  • DateTime在UTC上午09:00 创建一个新实例集,并将其毫秒存储在数据库中.这更好地解决了与此问题不完全相关的一些其他问题.但是当设置时间时AlarmManager,我需要在用户的时区上午09:00计算其毫秒值;
  • 完全忽略Joda DateTime并使用Java来处理提醒的设置Calendar.这将使我的应用程序在显示时间时依赖于过时的时区信息,但至少在使用AlarmManager或显示日期和时间进行调度时不会出现不一致.

我错过了什么?

我可能在想这个,我担心我可能会遗漏一些明显的东西.我呢?有没有什么方法可以继续在Android上使用JodaTime,而不是将我自己的时区管理添加到应用程序并完全忽略所有内置的Android格式化功能?

Ric*_*rdo 5

我认为其他答案没有抓住重点。是的,在保留时间信息时,您应该仔细考虑您的用例来决定如何最好地这样做。但即使你做了,这个问题带来的问题仍然存在。

以 Android 的闹钟应用为例,它的源代码可免费获取。如果您查看它的AlarmInstance类,这就是它在数据库中的建模方式:

private static final String[] QUERY_COLUMNS = {
        _ID,
        YEAR,
        MONTH,
        DAY,
        HOUR,
        MINUTES,
        LABEL,
        VIBRATE,
        RINGTONE,
        ALARM_ID,
        ALARM_STATE
};
Run Code Online (Sandbox Code Playgroud)

要知道警报实例何时应该触发,您可以调用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;
}
Run Code Online (Sandbox Code Playgroud)

请注意无论时区如何AlarmInstance存储它应该触发的确切时间。这可确保您每次呼叫getAlarmTime()时都能在用户所在的时区获得正确的触发时间。这里的问题是如果时区未更新,getAlarmTime()则无法获得正确的时间更改,例如,在 DST 开始时。

JodaTime在这种情况下派上用场,因为它带有自己的时区数据库。为了更好地处理日期计算,您可以考虑其他日期时间库,例如date4j,但这些库通常不处理它们自己的时区数据。

但是拥有自己的时区数据会给您的应用带来一个限制:您不能再依赖 Android 的时区。这意味着您不能使用它的Calendar类或它的格式化函数。JodaTime 也提供格式化功能,请使用它们。如果您必须转换为Calendar,而不是使用该toCalendar()方法,请创建一个类似于getAlarmTime()上面的方法,您可以在其中传递所需的确切时间。

或者,您可以检查是否存在时区不匹配,并像 Matt Johnson 在他的评论中建议的那样警告用户。如果你决定继续使用 Android 和 Joda 的功能,我同意他的观点:

是的 - 有两个事实来源,如果它们不同步,就会出现不匹配。检查版本、显示警告、要求更新等。您能做的可能不多。

除了您还可以做一件事:您可以自己更改 Android 的时区。您可能应该在这样做之前警告用户,但是您可以强制 Android 使用与 Joda 相同的时区偏移量:

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

检查后,如果不一样,您可以使用您从 Joda 正确时区信息的偏移量创建的“假”区更改 Android 的时区:

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());
}
Run Code Online (Sandbox Code Playgroud)

请记住,您需要获得<uses-permission android:name="android.permission.SET_TIME_ZONE"/>许可。

最后,更改时区将更改系统当前时间。不幸的是,只有系统应用程序可以设置时间,因此您能做的最好的事情就是打开用户的日期时间设置并提示他/她手动将其更改为正确的时间:

startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS));
Run Code Online (Sandbox Code Playgroud)

您还必须添加更多控件以确保在 DST 开始和结束时更新时区。就像您说的,您将添加自己的时区管理,但这是确保两个时区数据库之间一致性的唯一方法。