如何处理JodaTime';s和Android';s时区数据库差异?

如何处理JodaTime';s和Android';s时区数据库差异?,android,date,datetime,timezone,jodatime,Android,Date,Datetime,Timezone,Jodatime,我想用一个新的问题来扩展我在Reddit Android开发社区上开始的讨论:如何使用JodaTime库在具有过时时区信息的设备上管理应用程序附带的最新时区数据库 问题 目前的具体问题涉及一个特定的时区,“欧洲/加里宁格勒”。我可以重现这个问题:在安卓4.4设备上,如果我手动将其时区设置为上述时区,调用new DateTime()会将此DateTime实例设置为手机状态栏上显示的实际时间前一小时的时间 我创建了一个示例活动来说明这个问题。在其onCreate()上,我调用以下命令: @Overr

我想用一个新的问题来扩展我在Reddit Android开发社区上开始的讨论:如何使用JodaTime库在具有过时时区信息的设备上管理应用程序附带的最新时区数据库

问题 目前的具体问题涉及一个特定的时区,“欧洲/加里宁格勒”。我可以重现这个问题:在安卓4.4设备上,如果我手动将其时区设置为上述时区,调用
new DateTime()
会将此
DateTime
实例设置为手机状态栏上显示的实际时间前一小时的时间

我创建了一个示例
活动
来说明这个问题。在其
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);
}
ResourceZoneInfo.init()
是库的一部分,用于初始化Joda的时区数据库
addTimeZoneInfo
覆盖设备的时区,并在新视图中显示更新的时区信息。以下是一个结果示例:

请注意,对于“加里宁格勒”,Android是如何将其映射到“GMT+3:00”的,因为直到2014年10月26日都是如此(请参阅)。甚至一些网站仍然将这个时区显示为GMT+3:00,因为这一变化相对较新。但是,正确的是JodaTime显示的“GMT+2:00”

有缺陷的可能解决方案? 这是一个问题,因为无论我如何试图绕过它,最终,我都必须格式化时间,以便在用户所在时区向用户显示时间。当我使用JodaTime执行此操作时,时间的格式将不正确,因为它将与系统显示的预期时间不匹配

或者,假设我用UTC处理所有事情。当用户在日历中添加事件并为提醒选择时间时,我可以将其设置为UTC,像那样将其存储在db中,然后完成

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

例如,如果用户在UTC+1:00的某处,并将提醒设置为上午9:00,则我可以:

  • 在用户时区创建一个新的
    DateTime
    实例集,设置为上午9:00,并将其毫秒存储在数据库中。我还可以直接使用
    AlarmManager
    使用相同的毫秒
  • 创建一个新的
    DateTime
    实例集,设置为UTC时间上午9:00,并将其毫秒存储在数据库中。这更好地解决了一些与这个问题不完全相关的其他问题。但是当使用
    AlarmManager
    设置时间时,我需要计算用户时区上午9:00的毫秒值
  • 完全忽略Joda
    DateTime
    ,并使用Java的
    日历处理提醒设置。这将使我的应用程序在显示时间时依赖过时的时区信息,但至少在使用
    AlarmManager
    调度或显示日期和时间时不会出现不一致
我错过了什么?
我可能想得太多了,我担心我可能遗漏了一些明显的东西。是吗?除了在应用程序中添加我自己的时区管理功能和完全忽略所有内置的Android格式功能之外,我还有什么办法可以继续在Android上使用JodaTime吗?

你做错了。如果用户添加上午9点的日历条目 明年他指的是上午9点。如果时区数据库 在用户的设备发生变化或政府决定改变 夏令时的开始或结束。重要的是什么
墙上写着时钟。您需要在数据库中存储“上午9点”。

这通常是一个不可能解决的问题。您永远不能在给定的挂钟时间存储未来事件的增量时间(例如,纪元后的毫秒),并确信它将保持正确。考虑到任意时间,一个国家可以签署一项法律,即夏时制在时间之前30分钟生效,时间提前1小时。可以缓解此问题的解决方案是,不要要求手机在特定时间唤醒,而是定期唤醒,检查当前时间,然后检查是否应激活任何提醒。这可以通过调整唤醒时间来有效地实现,以反映下一次提醒设置的大致时间。例如,如果下一次提醒时间不是1年,请在360天后醒来,并开始更频繁地醒来,直到离提醒时间非常近。

因为您使用的是
AlarmManager
,在UTC中,那么您就正确了,您需要将您的时间投影到UTC以便安排时间

但你不必这样坚持下去。例如,如果您有重复发生的每日事件,则存储该事件应触发的当地时间。如果事件发生在特定时区(而不是设备的当前时区),则还应存储该时区ID

使用JodaTime将本地时间投影到UTC时间-然后将该值传递给
AlarmManager

