From 6dfa8a13837da495ae525a69fac825028ba403e1 Mon Sep 17 00:00:00 2001 From: Gabor Keszthelyi Date: Thu, 28 Dec 2017 16:58:41 +0100 Subject: [PATCH] Remove android.text.format.Time usage. #467 --- opentasks/build.gradle | 1 + .../utils/DateTimeToTimeConversionTest.java | 124 --------- .../dmfs/tasks/dashclock/TasksExtension.java | 13 +- .../groupings/AbstractGroupingFactory.java | 10 +- .../groupings/BaseTaskViewDescriptor.java | 57 ++-- .../org/dmfs/tasks/groupings/ByStartDate.java | 4 +- .../cursorloaders/TimeRangeCursorFactory.java | 50 ++-- .../cursorloaders/TimeRangeCursorLoader.java | 23 +- .../TimeRangeShortCursorFactory.java | 26 +- .../TimeRangeStartCursorFactory.java | 37 +-- .../TimeRangeStartCursorLoader.java | 23 +- .../tasks/homescreen/TaskListWidgetItem.java | 8 +- .../TaskListWidgetUpdaterService.java | 16 +- .../java/org/dmfs/tasks/model/ContentSet.java | 28 ++ .../dmfs/tasks/model/TaskFieldAdapters.java | 17 +- .../CombinedDateTimeFieldAdapter.java | 159 +++++++++++ .../adapters/SimpleDateTimeFieldAdapter.java | 108 ++++++++ .../model/adapters/TimeFieldAdapter.java | 251 ------------------ .../dmfs/tasks/model/constraints/After.java | 17 +- .../model/constraints/BeforeOrShiftTime.java | 27 +- .../tasks/model/constraints/NotAfter.java | 55 ---- .../tasks/model/constraints/NotBefore.java | 55 ---- .../tasks/model/constraints/ShiftIfAfter.java | 55 ---- .../tasks/model/constraints/ShiftTime.java | 68 ----- .../tasks/model/constraints/UpdateAllDay.java | 17 +- .../contentset/ContentSetDateTimeFields.java | 86 ++++++ .../tasks/model/defaults/DefaultAfter.java | 26 +- .../tasks/model/defaults/DefaultBefore.java | 29 +- .../notification/NotificationActionUtils.java | 54 ++-- .../NotificationUpdaterService.java | 53 ++-- .../readdata/ContentSetCombinedDateTime.java | 43 +++ ...er.java => DateTimeFormatterFunction.java} | 20 +- .../org/dmfs/tasks/share/TaskBindings.java | 4 +- .../dmfs/tasks/share/TaskShareDetails.java | 2 +- .../org/dmfs/tasks/utils/DateFormatter.java | 146 +++------- .../dmfs/tasks/utils/TimeChangeObserver.java | 13 - .../dmfs/tasks/widget/TimeFieldEditor.java | 140 ++++------ .../org/dmfs/tasks/widget/TimeFieldView.java | 33 +-- .../datetime/CombinedDateTime.java | 60 +++++ .../datetime/DateTimeDateTimeFields.java | 69 +++++ .../datetime/OptionalCombinedDateTime.java | 65 +++++ .../datetime/TimestampDateTime.java | 35 +++ .../general/MatchedCurrentDateTime.java | 49 ++++ .../datetime/general/MillisDuration.java | 32 +++ .../datetime/general/MovedToDate.java | 38 +++ .../datetime/general/MovedToTimeOfDay.java | 45 ++++ .../datetime/general/OptionalTimeZone.java | 45 ++++ .../ShiftedWithTimeZoneDifference.java | 66 +++++ .../datetime/general/StartOfHour.java | 41 +++ .../datetime/general/StartOfNextDay.java | 43 +++ .../datetime/general/StartOfNextHour.java | 46 ++++ .../datetime/general/StartOfNextMonth.java | 48 ++++ .../datetime/general/StartOfNextYear.java | 39 +++ .../datetime/general/TimeZones.java | 34 +++ .../datetimefields/DateTimeFields.java | 49 ++++ .../DelegatingDateTimeFields.java | 58 ++++ .../datetimefields/Evaluated.java | 62 +++++ .../jems/procedure/Procedure.java | 28 ++ .../readdata/EffectiveDueDate.java | 6 +- ...a => RowSnapshotComposedTaskDateTime.java} | 20 +- ...fectiveTimezone.java => TaskTimezone.java} | 13 +- .../cursor/BooleanCursorColumnValue.java | 45 ++++ .../cursor/CursorCombinedDateTime.java | 41 +++ .../readdata/cursor/CursorDateTimeFields.java | 85 ++++++ .../cursor/LongCursorColumnValue.java | 33 +++ .../cursor/NullSafeCursorColumnValue.java | 80 ++++++ .../cursor/StringCursorColumnValue.java | 33 +++ .../readdata/utils/LongBinaryBoolean.java | 43 +++ .../readdata/utils/StringBinaryBoolean.java | 44 +++ ...a => RowSnapshotCombinedDateTimeTest.java} | 14 +- 70 files changed, 2122 insertions(+), 1185 deletions(-) delete mode 100644 opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/adapters/CombinedDateTimeFieldAdapter.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/adapters/SimpleDateTimeFieldAdapter.java delete mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/adapters/TimeFieldAdapter.java delete mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/constraints/NotAfter.java delete mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/constraints/NotBefore.java delete mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/constraints/ShiftIfAfter.java delete mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/constraints/ShiftTime.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/model/contentset/ContentSetDateTimeFields.java create mode 100644 opentasks/src/main/java/org/dmfs/tasks/readdata/ContentSetCombinedDateTime.java rename opentasks/src/main/java/org/dmfs/tasks/share/{TimeFormatter.java => DateTimeFormatterFunction.java} (72%) create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/CombinedDateTime.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/DateTimeDateTimeFields.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/OptionalCombinedDateTime.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/TimestampDateTime.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/MatchedCurrentDateTime.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/MillisDuration.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/MovedToDate.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/MovedToTimeOfDay.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/OptionalTimeZone.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/ShiftedWithTimeZoneDifference.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfHour.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextDay.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextHour.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextMonth.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextYear.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/TimeZones.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DateTimeFields.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DelegatingDateTimeFields.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/Evaluated.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/jems/procedure/Procedure.java rename opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/{TaskDateTime.java => RowSnapshotComposedTaskDateTime.java} (69%) rename opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/{EffectiveTimezone.java => TaskTimezone.java} (67%) create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/BooleanCursorColumnValue.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorCombinedDateTime.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorDateTimeFields.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/LongCursorColumnValue.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/NullSafeCursorColumnValue.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/StringCursorColumnValue.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/LongBinaryBoolean.java create mode 100644 opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/StringBinaryBoolean.java rename opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/{TaskDateTimeTest.java => RowSnapshotCombinedDateTimeTest.java} (83%) diff --git a/opentasks/build.gradle b/opentasks/build.gradle index 9b5884782..73aa5cfa0 100644 --- a/opentasks/build.gradle +++ b/opentasks/build.gradle @@ -46,6 +46,7 @@ android { dependencies { implementation project(':opentasks-provider') + implementation project(':opentaskspal') implementation 'com.android.support:appcompat-v7:' + SUPPORT_LIBRARY_VERSION implementation 'com.android.support:design:' + SUPPORT_LIBRARY_VERSION implementation('org.dmfs:android-xml-magic:0.1.1') { diff --git a/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java b/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java deleted file mode 100644 index 2319050af..000000000 --- a/opentasks/src/androidTest/java/org/dmfs/tasks/utils/DateTimeToTimeConversionTest.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 yearMatch = dateTime.getYear() == time.year; - boolean monthMatch = dateTime.getMonth() == time.month; - boolean dayMatch = dateTime.getDayOfMonth() == time.monthDay; - boolean hourMatch = dateTime.getHours() == time.hour; - boolean minuteMatch = dateTime.getMinutes() == time.minute; - boolean secondsMatch = dateTime.getSeconds() == time.second; - - boolean allDaysMatch = time.allDay == dateTime.isAllDay(); - - boolean timeZoneMatch = - (dateTime.isFloating() && dateTime.isAllDay() && time.timezone.equals("UTC")) - || - // This is the regular case with non-floating DateTime - (dateTime.getTimeZone() != null && time.timezone.equals(dateTime.getTimeZone().getID())); - - return millisMatch - && yearMatch - && monthMatch - && dayMatch - && hourMatch - && minuteMatch - && secondsMatch - && allDaysMatch - && timeZoneMatch; - } - -} \ No newline at end of file diff --git a/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java b/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java index a814cac6f..12f144605 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java +++ b/opentasks/src/main/java/org/dmfs/tasks/dashclock/TasksExtension.java @@ -21,19 +21,20 @@ import android.content.SharedPreferences; import android.database.Cursor; import android.preference.PreferenceManager; -import android.text.format.Time; import com.google.android.apps.dashclock.api.DashClockExtension; import com.google.android.apps.dashclock.api.ExtensionData; import org.dmfs.provider.tasks.AuthorityUtil; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.EditTaskActivity; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.model.TaskFieldAdapters; -import org.dmfs.tasks.model.adapters.TimeFieldAdapter; +import org.dmfs.tasks.model.adapters.CombinedDateTimeFieldAdapter; +import org.dmfs.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; @@ -243,8 +244,8 @@ private String getTaskTitleDueString(Cursor c, boolean isAllDay) } else { - TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DUE, Instances.TZ, Instances.IS_ALLDAY); - Time dueTime = timeFieldAdapter.get(c); + FieldAdapter dateTimeFieldAdapter = new CombinedDateTimeFieldAdapter(Instances.DUE, Instances.TZ, Instances.IS_ALLDAY); + DateTime dueTime = dateTimeFieldAdapter.get(c); if (dueTime == null) { return null; @@ -263,8 +264,8 @@ private String getTaskTitleStartString(Cursor c, boolean isAllDay) } else { - TimeFieldAdapter timeFieldAdapter = new TimeFieldAdapter(Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY); - Time startTime = timeFieldAdapter.get(c); + FieldAdapter dateTimeFieldAdapter = new CombinedDateTimeFieldAdapter(Instances.DTSTART, Instances.TZ, Instances.IS_ALLDAY); + DateTime startTime = dateTimeFieldAdapter.get(c); if (startTime == null) { return null; diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java index d37d2b0f7..6d2a4dca5 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/AbstractGroupingFactory.java @@ -16,8 +16,10 @@ package org.dmfs.tasks.groupings; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract.Instances; -import org.dmfs.tasks.model.adapters.TimeFieldAdapter; +import org.dmfs.tasks.model.adapters.CombinedDateTimeFieldAdapter; +import org.dmfs.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.utils.ExpandableChildDescriptor; import org.dmfs.tasks.utils.ExpandableGroupDescriptor; @@ -42,12 +44,14 @@ public abstract class AbstractGroupingFactory /** * An adapter to load the due date from the instances projection. This is used by most groupings */ - public final static TimeFieldAdapter INSTANCE_DUE_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_DUE, Instances.TZ, Instances.IS_ALLDAY); + public final static FieldAdapter INSTANCE_DUE_ADAPTER = new CombinedDateTimeFieldAdapter(Instances.INSTANCE_DUE, Instances.TZ, + Instances.IS_ALLDAY); /** * An adapter to load the start date from the instances projection. This is used by most groupings */ - public final static TimeFieldAdapter INSTANCE_START_ADAPTER = new TimeFieldAdapter(Instances.INSTANCE_START, Instances.TZ, Instances.IS_ALLDAY); + public final static FieldAdapter INSTANCE_START_ADAPTER = new CombinedDateTimeFieldAdapter(Instances.INSTANCE_START, Instances.TZ, + Instances.IS_ALLDAY); /** * The authority of the content provider. diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java index 3449eee76..ece8bbb6c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/BaseTaskViewDescriptor.java @@ -20,19 +20,17 @@ import android.database.Cursor; import android.support.v4.util.SparseArrayCompat; import android.text.TextUtils; -import android.text.format.Time; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; import org.dmfs.tasks.model.TaskFieldAdapters; import org.dmfs.tasks.utils.DateFormatter; import org.dmfs.tasks.utils.DateFormatter.DateFormatContext; import org.dmfs.tasks.utils.ViewDescriptor; -import java.util.TimeZone; - /** * A base implementation of a {@link ViewDescriptor}. It has a number of commonly used methods. @@ -42,53 +40,32 @@ public abstract class BaseTaskViewDescriptor implements ViewDescriptor { - /** - * We use this to get the current time. - */ - protected Time mNow; - - - protected void setDueDate(TextView view, ImageView dueIcon, Time dueDate, boolean isClosed) + // TODO Add Nullable, NonNull annotations + protected void setDueDate(TextView view, ImageView dueIcon, DateTime dueDate, boolean isClosed) { - if (view != null && dueDate != null) + if (view == null) { - Time now = mNow; - if (now == null) - { - now = mNow = new Time(); - } - if (!now.timezone.equals(TimeZone.getDefault().getID())) - { - now.clear(TimeZone.getDefault().getID()); - } - - if (Math.abs(now.toMillis(false) - System.currentTimeMillis()) > 5000) - { - now.setToNow(); - now.normalize(true); - } + // TODO can this happen? + return; + } - dueDate.normalize(true); + if (dueDate != null) + { + // TODO cache nowAndHere of some time (it was 5 secs before) + DateTime nowAndHere = DateTime.nowAndHere(); - view.setText(new DateFormatter(view.getContext()).format(dueDate, now, DateFormatContext.LIST_VIEW)); + view.setText(new DateFormatter(view.getContext()).format(dueDate, nowAndHere, DateFormatContext.LIST_VIEW)); if (dueIcon != null) { dueIcon.setVisibility(View.VISIBLE); } - // highlight overdue dates & times, handle allDay tasks separately - if ((!dueDate.allDay && dueDate.before(now) || dueDate.allDay - && (dueDate.year < now.year || dueDate.yearDay <= now.yearDay && dueDate.year == now.year)) - && !isClosed) - { - view.setTextAppearance(view.getContext(), R.style.task_list_overdue_text); - } - else - { - view.setTextAppearance(view.getContext(), R.style.task_list_due_text); - } + // overdue tasks are highlighted + int textStyle = dueDate.after(nowAndHere) && !isClosed ? R.style.task_list_overdue_text : R.style.task_list_due_text; + view.setTextAppearance(view.getContext(), textStyle); + } - else if (view != null) + else { view.setText(""); if (dueIcon != null) diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java index 1a61f37f4..3d3cfa7b9 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/ByStartDate.java @@ -20,13 +20,13 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Paint; -import android.text.format.Time; import android.view.View; import android.widget.BaseExpandableListAdapter; import android.widget.ImageView; import android.widget.TextView; import org.dmfs.optional.NullSafe; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract.Instances; import org.dmfs.tasks.groupings.cursorloaders.TimeRangeCursorFactory; @@ -88,7 +88,7 @@ public void populateView(View view, Cursor cursor, BaseExpandableListAdapter ada TextView startDateField = getView(view, R.id.task_start_date); if (startDateField != null) { - Time startDate = INSTANCE_START_ADAPTER.get(cursor); + DateTime startDate = INSTANCE_START_ADAPTER.get(cursor); if (startDate != null) { diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java index be18d36c6..f7b1cafa6 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorFactory.java @@ -18,7 +18,11 @@ import android.database.Cursor; import android.database.MatrixCursor; -import android.text.format.Time; + +import org.dmfs.opentaskspal.datetime.general.StartOfNextMonth; +import org.dmfs.opentaskspal.datetime.general.StartOfNextYear; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; import java.util.Arrays; import java.util.List; @@ -105,7 +109,7 @@ public class TimeRangeCursorFactory extends AbstractCustomCursorFactory protected final List mProjectionList; - protected final Time mTime; + protected DateTime mTime; protected final TimeZone mTimezone; @@ -114,19 +118,18 @@ public TimeRangeCursorFactory(String[] projection) super(projection); mProjectionList = Arrays.asList(projection); mTimezone = TimeZone.getDefault(); - mTime = new Time(mTimezone.getID()); + mTime = new DateTime(mTimezone, System.currentTimeMillis()); } public Cursor getCursor() { - mTime.setToNow(); + mTime = DateTime.now(); MatrixCursor result = new MatrixCursor(mProjection); // get time of today 00:00:00 - Time time = new Time(mTimezone.getID()); - time.set(mTime.monthDay, mTime.month, mTime.year); + DateTime time = DateTime.now(mTimezone).startOfDay(); // null row, for tasks without due date if (mProjectionList.contains(RANGE_NULL_ROW)) @@ -134,7 +137,7 @@ public Cursor getCursor() result.addRow(makeRow(1, 0, null, null)); } - long t1 = time.toMillis(false); + long t1 = time.getTimestamp(); // open past row for overdue tasks if (mProjectionList.contains(RANGE_OPEN_PAST)) @@ -142,40 +145,33 @@ public Cursor getCursor() result.addRow(makeRow(2, TYPE_END_OF_YESTERDAY, MIN_TIME, t1)); } - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // today row - long t2 = time.toMillis(false); + long t2 = time.getTimestamp(); result.addRow(makeRow(3, TYPE_END_OF_TODAY, t1, t2)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // tomorrow row - long t3 = time.toMillis(false); + long t3 = time.getTimestamp(); result.addRow(makeRow(4, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time = time.addDuration(new Duration(1, 5, 0)); // next week row - long t4 = time.toMillis(false); + long t4 = time.getTimestamp(); result.addRow(makeRow(5, TYPE_END_IN_7_DAYS, t3, t4)); - time.set(1, time.month + 1, time.year); - time.normalize(true); + time = new StartOfNextMonth(time).value(); // month row - long t5 = time.toMillis(false); + long t5 = time.getTimestamp(); result.addRow(makeRow(6, TYPE_END_OF_A_MONTH, t4, t5)); - time.set(1, 0, time.year + 1); + time = new StartOfNextYear(time).value(); // rest of year row - long t6 = time.toMillis(false); + long t6 = time.getTimestamp(); result.addRow(makeRow(7, TYPE_END_OF_A_YEAR, t5, t6)); // open future for future tasks @@ -199,9 +195,9 @@ protected Object[] makeRow(int id, int type, Long start, Long end) if (start != null && start > MIN_TIME && end != null && end < MAX_TIME) { - mTime.set((start + end) >> 1); - insertValue(result, RANGE_YEAR, mTime.year); - insertValue(result, RANGE_MONTH, mTime.month); + mTime = new DateTime((start + end) >> 1); + insertValue(result, RANGE_YEAR, mTime.getYear()); + insertValue(result, RANGE_MONTH, mTime.getMonth()); } if (start == null || start <= MIN_TIME) diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java index 5912caffd..b8da7d4eb 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeCursorLoader.java @@ -19,13 +19,11 @@ import android.content.Context; import android.database.Cursor; import android.support.v4.content.Loader; -import android.text.format.Time; +import org.dmfs.opentaskspal.datetime.general.StartOfNextDay; import org.dmfs.tasks.utils.TimeChangeListener; import org.dmfs.tasks.utils.TimeChangeObserver; -import java.util.TimeZone; - /** * A very simple {@link Loader} that returns the {@link Cursor} from a {@link TimeRangeCursorFactory}. It also delivers a new Cursor each time the time or the @@ -35,10 +33,6 @@ */ public class TimeRangeCursorLoader extends CustomCursorLoader implements TimeChangeListener { - /** - * A helper to retrieve the timestamp for midnight. - */ - private final Time mMidnight = new Time(); private final TimeChangeObserver mTimeChangeObserver; @@ -48,7 +42,7 @@ public TimeRangeCursorLoader(Context context, String[] projection) // set trigger at midnight mTimeChangeObserver = new TimeChangeObserver(context, this); - mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); + mTimeChangeObserver.setNextAlarm(midnightTimestamp()); } @@ -56,7 +50,7 @@ public TimeRangeCursorLoader(Context context, String[] projection) public void onTimeUpdate(TimeChangeObserver observer) { // reset next alarm - observer.setNextAlarm(getMidnightTimestamp()); + observer.setNextAlarm(midnightTimestamp()); // notify LoaderManager onContentChanged(); @@ -67,7 +61,7 @@ public void onTimeUpdate(TimeChangeObserver observer) public void onAlarm(TimeChangeObserver observer) { // set next alarm - observer.setNextAlarm(getMidnightTimestamp()); + observer.setNextAlarm(midnightTimestamp()); // notify LoaderManager onContentChanged(); @@ -82,14 +76,9 @@ protected void onReset() } - private long getMidnightTimestamp() + private long midnightTimestamp() { - mMidnight.clear(TimeZone.getDefault().getID()); - mMidnight.setToNow(); - mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); - ++mMidnight.monthDay; - mMidnight.normalize(true); - return mMidnight.toMillis(false); + return new StartOfNextDay().value().getTimestamp(); } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java index aa3e7ebec..86f2908bc 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeShortCursorFactory.java @@ -18,7 +18,9 @@ import android.database.Cursor; import android.database.MatrixCursor; -import android.text.format.Time; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; /** @@ -41,35 +43,29 @@ public TimeRangeShortCursorFactory(String[] projection) @Override public Cursor getCursor() { - mTime.setToNow(); + mTime = DateTime.now(); MatrixCursor result = new MatrixCursor(mProjection); - Time time = new Time(mTimezone.getID()); - time.set(mTime.monthDay + 1, mTime.month, mTime.year); + DateTime time = DateTime.now(mTimezone).addDuration(new Duration(1, 1, 0)); // today row (including overdue) - long t2 = time.toMillis(false); + long t2 = time.getTimestamp(); result.addRow(makeRow(1, TYPE_END_OF_TODAY, MIN_TIME, t2)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // tomorrow row - long t3 = time.toMillis(false); + long t3 = time.getTimestamp(); result.addRow(makeRow(2, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time = time.addDuration(new Duration(1, 5, 0)); // next week row - long t4 = time.toMillis(false); + long t4 = time.getTimestamp(); result.addRow(makeRow(3, TYPE_END_IN_7_DAYS, t3, t4)); - time.monthDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // open future for future tasks (including tasks without dates) if (mProjectionList.contains(RANGE_OPEN_FUTURE)) diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java index e072b8161..8fb1a012b 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorFactory.java @@ -18,7 +18,9 @@ import android.database.Cursor; import android.database.MatrixCursor; -import android.text.format.Time; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; /** @@ -40,49 +42,36 @@ public TimeRangeStartCursorFactory(String[] projection) public Cursor getCursor() { - mTime.setToNow(); - ; + mTime = DateTime.now(); MatrixCursor result = new MatrixCursor(mProjection); // get time of today 00:00:00 - Time time = new Time(mTime.timezone); - time.set(mTime.monthDay, mTime.month, mTime.year); + DateTime time = DateTime.now(mTimezone).startOfDay(); // already started row - long t1 = time.toMillis(false); + long t1 = time.getTimestamp(); result.addRow(makeRow(1, TYPE_OVERDUE, MIN_TIME, t1)); - time.hour = 0; - time.minute = 0; - time.second = 0; - - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)).startOfDay(); // today row - long t2 = time.toMillis(false); + long t2 = time.getTimestamp(); result.addRow(makeRow(2, TYPE_END_OF_TODAY, t1, t2)); - time.monthDay += 1; - time.yearDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // tomorrow row - long t3 = time.toMillis(false); + long t3 = time.getTimestamp(); result.addRow(makeRow(3, TYPE_END_OF_TOMORROW, t2, t3)); - time.monthDay += 5; - time.yearDay += 5; - time.normalize(true); + time = time.addDuration(new Duration(1, 5, 0)); // next week row - long t4 = time.toMillis(false); + long t4 = time.getTimestamp(); result.addRow(makeRow(4, TYPE_END_IN_7_DAYS, t3, t4)); - time.monthDay += 1; - time.normalize(true); + time = time.addDuration(new Duration(1, 1, 0)); // open past future for future tasks (including tasks without dates) if (mProjectionList.contains(RANGE_OPEN_FUTURE)) diff --git a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java index 4f78af487..cbfed7f2c 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java +++ b/opentasks/src/main/java/org/dmfs/tasks/groupings/cursorloaders/TimeRangeStartCursorLoader.java @@ -19,13 +19,11 @@ import android.content.Context; import android.database.Cursor; import android.support.v4.content.Loader; -import android.text.format.Time; +import org.dmfs.opentaskspal.datetime.general.StartOfNextDay; import org.dmfs.tasks.utils.TimeChangeListener; import org.dmfs.tasks.utils.TimeChangeObserver; -import java.util.TimeZone; - /** * A very simple {@link Loader} that returns the {@link Cursor} from a {@link TimeRangeStartCursorFactory}. It also delivers a new Cursor each time the time or @@ -35,10 +33,6 @@ */ public class TimeRangeStartCursorLoader extends CustomCursorLoader implements TimeChangeListener { - /** - * A helper to retrieve the timestamp for midnight. - */ - private final Time mMidnight = new Time(); private final TimeChangeObserver mTimeChangeObserver; @@ -48,7 +42,7 @@ public TimeRangeStartCursorLoader(Context context, String[] projection) // set trigger at midnight mTimeChangeObserver = new TimeChangeObserver(context, this); - mTimeChangeObserver.setNextAlarm(getMidnightTimestamp()); + mTimeChangeObserver.setNextAlarm(midnightTimeStamp()); } @@ -56,7 +50,7 @@ public TimeRangeStartCursorLoader(Context context, String[] projection) public void onTimeUpdate(TimeChangeObserver observer) { // reset next alarm - observer.setNextAlarm(getMidnightTimestamp()); + observer.setNextAlarm(midnightTimeStamp()); // notify LoaderManager onContentChanged(); @@ -67,7 +61,7 @@ public void onTimeUpdate(TimeChangeObserver observer) public void onAlarm(TimeChangeObserver observer) { // set next alarm - observer.setNextAlarm(getMidnightTimestamp()); + observer.setNextAlarm(midnightTimeStamp()); // notify LoaderManager onContentChanged(); @@ -82,14 +76,9 @@ protected void onReset() } - private long getMidnightTimestamp() + private long midnightTimeStamp() { - mMidnight.clear(TimeZone.getDefault().getID()); - mMidnight.setToNow(); - mMidnight.set(mMidnight.monthDay, mMidnight.month, mMidnight.year); - ++mMidnight.monthDay; - mMidnight.normalize(true); - return mMidnight.toMillis(false); + return new StartOfNextDay().value().getTimestamp(); } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java index 17ae26326..3284c0c85 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetItem.java @@ -16,7 +16,7 @@ package org.dmfs.tasks.homescreen; -import android.text.format.Time; +import org.dmfs.rfc5545.DateTime; /** @@ -36,7 +36,7 @@ public class TaskListWidgetItem /** * The due date. */ - private final Time mDueDate; + private final DateTime mDueDate; /** * The task color. @@ -68,7 +68,7 @@ public class TaskListWidgetItem * @param isClosed * the flag to indicate if closed */ - public TaskListWidgetItem(int id, String title, Time due, int color, boolean isClosed) + public TaskListWidgetItem(int id, String title, DateTime due, int color, boolean isClosed) { mTaskId = id; mTaskTitle = title; @@ -94,7 +94,7 @@ public int getTaskColor() * * @return the due date */ - public Time getDueDate() + public DateTime getDueDate() { return mDueDate; } diff --git a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java index 8c9060f4b..bbc95950a 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java +++ b/opentasks/src/main/java/org/dmfs/tasks/homescreen/TaskListWidgetUpdaterService.java @@ -27,11 +27,11 @@ import android.database.sqlite.SQLiteDatabase; import android.net.Uri; import android.support.v4.content.CursorLoader; -import android.text.format.Time; import android.widget.RemoteViews; import android.widget.RemoteViewsService; import org.dmfs.provider.tasks.AuthorityUtil; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Instances; @@ -44,7 +44,6 @@ import org.dmfs.tasks.utils.WidgetConfigurationDatabaseHelper; import java.util.ArrayList; -import java.util.TimeZone; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -101,7 +100,7 @@ public static class TaskListViewsFactory implements RemoteViewsService.RemoteVie /** * This variable is used to store the current time for reference. */ - private Time mNow; + private DateTime mNow; /** * The resource from the {@link Application}. @@ -219,23 +218,18 @@ public RemoteViews getViewAt(int position) row.setTextViewText(android.R.id.title, taskTitle); row.setInt(R.id.task_list_color, "setBackgroundColor", items[position].getTaskColor()); - Time dueDate = items[position].getDueDate(); + DateTime dueDate = items[position].getDueDate(); if (dueDate != null) { if (mNow == null) { - mNow = new Time(); + mNow = DateTime.nowAndHere(); } - mNow.clear(TimeZone.getDefault().getID()); - mNow.setToNow(); - dueDate.normalize(true); - mNow.normalize(true); row.setTextViewText(android.R.id.text1, mDueDateFormatter.format(dueDate, DateFormatContext.WIDGET_VIEW)); // highlight overdue dates & times - if ((!dueDate.allDay && dueDate.before(mNow) || dueDate.allDay - && (dueDate.year < mNow.year || dueDate.yearDay <= mNow.yearDay && dueDate.year == mNow.year)) + if (dueDate.before(mNow) && !items[position].getIsClosed()) { row.setTextColor(android.R.id.text1, mResources.getColor(R.color.holo_red_light)); diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java b/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java index 1e32f0f21..6b43a8b73 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/ContentSet.java @@ -22,8 +22,10 @@ import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; import android.util.Log; +import org.dmfs.opentaskspal.jems.procedure.Procedure; import org.dmfs.tasks.utils.AsyncContentLoader; import org.dmfs.tasks.utils.ContentValueMapper; import org.dmfs.tasks.utils.OnContentLoadedListener; @@ -367,6 +369,7 @@ public void put(String key, Integer value) } + @Nullable public Integer getAsInteger(String key) { final ContentValues after = mAfterContentValues; @@ -402,6 +405,7 @@ public void put(String key, Long value) } + @Nullable public Long getAsLong(String key) { final ContentValues after = mAfterContentValues; @@ -437,6 +441,7 @@ public void put(String key, String value) } + @Nullable public String getAsString(String key) { final ContentValues after = mAfterContentValues; @@ -472,6 +477,7 @@ public void put(String key, Float value) } + @Nullable public Float getAsFloat(String key) { final ContentValues after = mAfterContentValues; @@ -523,6 +529,28 @@ public void finishBulkUpdate() } + /** + * Execute a bulk update for this {@link ContentSet}. + *

