如何处理 JodaTime 和 Android 的时区数据库差异?
How to handle JodaTime's and Android's timezone database differences?
我想扩展我在 Reddit Android 开发社区 yesterday 上开始的一个新问题:如何使用具有过时时区信息的设备上的 JodaTime 库?
问题
手头的具体问题与特定时区有关,“Europe/Kaliningrad”。我可以重现这个问题:在 Android 4.4 设备上,如果我手动将其时区设置为上述时区,调用 new DateTime()
会将此 DateTime
实例设置为前一小时的时间phone 状态栏上显示的实际时间。
我创建了一个示例 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
覆盖设备的时区并在其中显示更新时区信息的新视图。这是结果的示例:
请注意“加里宁格勒”,Android 如何将其映射到“GMT+3:00”,因为 2014 年 10 月 26 日之前都是如此(参见 Wikipedia article)。甚至一些网站仍将此时区显示为 GMT+3:00,因为此更改相对较新。然而,正确的是 JodaTime 显示的“GMT+2:00”。
有缺陷的可能解决方案?
这是一个问题,因为无论我如何尝试规避它,最后我都必须格式化时间以在他们的时区显示给用户。当我使用 JodaTime 执行此操作时,时间格式会不正确,因为它与系统显示的预期时间不匹配。
或者,假设我用 UTC 处理所有事情。当用户在日历中添加事件并选择提醒时间时,我可以将其设置为 UTC,像那样将其存储在数据库中并完成它。
但是,我需要用 Android 的 AlarmManager
设置提醒,而不是在 UTC 时间我转换了用户设置的时间,而是在相对于他们想要的时间提醒触发。这需要时区信息才能发挥作用。
例如,如果用户在 UTC+1:00 的某个地方并且他或她设置了 9:00am 的提醒,我可以:
- 在用户时区为 09:00am 创建一个新的
DateTime
实例集,并将其毫秒数存储在数据库中。我也可以直接使用与 AlarmManager
; 相同的毫秒数
- 在 UTC 为 09:00am 创建一个新的
DateTime
实例集,并将其毫秒数存储在数据库中。这更好地解决了与该问题不完全相关的其他一些问题。但是当用 AlarmManager
设置时间时,我需要在用户的时区计算它的毫秒值 09:00am;
- 完全忽略 Joda
DateTime
并使用 Java 的 Calendar
处理提醒设置。这将使我的应用程序在显示时间时依赖过时的时区信息,但至少在使用 AlarmManager
安排时间或显示日期和时间时不会出现不一致。
我错过了什么?
我可能想多了,恐怕我可能遗漏了一些明显的东西。我是吗?除了向应用程序添加我自己的时区管理并完全忽略所有内置 Android 格式化功能之外,有什么方法可以让我在 Android 上继续使用 JodaTime?
你做错了。如果用户添加一个上午 9 点的日历条目
明年他的意思是早上 9 点。时区数据库无关紧要
在用户的设备更改或政府决定更改
夏令时的开始或结束。重要的是什么
时钟在墙上说。您需要在数据库中存储“上午 9 点”。
这通常是一个不可能的问题。您永远无法在给定的挂钟时间为未来事件存储增量时间(例如纪元之后的毫秒数),并确信它会保持正确。考虑到对于任意时间,一个国家可能会签署一项法律,规定夏令时在该时间前 30 分钟生效,并且时间向前跳 1 小时。可以缓解此问题的解决方案是,不是要求 phone 在特定时间醒来,而是定期醒来,检查当前时间,然后检查是否应激活任何提醒。这可以通过调整唤醒时间来有效地完成,以反映下一次提醒设置的大致时间。例如,如果下一次提醒不是 1 年,则在 360 天后醒来,并开始更频繁地醒来,直到您非常接近提醒的时间。
由于您使用的是 AlarmManager
,并且在 UTC 中显示 it requires you to set the time,那么您是正确的,因为您需要将时间投射到 UTC 才能安排它。
但您不必坚持那样。例如,如果您有一个每天重复发生的事件,则存储该事件应该触发的本地时间。如果事件发生在特定时区(而不是设备的 当前 时区),则还要存储该时区 ID。
使用 JodaTime 将本地时间投影为 UTC 时间 - 然后将该值传递给 AlarmManager
。
定期(或至少,每当您对 JodaTime 的数据应用更新时),重新评估计划的 UTC 时间。根据需要取消并重新建立事件。
注意 DST 转换期间安排的时间。您可能有一个在特定日期无效的当地时间(可能应该提前),或者您可能有一个在特定日期不明确的本地时间(您可能应该选择 first 两个实例)。
最终,我认为您担心的是 Joda Time 数据可能比设备数据更准确。因此,闹钟可能会在正确的当地时间响起——但它可能与设备上显示的时间不匹配。我同意最终用户可能会感到困惑,但他们可能会感谢您做了正确的事情。无论如何,我认为您无能为力。
您可能会考虑的一件事是检查 TimeUtils.getTimeZoneDatabaseVersion()
并将其与您加载到 Joda Time 中的数据的版本号进行比较。如果它们不同步,您可以向您的用户显示一条警告消息,以更新您的应用程序(当您落后时),或更新他们设备的 tzdata(当设备落后时)。
快速搜索发现 these instructions and this app 用于更新 Android 上的 tzdata。 (我自己没有测试过。)
我认为其他答案没有抓住要点。是的,在持久化时间信息时,您应该仔细考虑您的用例以决定如何最好地做到这一点。但即使你做了,这个问题提出的问题仍然存在。
考虑一下 Android 的闹钟应用程序,它有源代码 freely available。如果您查看它的 AlarmInstance
class,这是它在数据库中的建模方式:
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()
无法获得正确的时间变化,例如,当 DST 开始时。
JodaTime comes in handy in this scenario because it ships with its own time zone database. You could consider other date time libraries such as date4j 为了方便更好地处理日期计算,但这些通常不处理它们自己的时区数据。
但是拥有自己的时区数据会给您的应用带来限制:您不能再依赖 Android 的时区。这意味着您不能使用它的 Calendar
class 或它的格式化功能。 JodaTime 也提供格式化功能,使用它们。如果您必须转换为 Calendar
,而不是使用 toCalendar()
方法,请创建一个类似于上面的 getAlarmTime()
的方法,您可以在其中传递您想要的确切时间。
或者,您可以检查是否存在时区不匹配,并像 Matt Johnson 在 中建议的那样警告用户。如果您决定继续使用 Android 和 Joda 的功能,我同意他的观点:
Yes - with two sources of truth, if they're out of sync, there will be
mismatches. Check the versions, show a warning, ask to be updated,
etc. There's probably not much more you can do than that.
除了您还可以做一件事:您可以自己更改 Android 的时区。您可能应该在这样做之前警告用户,然后您可以强制 Android 使用与 Joda 相同的时区偏移量:
public static boolean isSameOffset() {
long now = System.currentTimeMillis();
return DateTimeZone.getDefault().getOffset(now) == TimeZone.getDefault().getOffset(now);
}
检查后,如果不一样,你可以将Android的时区更改为你创建的"fake"时区与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"/>
权限。
最后,更改时区会更改系统当前时间。不幸的是,只有系统应用程序可以设置时间,所以您能做的最好的事情就是打开用户的日期时间设置并提示 him/her 手动将其更改为正确的时间:
startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS));
您还必须添加更多控件以确保在 DST 开始和结束时更新时区。正如您所说,您将添加自己的时区管理,但这是确保两个时区数据库之间一致性的唯一方法。
您需要覆盖 Joda Time,尤其是 Timezone Provider,并使用系统的时区而不是 IANA 数据库。
让我们展示一个基于系统 TimeZone class:
的 DateTimezone 示例
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;
}
}
我还创建了一个 Joda Time 的分支,它使用系统的时区,它是 available here。作为奖励,它在没有时区数据库的情况下重量更轻。
两句话什么是最好的方法:
使用 LocalDate 和 LocalDateTime classes 来存储和计算提醒的时间。但在调用 alarmManager.setExact 方法之前,将其转换为默认时区的 DateTime。但是用 try-catch 运算符包围它,因为 IllegalInstantException 可能会发生。因为那天不可能有这样的时间。在catch块中,根据夏令时调整时间。
我想扩展我在 Reddit Android 开发社区 yesterday 上开始的一个新问题:如何使用具有过时时区信息的设备上的 JodaTime 库?
问题
手头的具体问题与特定时区有关,“Europe/Kaliningrad”。我可以重现这个问题:在 Android 4.4 设备上,如果我手动将其时区设置为上述时区,调用 new DateTime()
会将此 DateTime
实例设置为前一小时的时间phone 状态栏上显示的实际时间。
我创建了一个示例 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
覆盖设备的时区并在其中显示更新时区信息的新视图。这是结果的示例:
请注意“加里宁格勒”,Android 如何将其映射到“GMT+3:00”,因为 2014 年 10 月 26 日之前都是如此(参见 Wikipedia article)。甚至一些网站仍将此时区显示为 GMT+3:00,因为此更改相对较新。然而,正确的是 JodaTime 显示的“GMT+2:00”。
有缺陷的可能解决方案?
这是一个问题,因为无论我如何尝试规避它,最后我都必须格式化时间以在他们的时区显示给用户。当我使用 JodaTime 执行此操作时,时间格式会不正确,因为它与系统显示的预期时间不匹配。
或者,假设我用 UTC 处理所有事情。当用户在日历中添加事件并选择提醒时间时,我可以将其设置为 UTC,像那样将其存储在数据库中并完成它。
但是,我需要用 Android 的 AlarmManager
设置提醒,而不是在 UTC 时间我转换了用户设置的时间,而是在相对于他们想要的时间提醒触发。这需要时区信息才能发挥作用。
例如,如果用户在 UTC+1:00 的某个地方并且他或她设置了 9:00am 的提醒,我可以:
- 在用户时区为 09:00am 创建一个新的
DateTime
实例集,并将其毫秒数存储在数据库中。我也可以直接使用与AlarmManager
; 相同的毫秒数
- 在 UTC 为 09:00am 创建一个新的
DateTime
实例集,并将其毫秒数存储在数据库中。这更好地解决了与该问题不完全相关的其他一些问题。但是当用AlarmManager
设置时间时,我需要在用户的时区计算它的毫秒值 09:00am; - 完全忽略 Joda
DateTime
并使用 Java 的Calendar
处理提醒设置。这将使我的应用程序在显示时间时依赖过时的时区信息,但至少在使用AlarmManager
安排时间或显示日期和时间时不会出现不一致。
我错过了什么?
我可能想多了,恐怕我可能遗漏了一些明显的东西。我是吗?除了向应用程序添加我自己的时区管理并完全忽略所有内置 Android 格式化功能之外,有什么方法可以让我在 Android 上继续使用 JodaTime?
你做错了。如果用户添加一个上午 9 点的日历条目 明年他的意思是早上 9 点。时区数据库无关紧要 在用户的设备更改或政府决定更改 夏令时的开始或结束。重要的是什么 时钟在墙上说。您需要在数据库中存储“上午 9 点”。
这通常是一个不可能的问题。您永远无法在给定的挂钟时间为未来事件存储增量时间(例如纪元之后的毫秒数),并确信它会保持正确。考虑到对于任意时间,一个国家可能会签署一项法律,规定夏令时在该时间前 30 分钟生效,并且时间向前跳 1 小时。可以缓解此问题的解决方案是,不是要求 phone 在特定时间醒来,而是定期醒来,检查当前时间,然后检查是否应激活任何提醒。这可以通过调整唤醒时间来有效地完成,以反映下一次提醒设置的大致时间。例如,如果下一次提醒不是 1 年,则在 360 天后醒来,并开始更频繁地醒来,直到您非常接近提醒的时间。
由于您使用的是 AlarmManager
,并且在 UTC 中显示 it requires you to set the time,那么您是正确的,因为您需要将时间投射到 UTC 才能安排它。
但您不必坚持那样。例如,如果您有一个每天重复发生的事件,则存储该事件应该触发的本地时间。如果事件发生在特定时区(而不是设备的 当前 时区),则还要存储该时区 ID。
使用 JodaTime 将本地时间投影为 UTC 时间 - 然后将该值传递给 AlarmManager
。
定期(或至少,每当您对 JodaTime 的数据应用更新时),重新评估计划的 UTC 时间。根据需要取消并重新建立事件。
注意 DST 转换期间安排的时间。您可能有一个在特定日期无效的当地时间(可能应该提前),或者您可能有一个在特定日期不明确的本地时间(您可能应该选择 first 两个实例)。
最终,我认为您担心的是 Joda Time 数据可能比设备数据更准确。因此,闹钟可能会在正确的当地时间响起——但它可能与设备上显示的时间不匹配。我同意最终用户可能会感到困惑,但他们可能会感谢您做了正确的事情。无论如何,我认为您无能为力。
您可能会考虑的一件事是检查 TimeUtils.getTimeZoneDatabaseVersion()
并将其与您加载到 Joda Time 中的数据的版本号进行比较。如果它们不同步,您可以向您的用户显示一条警告消息,以更新您的应用程序(当您落后时),或更新他们设备的 tzdata(当设备落后时)。
快速搜索发现 these instructions and this app 用于更新 Android 上的 tzdata。 (我自己没有测试过。)
我认为其他答案没有抓住要点。是的,在持久化时间信息时,您应该仔细考虑您的用例以决定如何最好地做到这一点。但即使你做了,这个问题提出的问题仍然存在。
考虑一下 Android 的闹钟应用程序,它有源代码 freely available。如果您查看它的 AlarmInstance
class,这是它在数据库中的建模方式:
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()
无法获得正确的时间变化,例如,当 DST 开始时。
JodaTime comes in handy in this scenario because it ships with its own time zone database. You could consider other date time libraries such as date4j 为了方便更好地处理日期计算,但这些通常不处理它们自己的时区数据。
但是拥有自己的时区数据会给您的应用带来限制:您不能再依赖 Android 的时区。这意味着您不能使用它的 Calendar
class 或它的格式化功能。 JodaTime 也提供格式化功能,使用它们。如果您必须转换为 Calendar
,而不是使用 toCalendar()
方法,请创建一个类似于上面的 getAlarmTime()
的方法,您可以在其中传递您想要的确切时间。
或者,您可以检查是否存在时区不匹配,并像 Matt Johnson 在
Yes - with two sources of truth, if they're out of sync, there will be mismatches. Check the versions, show a warning, ask to be updated, etc. There's probably not much more you can do than that.
除了您还可以做一件事:您可以自己更改 Android 的时区。您可能应该在这样做之前警告用户,然后您可以强制 Android 使用与 Joda 相同的时区偏移量:
public static boolean isSameOffset() {
long now = System.currentTimeMillis();
return DateTimeZone.getDefault().getOffset(now) == TimeZone.getDefault().getOffset(now);
}
检查后,如果不一样,你可以将Android的时区更改为你创建的"fake"时区与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"/>
权限。
最后,更改时区会更改系统当前时间。不幸的是,只有系统应用程序可以设置时间,所以您能做的最好的事情就是打开用户的日期时间设置并提示 him/her 手动将其更改为正确的时间:
startActivity(new Intent(android.provider.Settings.ACTION_DATE_SETTINGS));
您还必须添加更多控件以确保在 DST 开始和结束时更新时区。正如您所说,您将添加自己的时区管理,但这是确保两个时区数据库之间一致性的唯一方法。
您需要覆盖 Joda Time,尤其是 Timezone Provider,并使用系统的时区而不是 IANA 数据库。 让我们展示一个基于系统 TimeZone class:
的 DateTimezone 示例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;
}
}
我还创建了一个 Joda Time 的分支,它使用系统的时区,它是 available here。作为奖励,它在没有时区数据库的情况下重量更轻。
两句话什么是最好的方法: 使用 LocalDate 和 LocalDateTime classes 来存储和计算提醒的时间。但在调用 alarmManager.setExact 方法之前,将其转换为默认时区的 DateTime。但是用 try-catch 运算符包围它,因为 IllegalInstantException 可能会发生。因为那天不可能有这样的时间。在catch块中,根据夏令时调整时间。