Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenTaskPal module (ContentPal extension for TaskProvider). #421 #422

Merged
merged 23 commits into from
Oct 5, 2017

Conversation

lemonboston
Copy link
Contributor

Note that these are only the classes and constructors that were needed for the test cases, so this is not a complete ContentPal support for TaskProvider at the moment. I think we can extend it later, for example when test coverage is improved, since test and opentaskpal will be in same project so both can be extended in the same commits.

Also note some TODOs are pending on ContentPal PRs.

@lemonboston
Copy link
Contributor Author

Missed to create NoTimeData that clears start, due, duration, added it now.

Copy link
Owner

@dmfs dmfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first pass done.

{
public DueData(@NonNull Long dueTimeStamp)
{
super(new SimpleRowData<TaskContract.Tasks>(TaskContract.Tasks.DUE, dueTimeStamp));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also set START and DURATION to null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the need to set DURATION to null, but wouldn't this class be used also to just update DUE, keeping START? Or for that with need to use TimeData and retrieve START first?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if this changes the all-day flag? Start would become invalid in that case. I'd say that this can not be used to update only due.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(addressed)

.withValue(TaskContract.Tasks.DTSTART, null)
.withValue(TaskContract.Tasks.DUE, null)
.withValue(TaskContract.Tasks.DURATION, null);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A task without times is currently not allowed to recur, so RDATE, RRULE and EXDATE should also be set to null.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also wipe out all-day and time zone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

*/
public final class DueData extends DelegatingRowData<TaskContract.Tasks>
{
public DueData(@NonNull Long dueTimeStamp)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Date-time values should be typed DateTime.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(addressed)

*
* @author Gabor Keszthelyi
*/
public final class TimeZoneData implements RowData<TaskContract.Tasks>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting time zone alone doesn't make sense. Since it's part of DateTime it should be set by the other date & time RowData implementations.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally OpenTasksPal provides high-level access to the content provider. Check out SingleEventData in CalendarPal it takes DateTimes and automatically sets timestamps, time zones and all-day flags. Loading or setting a time zone or an all-day flag without loading or setting the timestamp too doesn't make much sense and is considered error prone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is only one timezone and all-day flag for tasks, I assume start and due with different timezone should be rejected, right? What about the all-day flag? Take it only from start, or validate if its the same for start and due?
Does it make sense to set all-day flag in DueTime from due?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No different time zones should not be rejected. Actually we should update the provider to support this case. For now just shift start to the due time zone.
But start and due must have the same all-day flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(addressed)

@lemonboston
Copy link
Contributor Author

Updated the time RowDatas, please check if they make sense. I would write unit test afterwards.
Also, there is a possibility to use that Equalator interface for the time zone comparison, is it okay to add it here now? (It could go to jems later.)

@lemonboston
Copy link
Contributor Author

Updated to latest contentpal master and removed all related files from here.

}

DateTime start = mStart;
if (mDue != null && !TimeZoneEqualator.INSTANCE.areEqual(mStart, mDue))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for this Equalator. DateTime is smart enough to return the same instance if the timezone is considered to be the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean that there is no need to compare the getId() String value but use reference equality on the getTimeZone() TimeZones? Like if (timeZone1 == timeZone2)?
Don't we depend on some undocumented internals of DateTime this way?
I've updated the code, removed the Equalator and added a comment about this behavior, but I think it would be clearer to check the getId() values. TimeZone doesn't provide equals(), so we are really depending on reference equality here which doesn't seem to be safe.
I can do that in private method if you are not consent with that Equalator.
(Although it could be easier to test that code in isolation and ideally just mock it out in (upcoming) TimeZoneTest if it could be injected there, but it couldn't.. so it may not be important.)

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No that's not what I meant. I've added a new comment to the new code. The comment above was a bit misleading.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(addressed)

public ContentProviderOperation.Builder updatedBuilder(@NonNull TransactionContext transactionContext, @NonNull ContentProviderOperation.Builder builder)
{
return builder
.withValue(TaskContract.Tasks.DUE, mDue.getTimestamp())
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also set allday flag and time zone.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

.withValue(TaskContract.Tasks.DTSTART, null)
.withValue(TaskContract.Tasks.DURATION, null)

// A task without times is currently not allowed to recur:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but this one has a time!? The point is that we can't update due without updating recurrence because updating due usually also affects recurrence.
For now it's ok to null the recurrence columns because recurrence is not really supported yet. But the reason stated in the comment doesn't apply here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that was copied from Slack, but it probably meant 'recurrence times' originally, should have added that... anyway, I've removed the comment.

*/
public final class OriginalInstanceSyncIdData extends DelegatingRowData<TaskContract.Tasks>
{
// TODO Should this be @Nullable?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No because you should not be allowed to clear this once it has been created.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed the TODO.

*/
public final class SyncIdData extends DelegatingRowData<TaskContract.Tasks>
{
// TODO Should this be @Nullable?
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, removed the TODO.


.withValue(TaskContract.Tasks.DURATION, duration)

// A task without times is currently not allowed to recur:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this task has times.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the comment.

*/
public final class TitleData extends DelegatingRowData<TaskContract.Tasks>
{
public TitleData(@Nullable CharSequence title)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title of a task should not be allowed to be null. You may set it to an empty string, but null doesn't make sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, changed to @NonNull.

@lemonboston
Copy link
Contributor Author

I see, thanks, I've updated the code. Although, I think we still depend a bit on the knowledge of DateTime internals, for example that as I see it ignores null input, which is good in this case.
But the upcoming unit test of TimeData will check these variations, so I guess it's okay.

@lemonboston
Copy link
Contributor Author

Waiting for the ContentProviderOperation.Builder/ RowData Matcher support now from contentpal to write the unit test(s), otherwise ready for review again (all comments addressed).

https://github.com/dmfs/ContentPal/pull/51
https://github.com/dmfs/ContentPal/pull/52
*/
new EqArg(TaskContract.Tasks.LIST_ID, taskListRow.values().charData(BaseColumns._ID).value("-1"))));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll wait for dmfs/ContentPal#75 to implement this. The current approach is not reliable enough.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#75 has been merged.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've replaced this with the ReferringTo(Predicate).

* A view onto the {@link TaskContract.Tasks} table which contains only tasks from a specific task list.
* Tasks created with {@link #insertOperation(UriParams)} will automatically be added to this task list.
* <p>
* Note, if you create a {@link View} (using {@link #view(ContentProviderClient, String...)}) with a virtual {@link RowSnapshot}, the {@link View} will always
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: I've added dmfs/ContentPal#81 to address this.

@Override
public ContentProviderOperation.Builder updatedBuilder(@NonNull TransactionContext transactionContext, @NonNull ContentProviderOperation.Builder builder)
{
return builder.withValue(TaskContract.TaskLists.LIST_NAME, mName.toString());
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not delegate to a CharSequenceRowData here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CharSequenceRowData allows null, so NameData wouldn't fail with that. Maybe we need another class for that. ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm.. I am not sure of the correct approach here actually. We don't validate for null just use the @Nullable @NonNull annotations. Should that be enough in these cases? I mean if there wasn't a .toString() call here, it wouldn't fail either without a validation.
Similar question come up with OriginalInstanceSyncIdData below.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apparently we need another delegate which doesn't allow null. Or make sure CharSequenceRowData doesn't allow null and find another, better solution to "erase" values in the database.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes back to the topic of whether to delete or leave a value alone when the incoming update doesn't have it (which may be represented as null). Because that's what makes using Optional instead of the nulls ambiguous as you mentioned before.
I don't know the actual usage in syncadapters.. can the operations maybe be distinguished based on whether they are in 'override' mode or 'fill in the missing data' mode?
If so, could this maybe be a flag? So we could either have an extended Optional model with that flag or pass it in the constructor as separate parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the class with a TODO about this nullability.

*/
public final class OriginalInstanceSyncIdData extends DelegatingRowData<TaskContract.Tasks>
{
public OriginalInstanceSyncIdData(@NonNull String originalInstanceSyncId)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OriginalInstanceSyncId could also be just a CharSequence. So you probably should delegate to CharSequenceRowData instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the class with a TODO about this nullability.

{
public SyncIdData(@NonNull String syncId)
{
super(new RawRowData<TaskContract.Tasks>(TaskContract.Tasks._SYNC_ID, syncId));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, could be a CharSequence

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the class with a TODO about this nullability.

}


public TimeData(@NonNull DateTime start, @NonNull String duration)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duration should be passed as a Duration object not as a String.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.

@NonNull
private final DateTime mStart;
@Nullable
private final DateTime mDue;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make them (mDue and mDuration) Optional instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I've changed it. We still use @Nullable at the ctor params? That's what I am not sure of, I don't see the end usage - is it going to be part of an 'automatic' syncronization mapping where it is good if 'null' can be passed on, or is the 'normal' usage, like how I would use the class from UI code is what we should aim for? In this latter case, I would put Optional in the constructor as well.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should not use null at all here.

it should look like this:

  private TimeData(@NonNull DateTime start, @NonNull Optional<DateTime> due, @NonNull Optional<Duration> duration)
    {
        mStart = start;
        mDue = due;
        mDuration = duration;
    }


    public TimeData(@NonNull DateTime start, @NonNull DateTime due)
    {
        this(start, new Present<>(due), absent());
    }

…

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, updated.

Copy link
Owner

@dmfs dmfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from a new nitpicks it looks ok.

@lemonboston
Copy link
Contributor Author

lemonboston commented Sep 25, 2017

@dmfs Could you please help a little with this?:
Can there be a nullpointer here?

withValue(TaskContract.Tasks.TZ, start.isAllDay() ? "UTC" : start.getTimeZone().getID())

I am not sure of the relationship between all day and the timezones and the ways DateTime can be constructed... can getTimeZone() return null here?

@lemonboston
Copy link
Contributor Author

About the DateTime timezone<->all-day flag question, I figured they are mutually exclusive, right?

I've update to latest contentpal and added unit tests. Let me know if I should squash.
The only outstanding TODOs are about the nullability, otherwise ready for review.

@dmfs
Copy link
Owner

dmfs commented Sep 27, 2017

Technically you could have a non-allday task without time zone. That's called floating time. But OpenTasks doesn't really support that so it's ok for now. Maybe create an issue to remind us about this.

Same goes for the nullable. We'll keep it like this and revisit later on when we may have a better idea of how to handle it.

@lemonboston
Copy link
Contributor Author

lemonboston commented Sep 28, 2017

Ok, I've created the tickets: #431, #432

Copy link
Owner

@dmfs dmfs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's go with this and see where it takes us. We still can adjust things if it turns out they don't work well.

@dmfs dmfs merged commit c6f10f2 into master Oct 5, 2017
@dmfs dmfs deleted the issues/421-opentaskpal branch October 6, 2017 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants