Skip to content

Commit 87ef4d3

Browse files
author
Gabor Keszthelyi
committed
Add provider operations data auto-correction to set timezone to null for all-day tasks. #601
1 parent 3c3781b commit 87ef4d3

File tree

11 files changed

+273
-10
lines changed

11 files changed

+273
-10
lines changed

.idea/dictionaries/dictionary.xml

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ buildscript {
66
}
77
dependencies {
88
classpath 'com.android.tools.build:gradle:3.0.1'
9+
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:' + KOTLIN_VERSION
910
}
1011
}
1112

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ BUILD_TOOLS_VERSION=27.0.1
33
MIN_SDK_VERSION=15
44
TARGET_SDK_VERSION=25
55
# dependency versions
6+
KOTLIN_VERSION=1.2.10
67
SUPPORT_LIBRARY_VERSION=25.4.0
78
# contentpal 9b087b2 -> 2017-12-12
89
CONTENTPAL_VERSION=9b087b2

opentasks-contract/src/main/java/org/dmfs/tasks/contract/TaskContract.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -660,14 +660,15 @@ public interface TaskColumns
660660

661661
/**
662662
* When this task starts in milliseconds since the epoch.
663+
* {@link #IS_ALLDAY} and {@link #TZ} have to be considered together with this timestamp create the date-time.
663664
* <p>
664665
* Value: Long
665-
* </p>
666666
*/
667667
String DTSTART = "dtstart";
668668

669669
/**
670670
* Boolean: flag that indicates that this is an all-day task.
671+
* This has to be considered to get the date-time for the {@link #DTSTART} or {@link #DUE} timestamps.
671672
*/
672673
String IS_ALLDAY = "is_allday";
673674

@@ -688,21 +689,25 @@ public interface TaskColumns
688689
String LAST_MODIFIED = "last_modified";
689690

690691
/**
691-
* String: An Olson Id of the time zone of this task. If this value is <code>null</code>, it's automatically replaced by the local time zone.
692+
* An Olson Id of the time zone of this task.
693+
* This has to be considered together with {@link #IS_ALLDAY} to get the date-time for the {@link #DTSTART} or {@link #DUE} timestamps.
694+
* It is {@code null} when {@link #IS_ALLDAY} is true, or when the date-time is a floating time (local time without timezone).
695+
* <p>
696+
* Value: String
692697
*/
693698
String TZ = "tz";
694699

695700
/**
696-
* When this task is due in milliseconds since the epoch. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task
701+
* When this task is due in milliseconds since the epoch. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or neither if the task
697702
* has no due date).
703+
* {@link #IS_ALLDAY} and {@link #TZ} have to be considered together with this timestamp to create the date-time.
698704
* <p>
699705
* Value: Long
700-
* </p>
701706
*/
702707
String DUE = "due";
703708