定期(或至少在您对JodaTime的数据应用更新时)重新评估计划的UTC时间。根据需要取消并重新建立事件

注意DST转换期间安排的时间。您可能有一个在特定日期无效的本地时间(可能应该提前),或者您可能有一个在特定日期不明确的本地时间(您可能应该选择两个实例中的第一个)

最终,我认为您关心的是Joda时间数据可能比设备数据更准确。因此,警报可能会在正确的当地时间发出,但可能不会在夜间发出
private static final String[] QUERY_COLUMNS = {
        _ID,
        YEAR,
        MONTH,
        DAY,
        HOUR,
        MINUTES,
        LABEL,
        VIBRATE,
        RINGTONE,
        ALARM_ID,
        ALARM_STATE
};
/**
 * 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;
}
public static boolean isSameOffset() {
    long now = System.currentTimeMillis();
    return DateTimeZone.getDefault().getOffset(now) == TimeZone.getDefault().getOffset(now);
}
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());
}
startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS));
public class AndroidOldDateTimeZone extends DateTimeZone {

    private final TimeZone mTz;
    private final Calendar mCalendar;
    private long[] mTransition;

    public AndroidOldDateTimeZone(final String id) {
        super(id);
        mTz = TimeZone.getTimeZone(id);
        mCalendar = GregorianCalendar.getInstance(mTz);
        mTransition = new long[0];

        try {
            final Class tzClass = mTz.getClass();
            final Field field = tzClass.getDeclaredField("mTransitions");
            field.setAccessible(true);
            final Object transitions = field.get(mTz);

            if (transitions instanceof long[]) {
                mTransition = (long[]) transitions;
            } else if (transitions instanceof int[]) {
                final int[] intArray = (int[]) transitions;
                final int size = intArray.length;
                mTransition = new long[size];
                for (int i = 0; i < size; i++) {
                    mTransition[i] = intArray[i];
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public TimeZone getTz() {
        return mTz;
    }

    @Override
    public long previousTransition(final long instant) {
        if (mTransition.length == 0) {
            return instant;
        }

        final int index = findTransitionIndex(instant, false);

        if (index <= 0) {
            return instant;
        }

        return mTransition[index - 1] * 1000;
    }

    @Override
    public long nextTransition(final long instant) {
        if (mTransition.length == 0) {
            return instant;
        }

        final int index = findTransitionIndex(instant, true);

        if (index > mTransition.length - 2) {
            return instant;
        }

        return mTransition[index + 1] * 1000;
    }

    @Override
    public boolean isFixed() {
        return mTransition.length > 0 &&
               mCalendar.getMinimum(Calendar.DST_OFFSET) == mCalendar.getMaximum(Calendar.DST_OFFSET) &&
               mCalendar.getMinimum(Calendar.ZONE_OFFSET) == mCalendar.getMaximum(Calendar.ZONE_OFFSET);
    }

    @Override
    public boolean isStandardOffset(final long instant) {
        mCalendar.setTimeInMillis(instant);
        return mCalendar.get(Calendar.DST_OFFSET) == 0;
    }

    @Override
    public int getStandardOffset(final long instant) {
        mCalendar.setTimeInMillis(instant);
        return mCalendar.get(Calendar.ZONE_OFFSET);
    }

    @Override
    public int getOffset(final long instant) {
        return mTz.getOffset(instant);
    }

    @Override
    public String getShortName(final long instant, final Locale locale) {
        return getName(instant, locale, true);
    }

    @Override
    public String getName(final long instant, final Locale locale) {
        return getName(instant, locale, false);
    }

    private String getName(final long instant, final Locale locale, final boolean isShort) {
        return mTz.getDisplayName(!isStandardOffset(instant),
               isShort ? TimeZone.SHORT : TimeZone.LONG,
               locale == null ? Locale.getDefault() : locale);
    }

    @Override
    public String getNameKey(final long instant) {
        return null;
    }

    @Override
    public TimeZone toTimeZone() {
        return (TimeZone) mTz.clone();
    }

    @Override
    public String toString() {
        return mTz.getClass().getSimpleName();
    }

    @Override
    public boolean equals(final Object o) {
        return (o instanceof AndroidOldDateTimeZone) && mTz == ((AndroidOldDateTimeZone) o).getTz();
    }

    @Override
    public int hashCode() {
        return 31 * super.hashCode() + mTz.hashCode();
    }

    private long roundDownMillisToSeconds(final long millis) {
        return millis < 0 ? (millis - 999) / 1000 : millis / 1000;
    }

    private int findTransitionIndex(final long millis, final boolean isNext) {
        final long seconds = roundDownMillisToSeconds(millis);
        int index = isNext ? mTransition.length : -1;
        for (int i = 0; i < mTransition.length; i++) {
            if (mTransition[i] == seconds) {
                index = i;
            }
        }
        return index;
    }
}