Commit 4030355e authored by bernhard's avatar bernhard

Merge remote-tracking branch 'upstream/master' into tasks

# Conflicts:
#	build.gradle
parents db22ee8b e04df070
Pipeline #30 canceled with stages
......@@ -9,9 +9,10 @@ cache:
test:
script:
- (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh)
- adb install src/androidTest/resources/org.dmfs.tasks_6880.apk
- ./gradlew check connectedCheck
# - (cd /sdk/emulator; ./emulator @test -no-audio -no-window & wait-for-emulator.sh)
# - adb install src/androidTest/resources/org.dmfs.tasks_6880.apk
# - ./gradlew check connectedCheck
- ./gradlew check
artifacts:
paths:
- build/outputs/lint-results-debug.html
......
buildscript {
ext.kotlin_version = '1.2.70'
ext.dokka_version = '0.9.16'
ext.kotlin_version = '1.3.11'
ext.dokka_version = '0.9.17'
repositories {
jcenter()
......@@ -30,22 +30,16 @@ ext {
android {
compileSdkVersion 28
buildToolsVersion '28.0.2'
buildToolsVersion '28.0.3'
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "String", "version_ical4j", "\"$ical4j_version\""
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
lintOptions {
disable 'AllowBackup'
disable 'InvalidPackage'
......@@ -66,8 +60,8 @@ dependencies {
api "org.mnode.ical4j:ical4j:$ical4j_version"
implementation 'org.slf4j:slf4j-jdk14:1.7.25'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
testImplementation 'junit:junit:4.12'
}
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
......@@ -512,7 +512,7 @@ public final class TaskContract
String DESCRIPTION = "description";
/**
* An URL for this task. Must be a valid URL if not <code>null</code>-
* The URL iCalendar field for this task. Must be a valid URI if not <code>null</code>-
* <p>
* Value: String
* </p>
......@@ -1036,27 +1036,80 @@ public final class TaskContract
String INSTANCE_DURATION = "instance_duration";
/**
* The start of the original instance as specified in the master task. For non-recurring task instances this equals the value of {@link
* #INSTANCE_START}, except that `null` values are represented as `0`.
* The start of the original instance as specified in the master task. For non-recurring task instances this is {@code null}.
* <p>
* For recurring tasks, these are the timestamps which have been derived from the recurrence rule or dates, except those specified as exdates.
*/
String INSTANCE_ORIGINAL_TIME = "instance_original_time";
/**
* The distance of the instance from the current one. For closed instances this is always {@code -1}, for the current instance this is {@code 0}. For
* the instance after the current one this is {@code 1}, for the instance after that one it's {@code 2}, etc..
* <p>
* Value: Integer
* <p>
* read-only
*/
String DISTANCE_FROM_CURRENT = "distance_from_current";
}
/**
* Instances of a task. At present this table is read only. Currently it contains exactly one entry per task (and task exception), so it's merely a copy of
* {@link Tasks}.
* A table containing one entry per task instance. This table is writable in order to allow modification of single instances of a task. Write operations to
* this table will be converted into operations on overrides and forwarded to the task table.
* <p>
* Note: The {@link #DTSTART}, {@link #DUE} values of instances of recurring tasks represent the actual instance values, i.e. they are different for each
* instance ({@link #DURATION} is always {@code null}).
* <p>
* Also, none of the instances are recurring themselves, so {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} are always {@code null}.
* <p>
* TODO: Insert all instances of recurring tasks.
* <p>
* The following operations are supported:
* <p>
* TODO: Insert all instances of recurring the tasks.
* </p>
* <h2>Insert</h2>
* <p>
* TODO: In later releases it's planned to provide a convenient interface to add, change or delete task instances via this URI.
* </p>
* Note, the data of an insert must not contain the fields {@link #RRULE}, {@link #RDATE} or {@link #EXDATE}. If the new instance belongs to an existing
* task the data must contain the fields {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME}. Also note, this table supports writing {@link
* #DURATION} (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null}
* {@link #DUE} date. Reading the task in the tasks table will, however, return the original {@link #DURATION}.
* <p>
* If there already is an instance (with or without override) for the given {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME} an exception
* is thrown.
* <p>
* <table> <tr><th>ORIGINAL_INSTANCE_ID value</th><th>Result</th></tr> <tr><td>absent or empty</td><td>A new non-recurring task is created with the given
* values.</td></tr> <tr><td>a valid {@link Tasks} row {@code _ID}</td><td>An {@link #RDATE} for the given {@link #ORIGINAL_INSTANCE_TIME} time is added to
* the given master task, any {@link #EXDATE} for this time is removed. The task is inserted as an override to the given master. No fields are inherited
* though. {@link #ORIGINAL_INSTANCE_ALLDAY} will be set to {@link #IS_ALLDAY} of the master.
* <p>
* Note, if the given master is non-recurring, this operation will turn it into a recurring task. </td></tr> <tr><td>invalid {@link Tasks} row {@code
* _ID}</td><td>An exception is thrown.</td></tr></table>
* <p>
* <h2>Update</h2>
* <p>
* Note, the data of an update must not contain any fields related to recurrence ({@link #RRULE}, {@link #RDATE}, {@link #EXDATE}, {@link
* #ORIGINAL_INSTANCE_ID}, {@link #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY}). Also note, this table supports writing {@link #DURATION}
* (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE}
* date. Reading the task in the tasks table will, however, return the original {@link #DURATION}.
* <p>
* <table> <tr><th>Target task type</th><th>Result</th></tr> <tr><td>Recurring master task</td><td>A new override is created with the given data.<p> Note,
* any fields which are not provided are inherited from the master, except for {@link #DTSTART} and {@link #DUE} which will be inherited from the instance
* and {@link #DURATION}, {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} which are set to {@code null}. {@link #ORIGINAL_INSTANCE_ID}, {@link
* #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY} will be set accordingly.</td></tr> <tr><td>Single instance task</td><td>The task is
* updated with the given values.</td></tr> <tr><td>Recurrence override with existing master</td><td>The task is updated with the given values.</td></tr>
* <tr><td>Recurrence override without existing master</td><td>The task is updated with the given values.</td></tr> </table>
* <p>
* <h2>Delete</h2>
* <p>
* <table> <tr><th>Target task type</th><th>Result</th></tr> <tr><td>Recurring master task</td><td>An {@link #EXDATE} for this instance is added, any {@link
* #RDATE} for this instance is removed. The instance row is removed.<p> TODO: mark the task deleted if the remaining recurrence set is empty </td></tr>
* <tr><td>Single instance task</td><td>The {@link Tasks#_DELETED} flag of the task is set.</td></tr> <tr><td>Recurrence override with existing
* master</td><td>The {@link Tasks#_DELETED} flag of the override is set, an {@link #EXDATE} for this instance is added to the master, any {@link #RDATE}
* for this instance is removed from the master. TODO: mark the master deleted if the remaining recurrence set of the master is empty </td></tr>
* <tr><td>Recurrence override without existing master</td><td>The {@link Tasks#_DELETED} flag of the task is set.</td></tr> </table>
*
* @author Yannic Ahrens <yannic@dmfs.org>
* @author Yannic Ahrens
* @author Marten Gajda
*/
public static final class Instances implements TaskColumns, InstanceColumns
{
......
......@@ -34,7 +34,7 @@ public final class UriFactory
UriFactory(String authority)
{
mAuthority = authority;
mUriMap.put((String) null, Uri.parse("content://" + authority));
mUriMap.put(null, Uri.parse("content://" + authority));
}
......
......@@ -15,9 +15,9 @@ import android.content.ContentUris
import android.content.ContentValues
import android.os.Build
import android.provider.CalendarContract
import android.support.test.InstrumentationRegistry
import android.support.test.filters.MediumTest
import android.support.test.rule.GrantPermissionRule
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import at.bitfire.ical4android.impl.TestCalendar
import org.junit.After
import org.junit.Assert.assertEquals
......@@ -41,11 +41,12 @@ class AndroidCalendarTest {
@Before
fun prepare() {
provider = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
provider = InstrumentationRegistry.getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
}
@After
fun shutdown() {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
......
......@@ -15,9 +15,9 @@ import android.content.ContentValues
import android.net.Uri
import android.os.Build
import android.provider.CalendarContract
import android.support.test.InstrumentationRegistry.getInstrumentation
import android.support.test.filters.MediumTest
import android.support.test.rule.GrantPermissionRule
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.rule.GrantPermissionRule
import at.bitfire.ical4android.AndroidCalendar.Companion.syncAdapterURI
import at.bitfire.ical4android.impl.TestCalendar
import at.bitfire.ical4android.impl.TestEvent
......@@ -53,7 +53,7 @@ class AndroidEventTest {
@Before
fun prepare() {
provider = getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)
provider = getInstrumentation().targetContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)!!
AndroidCalendar.insertColors(provider, testAccount)
......@@ -67,6 +67,7 @@ class AndroidEventTest {
fun shutdown() {
calendar.delete()
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= 24)
provider.close()
else
......
......@@ -11,8 +11,8 @@ package at.bitfire.ical4android
import android.accounts.Account
import android.content.ContentUris
import android.content.ContentValues
import android.support.test.InstrumentationRegistry.getInstrumentation
import android.support.test.filters.MediumTest
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import at.bitfire.ical4android.impl.TestTaskList
import org.dmfs.tasks.contract.TaskContract
import org.junit.After
......
......@@ -11,8 +11,8 @@ package at.bitfire.ical4android
import android.accounts.Account
import android.content.ContentUris
import android.net.Uri
import android.support.test.InstrumentationRegistry.getInstrumentation
import android.support.test.filters.MediumTest
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import at.bitfire.ical4android.impl.TestTask
import at.bitfire.ical4android.impl.TestTaskList
import net.fortuna.ical4j.model.Date
......
......@@ -9,7 +9,7 @@
package at.bitfire.ical4android
import android.content.ContentValues
import android.support.test.filters.SmallTest
import androidx.test.filters.SmallTest
import net.fortuna.ical4j.data.CalendarBuilder
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
......@@ -98,6 +98,7 @@ class MiscUtilsTest {
}
@Suppress("unused")
private class TestClass {
private val s = "test"
val i = 2
......
......@@ -8,7 +8,7 @@
package at.bitfire.ical4android
import android.support.test.runner.permission.PermissionRequester
import androidx.test.runner.permission.PermissionRequester
import junit.framework.AssertionFailedError
object TestUtils {
......
......@@ -44,7 +44,8 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
info.put(Calendars.ALLOWED_ATTENDEE_TYPES, "${Attendees.TYPE_NONE},${Attendees.TYPE_OPTIONAL},${Attendees.TYPE_REQUIRED},${Attendees.TYPE_RESOURCE}")
Constants.log.info("Creating local calendar: " + info.toString())
return provider.insert(syncAdapterURI(Calendars.CONTENT_URI, account), info)
return provider.insert(syncAdapterURI(Calendars.CONTENT_URI, account), info) ?:
throw Exception("Couldn't create calendar: provider returned null")
}
fun insertColors(provider: ContentProviderClient, account: Account) {
......@@ -150,13 +151,13 @@ abstract class AndroidCalendar<out T: AndroidEvent>(
/**
* Queries events from this calendar. Adds a WHERE clause that restricts the
* query to [Events.CALENDAR_ID] = [id].
* @param where selection
* @param whereArgs arguments for selection
* @param _where selection
* @param _whereArgs arguments for selection
* @return events from this calendar which match the selection
*/
fun queryEvents(where: String? = null, whereArgs: Array<String>? = null): List<T> {
val where = "(${where ?: "1"}) AND " + Events.CALENDAR_ID + "=?"
val whereArgs = (whereArgs ?: arrayOf()) + id.toString()
fun queryEvents(_where: String? = null, _whereArgs: Array<String>? = null): List<T> {
val where = "(${_where ?: "1"}) AND " + Events.CALENDAR_ID + "=?"
val whereArgs = (_whereArgs ?: arrayOf()) + id.toString()
val events = LinkedList<T>()
provider.query(eventsSyncURI(), null, where, whereArgs, null)?.use { cursor ->
......
......@@ -59,7 +59,8 @@ abstract class AndroidTaskList<out T: AndroidTask>(
info.put(TaskLists.ACCESS_LEVEL, 0)
Constants.log.info("Creating local task list: " + info.toString())
return provider.client.insert(TaskProvider.syncAdapterUri(provider.taskListsUri(), account), info)
return provider.client.insert(TaskProvider.syncAdapterUri(provider.taskListsUri(), account), info) ?:
throw CalendarStorageException("Couldn't create task list (empty result from provider)")
}
fun<T: AndroidTaskList<AndroidTask>> findByID(account: Account, provider: TaskProvider, factory: AndroidTaskListFactory<T>, id: Long): T {
......@@ -113,13 +114,13 @@ abstract class AndroidTaskList<out T: AndroidTask>(
/**
* Queries tasks from this task list. Adds a WHERE clause that restricts the
* query to [Tasks.LIST_ID] = [id].
* @param where selection
* @param whereArgs arguments for selection
* @param _where selection
* @param _whereArgs arguments for selection
* @return events from this task list which match the selection
*/
fun queryTasks(where: String? = null, whereArgs: Array<String>? = null): List<T> {
val where = "(${where ?: "1"}) AND ${Tasks.LIST_ID}=?"
val whereArgs = (whereArgs ?: arrayOf()) + id.toString()
fun queryTasks(_where: String? = null, _whereArgs: Array<String>? = null): List<T> {
val where = "(${_where ?: "1"}) AND ${Tasks.LIST_ID}=?"
val whereArgs = (_whereArgs ?: arrayOf()) + id.toString()
val tasks = LinkedList<T>()
provider.client.query(
......
......@@ -176,7 +176,7 @@ class Event: ICalendar() {
is Organizer -> e.organizer = prop
is Attendee -> e.attendees += prop
is LastModified -> e.lastModified = prop
is ProdId, is DtStamp -> { /* don't save those as unknown properties */ }
is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
else -> e.unknownProperties += prop
}
......
......@@ -10,9 +10,8 @@ package at.bitfire.ical4android
import net.fortuna.ical4j.data.CalendarOutputter
import net.fortuna.ical4j.data.ParserException
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.Calendar
import net.fortuna.ical4j.model.Component
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.component.VToDo
import net.fortuna.ical4j.model.property.*
......@@ -26,7 +25,7 @@ import java.util.logging.Level
class Task: ICalendar() {
var createdAt: Long? = null
var createdAt: Long? = null
var lastModified: Long? = null
var summary: String? = null
......@@ -50,6 +49,8 @@ class Task: ICalendar() {
val rDates = LinkedList<RDate>()
val exDates = LinkedList<ExDate>()
val unknownProperties = LinkedList<Property>()
companion object {
/**
......@@ -108,6 +109,8 @@ class Task: ICalendar() {
is RRule -> t.rRule = prop
is RDate -> t.rDates += prop
is ExDate -> t.exDates += prop
is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
else -> t.unknownProperties += prop
}
// there seem to be many invalid tasks out there because of some defect clients,
......@@ -125,19 +128,19 @@ class Task: ICalendar() {
}
fun write(os: OutputStream) {
val ical = Calendar()
ical.properties += Version.VERSION_2_0
ical.properties += prodId
fun write(os: OutputStream) {
val ical = Calendar()
ical.properties += Version.VERSION_2_0
ical.properties += prodId
val todo = VToDo()
ical.components += todo
val props = todo.properties
val todo = VToDo()
ical.components += todo
val props = todo.properties
uid?.let { props += Uid(it) }
sequence?.let { if (it != 0) props += Sequence(sequence as Int) }
createdAt?.let { props += Created(DateTime(it)) }
createdAt?.let { props += Created(DateTime(it)) }
lastModified?.let { props += LastModified(DateTime(it)) }
summary?.let { props += Summary(it) }
......@@ -145,7 +148,7 @@ class Task: ICalendar() {
geoPosition?.let { props += it }
description?.let { props += Description(it) }
color?.let { props += Color(EventColor.nearestMatch(it)) }
url?.let {
url?.let {
try {
props += Url(URI(it))
} catch (e: URISyntaxException) {
......@@ -155,7 +158,7 @@ class Task: ICalendar() {
organizer?.let { props += it }
if (priority != Priority.UNDEFINED.level)
props += Priority(priority)
props += Priority(priority)
classification?.let { props += it }
status?.let { props += it }
......@@ -163,12 +166,12 @@ class Task: ICalendar() {
rDates.forEach { props += it }
exDates.forEach { props += it }
// remember used time zones
val usedTimeZones = HashSet<TimeZone>()
due?.let {
// remember used time zones
val usedTimeZones = HashSet<TimeZone>()
due?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
}
}
duration?.let(props::add)
dtStart?.let {
props += it
......@@ -178,13 +181,15 @@ class Task: ICalendar() {
props += it
it.timeZone?.let(usedTimeZones::add)
}
percentComplete?.let { props += PercentComplete(it) }
percentComplete?.let { props += PercentComplete(it) }
// add VTIMEZONE components
props.addAll(unknownProperties)
// add VTIMEZONE components
usedTimeZones.forEach { ical.components += it.vTimeZone }
CalendarOutputter(false).output(ical, os)
}
}
fun isAllDay(): Boolean {
......
......@@ -113,7 +113,7 @@ class TaskProvider private constructor(
class ProviderTooOldException(
val provider: ProviderName,
val installedVersionCode: Int,
installedVersionCode: Int,
val installedVersionName: String
): Exception("Package ${provider.packageName} has version $installedVersionName ($installedVersionCode), " +
"required: ${provider.minVersionName} (${provider.minVersionCode})")
......
......@@ -11,7 +11,6 @@ import org.junit.Assert.*
import org.junit.Test
import java.io.FileNotFoundException
import java.io.InputStreamReader
import java.lang.AssertionError
import java.nio.charset.Charset
class EventTest {
......@@ -20,7 +19,7 @@ class EventTest {
@Test
fun testCalendarProperties() {
javaClass.classLoader.getResourceAsStream("events/multiple.ics").use { stream ->
javaClass.classLoader!!.getResourceAsStream("events/multiple.ics").use { stream ->
val properties = mutableMapOf<String, String>()
Event.fromReader(InputStreamReader(stream, Charsets.UTF_8), properties)
assertEquals(1, properties.size)
......@@ -201,7 +200,7 @@ class EventTest {
}
private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): List<Event> =
javaClass.classLoader.getResourceAsStream("events/$fname").use { stream ->
javaClass.classLoader!!.getResourceAsStream("events/$fname").use { stream ->
return Event.fromReader(InputStreamReader(stream, charset))
}
......
......@@ -98,7 +98,7 @@ class TaskTest {
/* helpers */
private fun parseCalendar(fname: String, charset: Charset = Charsets.UTF_8): Task {
javaClass.classLoader.getResourceAsStream("tasks/$fname").use { stream ->
javaClass.classLoader!!.getResourceAsStream("tasks/$fname").use { stream ->
return Task.fromReader(InputStreamReader(stream, charset)).first()
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment