Commit cf68aac5 authored by Ricki Hirner's avatar Ricki Hirner

OpenTasks update

* move contract to subdirectory (copied from recent OpenTasks)
* support minimum required version for OpenTasks
* use uid field for tasks
* support task color field
parent bff8e5b8
buildscript {
ext.kotlin_version = '1.1.61'
ext.kotlin_version = '1.2.10'
ext.dokka_version = '0.9.15'
repositories {
......@@ -47,6 +47,10 @@ android {
exclude 'META-INF/DEPENDENCIES'
exclude 'META-INF/LICENSE'
}
sourceSets {
main.java.srcDirs = [ "src/main/java", "opentasks-contract/src/main/java" ]
}
}
dependencies {
......
<manifest package="org.dmfs.tasks.contract">
<application/>
</manifest>
/*
* Copyright (C) 2014 Marten Gajda <marten@dmfs.org>
* 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.
......@@ -12,45 +12,46 @@
* 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.provider.tasks;
package org.dmfs.tasks.contract;
import android.net.Uri;
import java.util.HashMap;
import java.util.Map;
import android.net.Uri;
public class UriFactory
/**
* TODO
*/
public final class UriFactory
{
public final String authority;
private final Map<String, Uri> mUriMap = new HashMap<String, Uri>(16);
private final String mAuthority;
private final Map<String, Uri> mUriMap = new HashMap<String, Uri>(16);
UriFactory(String authority)
{
this.authority = authority;
mUriMap.put((String) null, Uri.parse("content://" + authority));
}
UriFactory(String authority)
{
mAuthority = authority;
mUriMap.put((String) null, Uri.parse("content://" + authority));
}
void addUri(String path)
{
mUriMap.put(path, Uri.parse("content://" + authority + "/" + path));
}
void addUri(String path)
{
mUriMap.put(path, Uri.parse("content://" + mAuthority + "/" + path));
}
public Uri getUri()
{
return mUriMap.get(null);
}
Uri getUri()
{
return mUriMap.get(null);
}
public Uri getUri(String path)
{
return mUriMap.get(path);
}
Uri getUri(String path)
{
return mUriMap.get(path);
}
}
......@@ -21,7 +21,7 @@ import android.provider.CalendarContract;
import android.test.InstrumentationTestCase;
import android.util.Log;
import org.dmfs.provider.tasks.TaskContract;
import org.dmfs.tasks.contract.TaskContract;
import java.io.FileNotFoundException;
......
......@@ -19,7 +19,7 @@ import android.net.Uri;
import android.test.InstrumentationTestCase;
import android.util.Log;
import org.dmfs.provider.tasks.TaskContract;
import org.dmfs.tasks.contract.TaskContract;
import java.io.FileNotFoundException;
......@@ -34,7 +34,7 @@ public class AndroidTaskListTest extends InstrumentationTestCase {
@Override
public void setUp() throws Exception {
provider = AndroidTaskList.acquireTaskProvider(getInstrumentation().getContext().getContentResolver());
provider = AndroidTaskList.acquireTaskProvider(getInstrumentation().getContext());
assertNotNull(provider);
Log.i(TAG, "Acquired context for " + provider.getName());
}
......
......@@ -26,7 +26,7 @@ import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.Due;
import net.fortuna.ical4j.model.property.Organizer;
import org.dmfs.provider.tasks.TaskContract;
import org.dmfs.tasks.contract.TaskContract;
import java.io.FileNotFoundException;
import java.net.URISyntaxException;
......@@ -63,7 +63,7 @@ public class AndroidTaskTest extends InstrumentationTestCase {
@Override
public void setUp() throws RemoteException, FileNotFoundException, CalendarStorageException {
provider = AndroidTaskList.acquireTaskProvider(getInstrumentation().getTargetContext().getContentResolver());
provider = AndroidTaskList.acquireTaskProvider(getInstrumentation().getTargetContext());
assertNotNull("Couldn't access task provider", provider);
taskList = TestTaskList.findOrCreate(testAccount, provider);
......
......@@ -18,7 +18,7 @@ import android.content.ContentValues;
import android.net.Uri;
import android.util.Log;
import org.dmfs.provider.tasks.TaskContract;
import org.dmfs.tasks.contract.TaskContract;
import at.bitfire.ical4android.AndroidTaskList;
import at.bitfire.ical4android.AndroidTaskListFactory;
......
......@@ -19,7 +19,7 @@ import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Dur
import net.fortuna.ical4j.model.property.*
import org.dmfs.provider.tasks.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.net.URI
import java.net.URISyntaxException
......@@ -77,12 +77,14 @@ abstract class AndroidTask(
MiscUtils.removeEmptyStrings(values)
task.uid = values.getAsString(Tasks._UID)
task.sequence = values.getAsInteger(Tasks.SYNC_VERSION)
task.summary = values.getAsString(Tasks.TITLE)
task.location = values.getAsString(Tasks.LOCATION)
values.getAsString(Tasks.GEO)?.let { task.geoPosition = Geo(it) }
task.description = values.getAsString(Tasks.DESCRIPTION)
task.color = values.getAsInteger(Tasks.TASK_COLOR)
task.url = values.getAsString(Tasks.URL)
values.getAsString(Tasks.ORGANIZER)?.let {
......@@ -195,13 +197,15 @@ abstract class AndroidTask(
val task = requireNotNull(task)
builder
//.withValue(Tasks._UID, task.uid) // not available in F-Droid OpenTasks version yet (15 Oct 2015)
.withValue(Tasks._UID, task.uid)
.withValue(Tasks.SYNC_VERSION, task.sequence)
.withValue(Tasks.TITLE, task.summary)
.withValue(Tasks.LOCATION, task.location)
builder .withValue(Tasks.GEO, task.geoPosition?.value)
builder .withValue(Tasks.DESCRIPTION, task.description)
.withValue(Tasks.TASK_COLOR, task.color)
.withValue(Tasks.URL, task.url)
var organizer: String? = null
......
......@@ -9,14 +9,14 @@
package at.bitfire.ical4android
import android.accounts.Account
import android.content.ContentResolver
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.database.DatabaseUtils
import android.net.Uri
import org.dmfs.provider.tasks.TaskContract
import org.dmfs.provider.tasks.TaskContract.TaskLists
import org.dmfs.provider.tasks.TaskContract.Tasks
import org.dmfs.tasks.contract.TaskContract
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.util.*
......@@ -46,18 +46,18 @@ abstract class AndroidTaskList<out T: AndroidTask>(
companion object {
/**
* Acquires a ContentProviderClient for a supported task provider. If multiple providers are
* Acquires a [ContentProviderClient] for a supported task provider. If multiple providers are
* available, a pre-defined priority list is taken into account.
* @return A TaskProvider, or null if task storage is not available/accessible.
* @return A [TaskProvider], or null if task storage is not available/accessible.
* Caller is responsible for calling release()!
*/
@JvmStatic
fun acquireTaskProvider(resolver: ContentResolver): TaskProvider? {
fun acquireTaskProvider(context: Context): TaskProvider? {
val byPriority = arrayOf(
TaskProvider.ProviderName.OpenTasks
)
for (name in byPriority)
TaskProvider.acquire(resolver, name)?.let { return it }
TaskProvider.acquire(context, name)?.let { return it }
return null
}
......
......@@ -8,9 +8,10 @@
package at.bitfire.ical4android
/**
* Represents an RGBA COLOR value, as specified in https://tools.ietf.org/html/rfc7986#section-5.9
*/
enum class EventColor(val rgba: Int) {
// for use as COLOR property [https://tools.ietf.org/html/rfc7986#section-5.9]
// values taken from https://www.w3.org/TR/2011/REC-css3-color-20110607/#svg-color
aliceblue(0xfff0f8ff.toInt()),
antiquewhite(0xfffaebd7.toInt()),
......@@ -158,5 +159,33 @@ enum class EventColor(val rgba: Int) {
white(0xffffffff.toInt()),
whitesmoke(0xfff5f5f5.toInt()),
yellow(0xffffff00.toInt()),
yellowgreen(0xff9acd32.toInt())
yellowgreen(0xff9acd32.toInt());
companion object {
/**
* Finds the best matching [EventColor] for a given RGBA value using a weighted Euclidian
* distance formula for RGB (A is being ignored).
*/
fun nearestMatch(rgba: Int): EventColor {
val rgb = rgba and 0xFFFFFF
val distance = values().map {
val cssColor = it.rgba and 0xFFFFFF
val r1 = rgb shr 16
val r2 = cssColor shr 16
val r = (r1 + r2)/2.0
val deltaR = r1 - r2
val deltaG = ((rgb shr 8) and 0xFF) - ((cssColor shr 8) and 0xFF)
val deltaB = (rgb and 0xFF) - (cssColor and 0xFF)
val deltaR2 = deltaR*deltaR
val deltaG2 = deltaG*deltaG
Math.sqrt(2.0*deltaR2 + 4.0*deltaG2 + 3.0*deltaB*deltaB + (r*(deltaR2 - deltaG2))/256.0)
}
val idx = distance.withIndex().minBy { it.value }!!.index
return values()[idx]
}
}
}
\ No newline at end of file
......@@ -32,6 +32,7 @@ class Task: iCalendar() {
var summary: String? = null
var location: String? = null
var description: String? = null
var color: Int? = null
var url: String? = null
var organizer: Organizer? = null
var geoPosition: Geo? = null
......@@ -97,6 +98,7 @@ class Task: iCalendar() {
is Location -> t.location = prop.value
is Geo -> t.geoPosition = prop
is Description -> t.description = prop.value
is Color -> t.color = prop.value?.rgba
is Url -> t.url = prop.value
is Organizer -> t.organizer = prop
is Priority -> t.priority = prop.level
......@@ -147,6 +149,7 @@ class Task: iCalendar() {
location?.let { props += Location(it) }
geoPosition?.let { props += it }
description?.let { props += Description(it) }
color?.let { props += Color(EventColor.nearestMatch(it)) }
url?.let {
try {
props += Url(URI(it))
......
......@@ -10,9 +10,10 @@ package at.bitfire.ical4android
import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import org.dmfs.provider.tasks.TaskContract
import org.dmfs.tasks.contract.TaskContract
import java.io.Closeable
import java.util.logging.Level
......@@ -22,10 +23,13 @@ class TaskProvider private constructor(
): Closeable {
enum class ProviderName(
val authority: String
val authority: String,
val packageName: String,
val minVersionCode: Int,
val minVersionName: String
) {
//Mirakel("de.azapps.mirakel.provider"),
OpenTasks("org.dmfs.tasks")
OpenTasks("org.dmfs.tasks", "org.dmfs.tasks", 103, "1.1.8.2")
}
companion object {
......@@ -42,9 +46,11 @@ class TaskProvider private constructor(
*/
@SuppressLint("Recycle")
@JvmStatic
fun acquire(resolver: ContentResolver, name: TaskProvider.ProviderName): TaskProvider? {
fun acquire(context: Context, name: TaskProvider.ProviderName): TaskProvider? {
return try {
val client = resolver.acquireContentProviderClient(name.authority)
checkVersion(context, name)
val client = context.contentResolver.acquireContentProviderClient(name.authority)
if (client != null)
TaskProvider(name, client)
else
......@@ -52,13 +58,33 @@ class TaskProvider private constructor(
} catch(e: SecurityException) {
Constants.log.log(Level.WARNING, "Not allowed to access task provider", e)
null
} catch(e: PackageManager.NameNotFoundException) {
Constants.log.warning("Package ${name.packageName} not installed")
null
}
}
@JvmStatic
fun fromProviderClient(client: ContentProviderClient) =
// at the moment, only OpenTasks is supported
TaskProvider(ProviderName.OpenTasks, client)
fun fromProviderClient(context: Context, client: ContentProviderClient): TaskProvider {
// at the moment, only OpenTasks is supported
checkVersion(context, ProviderName.OpenTasks)
return TaskProvider(ProviderName.OpenTasks, client)
}
/**
* Checks the version code of an installed tasks provider.
* @throws PackageManager.NameNotFoundException if the tasks provider is not installed
* @throws IllegalArgumentException if the tasks provider is installed, but doesn't meet the minimum version requirement
* */
private fun checkVersion(context: Context, name: ProviderName) {
// check whether package is available with required minimum version
val info = context.packageManager.getPackageInfo(name.packageName, 0)
if (info.versionCode < name.minVersionCode) {
val exception = ProviderTooOldException(name, info.versionCode, info.versionName)
Constants.log.log(Level.WARNING, "Task provider too old", exception)
throw exception
}
}
}
......@@ -75,4 +101,12 @@ class TaskProvider private constructor(
client.release()
}
class ProviderTooOldException(
val provider: ProviderName,
val installedVersionCode: Int,
val installedVersionName: String
): Exception("Package ${provider.packageName} has version $installedVersionName ($installedVersionCode), " +
"required: ${provider.minVersionName} (${provider.minVersionCode})")
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.ical4android
import org.junit.Assert.assertEquals
import org.junit.Test
class EventColorTest {
@Test
fun testNearestMatch() {
// every color is its own nearest match
EventColor.values().forEach {
assertEquals(it.rgba, EventColor.nearestMatch(it.rgba).rgba)
}
}
}
\ No newline at end of file
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