704709
/**
705-
* The duration of this task. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task has no due date). Setting a
710+
* The duration of this task. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or neither if the task has no due date). Setting a
706711
* {@link #DURATION} is not allowed when {@link #DTSTART} is <code>null</code>. The Value must be a duration string as in <a
707712
* href="http://tools.ietf.org/html/rfc5545#section-3.3.6">RFC 5545 Section 3.3.6</a>.
708713
* <p>

opentasks-provider/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
apply plugin: 'com.android.library'
2+
apply plugin: 'kotlin-android'
23

34
android {
45
compileSdkVersion COMPILE_SDK_VERSION.toInteger()
@@ -41,6 +42,8 @@ dependencies {
4142
androidTestImplementation 'com.android.support:support-annotations:' + SUPPORT_LIBRARY_VERSION
4243
androidTestImplementation 'com.android.support.test:runner:0.5'
4344
androidTestImplementation 'com.android.support.test:rules:0.5'
45+
46+
testImplementation 'org.jetbrains.anko:anko-sqlite:0.10.4'
4447
testImplementation 'org.robolectric:robolectric:' + ROBOLECTRIC_VERSION
4548
testImplementation 'junit:junit:4.12'
4649
testImplementation 'org.mockito:mockito-core:2.10.0'
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright 2018 dmfs GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.dmfs.provider.tasks;
18+
19+
import android.content.ContentProviderClient;
20+
import android.content.ContentResolver;
21+
import android.content.Context;
22+
import android.os.Build;
23+
import android.support.test.InstrumentationRegistry;
24+
import android.support.test.runner.AndroidJUnit4;
25+
26+
import org.dmfs.android.contentpal.Operation;
27+
import org.dmfs.android.contentpal.OperationsQueue;
28+
import org.dmfs.android.contentpal.RowSnapshot;
29+
import org.dmfs.android.contentpal.operations.Assert;
30+
import org.dmfs.android.contentpal.operations.BulkDelete;
31+
import org.dmfs.android.contentpal.operations.Put;
32+
import org.dmfs.android.contentpal.queues.BasicOperationsQueue;
33+
import org.dmfs.android.contentpal.rowdata.CharSequenceRowData;
34+
import org.dmfs.android.contentpal.rowsnapshots.VirtualRowSnapshot;
35+
import org.dmfs.android.contenttestpal.operations.AssertEmptyTable;
36+
import org.dmfs.iterables.SingletonIterable;
37+
import org.dmfs.iterables.elementary.Seq;
38+
import org.dmfs.opentaskspal.tables.InstanceTable;
39+
import org.dmfs.opentaskspal.tables.LocalTaskListsTable;
40+
import org.dmfs.opentaskspal.tables.TaskListScoped;
41+
import org.dmfs.opentaskspal.tables.TaskListsTable;
42+
import org.dmfs.opentaskspal.tables.TasksTable;
43+
import org.dmfs.opentaskspal.tasklists.NameData;
44+
import org.dmfs.provider.tasks.processors.tasks.AutoCompleting;
45+
import org.dmfs.tasks.contract.TaskContract.TaskLists;
46+
import org.dmfs.tasks.contract.TaskContract.Tasks;
47+
import org.junit.After;
48+
import org.junit.Before;
49+
import org.junit.Test;
50+
import org.junit.runner.RunWith;
51+
52+
import static org.dmfs.android.contenttestpal.ContentMatcher.resultsIn;
53+
import static org.junit.Assert.assertThat;
54+
55+
56+
/**
57+
* Test cases for {@link TaskProvider} that check data auto-corrections/auto-completions done by provider for task insert/update.
58+
* <p>
59+
* Implementation is in {@link AutoCompleting}.
60+
*
61+
* @author Gabor Keszthelyi
62+
*/
63+
@RunWith(AndroidJUnit4.class)
64+
public class TaskProviderAutoCompletingTest
65+
{
66+
private ContentResolver mResolver;
67+
private String mAuthority;
68+
private Context mContext;
69+
private ContentProviderClient mClient;
70+
71+
72+
@Before
73+
public void setUp() throws Exception
74+
{
75+
mContext = InstrumentationRegistry.getTargetContext();
76+
mResolver = mContext.getContentResolver();
77+
mAuthority = AuthorityUtil.taskAuthority(mContext);
78+
mClient = mContext.getContentResolver().acquireContentProviderClient(mAuthority);
79+
80+
// Assert that tables are empty:
81+
OperationsQueue queue = new BasicOperationsQueue(mClient);
82+
queue.enqueue(new Seq<Operation<?>>(
83+
new AssertEmptyTable<>(new TasksTable(mAuthority)),
84+
new AssertEmptyTable<>(new TaskListsTable(mAuthority)),
85+
new AssertEmptyTable<>(new InstanceTable(mAuthority))));
86+
queue.flush();
87+
}
88+
89+
90+
@After
91+
public void tearDown() throws Exception
92+
{
93+
/*
94+
TODO When Test Orchestration is available, there will be no need for clean up here and check in setUp(), every test method will run in separate instrumentation
95+
https://android-developers.googleblog.com/2017/07/android-testing-support-library-10-is.html
96+
https://developer.android.com/training/testing/junit-runner.html#using-android-test-orchestrator
97+
*/
98+
99+
// Clear the DB:
100+
BasicOperationsQueue queue = new BasicOperationsQueue(mClient);
101+
queue.enqueue(new SingletonIterable<Operation<?>>(new BulkDelete<>(new LocalTaskListsTable(mAuthority))));
102+
queue.flush();
103+
104+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
105+
{
106+
mClient.close();
107+
}
108+
else
109+
{
110+
mClient.release();
111+
}
112+
}
113+
114+
115+
/**
116+
* Test that timezone for all-day tasks are set to null.
117+
*/
118+
@Test
119+
public void testInsertTaskWithAllDayAndTimeZone()
120+
{
121+
RowSnapshot<TaskLists> taskList = new VirtualRowSnapshot<>(new LocalTaskListsTable(mAuthority));
122+
RowSnapshot<Tasks> allDayTask = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
123+
RowSnapshot<Tasks> notAllDayTask = new VirtualRowSnapshot<>(new TaskListScoped(taskList, new TasksTable(mAuthority)));
124+
125+
long start = System.currentTimeMillis();
126+
127+
assertThat(new Seq<>(
128+
new Put<>(taskList, new NameData("list1")),
129+
new Put<>(allDayTask, (transactionContext, builder) -> builder
130+
.withValue(Tasks.DTSTART, start)
131+
.withValue(Tasks.TZ, "Europe/Berlin")
132+
.withValue(Tasks.IS_ALLDAY, 1)
133+
),
134+
new Put<>(notAllDayTask, (transactionContext, builder) -> builder
135+
.withValue(Tasks.DTSTART, start)
136+
.withValue(Tasks.TZ, "Europe/Paris")
137+
.withValue(Tasks.IS_ALLDAY, 0)
138+
)
139+
140+
), resultsIn(mClient,
141+
new Assert<>(taskList, new NameData("list1")),
142+
new Assert<>(allDayTask, new CharSequenceRowData<>(Tasks.TZ, null)),
143+
new Assert<>(notAllDayTask, new CharSequenceRowData<>(Tasks.TZ, "Europe/Paris"))
144+
));
145+
}
146+
147+
}