+ * Shortcut for using {@link #startBulkUpdate()} and {@link #finishBulkUpdate()}. + * + * @param body + * do the changes to {@link ContentSet} here + */ + public void bulkUpdate(Procedure body) + { + startBulkUpdate(); + try + { + body.process(this); + } + finally + { + finishBulkUpdate(); + } + } + + /** * Remove the value with the given key from the ContentSet. This is actually replacing the value by null. * diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java index f1195736d..2208e8766 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java +++ b/opentasks/src/main/java/org/dmfs/tasks/model/TaskFieldAdapters.java @@ -16,21 +16,21 @@ package org.dmfs.tasks.model; -import android.text.format.Time; - +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract; import org.dmfs.tasks.contract.TaskContract.Tasks; import org.dmfs.tasks.model.adapters.BooleanFieldAdapter; import org.dmfs.tasks.model.adapters.ChecklistFieldAdapter; import org.dmfs.tasks.model.adapters.ColorFieldAdapter; +import org.dmfs.tasks.model.adapters.CombinedDateTimeFieldAdapter; import org.dmfs.tasks.model.adapters.CustomizedDefaultFieldAdapter; import org.dmfs.tasks.model.adapters.DescriptionStringFieldAdapter; import org.dmfs.tasks.model.adapters.FieldAdapter; import org.dmfs.tasks.model.adapters.FloatFieldAdapter; import org.dmfs.tasks.model.adapters.FormattedStringFieldAdapter; import org.dmfs.tasks.model.adapters.IntegerFieldAdapter; +import org.dmfs.tasks.model.adapters.SimpleDateTimeFieldAdapter; import org.dmfs.tasks.model.adapters.StringFieldAdapter; -import org.dmfs.tasks.model.adapters.TimeFieldAdapter; import org.dmfs.tasks.model.adapters.TimezoneFieldAdapter; import org.dmfs.tasks.model.adapters.UriFieldAdapter; import org.dmfs.tasks.model.constraints.AdjustPercentComplete; @@ -113,24 +113,25 @@ public final class TaskFieldAdapters /** * Private adapter for the start date of a task. We need this to reference DTSTART from DUE. */ - private final static TimeFieldAdapter _DTSTART = new TimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); - private final static TimeFieldAdapter _DUE = new TimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); + private final static FieldAdapter _DTSTART = new CombinedDateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); + private final static FieldAdapter _DUE = new CombinedDateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); /** * Adapter for the due date of a task. */ - public final static FieldAdapter

+ * Combined date-time values are stored as three values: + *

    + *
  • a timestamp in milliseconds since the epoch
  • + *
  • a time zone
  • + *
  • an all-day flag
  • + *
+ * + * @author Gabor Keszthelyi + */ +public final class CombinedDateTimeFieldAdapter extends FieldAdapter +{ + private final String mTimestampField; + private final String mTimeZoneField; + private final String mIsAllDayField; + + + /** + * Constructor for a new {@link CombinedDateTimeFieldAdapter}. + * + * @param timestampField + * The name of the field that holds the time stamp in milliseconds. + * @param timeZoneField + * The name of the field that holds the time zone (as Olson ID). + * @param isAllDayField + * The name of the field that indicated that this time is a date not a date-time. + */ + public CombinedDateTimeFieldAdapter(@NonNull String timestampField, + @NonNull String timeZoneField, + @NonNull String isAllDayField) + { + mTimestampField = timestampField; + mTimeZoneField = timeZoneField; + mIsAllDayField = isAllDayField; + } + + + @Override + public DateTime get(@NonNull ContentSet values) + { + return new ContentSetCombinedDateTime(values, mTimestampField, mTimeZoneField, mIsAllDayField).value(null); + } + + + @Override + public DateTime get(@NonNull Cursor cursor) + { + return new CursorCombinedDateTime(cursor, mTimestampField, mTimeZoneField, mIsAllDayField).value(null); + } + + + // TODO @NonNull and @Nullable annotations in this class are all assumptions until #644 is completed. + // TODO getDefault()'s param may change to @Nullable - may need to update usage + @Override + public DateTime getDefault(@NonNull ContentSet values) + { + TimeZone timeZone = new OptionalTimeZone(values.getAsString(mTimeZoneField)).value(TimeZones.UTC); + boolean isAllDay = new StringBinaryBoolean(values.getAsString(mIsAllDayField)).value(); + return new MatchedCurrentDateTime(timeZone, isAllDay).value(); + } + + + @Override + public void set(@NonNull ContentSet values, @Nullable DateTime dateTime) + { + values.bulkUpdate(contentSet -> + { + if (dateTime != null) + { + DateTimeFields dateTimeFields = new DateTimeDateTimeFields(dateTime); + values.put(mTimestampField, dateTimeFields.timestamp()); + values.put(mTimeZoneField, dateTimeFields.timeZoneId()); + values.put(mIsAllDayField, dateTimeFields.isAllDay()); + } + else + { + // write timestamp only, other fields may still use all-day and timezone + values.put(mTimestampField, (Long) null); + } + }); + } + + + @Override + public void set(@NonNull ContentValues values, @Nullable DateTime dateTime) + { + // TODO How to remove semantic code duplication with the above + if (dateTime != null) + { + DateTimeFields dateTimeFields = new DateTimeDateTimeFields(dateTime); + values.put(mTimestampField, dateTimeFields.timestamp()); + values.put(mTimeZoneField, dateTimeFields.timeZoneId()); + values.put(mIsAllDayField, dateTimeFields.isAllDay()); + } + else + { + // write timestamp only, other fields may still use all-day and timezone + values.put(mTimestampField, (Long) null); + } + } + + + @Override + public void registerListener(ContentSet values, OnContentChangeListener listener, boolean initalNotification) + { + values.addOnChangeListener(listener, mTimestampField, initalNotification); + values.addOnChangeListener(listener, mTimeZoneField, initalNotification); + values.addOnChangeListener(listener, mIsAllDayField, initalNotification); + } + + + @Override + public void unregisterListener(ContentSet values, OnContentChangeListener listener) + { + values.removeOnChangeListener(listener, mTimestampField); + values.removeOnChangeListener(listener, mTimeZoneField); + values.removeOnChangeListener(listener, mIsAllDayField); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/adapters/SimpleDateTimeFieldAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/adapters/SimpleDateTimeFieldAdapter.java new file mode 100644 index 000000000..c02f92ccb --- /dev/null +++ b/opentasks/src/main/java/org/dmfs/tasks/model/adapters/SimpleDateTimeFieldAdapter.java @@ -0,0 +1,108 @@ +/* + * 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.model.adapters; + +import android.content.ContentValues; +import android.database.Cursor; +import android.support.annotation.NonNull; + +import org.dmfs.rfc5545.DateTime; +import org.dmfs.tasks.model.ContentSet; +import org.dmfs.tasks.model.OnContentChangeListener; + + +/** + * Knows how to load and store a 'simple' {@link DateTime} value, i.e. one that doesn't use timezone and all-day flag, just timestamp. + * + * @author Gabor Keszthelyi + */ +public final class SimpleDateTimeFieldAdapter extends FieldAdapter +{ + private final String mTimestampField; + + + /** + * Constructor for a new {@link SimpleDateTimeFieldAdapter}. + * + * @param timestampField + * The name of the field that holds the time stamp in milliseconds. + */ + public SimpleDateTimeFieldAdapter(@NonNull String timestampField) + { + mTimestampField = timestampField; + } + + + @Override + public DateTime get(ContentSet values) + { + Long timeStamp = values.getAsLong(mTimestampField); + return timeStamp == null ? null : new DateTime(timeStamp); + } + + + @Override + public DateTime get(Cursor cursor) + { + long timeStamp = cursor.getLong(cursor.getColumnIndexOrThrow(mTimestampField)); + // TODO What is tha value of it when it's null or empty? + return timeStamp > 0 ? new DateTime(timeStamp) : null; + } + + + @Override + public DateTime getDefault(ContentSet values) + { + return DateTime.now(); + } + + + @Override + public void set(ContentSet values, DateTime value) + { + values.startBulkUpdate(); + try + { + values.put(mTimestampField, value == null ? null : value.getTimestamp()); + } + finally + { + values.finishBulkUpdate(); + } + } + + + @Override + public void set(ContentValues values, DateTime value) + { + values.put(mTimestampField, value == null ? null : value.getTimestamp()); + } + + + @Override + public void registerListener(ContentSet values, OnContentChangeListener listener, boolean initalNotification) + { + values.addOnChangeListener(listener, mTimestampField, initalNotification); + } + + + @Override + public void unregisterListener(ContentSet values, OnContentChangeListener listener) + { + values.removeOnChangeListener(listener, mTimestampField); + } +} diff --git a/opentasks/src/main/java/org/dmfs/tasks/model/adapters/TimeFieldAdapter.java b/opentasks/src/main/java/org/dmfs/tasks/model/adapters/TimeFieldAdapter.java deleted file mode 100644 index 3cf74a0cd..000000000 --- a/opentasks/src/main/java/org/dmfs/tasks/model/adapters/TimeFieldAdapter.java +++ /dev/null @@ -1,251 +0,0 @@ -/* - * 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.model.adapters; - -import android.content.ContentValues; -import android.database.Cursor; -import android.text.format.Time; - -import org.dmfs.tasks.model.ContentSet; -import org.dmfs.tasks.model.OnContentChangeListener; - -import java.util.TimeZone; - - -/** - * Knows how to load and store {@link Time} values in a {@link ContentSet}. - *

- * Time values are stored as three values: - *

    - *
  • a timestamp in milliseconds since the epoch
  • - *
  • a time zone
  • - *
  • an allday flag
  • - *
- *

- * This adapter combines those three fields in a {@link ContentValues} to a Time value. If the time zone field is null the time zone is always set - * to UTC. - * - * @author Marten Gajda - */ -public final class TimeFieldAdapter extends FieldAdapter

- * TODO: use Duration class to get the duration in days and shift without summer/winter time switches - * - * @author Marten Gajda - */ -public class ShiftTime extends AbstractConstraint

- * Uses a {@link Function} of {@link Time} to {@link String} received in the constructor to format the times. + * Uses a {@link Function} of {@link DateTime} to {@link String} received in the constructor to format the times. * * @author Gabor Keszthelyi */ diff --git a/opentasks/src/main/java/org/dmfs/tasks/share/TaskShareDetails.java b/opentasks/src/main/java/org/dmfs/tasks/share/TaskShareDetails.java index e38e0b535..35e0a4012 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/share/TaskShareDetails.java +++ b/opentasks/src/main/java/org/dmfs/tasks/share/TaskShareDetails.java @@ -87,7 +87,7 @@ public CharSequence value() new Composite( new AndroidBindings(mContext), new SingletonBindings("$task", new TaskBindings(mContentSet, mModel)), - new SingletonBindings("tformat", new TimeFormatter(mContext, mContentSet)))); + new SingletonBindings("tformat", new DateTimeFormatterFunction(mContext, mContentSet)))); Log.v("TaskShareDetails", output); return output; 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 392a3eeff..49c368383 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/DateFormatter.java @@ -18,20 +18,16 @@ import android.content.Context; import android.content.res.Resources; -import android.support.annotation.VisibleForTesting; import android.text.format.DateUtils; -import android.text.format.Time; import org.dmfs.jems.pair.Pair; import org.dmfs.jems.pair.elementary.ValuePair; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; -import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Formatter; import java.util.Locale; -import java.util.TimeZone; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.FORMAT_SHOW_TIME; @@ -56,9 +52,9 @@ public enum DateFormatContext RELATIVE { @Override - public boolean useRelative(Time now, Time date) + public boolean useRelative(DateTime now, DateTime date) { - return Math.abs(date.toMillis(false) - now.toMillis(false)) < 7 * 24 * 3600 * 1000; + return Math.abs(date.getTimestamp() - now.getTimestamp()) < 7 * 24 * 3600 * 1000; } }, @@ -68,9 +64,9 @@ public boolean useRelative(Time now, Time date) DETAILS_VIEW { @Override - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { - if (date.allDay) + if (date.isAllDay()) { return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY; } @@ -90,9 +86,9 @@ public int getDateUtilsFlags(Time now, Time date) LIST_VIEW { @Override - public boolean useRelative(Time now, Time date) + public boolean useRelative(DateTime now, DateTime date) { - return Math.abs(date.toMillis(false) - now.toMillis(false)) < 7 * 24 * 3600 * 1000; + return Math.abs(date.getTimestamp() - now.getTimestamp()) < 7 * 24 * 3600 * 1000; } }, @@ -104,14 +100,14 @@ public boolean useRelative(Time now, Time date) WIDGET_VIEW { @Override - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { int result = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; - if (!date.allDay) + if (!date.isAllDay()) { result |= FORMAT_SHOW_TIME; } - if (now.year != date.year) + if (now.getYear() != date.getYear()) { result |= DateUtils.FORMAT_SHOW_YEAR; } @@ -125,7 +121,7 @@ public int getDateUtilsFlags(Time now, Time date) DASHCLOCK_VIEW { @Override - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { return FORMAT_SHOW_TIME; } @@ -137,14 +133,14 @@ public int getDateUtilsFlags(Time now, Time date) NOTIFICATION_VIEW_DATE { @Override - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; } @Override - public boolean useRelative(Time now, Time date) + public boolean useRelative(DateTime now, DateTime date) { return true; } @@ -156,28 +152,28 @@ public boolean useRelative(Time now, Time date) NOTIFICATION_VIEW_TIME { @Override - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { return FORMAT_SHOW_TIME; } @Override - public boolean useRelative(Time now, Time date) + public boolean useRelative(DateTime now, DateTime date) { return false; } }; - public int getDateUtilsFlags(Time now, Time date) + public int getDateUtilsFlags(DateTime now, DateTime date) { - if (now.year == date.year && now.yearDay == date.yearDay) + if (now.toAllDay().equals(date.toAllDay())) { // today, show time only return FORMAT_SHOW_TIME; } - else if (now.year == date.year) + else if (now.getYear() == date.getYear()) { // this year, don't include the year return DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY; @@ -190,63 +186,33 @@ else if (now.year == date.year) } - public boolean useRelative(Time now, Time date) + public boolean useRelative(DateTime now, DateTime date) { return false; } } - /** - * The format we use for due dates other than today. - */ - private final DateFormat mDateFormat = DateFormat.getDateInstance(SimpleDateFormat.MEDIUM); - /** * A context to load resource string. */ private Context mContext; - /** - * A helper to get the current date & time. - */ - private Time mNow; - private static Pair sIs12hourFormatCache; public DateFormatter(Context context) { mContext = context; - mNow = new Time(); } /** * Format the given due date. The result depends on the current date and on the all-day flag of the due date. - * - * @param date - * The due date to format. - * @param useToday - * true to write "today" instead of the date when the date is on the present day - * - * @return A string with the formatted due date. - */ - public String format(Time date, DateFormatContext dateContext) - { - mNow.clear(TimeZone.getDefault().getID()); - mNow.setToNow(); - return format(date, mNow, dateContext); - } - - - /** - * Same as {@link #format(Time, DateFormatContext)} just with {@link DateTime}s. - * ({@link Time} will eventually be replaced with {@link DateTime} in the project) */ public String format(DateTime date, DateFormatContext dateContext) { - return format(toTime(date), dateContext); + return format(date, DateTime.now(), dateContext); } @@ -255,26 +221,19 @@ public String format(DateTime date, DateFormatContext dateContext) * * @param date * The due date to format. - * @param useToday - * true to write "today" instead of the date when the date is on the present day * * @return A string with the formatted due date. */ - public String format(Time date, Time now, DateFormatContext dateContext) + public String format(DateTime date, DateTime now, DateFormatContext dateContext) { - - // normalize time to ensure yearDay is set properly - date.normalize(false); - if (dateContext.useRelative(now, date)) { - long delta = Math.abs(now.toMillis(false) - date.toMillis(false)); + long delta = Math.abs(now.getTimestamp() - date.getTimestamp()); - if (date.allDay) + if (date.isAllDay()) { - Time allDayNow = new Time("UTC"); - allDayNow.set(now.monthDay, now.month, now.year); - return DateUtils.getRelativeTimeSpanString(date.toMillis(false), allDayNow.toMillis(false), DAY_IN_MILLIS).toString(); + DateTime allDayNow = now.toAllDay(); + return DateUtils.getRelativeTimeSpanString(date.getTimestamp(), allDayNow.getTimestamp(), DAY_IN_MILLIS).toString(); } else if (delta < 60 * 1000) { @@ -284,77 +243,36 @@ else if (delta < 60 * 1000) else if (delta < 60 * 60 * 1000) { // time is within this hour, show number of minutes left - return DateUtils.getRelativeTimeSpanString(date.toMillis(false), now.toMillis(false), DateUtils.MINUTE_IN_MILLIS).toString(); + return DateUtils.getRelativeTimeSpanString(date.getTimestamp(), now.getTimestamp(), DateUtils.MINUTE_IN_MILLIS).toString(); } else if (delta < 24 * 60 * 60 * 1000) { // time is within 24 hours, show relative string with time // FIXME: instead of using a fixed 24 hour interval this should be aligned to midnight tomorrow and yesterday - return routingGetRelativeDateTimeString(mContext, date.toMillis(false), DAY_IN_MILLIS, WEEK_IN_MILLIS, + return routingGetRelativeDateTimeString(mContext, date.getTimestamp(), DAY_IN_MILLIS, WEEK_IN_MILLIS, dateContext.getDateUtilsFlags(now, date)).toString(); } else { - return DateUtils.getRelativeTimeSpanString(date.toMillis(false), now.toMillis(false), DAY_IN_MILLIS).toString(); + return DateUtils.getRelativeTimeSpanString(date.getTimestamp(), now.getTimestamp(), DAY_IN_MILLIS).toString(); } } - return date.allDay ? formatAllDay(date, now, dateContext) : formatNonAllDay(date, now, dateContext); + return date.isAllDay() ? formatAllDay(date, now, dateContext) : formatNonAllDay(date, now, dateContext); } - /** - * Same as {@link #format(Time, Time, DateFormatContext)} just with {@link DateTime}s. - * ({@link Time} will eventually be replaced with {@link DateTime} in the project) - */ - public String format(DateTime date, DateTime now, DateFormatContext dateContext) - { - return format(toTime(date), toTime(now), dateContext); - } - - - private String formatAllDay(Time date, Time now, DateFormatContext dateContext) + private String formatAllDay(DateTime date, DateTime now, DateFormatContext dateContext) { // use DataRange in order to set the correct timezone - return DateUtils.formatDateRange(mContext, new Formatter(Locale.getDefault()), date.toMillis(false), date.toMillis(false), + return DateUtils.formatDateRange(mContext, new Formatter(Locale.getDefault()), date.getTimestamp(), date.getTimestamp(), dateContext.getDateUtilsFlags(now, date), "UTC").toString(); } - private String formatNonAllDay(Time date, Time now, DateFormatContext dateContext) - { - 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) + private String formatNonAllDay(DateTime date, DateTime now, DateFormatContext dateContext) { - 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 ? "UTC" : 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; + return DateUtils.formatDateTime(mContext, date.getTimestamp(), dateContext.getDateUtilsFlags(now, date)); } diff --git a/opentasks/src/main/java/org/dmfs/tasks/utils/TimeChangeObserver.java b/opentasks/src/main/java/org/dmfs/tasks/utils/TimeChangeObserver.java index 6cd2fa478..1221a4ae6 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/utils/TimeChangeObserver.java +++ b/opentasks/src/main/java/org/dmfs/tasks/utils/TimeChangeObserver.java @@ -21,7 +21,6 @@ import android.content.Intent; import android.content.IntentFilter; import android.os.Handler; -import android.text.format.Time; import android.util.Log; @@ -102,18 +101,6 @@ public void setNextAlarm(long alarm) } - /** - * Set an alarm time. Any previously set alarm will be discarded, that means only one alarm can be set at a time. - * - * @param alarm - * The time when to trigger the alarm. - */ - public void setNextAlarm(Time alarm) - { - setNextAlarm(alarm.toMillis(false)); - } - - /** * A {@link Runnable} that notifies our listener when the alarm is triggered. */ diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldEditor.java b/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldEditor.java index 4aef6ab9f..6b03942e2 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldEditor.java +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldEditor.java @@ -26,8 +26,6 @@ import android.graphics.drawable.ColorDrawable; import android.os.Build; import android.os.Build.VERSION_CODES; -import android.text.TextUtils; -import android.text.format.Time; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; @@ -38,6 +36,11 @@ import android.widget.NumberPicker; import android.widget.TimePicker; +import org.dmfs.opentaskspal.datetime.general.MovedToDate; +import org.dmfs.opentaskspal.datetime.general.MovedToTimeOfDay; +import org.dmfs.opentaskspal.datetime.general.ShiftedWithTimeZoneDifference; +import org.dmfs.optional.NullSafe; +import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.R; import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.model.FieldDescriptor; @@ -64,7 +67,7 @@ public final class TimeFieldEditor extends AbstractFieldEditor implements OnDate /** * The adapter to load the values from a {@link ContentSet}. */ - private FieldAdapter

- * Example: - *

- *

- *

-     * input time: 2013-04-02 16:00 Europe/Berlin (GMT+02:00)
-     * input timeZone: America/New_York (GMT-04:00)
-     * 
- *

- * will result in - *

- *

-     * 2013-04-02 10:00 Europe/Berlin (because the original time is equivalent to 2013-04-02 10:00 America/New_York)
-     * 
- *

- * All-day times are not modified. - * - * @param time - * The {@link Time} to update. - * @param timeZone - * A time zone id. - */ - private void applyTimeInTimeZone(Time time, String timeZone) - { - if (!time.allDay) - { - /* - * Switch to timeZone and reset back to original time zone. That updates date & time to the values in timeZone but keeps the original time zone. - */ - String originalTimeZone = time.timezone; - time.switchTimezone(timeZone); - time.timezone = originalTimeZone; - time.set(time.second, time.minute, time.hour, time.monthDay, time.month, time.year); - } - } - - @Override public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) { if (ALLDAY.get(mValues)) { - mDateTime.timezone = Time.TIMEZONE_UTC; - mDateTime.set(dayOfMonth, monthOfYear, year); + mDateTime = new DateTime(year, monthOfYear, dayOfMonth, 0, 0, 0); } else { - mDateTime.year = year; - mDateTime.month = monthOfYear; - mDateTime.monthDay = dayOfMonth; - mDateTime.normalize(true); + mDateTime = new MovedToDate(year, monthOfYear, dayOfMonth, mDateTime).value(); } mUpdated = true; mAdapter.validateAndSet(mValues, mDateTime); @@ -273,8 +233,7 @@ public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth @Override public void onTimeSet(TimePicker view, int hourOfDay, int minute) { - mDateTime.hour = hourOfDay; - mDateTime.minute = minute; + mDateTime = new MovedToTimeOfDay(hourOfDay, minute, mDateTime).value(); mUpdated = true; mAdapter.validateAndSet(mValues, mDateTime); } @@ -283,9 +242,8 @@ public void onTimeSet(TimePicker view, int hourOfDay, int minute) @Override public void onContentChanged(ContentSet contentSet) { - Time newTime = mAdapter.get(mValues); - if (!mUpdated && newTime != null && mDateTime != null && Time.compare(newTime, mDateTime) == 0 - && TextUtils.equals(newTime.timezone, mDateTime.timezone) && newTime.allDay == mDateTime.allDay) + DateTime newTime = mAdapter.get(mValues); + if (!mUpdated && newTime != null && mDateTime != null && mDateTime.equals(newTime)) { // nothing has changed return; @@ -294,65 +252,73 @@ public void onContentChanged(ContentSet contentSet) if (newTime != null) { - if (mDateTime != null && mDateTime.timezone != null && !TextUtils.equals(mDateTime.timezone, newTime.timezone) && !newTime.allDay) + if (mDateTime != null && mDateTime.getTimeZone() != null + && mDateTime.getTimeZone().equals(newTime.getTimeZone()) + && !newTime.isAllDay()) { /* * Time zone has been changed. * * We don't want to change date and hour in the editor, so apply the old time zone. */ - applyTimeInTimeZone(newTime, mDateTime.timezone); + newTime = new ShiftedWithTimeZoneDifference(mDateTime.getTimeZone(), newTime).value(); } - if (mDateTime != null && mDateTime.allDay != newTime.allDay) + if (mDateTime != null && mDateTime.isAllDay() != newTime.isAllDay()) { /* * The all-day flag has been changed, we may have to restore time and time zone for the UI. */ - if (!newTime.allDay) + if (!newTime.isAllDay()) { /* * Try to restore the time or set a reasonable time if we didn't have any before. */ + int hour; + int minute; if (mOldHour >= 0 && mOldMinutes >= 0) { - newTime.hour = mOldHour; - newTime.minute = mOldMinutes; + hour = mOldHour; + minute = mOldMinutes; } else { - Time defaultDate = mAdapter.getDefault(contentSet); - applyTimeInTimeZone(defaultDate, TimeZone.getDefault().getID()); - newTime.hour = defaultDate.hour; - newTime.minute = defaultDate.minute; + DateTime defaultDate = mAdapter.getDefault(contentSet); + defaultDate = new ShiftedWithTimeZoneDifference(TimeZone.getDefault(), defaultDate).value(); + hour = defaultDate.getHours(); + minute = defaultDate.getMinutes(); } /* * All-day events are floating and have no time zone (though it might be set to UTC). * * Restore previous time zone if possible, otherwise pick a reasonable default value. */ - newTime.timezone = mTimezone == null ? TimeZone.getDefault().getID() : mTimezone; - newTime.normalize(true); + TimeZone timeZone = mTimezone == null ? TimeZone.getDefault() : mTimezone; + + // TODO Could we use ShiftedWithTimeZoneDifference here? (TimeZone is not from the original here) + newTime = new DateTime(timeZone, + newTime.getYear(), newTime.getMonth(), newTime.getDayOfMonth(), + hour, minute, 0); } else { // apply time zone shift to end up with the right day - newTime.set(mDateTime.toMillis(false) + TimeZone.getTimeZone(mDateTime.timezone).getOffset(mDateTime.toMillis(false))); - newTime.set(newTime.monthDay, newTime.month, newTime.year); + // TODO Is this correct?: + newTime = newTime.shiftTimeZone(mDateTime.getTimeZone()); } } - if (!newTime.allDay) + if (!newTime.isAllDay()) { // preserve current time zone - mTimezone = newTime.timezone; + mTimezone = newTime.getTimeZone(); } /* * Update UI. Ensure we show the time in the correct time zone. */ - Date currentDate = new Date(newTime.toMillis(false)); - TimeZone timeZone = TimeZone.getTimeZone(newTime.timezone); + Date currentDate = new Date(newTime.getTimestamp()); + TimeZone timeZone = new NullSafe<>(newTime.getTimeZone()).value(TimeZone.getDefault()); if (mDatePickerButton != null) { @@ -363,7 +329,7 @@ public void onContentChanged(ContentSet contentSet) if (mTimePickerButton != null) { - if (!newTime.allDay) + if (!newTime.isAllDay()) { mDefaultTimeFormat.setTimeZone(timeZone); String formattedTime = mDefaultTimeFormat.format(currentDate); @@ -376,10 +342,10 @@ public void onContentChanged(ContentSet contentSet) } } - if (!newTime.allDay) + if (!newTime.isAllDay()) { - mOldHour = newTime.hour; - mOldMinutes = newTime.minute; + mOldHour = newTime.getHours(); + mOldMinutes = newTime.getMinutes(); } if (mClearDateButton != null) @@ -387,8 +353,7 @@ public void onContentChanged(ContentSet contentSet) mClearDateButton.setEnabled(true); } - if (mDateTime == null || Time.compare(newTime, mDateTime) != 0 || !TextUtils.equals(newTime.timezone, mDateTime.timezone) - || newTime.allDay != mDateTime.allDay) + if (mDateTime == null || !mDateTime.equals(newTime)) { // We have modified the time, so update contentSet. mDateTime = newTime; @@ -422,18 +387,19 @@ public void onContentChanged(ContentSet contentSet) /** * A workaround method to display DatePicker while avoiding crashed on Samsung Android 5.0 devices * - * @see http://stackoverflow.com/questions/28345413/datepicker-crash-in-samsung-with-android-5-0 + * @see DatePicker crash in samsung with android 5.0 */ private Dialog getDatePickerWithSamsungWorkaround() { // The datepicker on Samsung Android 5.0 devices crashes for certain languages, e.g. french and polish // We fall back to the holo datepicker in this case. German and English are confirmed to work. if (Build.VERSION.SDK_INT == VERSION_CODES.LOLLIPOP && Build.MANUFACTURER.equalsIgnoreCase("samsung") - && !("en".equals(Locale.getDefault().getLanguage().toString()))) + && !("en".equals(Locale.getDefault().getLanguage()))) { // get holo picker - DatePickerDialog dialog = new DatePickerDialog(getContext(), R.style.DatePickerHolo, TimeFieldEditor.this, mDateTime.year, mDateTime.month, - mDateTime.monthDay); + DatePickerDialog dialog = new DatePickerDialog(getContext(), R.style.DatePickerHolo, TimeFieldEditor.this, mDateTime.getYear(), + mDateTime.getMonth(), + mDateTime.getDayOfMonth()); dialog.getWindow().setBackgroundDrawable(new ColorDrawable(0)); // change divider color @@ -474,7 +440,7 @@ private Dialog getDatePickerWithSamsungWorkaround() } else { - return new DatePickerDialog(getContext(), TimeFieldEditor.this, mDateTime.year, mDateTime.month, mDateTime.monthDay); + return new DatePickerDialog(getContext(), TimeFieldEditor.this, mDateTime.getYear(), mDateTime.getMonth(), mDateTime.getDayOfMonth()); } } } diff --git a/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldView.java b/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldView.java index ea696336d..1b513f3f0 100644 --- a/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldView.java +++ b/opentasks/src/main/java/org/dmfs/tasks/widget/TimeFieldView.java @@ -18,12 +18,14 @@ import android.content.Context; import android.text.format.DateFormat; -import android.text.format.Time; import android.util.AttributeSet; import android.view.View; import android.view.View.OnClickListener; import android.widget.TextView; +import org.dmfs.opentaskspal.datetime.general.TimeZones; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; import org.dmfs.tasks.R; import org.dmfs.tasks.model.ContentSet; import org.dmfs.tasks.model.FieldDescriptor; @@ -47,15 +49,10 @@ */ public final class TimeFieldView extends AbstractFieldView implements OnClickListener { - /** - * {@link TimeZone} UTC, we use it when showing all-day dates. - */ - private final static TimeZone UTC = TimeZone.getTimeZone(Time.TIMEZONE_UTC); - /** * The {@link FieldAdapter} of the field for this view. */ - private FieldAdapter

+ * Example: + *

+ *

+ * input time: 2013-04-02 16:00 Europe/Berlin (GMT+02:00)
+ * input timeZone: America/New_York (GMT-04:00)
+ * 
+ *

+ * will result in + *

+ *

+ * 2013-04-02 10:00 Europe/Berlin (because the original time is equivalent to 2013-04-02 10:00 America/New_York)
+ * 
+ *

+ * All-day times are not modified. + * + * @author Gabor Keszthelyi + */ +// TODO Is this what we need? +// TODO Test +public final class ShiftedWithTimeZoneDifference extends DelegatingSingle +{ + public ShiftedWithTimeZoneDifference(TimeZone timeZone, DateTime original) + { + super(() -> + { + if (original.isAllDay()) + { + return original; + } + + TimeZone originalTimeZone = original.getTimeZone(); + + return original.shiftTimeZone(timeZone).swapTimeZone(originalTimeZone); + }); + } + +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfHour.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfHour.java new file mode 100644 index 000000000..59e3e2468 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfHour.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import org.dmfs.jems.single.Single; +import org.dmfs.jems.single.decorators.DelegatingSingle; +import org.dmfs.rfc5545.DateTime; + + +/** + * {@link Single} for a {@link DateTime} that is the same as the provided one except that it is moved to the start of the hour. + * e.g.: 2018 Jan 18 20:44:33 -> 2018 Jan 18 20:00:00 + * + * @author Gabor Keszthelyi + */ +public final class StartOfHour extends DelegatingSingle +{ + public StartOfHour(DateTime original) + { + super(() -> original.isAllDay() ? + // All-day date-time is already at the start of the hour with 00:00 + original + : + new DateTime(original.getTimeZone(), original.getYear(), + original.getMonth(), original.getDayOfMonth(), original.getHours(), 0, 0)); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextDay.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextDay.java new file mode 100644 index 000000000..cfda8c53f --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextDay.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import org.dmfs.jems.single.Single; +import org.dmfs.jems.single.decorators.DelegatingSingle; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; + + +/** + * {@link Single} for a {@link DateTime} that represents the start (00:00) of the day after the provided one. + * + * @author Gabor Keszthelyi + */ +public final class StartOfNextDay extends DelegatingSingle +{ + public StartOfNextDay(DateTime original) + { + // TODO Is there a concern about DST here? + super(() -> DateTime.now().addDuration(new Duration(1, 1, 0)).startOfDay()); + } + + + public StartOfNextDay() + { + this(DateTime.now()); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextHour.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextHour.java new file mode 100644 index 000000000..2a90f71f9 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextHour.java @@ -0,0 +1,46 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import org.dmfs.jems.single.Single; +import org.dmfs.jems.single.decorators.DelegatingSingle; +import org.dmfs.rfc5545.DateTime; +import org.dmfs.rfc5545.Duration; + + +/** + * {@link Single} for a {@link DateTime} that is the same as the provided one except that it is moved to the start of the next hour. + * e.g.: 2018 Jan 18 20:44:33 -> 2018 Jan 18 21:00:00 + * + * @author Gabor Keszthelyi + */ +public final class StartOfNextHour extends DelegatingSingle +{ + public StartOfNextHour(DateTime original) + { + super(() -> + { + if (original.isAllDay()) + { + throw new IllegalArgumentException("Cannot move all-day DateTime to next hour"); + } + + DateTime oneHourLater = original.addDuration(new Duration(1, 0, 1, 0, 0)); + return new StartOfHour(oneHourLater).value(); + }); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextMonth.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextMonth.java new file mode 100644 index 000000000..7a780632e --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextMonth.java @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import org.dmfs.jems.single.decorators.DelegatingSingle; +import org.dmfs.rfc5545.DateTime; + + +/** + * @author Gabor Keszthelyi + */ +// TODO Test +public final class StartOfNextMonth extends DelegatingSingle +{ + public StartOfNextMonth(DateTime original) + { + super(() -> + { + int month = original.getMonth(); + int year = original.getYear(); + if (month == 12) + { + month = 1; + year++; + } + return new DateTime( + original.getTimeZone(), + year, + month, + 1, + 0, 0, 0).toAllDay(); + }); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextYear.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextYear.java new file mode 100644 index 000000000..bfc584a6f --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/StartOfNextYear.java @@ -0,0 +1,39 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import org.dmfs.jems.single.decorators.DelegatingSingle; +import org.dmfs.rfc5545.DateTime; + + +/** + * @author Gabor Keszthelyi + */ + +public final class StartOfNextYear extends DelegatingSingle +{ + public StartOfNextYear(DateTime original) + { + super(() -> + new DateTime( + original.getTimeZone(), + original.getYear() + 1, + 1, + 1, + 0, 0, 0).toAllDay()); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/TimeZones.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/TimeZones.java new file mode 100644 index 000000000..03243d5b1 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetime/general/TimeZones.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018 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.opentaskspal.datetime.general; + +import java.util.TimeZone; + + +/** + * @author Gabor Keszthelyi + */ +public final class TimeZones +{ + public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + + + private TimeZones() + { + + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DateTimeFields.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DateTimeFields.java new file mode 100644 index 000000000..bb219af74 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DateTimeFields.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 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.opentaskspal.datetimefields; + +import android.support.annotation.Nullable; + + +/** + * The 3 values that together represent a start/due date-time in the provider. + * Corresponds one to one to the actual values in the database. + * + * @author Gabor Keszthelyi + */ +public interface DateTimeFields +{ + + /** + * The timestamp. ({@code null} if it's empty) + */ + @Nullable + Long timestamp(); + + /** + * Time zone id. ({@code null} if it's empty) + */ + @Nullable + String timeZoneId(); + + /** + * All-day flag of the date-time. 1 for true, 0 for false. ({@code null} if it's empty) + */ + @Nullable + Long isAllDay(); + +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DelegatingDateTimeFields.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DelegatingDateTimeFields.java new file mode 100644 index 000000000..fa7b6ab1b --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/DelegatingDateTimeFields.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 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.opentaskspal.datetimefields; + +import android.support.annotation.Nullable; + + +/** + * @author Gabor Keszthelyi + */ +public abstract class DelegatingDateTimeFields implements DateTimeFields +{ + private final DateTimeFields mDateTimeFields; + + + protected DelegatingDateTimeFields(DateTimeFields delegate) + { + mDateTimeFields = delegate; + } + + + @Nullable + @Override + public final Long timestamp() + { + return null; + } + + + @Nullable + @Override + public final String timeZoneId() + { + return null; + } + + + @Nullable + @Override + public final Long isAllDay() + { + return null; + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/Evaluated.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/Evaluated.java new file mode 100644 index 000000000..2690506e3 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/datetimefields/Evaluated.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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.opentaskspal.datetimefields; + +import android.support.annotation.Nullable; + + +/** + * @author Gabor Keszthelyi + */ +public final class Evaluated implements DateTimeFields +{ + private final Long mTimestamp; + private final String mTimeZoneId; + private final Long mIsAllDay; + + + public Evaluated(DateTimeFields delegate) + { + mTimestamp = delegate.timestamp(); + mTimeZoneId = delegate.timeZoneId(); + mIsAllDay = delegate.isAllDay(); + } + + + @Nullable + @Override + public Long timestamp() + { + return mTimestamp; + } + + + @Nullable + @Override + public String timeZoneId() + { + return mTimeZoneId; + } + + + @Nullable + @Override + public Long isAllDay() + { + return mIsAllDay; + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/jems/procedure/Procedure.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/jems/procedure/Procedure.java new file mode 100644 index 000000000..df8178b29 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/jems/procedure/Procedure.java @@ -0,0 +1,28 @@ +/* + * Copyright 2018 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.opentaskspal.jems.procedure; + +/** + * @author Gabor Keszthelyi + * @deprecated use it from jems when available + */ +@Deprecated +public interface Procedure +{ + void process(T t); + +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveDueDate.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveDueDate.java index e5cf713c3..c6df33fd5 100644 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveDueDate.java +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveDueDate.java @@ -41,7 +41,7 @@ public final class EffectiveDueDate extends DelegatingOptional { public static final Projection PROJECTION = new Composite<>( new MultiProjection<>(Tasks.DUE, Tasks.DTSTART), - TaskDateTime.PROJECTION, + RowSnapshotComposedTaskDateTime.PROJECTION, TaskDuration.PROJECTION); @@ -49,9 +49,9 @@ public EffectiveDueDate(@NonNull RowDataSnapshot rowDataSnapshot) { super(new FirstPresent<>( new Seq<>( - new TaskDateTime(Tasks.DUE, rowDataSnapshot), + new RowSnapshotComposedTaskDateTime(Tasks.DUE, rowDataSnapshot), new Zipped<>( - new TaskDateTime(Tasks.DTSTART, rowDataSnapshot), + new RowSnapshotComposedTaskDateTime(Tasks.DTSTART, rowDataSnapshot), new TaskDuration(rowDataSnapshot), DateTime::addDuration)))); } diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskDateTime.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/RowSnapshotComposedTaskDateTime.java similarity index 69% rename from opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskDateTime.java rename to opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/RowSnapshotComposedTaskDateTime.java index 2b5879b97..f56eee73c 100644 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskDateTime.java +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/RowSnapshotComposedTaskDateTime.java @@ -22,9 +22,9 @@ import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.android.contentpal.projections.Composite; import org.dmfs.android.contentpal.projections.SingleColProjection; +import org.dmfs.opentaskspal.datetime.OptionalCombinedDateTime; import org.dmfs.optional.Optional; import org.dmfs.optional.decorators.DelegatingOptional; -import org.dmfs.optional.decorators.Mapped; import org.dmfs.rfc5545.DateTime; import org.dmfs.tasks.contract.TaskContract.Tasks; @@ -35,21 +35,19 @@ * @author Marten Gajda * @author Gabor Keszthelyi */ -public final class TaskDateTime extends DelegatingOptional +public final class RowSnapshotComposedTaskDateTime extends DelegatingOptional { public static final Projection PROJECTION = new Composite<>( new SingleColProjection<>(Tasks.IS_ALLDAY), - EffectiveTimezone.PROJECTION); + TaskTimezone.PROJECTION); - public TaskDateTime(@NonNull String columnName, @NonNull final RowDataSnapshot rowData) + public RowSnapshotComposedTaskDateTime(@NonNull String columnName, @NonNull final RowDataSnapshot rowData) { - super(new Mapped<>( - - (Long timeStamp) -> rowData.data(Tasks.IS_ALLDAY, "1"::equals).value(false) ? - new DateTime(timeStamp).toAllDay() : - new DateTime(timeStamp).shiftTimeZone(new EffectiveTimezone(rowData).value()), - - rowData.data(columnName, Long::valueOf))); + super(new OptionalCombinedDateTime( + rowData.data(columnName, Long::valueOf), + new TaskTimezone(rowData), + () -> rowData.data(Tasks.IS_ALLDAY, "1"::equals).value(false) + )); } } diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveTimezone.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskTimezone.java similarity index 67% rename from opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveTimezone.java rename to opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskTimezone.java index 502a50a1f..80bb65ff8 100644 --- a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/EffectiveTimezone.java +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/TaskTimezone.java @@ -21,28 +21,27 @@ import org.dmfs.android.contentpal.Projection; import org.dmfs.android.contentpal.RowDataSnapshot; import org.dmfs.android.contentpal.projections.SingleColProjection; -import org.dmfs.jems.single.Single; -import org.dmfs.jems.single.decorators.DelegatingSingle; -import org.dmfs.jems.single.elementary.ValueSingle; +import org.dmfs.optional.Optional; +import org.dmfs.optional.decorators.DelegatingOptional; import org.dmfs.tasks.contract.TaskContract.Tasks; import java.util.TimeZone; /** - * The {@link Single} effective {@link TimeZone} of a task. If the task has no TimeZone, i.e. is floating, this will return the local {@link TimeZone}. + * {@link Optional} for the stored {@link TimeZone} of a task. * * @author Marten Gajda * @author Gabor Keszthelyi */ -public final class EffectiveTimezone extends DelegatingSingle +public final class TaskTimezone extends DelegatingOptional { public static final Projection PROJECTION = new SingleColProjection<>(Tasks.TZ); - public EffectiveTimezone(@NonNull RowDataSnapshot rowData) + public TaskTimezone(@NonNull RowDataSnapshot rowData) { - super(new ValueSingle<>(rowData.data(Tasks.TZ, TimeZone::getTimeZone).value())); + super(rowData.data(Tasks.TZ, TimeZone::getTimeZone)); } } diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/BooleanCursorColumnValue.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/BooleanCursorColumnValue.java new file mode 100644 index 000000000..824612628 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/BooleanCursorColumnValue.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; + +import org.dmfs.jems.single.Single; + + +/** + * @author Gabor Keszthelyi + */ +public final class BooleanCursorColumnValue implements Single +{ + private final Cursor mCursor; + private final String mColumnName; + + + public BooleanCursorColumnValue(Cursor cursor, String columnName) + { + mCursor = cursor; + mColumnName = columnName; + } + + + @Override + public Boolean value() + { + return mCursor.getInt(mCursor.getColumnIndexOrThrow(mColumnName)) == 1; + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorCombinedDateTime.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorCombinedDateTime.java new file mode 100644 index 000000000..d4da4201c --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorCombinedDateTime.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; +import android.support.annotation.NonNull; + +import org.dmfs.opentaskspal.datetime.OptionalCombinedDateTime; +import org.dmfs.optional.decorators.DelegatingOptional; +import org.dmfs.rfc5545.DateTime; + + +/** + * {@link DateTime} value for a task field, taken from a {@link Cursor}. + * + * @author Gabor Keszthelyi + */ +public final class CursorCombinedDateTime extends DelegatingOptional +{ + public CursorCombinedDateTime(@NonNull Cursor cursor, + @NonNull String timestampColumn, + @NonNull String timeZoneColumn, + @NonNull String isAllDayColumn) + { + super(new OptionalCombinedDateTime(new CursorDateTimeFields(cursor, timestampColumn, timeZoneColumn, isAllDayColumn))); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorDateTimeFields.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorDateTimeFields.java new file mode 100644 index 000000000..eb58091ea --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/CursorDateTimeFields.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import org.dmfs.opentaskspal.datetimefields.DateTimeFields; +import org.dmfs.opentaskspal.datetimefields.DelegatingDateTimeFields; +import org.dmfs.opentaskspal.datetimefields.Evaluated; + + +/** + * @author Gabor Keszthelyi + */ +public final class CursorDateTimeFields extends DelegatingDateTimeFields +{ + public CursorDateTimeFields(@NonNull Cursor cursor, + @NonNull String timestampColumn, + @NonNull String timeZoneColumn, + @NonNull String isAllDayColumn) + { + super(new Evaluated(new DeferringCursorDateTimeFields(cursor, timestampColumn, timeZoneColumn, isAllDayColumn))); + } + + + private static final class DeferringCursorDateTimeFields implements DateTimeFields + { + private final Cursor mCursor; + private final String mTimestampColumn; + private final String mTimeZoneColumn; + private final String mIsAllDayColumn; + + + private DeferringCursorDateTimeFields(@NonNull Cursor cursor, + @NonNull String timestampColumn, + @NonNull String timeZoneColumn, + @NonNull String isAllDayColumn) + { + mCursor = cursor; + mTimestampColumn = timestampColumn; + mTimeZoneColumn = timeZoneColumn; + mIsAllDayColumn = isAllDayColumn; + } + + + @Nullable + @Override + public Long timestamp() + { + return new LongCursorColumnValue(mCursor, mTimestampColumn).value(null); + } + + + @Nullable + @Override + public String timeZoneId() + { + return new StringCursorColumnValue(mCursor, mTimeZoneColumn).value(null); + } + + + @Nullable + @Override + public Long isAllDay() + { + return new LongCursorColumnValue(mCursor, mIsAllDayColumn).value(null); + } + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/LongCursorColumnValue.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/LongCursorColumnValue.java new file mode 100644 index 000000000..d719836c0 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/LongCursorColumnValue.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; + +import org.dmfs.optional.decorators.DelegatingOptional; + + +/** + * @author Gabor Keszthelyi + */ +public final class LongCursorColumnValue extends DelegatingOptional +{ + public LongCursorColumnValue(Cursor cursor, String columnName) + { + super(new NullSafeCursorColumnValue<>(cursor, columnName, Cursor::getLong)); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/NullSafeCursorColumnValue.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/NullSafeCursorColumnValue.java new file mode 100644 index 000000000..a284fe19c --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/NullSafeCursorColumnValue.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; +import android.support.annotation.NonNull; + +import org.dmfs.jems.function.BiFunction; +import org.dmfs.optional.Optional; + +import java.util.NoSuchElementException; + + +/** + * @author Gabor Keszthelyi + */ +// TODO Should these Cursor adapters go to different module? +public final class NullSafeCursorColumnValue implements Optional +{ + private final Cursor mCursor; + private final String mColumnName; + private final BiFunction mGetFunction; + + private Integer mCachedColumnIndex; + + + public NullSafeCursorColumnValue(@NonNull Cursor cursor, + @NonNull String columnName, + @NonNull BiFunction getFunction) + { + mCursor = cursor; + mColumnName = columnName; + mGetFunction = getFunction; + } + + + @Override + public boolean isPresent() + { + return !mCursor.isNull(cachedColumnIndex()); + } + + + @Override + public T value(T defaultValue) + { + return isPresent() ? value() : defaultValue; + } + + + @Override + public T value() throws NoSuchElementException + { + return mGetFunction.value(mCursor, cachedColumnIndex()); + } + + + private Integer cachedColumnIndex() + { + if (mCachedColumnIndex == null) + { + mCachedColumnIndex = mCursor.getColumnIndexOrThrow(mColumnName); + } + return mCachedColumnIndex; + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/StringCursorColumnValue.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/StringCursorColumnValue.java new file mode 100644 index 000000000..4ed9c97b1 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/cursor/StringCursorColumnValue.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.cursor; + +import android.database.Cursor; + +import org.dmfs.optional.decorators.DelegatingOptional; + + +/** + * @author Gabor Keszthelyi + */ +public final class StringCursorColumnValue extends DelegatingOptional +{ + public StringCursorColumnValue(Cursor cursor, String columnName) + { + super(new NullSafeCursorColumnValue<>(cursor, columnName, Cursor::getString)); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/LongBinaryBoolean.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/LongBinaryBoolean.java new file mode 100644 index 000000000..1621f93c7 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/LongBinaryBoolean.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.utils; + +import android.support.annotation.Nullable; + +import org.dmfs.jems.single.Single; + + +/** + * @author Gabor Keszthelyi + */ +public final class LongBinaryBoolean implements Single +{ + private final Long mLongValue; + + + public LongBinaryBoolean(@Nullable Long longValue) + { + mLongValue = longValue; + } + + + @Override + public Boolean value() + { + return Long.valueOf(1).equals(mLongValue); + } +} diff --git a/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/StringBinaryBoolean.java b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/StringBinaryBoolean.java new file mode 100644 index 000000000..acc1cda14 --- /dev/null +++ b/opentaskspal/src/main/java/org/dmfs/opentaskspal/readdata/utils/StringBinaryBoolean.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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.opentaskspal.readdata.utils; + +import android.support.annotation.Nullable; + +import org.dmfs.jems.single.Single; + + +/** + * @author Gabor Keszthelyi + */ +public final class StringBinaryBoolean implements Single +{ + @Nullable + private final String mStringValue; + + + public StringBinaryBoolean(@Nullable String stringValue) + { + mStringValue = stringValue; + } + + + @Override + public Boolean value() + { + return "1".equals(mStringValue); + } +} diff --git a/opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/TaskDateTimeTest.java b/opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/RowSnapshotCombinedDateTimeTest.java similarity index 83% rename from opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/TaskDateTimeTest.java rename to opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/RowSnapshotCombinedDateTimeTest.java index 699de6a3d..53eef6f56 100644 --- a/opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/TaskDateTimeTest.java +++ b/opentaskspal/src/test/java/org/dmfs/opentaskspal/readdata/RowSnapshotCombinedDateTimeTest.java @@ -37,11 +37,11 @@ /** - * Unit test for {@link TaskDateTime}. + * Unit test for {@link RowSnapshotComposedTaskDateTime}. * * @author Gabor Keszthelyi */ -public final class TaskDateTimeTest +public final class RowSnapshotCombinedDateTimeTest { @Test @@ -49,8 +49,9 @@ public void test_whenColumnValueIsAbsent_shouldBeAbsent() { RowDataSnapshot mockData = failingMock(RowDataSnapshot.class); doReturn(absent()).when(mockData).data(eq(Tasks.DTSTART), any()); + doReturn(absent()).when(mockData).data(eq(Tasks.TZ), any()); - assertThat(new TaskDateTime(Tasks.DTSTART, mockData), AbsentMatcher.isAbsent()); + assertThat(new RowSnapshotComposedTaskDateTime(Tasks.DTSTART, mockData), AbsentMatcher.isAbsent()); } @@ -62,8 +63,9 @@ public void test_whenIsAllDayIsPresentAndTrue_shouldReturnAllDayDateTime() RowDataSnapshot mockData = failingMock(RowDataSnapshot.class); doReturn(new Present<>(timeStamp)).when(mockData).data(eq(Tasks.DTSTART), any()); doReturn(new Present<>(true)).when(mockData).data(eq(Tasks.IS_ALLDAY), any()); + doReturn(new Present<>(TimeZone.getTimeZone("UTC"))).when(mockData).data(eq(Tasks.TZ), any()); - DateTime actual = new TaskDateTime(Tasks.DTSTART, mockData).value(); + DateTime actual = new RowSnapshotComposedTaskDateTime(Tasks.DTSTART, mockData).value(); assertTrue(actual.isAllDay()); assertEquals(new DateTime(timeStamp).toAllDay(), actual); } @@ -79,7 +81,7 @@ public void test_whenIsAllDayIsPresentAndFalse_shouldReturnNotAllDayDateTime() doReturn(new Present<>(false)).when(mockData).data(eq(Tasks.IS_ALLDAY), any()); doReturn(new Present<>(TimeZone.getTimeZone("UTC"))).when(mockData).data(eq(Tasks.TZ), any()); - DateTime actual = new TaskDateTime(Tasks.DTSTART, mockData).value(); + DateTime actual = new RowSnapshotComposedTaskDateTime(Tasks.DTSTART, mockData).value(); assertFalse(actual.isAllDay()); assertEquals(timeStamp, actual.getTimestamp()); } @@ -95,7 +97,7 @@ public void test_whenIsAllDayIsFalse_shouldReturnDateTimeWithTimeZoneShifted() doReturn(new Present<>(false)).when(mockData).data(eq(Tasks.IS_ALLDAY), any()); doReturn(new Present<>(TimeZone.getTimeZone("Europe/Berlin"))).when(mockData).data(eq(Tasks.TZ), any()); - DateTime actual = new TaskDateTime(Tasks.DTSTART, mockData).value(); + DateTime actual = new RowSnapshotComposedTaskDateTime(Tasks.DTSTART, mockData).value(); assertFalse(actual.isAllDay()); assertEquals(timeStamp, actual.getTimestamp()); assertEquals("Europe/Berlin", actual.getTimeZone().getID());