From 9fad3e6e2f60925a40c54af69f25b1a439a416d4 Mon Sep 17 00:00:00 2001 From: Gabor Keszthelyi Date: Tue, 17 Oct 2017 21:23:08 +0200 Subject: [PATCH] DateTime support in DateFormatter. #476 --- gradle.properties | 3 +- opentasks/build.gradle | 8 ++ .../utils/DateTimeToTimeConversionTest.java | 112 ++++++++++++++++++ .../java/org/dmfs/tasks/ViewTaskFragment.java | 4 +- .../org/dmfs/tasks/utils/DateFormatter.java | 63 +++++++++- 5 files changed, 183 insertions(+), 7 deletions(-) create mode 100644 opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java diff --git a/gradle.properties b/gradle.properties index 70f4874e5..e74433aa4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,4 +6,5 @@ TARGET_SDK_VERSION=25 SUPPORT_LIBRARY_VERSION=25.0.1 CONTENTPAL_VERSION=fc8cca91 ROBOLECTRIC_VERSION=3.1.4 -JEMS_VERSION=1.13 \ No newline at end of file +JEMS_VERSION=1.13 +ANDROID_TEST_RUNNER_VERSION=0.5 diff --git a/opentasks/build.gradle b/opentasks/build.gradle index 7ec8adfca..72a73ff2c 100644 --- a/opentasks/build.gradle +++ b/opentasks/build.gradle @@ -19,6 +19,7 @@ android { targetSdkVersion TARGET_SDK_VERSION.toInteger() versionCode gitCommitNo() * 10 // spread version code to allow inserting versions if necessary versionName version + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -65,4 +66,11 @@ dependencies { compile 'org.dmfs:jems:' + JEMS_VERSION testCompile 'junit:junit:4.12' + + androidTestCompile ('com.android.support.test:runner:' + ANDROID_TEST_RUNNER_VERSION) { + exclude group: 'com.android.support', module: 'support-annotations' + } + androidTestCompile ('com.android.support.test:rules:' + ANDROID_TEST_RUNNER_VERSION) { + exclude group: 'com.android.support', module: 'support-annotations' + } } diff --git a/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java b/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java new file mode 100644 index 000000000..b189a0a95 --- /dev/null +++ b/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2017 dmfs GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dmfs.tasks.utils; + +import android.support.test.runner.AndroidJUnit4; +import android.text.format.Time; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.TimeZone; + + +/** + * Test for {@link DateFormatter#toTime(DateTime)} method. + * + * @author Gabor Keszthelyi + */ +@RunWith(AndroidJUnit4.class) +public class DateTimeToTimeConversionTest +{ + + @Test + public void test_toTime_withVariousDateTimes() + { + assertCorrectlyConverted(DateTime.now()); + + assertCorrectlyConverted(DateTime.now(TimeZone.getTimeZone("UTC+04:00"))); + + assertCorrectlyConverted(DateTime.nowAndHere()); + + assertCorrectlyConverted(new DateTime(1509473781000L)); + + assertCorrectlyConverted(new DateTime(1509473781000L).addDuration(new Duration(1, 1, 0))); + + assertCorrectlyConverted(DateTime.now(TimeZone.getTimeZone("UTC+04:00")).shiftTimeZone(TimeZone.getTimeZone("UTC+05:00"))); + + // Floating, all-day + assertCorrectlyConverted(DateTime.now().toAllDay()); + + // Not DST (March 2017 in Hungary): + assertCorrectlyConverted(new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 2 - 1, 7, 15, 0, 0)); + assertCorrectlyConverted(new DateTime(2017, 2 - 1, 7, 15, 0, 0).shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + assertCorrectlyConverted(new DateTime(2017, 2 - 1, 7, 15, 0, 0).swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + + // DST (July 2017 in Hungary): + assertCorrectlyConverted(new DateTime(TimeZone.getTimeZone("Europe/Budapest"), 2017, 7 - 1, 7, 15, 0, 0)); + assertCorrectlyConverted(new DateTime(2017, 7 - 1, 7, 15, 0, 0).shiftTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + assertCorrectlyConverted(new DateTime(2017, 7 - 1, 7, 15, 0, 0).swapTimeZone(TimeZone.getTimeZone("Europe/Budapest"))); + } + + + @Test(expected = IllegalArgumentException.class) + public void test_toTime_forFloatingButNotAllDayDateTime_throwsSinceItIsNotSupported() + { + new DateFormatter(null).toTime(new DateTime(2017, 7 - 1, 7, 15, 0, 0)); + } + + + private void assertCorrectlyConverted(DateTime dateTime) + { + Time time = new DateFormatter(null).toTime(dateTime); + if (!isEquivalentDateTimeAndTime(dateTime, time)) + { + throw new AssertionError(String.format("DateTime=%s and Time=%s are not equivalent", dateTime, time)); + } + } + + + /** + * Contains the definition/requirement of when a {@link DateTime} and {@link Time} is considered equivalent in this project. + */ + private boolean isEquivalentDateTimeAndTime(DateTime dateTime, Time time) + { + // android.text.Time doesn't seem to store in millis precision, there is a 1000 multiplier used there internally + // when calculating millis, so we can only compare in this precision: + boolean millisMatch = + dateTime.getTimestamp() / 1000 + == + time.toMillis(false) / 1000; + + boolean allDaysMatch = time.allDay == dateTime.isAllDay(); + + boolean timeZoneMatch = + // If DateTime is floating, all-day then if the all-day flag is matched with Time (checked earlier) + // then we consider the Time's timezone matching, we ignore that basically, + // because Time always has a time zone, and there is no other way to represent all-day date-times with Time. + (dateTime.isFloating() && dateTime.isAllDay()) + || + // This is the regular case with non-floating DateTime + (dateTime.getTimeZone() != null && time.timezone.equals(dateTime.getTimeZone().getID())); + + return millisMatch && allDaysMatch && timeZoneMatch; + } + +} \ No newline at end of file diff --git a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java index 4f2d5f5df..b40ed003c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java +++ b/opentasks/src/main/java/org/dmfs/tasks/ViewTaskFragment.java @@ -400,7 +400,7 @@ public void loadUri(Uri uri) if ((oldUri == null) != (uri == null)) { - /* + /* * getActivity().invalidateOptionsMenu() doesn't work in Android 2.x so use the compat lib */ ActivityCompat.invalidateOptionsMenu(getActivity()); @@ -491,7 +491,7 @@ public void onModelLoaded(Model model) @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - /* + /* * Don't show any options if we don't have a task to show. */ if (mTaskUri != null) diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java b/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java index 7edf4f40f..3f3f14b2a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java @@ -20,9 +20,11 @@ import android.annotation.TargetApi; import android.content.Context; import android.os.Build; +import android.support.annotation.VisibleForTesting; import android.text.format.DateUtils; import android.text.format.Time; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; import java.text.DateFormat; @@ -199,9 +201,9 @@ public boolean useRelative(Time now, Time date) /** - * The formatter we use for due dates other than today. + * The format we use for due dates other than today. */ - private final DateFormat mDateFormatter = DateFormat.getDateInstance(SimpleDateFormat.MEDIUM); + private final DateFormat mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.MEDIUM); /** * A context to load resource string. @@ -240,6 +242,17 @@ public String format(Time date, DateFormatContext dateContext) } + /** + * Same as {@link #format(Time, DateFormatContext)} just with {@link DateTime}s. + * ({@link Time} will eventually be replaced with {@link DateTime} in the project) + */ + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public String format(DateTime date, DateFormatContext dateContext) + { + return format(toTime(date), dateContext); + } + + /** * Format the given due date. The result depends on the current date and on the all-day flag of the due date. * @@ -294,6 +307,17 @@ else if (delta < 24 * 60 * 60 * 1000) } + /** + * Same as {@link #format(Time, Time, DateFormatContext)} just with {@link DateTime}s. + * ({@link Time} will eventually be replaced with {@link DateTime} in the project) + */ + @TargetApi(Build.VERSION_CODES.GINGERBREAD) + public String format(DateTime date, DateTime now, DateFormatContext dateContext) + { + return format(toTime(date), toTime(now), dateContext); + } + + @SuppressLint("NewApi") private String formatAllDay(Time date, Time now, DateFormatContext dateContext) { @@ -305,8 +329,8 @@ private String formatAllDay(Time date, Time now, DateFormatContext dateContext) } else { - mDateFormatter.setTimeZone(TimeZone.getTimeZone("UTC")); - return mDateFormatter.format(new Date(date.toMillis(false))); + mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return mDateFormat.format(new Date(date.toMillis(false))); } } @@ -315,4 +339,35 @@ private String formatNonAllDay(Time date, Time now, DateFormatContext dateContex { return DateUtils.formatDateTime(mContext, date.toMillis(false), dateContext.getDateUtilsFlags(now, date)); } + + + /** + * {@link Time} will eventually be replaced with {@link DateTime} in the project. + * This conversion function is only needed in the transition period. + */ + @VisibleForTesting + Time toTime(DateTime dateTime) + { + if (dateTime.isFloating() && !dateTime.isAllDay()) + { + throw new IllegalArgumentException("Cannot support floating DateTime that is not all-day, can't represent it with Time"); + } + + // Time always needs a TimeZone (default ctor falls back to TimeZone.getDefault()) + String timeZoneId = dateTime.getTimeZone() == null ? TimeZone.getDefault().getID() : dateTime.getTimeZone().getID(); + Time time = new Time(timeZoneId); + + time.set(dateTime.getTimestamp()); + + // TODO Would using time.set(monthDay, month, year) be better? + if (dateTime.isAllDay()) + { + time.allDay = true; + // This is needed as per time.allDay docs: + time.hour = 0; + time.minute = 0; + time.second = 0; + } + return time; + } }