opentasks-provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public interface OnDatabaseOperationListener
6060
/**
6161
* The database version.
6262
*/
63-
private static final int DATABASE_VERSION = 19;
63+
private static final int DATABASE_VERSION = 20;
6464

6565

6666
/**
@@ -811,6 +811,11 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
811811
db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
812812
}
813813

814+
if (oldVersion < 20)
815+
{
816+
db.execSQL("UPDATE " + Tables.TASKS + " SET " + Tasks.TZ + " = NULL WHERE " + Tasks.IS_ALLDAY + " = 1");
817+
}
818+
814819
// upgrade FTS
815820
FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion);
816821

opentasks-provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,11 @@ else if (!isSyncAdapter)
222222
task.set(TaskAdapter.COMPLETED, null);
223223
}
224224
}
225+
226+
// Set timezone to null for all-day date-times
227+
if (task.isUpdated(TaskAdapter.IS_ALLDAY) && task.valueOf(TaskAdapter.IS_ALLDAY))
228+
{
229+
task.set(TaskAdapter.TIMEZONE_RAW, null);
230+
}
225231
}
226232
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/*
2+
* Copyright 2017 dmfs GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.dmfs.provider.tasks
18+
19+
20+
import android.database.sqlite.SQLiteOpenHelper
21+
import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables.TASKS
22+
import org.dmfs.tasks.contract.TaskContract.Tasks.*
23+
import org.hamcrest.Matchers.equalTo
24+
import org.hamcrest.Matchers.not
25+
import org.jetbrains.anko.db.MapRowParser
26+
import org.jetbrains.anko.db.insert
27+
import org.jetbrains.anko.db.select
28+
import org.junit.Assert.assertThat
29+
import org.junit.Test
30+
import org.junit.runner.RunWith
31+
import org.robolectric.RobolectricTestRunner
32+
import org.robolectric.RuntimeEnvironment
33+
import org.robolectric.annotation.Config
34+
35+
/**
36+
* Test for checking the SQLite database upgrades.
37+
*
38+
* @author Gabor Keszthelyi
39+
*/
40+
@RunWith(RobolectricTestRunner::class)
41+
@Config(manifest = Config.NONE)
42+
class TaskDataBaseUpgradeTest {
43+
44+
45+
/**
46+
* On upgrading to version 20 the timezones of tasks with [IS_ALLDAY] true have to be replaced with `null`.
47+
*/
48+
@Test
49+
fun test_20_allDayTimeZonesToNull() {
50+
51+
val dbHelper: SQLiteOpenHelper = TaskDatabaseHelper(RuntimeEnvironment.application, null)
52+
53+
val expectedTimeZones: Map<Long, String?> = dbHelper.writableDatabase.use {
54+
55+
val lId = LIST_ID to 42 // LIST_ID is a required column in the table
56+
57+
mapOf(
58+
it.insert(TASKS, IS_ALLDAY to "1", TZ to "UTC", lId) to null,
59+
60+
it.insert(TASKS, IS_ALLDAY to "1", TZ to "Europe/Berlin", lId) to null,
61+
62+
it.insert(TASKS, IS_ALLDAY to "1", TZ to null, lId) to null,
63+
64+
it.insert(TASKS, IS_ALLDAY to "0", TZ to "Europe/Paris", lId) to "Europe/Paris",
65+
66+
it.insert(TASKS, IS_ALLDAY to null, TZ to "America/Chicago", lId) to "America/Chicago",
67+
68+
it.insert(TASKS, IS_ALLDAY to null, TZ to null, lId) to null
69+
)
70+
}
71+
72+
dbHelper.writableDatabase.use {
73+
dbHelper.onUpgrade(it, 19, 20)
74+
}
75+
76+
dbHelper.readableDatabase.use {
77+
val size = it.select(TASKS)
78+
.columns(_ID, TZ)
79+
.parseList(object : MapRowParser<String> {
80+
81+
override fun parseRow(columns: Map<String, Any?>): String {
82+
83+
// Assert that the actual and expected time zones are the same:
84+
assertThat(expectedTimeZones[columns[_ID]], equalTo(columns[TZ]))
85+
86+
return "ignored";
87+
}
88+
}).size
89+
90+
assertThat(size, not(0)) // Just to make sure that there were rows actually
91+
}
92+
}
93+
}

opentaskspal/src/main/java/org/dmfs/opentaskspal/tasks/TimeData.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ private static ContentProviderOperation.Builder doUpdateBuilder(DateTime start,
9393
{
9494
return builder
9595
.withValue(TaskContract.Tasks.DTSTART, start.getTimestamp())
96-
.withValue(TaskContract.Tasks.TZ, start.isAllDay() ? "UTC" : start.getTimeZone().getID())
96+
.withValue(TaskContract.Tasks.TZ, start.isAllDay() ? null : start.getTimeZone().getID())
9797
.withValue(TaskContract.Tasks.IS_ALLDAY, start.isAllDay() ? 1 : 0)
9898

9999
.withValue(TaskContract.Tasks.DUE, due.isPresent() ? due.value().getTimestamp() : null)

0 commit comments

Comments
 (0)