feat(store)!: delete the vendored dmfs provider
Phase 5 of docs/OWN-STORE.md. The :provider module goes — 84 Java files, 14,555 lines, its <provider>, its two custom permissions, its 13 translated strings and its three dmfs runtime dependencies. Room has been the default since the previous commit and every v0.3.x install has been imported, so nothing reads it any more. StorageMode.LOCAL is gone with it; OWN and EXTERNAL are what remain. ProviderResolver narrows to what it was always really for — discovering external providers — and answers null in OWN mode, where there is no authority to resolve. Callers that need to tell that apart from "External with nothing installed" ask mode(). ProviderStatus is unconditionally READY in OWN mode: the permission gate only ever applied to External, and that is now visibly true rather than a special case inside it. A stored LOCAL is read as OWN rather than as an unparseable value. Left to fall through to autoMode, someone who had explicitly chosen local storage while also having OpenTasks granted would have been sent to OpenTasks instead. ProviderChangeReceiver's manifest filter drops our own authority — safe now, because nothing of ours broadcasts ACTION_PROVIDER_CHANGED. In OWN mode Room's InvalidationTracker covers foreground changes and nothing outside the app can change our data. When SYNC.md phase 3 lands, the sync worker must call ReminderScheduler.sync() itself; that is the replacement for the broadcast and it belongs in the sync work. lib-recur stays as a direct dependency and is still Apache-2.0 dmfs, so the attribution is still owed — now as a normal third-party dependency. provider/PROVENANCE.md is replaced by a postscript in STORAGE-DECISION.md recording that the fork existed, why, and the one detail that still binds us: tasks.org is DB 22 and has no is_recurring, so TaskMapper must keep deriving recurrence from rrule/rdate. BREAKING: the de.jeanlucmakiola.agendula.tasks authority and both custom permissions are gone. Anyone who pointed DAVx5 or another app at that authority loses it; External mode is the answer. Needs calling out in the release notes. Verified: the APK declares no ContentProvider, no custom permission and no agendula.tasks authority, and carries no dmfs provider classes.
This commit is contained in:
24
README.md
24
README.md
@@ -16,8 +16,9 @@ Open standards, no account required.</p>
|
|||||||
|
|
||||||
Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula).
|
Agendula is the task-list sibling to [Calendula](https://codeberg.org/jlmakiola/calendula).
|
||||||
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula
|
Where Calendula is a pure front-end over Android's `CalendarContract`, Agendula
|
||||||
speaks the **dmfs `TaskContract`** — the same shape DAVx5 (and SmoothSync,
|
keeps its own store, designed around RFC 5545's `VTODO` — the same tasks DAVx5
|
||||||
DecSync, …) syncs your CalDAV `VTODO` tasks into.
|
(and SmoothSync, DecSync, …) sync out of your CalDAV server. It can also read and
|
||||||
|
write a tasks provider you already have, for anyone already syncing that way.
|
||||||
|
|
||||||
The name rhymes with its sibling on purpose: **Agendula** is *agenda* — Latin for
|
The name rhymes with its sibling on purpose: **Agendula** is *agenda* — Latin for
|
||||||
“things to be done” — given Calendula's `-ula` ending. Calendula keeps your days;
|
“things to be done” — given Calendula's `-ula` ending. Calendula keeps your days;
|
||||||
@@ -28,18 +29,23 @@ many small *florets* — so the two apps are florets of one bloom.)
|
|||||||
|
|
||||||
| | Where | Sync | Needs |
|
| | Where | Sync | Needs |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| **On your device** *(default)* | Agendula's own task store, bundled in the app | none yet — sync of our own is planned | nothing. No account, no permissions, no other app |
|
| **On your device** *(default)* | Agendula's own database | none yet — CalDAV sync of our own is planned | nothing. No account, no permissions, no other app |
|
||||||
| **In a provider you already use** | OpenTasks or tasks.org | whatever syncs it for you — DAVx5 and friends | that app installed, and its read/write permission |
|
| **In a provider you already use** | OpenTasks or tasks.org | whatever syncs it for you — DAVx5 and friends | that app installed, and its read/write permission |
|
||||||
|
|
||||||
Agendula carries its own copy of the Apache-2.0 dmfs task provider, under its own
|
Agendula's own store is an ordinary app database — nothing is published to other
|
||||||
name — so it **coexists with OpenTasks rather than replacing it**, and installing
|
apps, so there is no authority to clash over and no permission to grant. It
|
||||||
one never breaks the other. It is a fork of a proven schema, not a database
|
**coexists with OpenTasks rather than replacing it**: installing one never breaks
|
||||||
written from scratch, which is why every CalDAV engine already understands it.
|
the other, and if you already sync through a provider, that keeps working exactly
|
||||||
|
as it did.
|
||||||
|
|
||||||
|
Recurring tasks are expanded per RFC 5545, and everything the schema does not
|
||||||
|
model is round-tripped verbatim rather than dropped — so passing your tasks
|
||||||
|
through Agendula does not quietly lose fields a server sent.
|
||||||
|
|
||||||
Your tasks are exportable as standard iCalendar `.ics` files at any time, because
|
Your tasks are exportable as standard iCalendar `.ics` files at any time, because
|
||||||
data you can't take with you isn't really yours.
|
data you can't take with you isn't really yours.
|
||||||
|
|
||||||
> **Status: backend complete, UI catching up.** Storage, provider, reads and
|
> **Status: backend complete, UI catching up.** Storage, reads and
|
||||||
> writes, smart-list filtering, a self-scheduled reminder engine, and export are
|
> writes, smart-list filtering, a self-scheduled reminder engine, and export are
|
||||||
> built and unit-tested. The Material 3 Expressive screens are being built on
|
> built and unit-tested. The Material 3 Expressive screens are being built on
|
||||||
> top, one at a time — the storage-mode picker and export screen are not there
|
> top, one at a time — the storage-mode picker and export screen are not there
|
||||||
@@ -50,7 +56,7 @@ data you can't take with you isn't really yours.
|
|||||||
|
|
||||||
## Sync sources (by design)
|
## Sync sources (by design)
|
||||||
|
|
||||||
In provider mode Agendula works with anything that writes to that provider —
|
In external-provider mode Agendula works with anything that writes to that provider —
|
||||||
**DAVx5** (CalDAV), **SmoothSync**, **CalDAV-Sync**, **DecSync CC**, or any
|
**DAVx5** (CalDAV), **SmoothSync**, **CalDAV-Sync**, **DecSync CC**, or any
|
||||||
Android sync adapter — because it builds on the provider, not on any one sync
|
Android sync adapter — because it builds on the provider, not on any one sync
|
||||||
app. Google Tasks / Microsoft To Do are out of scope by design (proprietary; they
|
app. Google Tasks / Microsoft To Do are out of scope by design (proprietary; they
|
||||||
|
|||||||
@@ -145,11 +145,6 @@ ksp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Agendula's own task store — the dmfs provider vendored under our authority.
|
|
||||||
// Contributes a <provider> to the merged manifest; no app code imports from it
|
|
||||||
// except ProviderResolver, which reads the authority out of its resources.
|
|
||||||
implementation(project(":provider"))
|
|
||||||
|
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
@@ -170,10 +165,9 @@ dependencies {
|
|||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
|
|
||||||
// RFC 5545 recurrence expansion, in-process. Pulled in directly rather than
|
// RFC 5545 recurrence expansion, in-process. Pinned at 0.12.2 — 0.16.0
|
||||||
// through :provider: the own store expands occurrences itself. Pinned at
|
// removed RecurrenceSet. rfc5545-datetime comes with it and is part of its
|
||||||
// 0.12.2 — 0.16.0 removed RecurrenceSet. rfc5545-datetime comes with it and
|
// API surface, so it isn't declared separately.
|
||||||
// is part of its API surface, so it isn't declared separately.
|
|
||||||
implementation(libs.dmfs.lib.recur)
|
implementation(libs.dmfs.lib.recur)
|
||||||
|
|
||||||
implementation(libs.androidx.room.runtime)
|
implementation(libs.androidx.room.runtime)
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
runtime by the permission flow, and only once the user has actually
|
runtime by the permission flow, and only once the user has actually
|
||||||
selected External mode. Both are dangerous-level.
|
selected External mode. Both are dangerous-level.
|
||||||
|
|
||||||
Agendula's own bundled provider needs NO entry here: it runs under our uid
|
StorageMode.OWN needs nothing here: it is a Room database in our own data
|
||||||
and a same-uid caller bypasses a provider's permission checks outright.
|
directory. Agendula publishes no ContentProvider and declares no
|
||||||
Its permissions are declared by the :provider module, for other apps. -->
|
permissions of its own. -->
|
||||||
<uses-permission android:name="org.dmfs.permission.READ_TASKS" />
|
<uses-permission android:name="org.dmfs.permission.READ_TASKS" />
|
||||||
<uses-permission android:name="org.dmfs.permission.WRITE_TASKS" />
|
<uses-permission android:name="org.dmfs.permission.WRITE_TASKS" />
|
||||||
<uses-permission android:name="org.tasks.permission.READ_TASKS" />
|
<uses-permission android:name="org.tasks.permission.READ_TASKS" />
|
||||||
@@ -80,16 +80,16 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
<!-- Re-sync reminders when the provider changes — our own writes, and
|
<!-- Re-sync reminders when an external provider changes — DAVx5 pulling
|
||||||
external sync (DAVx5) in External mode. Every authority we might be
|
tasks while Agendula is backgrounded. External mode only: in OWN mode
|
||||||
pointed at needs listing, because an intent-filter host must be a
|
nothing outside the app can change our data, and Room's
|
||||||
literal: our bundled provider first, then the two external ones. -->
|
InvalidationTracker covers our own writes. An intent-filter host must
|
||||||
|
be a literal, so both external authorities are listed. -->
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".data.reminders.ProviderChangeReceiver"
|
android:name=".data.reminders.ProviderChangeReceiver"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.PROVIDER_CHANGED" />
|
<action android:name="android.intent.action.PROVIDER_CHANGED" />
|
||||||
<data android:scheme="content" android:host="de.jeanlucmakiola.agendula.tasks" />
|
|
||||||
<data android:scheme="content" android:host="org.tasks.opentasks" />
|
<data android:scheme="content" android:host="org.tasks.opentasks" />
|
||||||
<data android:scheme="content" android:host="org.dmfs.tasks" />
|
<data android:scheme="content" android:host="org.dmfs.tasks" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -95,13 +95,15 @@ class SettingsPrefs @Inject constructor(
|
|||||||
* upgrading Posture A user pointed at the provider that holds their data.
|
* upgrading Posture A user pointed at the provider that holds their data.
|
||||||
*/
|
*/
|
||||||
val storageMode: Flow<StorageMode?> = dataStore.data.map { p ->
|
val storageMode: Flow<StorageMode?> = dataStore.data.map { p ->
|
||||||
p[STORAGE_MODE]
|
when (val stored = p[STORAGE_MODE]) {
|
||||||
?.let { runCatching { StorageMode.valueOf(it) }.getOrNull() }
|
null -> null
|
||||||
// A stored LOCAL means "the bundled dmfs provider", and after the
|
// 0.3.x's value for the bundled dmfs provider. That store is gone and
|
||||||
// one-shot import that store no longer holds the user's tasks — its
|
// its data was imported into OWN, so read it as OWN rather than
|
||||||
// file has been renamed away. Read it as OWN so someone who chose
|
// letting it fall through to autoMode — someone who chose local
|
||||||
// local storage explicitly lands on the store their data is now in.
|
// storage explicitly would otherwise be sent to an external provider.
|
||||||
?.let { if (it == StorageMode.LOCAL) StorageMode.OWN else it }
|
"LOCAL" -> StorageMode.OWN
|
||||||
|
else -> runCatching { StorageMode.valueOf(stored) }.getOrNull()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun setStorageMode(mode: StorageMode) = dataStore.edit { it[STORAGE_MODE] = mode.name }
|
suspend fun setStorageMode(mode: StorageMode) = dataStore.edit { it[STORAGE_MODE] = mode.name }
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import kotlin.time.Instant
|
|||||||
* [ProviderResolver] without rebuilding the object graph. Both delegates are
|
* [ProviderResolver] without rebuilding the object graph. Both delegates are
|
||||||
* singletons, so this chooses between two existing objects.
|
* singletons, so this chooses between two existing objects.
|
||||||
*
|
*
|
||||||
* The [StorageMode.LOCAL] branch disappears with the `:provider` module; after
|
* Room versus a third-party ContentProvider — nothing else.
|
||||||
* that this is just Room versus a third-party ContentProvider.
|
|
||||||
*/
|
*/
|
||||||
class ModeRoutingTasksDataSource(
|
class ModeRoutingTasksDataSource(
|
||||||
private val resolver: ProviderResolver,
|
private val resolver: ProviderResolver,
|
||||||
@@ -26,9 +25,9 @@ class ModeRoutingTasksDataSource(
|
|||||||
) : TasksDataSource {
|
) : TasksDataSource {
|
||||||
|
|
||||||
private fun active(): TasksDataSource =
|
private fun active(): TasksDataSource =
|
||||||
when (resolver.storageMode ?: resolver.autoMode()) {
|
when (resolver.mode()) {
|
||||||
StorageMode.OWN -> room.get()
|
StorageMode.OWN -> room.get()
|
||||||
StorageMode.LOCAL, StorageMode.EXTERNAL -> external.get()
|
StorageMode.EXTERNAL -> external.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun taskLists(): List<TaskList> = active().taskLists()
|
override fun taskLists(): List<TaskList> = active().taskLists()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The three platform facts [ProviderResolver] needs, behind an interface.
|
* The two platform facts [ProviderResolver] needs, behind an interface.
|
||||||
*
|
*
|
||||||
* Same seam the data source uses, for the same reason: which store a returning
|
* Same seam the data source uses, for the same reason: which store a returning
|
||||||
* user lands on is decided by [ProviderResolver.autoMode], getting it wrong shows
|
* user lands on is decided by [ProviderResolver.autoMode], getting it wrong shows
|
||||||
@@ -18,12 +18,6 @@ import javax.inject.Singleton
|
|||||||
*/
|
*/
|
||||||
interface ProviderEnvironment {
|
interface ProviderEnvironment {
|
||||||
|
|
||||||
/** Our own bundled provider's authority, from the `:provider` module's resources. */
|
|
||||||
val ownAuthority: String
|
|
||||||
|
|
||||||
/** Our own package name. */
|
|
||||||
val ownPackage: String
|
|
||||||
|
|
||||||
/** The package declaring [authority], or `null` when nothing on the device does. */
|
/** The package declaring [authority], or `null` when nothing on the device does. */
|
||||||
fun packageDeclaring(authority: String): String?
|
fun packageDeclaring(authority: String): String?
|
||||||
|
|
||||||
@@ -36,13 +30,6 @@ class AndroidProviderEnvironment @Inject constructor(
|
|||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
) : ProviderEnvironment {
|
) : ProviderEnvironment {
|
||||||
|
|
||||||
override val ownAuthority: String
|
|
||||||
// Read from the module that declares it, never written as a literal: the
|
|
||||||
// authority lives in exactly one place, its own string resource.
|
|
||||||
get() = context.getString(de.jeanlucmakiola.agendula.provider.R.string.agendula_tasks_authority)
|
|
||||||
|
|
||||||
override val ownPackage: String get() = context.packageName
|
|
||||||
|
|
||||||
override fun packageDeclaring(authority: String): String? =
|
override fun packageDeclaring(authority: String): String? =
|
||||||
context.packageManager.resolveContentProvider(authority, 0)?.packageName
|
context.packageManager.resolveContentProvider(authority, 0)?.packageName
|
||||||
|
|
||||||
|
|||||||
@@ -4,39 +4,26 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tasks provider Agendula can talk to. The same dmfs `TaskProvider` backs every
|
* An external tasks provider Agendula can talk to. Every candidate runs the same
|
||||||
* candidate — ours included, since `:provider` *is* that provider vendored — so
|
* dmfs `TaskProvider`, so the [TasksContract] columns apply to either.
|
||||||
* the [TasksContract] columns apply regardless of which is active.
|
|
||||||
*/
|
*/
|
||||||
data class TaskProvider(
|
data class TaskProvider(
|
||||||
val authority: String,
|
val authority: String,
|
||||||
val readPermission: String,
|
val readPermission: String,
|
||||||
val writePermission: String,
|
val writePermission: String,
|
||||||
val packageName: String? = null,
|
val packageName: String? = null,
|
||||||
/**
|
|
||||||
* True for Agendula's own bundled provider. It runs in our process under our
|
|
||||||
* uid, and a same-uid caller bypasses a provider's permission checks outright,
|
|
||||||
* so [ProviderResolver.hasPermission] must never gate on a grant for it.
|
|
||||||
*/
|
|
||||||
val isOwn: Boolean = false,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The A/B seam: the only class in the app that knows an authority exists.
|
* Discovers the *external* tasks providers (OpenTasks, tasks.org) that
|
||||||
|
* [StorageMode.EXTERNAL] can be pointed at.
|
||||||
*
|
*
|
||||||
* - **Posture A** — an *external* provider (OpenTasks, tasks.org). Still fully
|
* This used to be the A/B seam between an external provider and one Agendula
|
||||||
* supported; it stopped being the default and became a user choice.
|
* bundled itself. That second half is gone: [StorageMode.OWN] is a Room database
|
||||||
* - **Posture B** — our own bundled provider, under our **own** authority. It
|
* with no authority, no ContentResolver and nothing to permit, so there is
|
||||||
* coexists with everything and replaces nothing.
|
* nothing here for it to resolve. See `docs/OWN-STORE.md`.
|
||||||
*
|
*
|
||||||
* Both terms were redefined by `docs/STORAGE-AND-SYNC.md`. Posture B used to mean
|
* Which store is active comes from [storageMode]; how that gets decided when the
|
||||||
* "bundle OpenTasks and squat `org.dmfs.tasks`"; that is a dead end and is not
|
|
||||||
* coming back, because two apps cannot declare the same authority
|
|
||||||
* (`INSTALL_FAILED_CONFLICTING_PROVIDER`) or the same permission name
|
|
||||||
* (`INSTALL_FAILED_DUPLICATE_PERMISSION`) — anyone with OpenTasks installed would
|
|
||||||
* simply have been unable to install Agendula.
|
|
||||||
*
|
|
||||||
* Which one is active comes from [storageMode]; how that gets decided when the
|
|
||||||
* user has not chosen is [autoMode].
|
* user has not chosen is [autoMode].
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -54,23 +41,17 @@ class ProviderResolver @Inject constructor(
|
|||||||
@Volatile
|
@Volatile
|
||||||
var storageMode: StorageMode? = null
|
var storageMode: StorageMode? = null
|
||||||
|
|
||||||
/** Agendula's own provider. Always present — it ships inside the APK. */
|
/** The active store, resolving the undecided case through [autoMode]. */
|
||||||
val own: TaskProvider by lazy {
|
fun mode(): StorageMode = storageMode ?: autoMode()
|
||||||
TaskProvider(
|
|
||||||
authority = environment.ownAuthority,
|
|
||||||
readPermission = OWN_READ_PERMISSION,
|
|
||||||
writePermission = OWN_WRITE_PERMISSION,
|
|
||||||
packageName = environment.ownPackage,
|
|
||||||
isOwn = true,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The active provider, or `null` when [StorageMode.EXTERNAL] is chosen and none is installed. */
|
/**
|
||||||
fun resolve(): TaskProvider? = when (storageMode ?: autoMode()) {
|
* The provider to query, or `null` — either because [StorageMode.OWN] is
|
||||||
StorageMode.LOCAL -> own
|
* active and there is no provider involved at all, or because
|
||||||
// Room has no authority, no ContentResolver and nothing to permit. The
|
* [StorageMode.EXTERNAL] is and none is installed. Callers that need to tell
|
||||||
// provider entry stands in so ProviderStatus stays READY; nothing queries it.
|
* those apart ask [mode].
|
||||||
StorageMode.OWN -> own
|
*/
|
||||||
|
fun resolve(): TaskProvider? = when (mode()) {
|
||||||
|
StorageMode.OWN -> null
|
||||||
StorageMode.EXTERNAL -> resolveExternal()
|
StorageMode.EXTERNAL -> resolveExternal()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,9 +59,9 @@ class ProviderResolver @Inject constructor(
|
|||||||
* What to use when the user has not chosen — and the one piece of real
|
* What to use when the user has not chosen — and the one piece of real
|
||||||
* judgement in this class, because getting it wrong loses people their data.
|
* judgement in this class, because getting it wrong loses people their data.
|
||||||
*
|
*
|
||||||
* Ranking our own provider first unconditionally would be wrong: someone who
|
* Ranking our own store first unconditionally would be wrong: someone who
|
||||||
* has been using Agendula over OpenTasks since 0.3.x would update, land on an
|
* has been using Agendula over OpenTasks since 0.3.x would update, land on an
|
||||||
* empty bundled store, and reasonably conclude their tasks were deleted.
|
* empty database, and reasonably conclude their tasks were deleted.
|
||||||
*
|
*
|
||||||
* So the tell is **whether we already hold an external provider's runtime
|
* So the tell is **whether we already hold an external provider's runtime
|
||||||
* permission**. That is a dangerous permission — it can only be there because
|
* permission**. That is a dangerous permission — it can only be there because
|
||||||
@@ -106,23 +87,9 @@ class ProviderResolver @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun hasPermission(provider: TaskProvider): Boolean =
|
fun hasPermission(provider: TaskProvider): Boolean =
|
||||||
// Same uid, same process: there is nothing to grant, and asking would put a
|
environment.isGranted(provider.readPermission) && environment.isGranted(provider.writePermission)
|
||||||
// permission dialog in front of a purely local app for no reason. This is
|
|
||||||
// the bypass docs/STORAGE-AND-SYNC.md calls for — without it
|
|
||||||
// ProviderStatus.NEEDS_PERMISSION fires in Local mode and the onboarding
|
|
||||||
// gate asks for a permission that can never be granted.
|
|
||||||
provider.isOwn ||
|
|
||||||
(environment.isGranted(provider.readPermission) && environment.isGranted(provider.writePermission))
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* Declared by the `:provider` module's manifest. Listed here so the app can
|
|
||||||
* name them; nothing ever requests them, since [hasPermission] short-circuits
|
|
||||||
* for our own provider.
|
|
||||||
*/
|
|
||||||
const val OWN_READ_PERMISSION = "de.jeanlucmakiola.agendula.permission.READ_TASKS"
|
|
||||||
const val OWN_WRITE_PERMISSION = "de.jeanlucmakiola.agendula.permission.WRITE_TASKS"
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verified on-device: tasks.org exposes `org.tasks.opentasks` backed by
|
* Verified on-device: tasks.org exposes `org.tasks.opentasks` backed by
|
||||||
* `org.dmfs.provider.tasks.TaskProvider`, guarded by `org.tasks.permission.*`
|
* `org.dmfs.provider.tasks.TaskProvider`, guarded by `org.tasks.permission.*`
|
||||||
|
|||||||
@@ -1,46 +1,26 @@
|
|||||||
package de.jeanlucmakiola.agendula.data.tasks
|
package de.jeanlucmakiola.agendula.data.tasks
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Which task store backs the app — the user's choice, per `docs/STORAGE-AND-SYNC.md`.
|
* Which task store backs the app — the user's choice, per `docs/OWN-STORE.md`.
|
||||||
*
|
*
|
||||||
* Only two values, though the document describes three modes. **Synced is not a
|
* Only two values, though `docs/STORAGE-AND-SYNC.md` describes three modes.
|
||||||
* third store**: it is [LOCAL] with an account attached, so it is derived state
|
* **Synced is not a third store**: it is [OWN] with an account attached to a
|
||||||
* (does an account of ours exist?) rather than something the user picks. Adding
|
* list, which is derived state rather than something the user picks. Attaching
|
||||||
* a `SYNCED` constant here would imply otherwise.
|
* one is a plain `UPDATE task_lists SET account_id = ?` — not the full data
|
||||||
*
|
* migration it was under the dmfs provider, whose `ACCOUNT_TYPE` was write-once.
|
||||||
* ⚠️ This used to add "so switching sync on is never a migration". That is wrong.
|
|
||||||
* `TaskLists.ACCOUNT_TYPE` is write-once in the provider — `processors/lists/
|
|
||||||
* Validating.java:68-76` throws `IllegalArgumentException` on any attempt to
|
|
||||||
* change it — so attaching an account to an existing local list means recreating
|
|
||||||
* every list and every task under the new account. See `docs/SYNC.md`.
|
|
||||||
*
|
|
||||||
* Nothing above the data layer reads this; it selects an authority for
|
|
||||||
* [ProviderResolver] and stops there.
|
|
||||||
*/
|
*/
|
||||||
enum class StorageMode {
|
enum class StorageMode {
|
||||||
/**
|
/**
|
||||||
* Agendula's own bundled provider (the `:provider` module). Always available —
|
* Agendula's own Room database. The default, and always available: there is
|
||||||
* it ships in the APK — and needs no permission grant at all, because
|
* no authority, no ContentResolver and no permission to grant.
|
||||||
* same-uid access to your own provider skips the permission check entirely.
|
|
||||||
*
|
|
||||||
* On its way out: [OWN] replaces it once the Room store is the default, and
|
|
||||||
* this constant leaves with the `:provider` module. See `docs/OWN-STORE.md`.
|
|
||||||
*/
|
|
||||||
LOCAL,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agendula's own Room database. Named as a third value rather than renaming
|
|
||||||
* [LOCAL] because both stores exist at once while the migration runs — a
|
|
||||||
* rename now would make `OWN` mean the dmfs provider for several phases and
|
|
||||||
* Room afterwards.
|
|
||||||
*/
|
*/
|
||||||
OWN,
|
OWN,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tasks provider app already on the device (OpenTasks, tasks.org), synced by
|
* A tasks provider app already on the device (OpenTasks, tasks.org), synced by
|
||||||
* whatever that provider's engine is — DAVx5 and friends. This is the original
|
* whatever that provider's engine is — DAVx5 and friends. Still fully
|
||||||
* Posture A, still fully supported, but now a choice rather than the only way.
|
* supported, now a choice rather than the only way. Requires that provider's
|
||||||
* Requires that provider's runtime read/write permissions.
|
* runtime read/write permissions.
|
||||||
*/
|
*/
|
||||||
EXTERNAL,
|
EXTERNAL,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,10 @@ class TasksRepositoryImpl @Inject constructor(
|
|||||||
withContext(io) { dataSource.createLocalList(name, color) }
|
withContext(io) { dataSource.createLocalList(name, color) }
|
||||||
|
|
||||||
override fun providerStatus(): ProviderStatus {
|
override fun providerStatus(): ProviderStatus {
|
||||||
|
// Our own store is always ready: it ships with the app, needs no provider
|
||||||
|
// and no grant. The permission gate only ever applied to External mode —
|
||||||
|
// now that is visibly true rather than a special case inside it.
|
||||||
|
if (providerResolver.mode() == StorageMode.OWN) return ProviderStatus.READY
|
||||||
val provider = providerResolver.resolve() ?: return ProviderStatus.NO_PROVIDER
|
val provider = providerResolver.resolve() ?: return ProviderStatus.NO_PROVIDER
|
||||||
return if (providerResolver.hasPermission(provider)) ProviderStatus.READY
|
return if (providerResolver.hasPermission(provider)) ProviderStatus.READY
|
||||||
else ProviderStatus.NEEDS_PERMISSION
|
else ProviderStatus.NEEDS_PERMISSION
|
||||||
|
|||||||
@@ -41,13 +41,8 @@ class PermissionViewModel @Inject constructor(
|
|||||||
val provider = providerResolver.resolve()
|
val provider = providerResolver.resolve()
|
||||||
_state.value = PermissionUiState(
|
_state.value = PermissionUiState(
|
||||||
status = repository.providerStatus(),
|
status = repository.providerStatus(),
|
||||||
// Never our own provider's permissions. They are declared for *other*
|
// Null in OWN mode, where there is no provider and nothing to grant.
|
||||||
// apps to hold; requesting them here would show a dialog for a
|
|
||||||
// permission that a same-uid caller does not need and the system will
|
|
||||||
// not meaningfully grant. In practice this branch is unreachable in
|
|
||||||
// Local mode, since the status is already READY — belt and braces.
|
|
||||||
permissionsToRequest = provider
|
permissionsToRequest = provider
|
||||||
?.takeUnless { it.isOwn }
|
|
||||||
?.let { listOf(it.readPermission, it.writePermission) }
|
?.let { listOf(it.readPermission, it.writePermission) }
|
||||||
.orEmpty(),
|
.orEmpty(),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ class ProviderResolverTest {
|
|||||||
val installed: Map<String, String> = emptyMap(),
|
val installed: Map<String, String> = emptyMap(),
|
||||||
val granted: Set<String> = emptySet(),
|
val granted: Set<String> = emptySet(),
|
||||||
) : ProviderEnvironment {
|
) : ProviderEnvironment {
|
||||||
override val ownAuthority = "de.jeanlucmakiola.agendula.tasks"
|
|
||||||
override val ownPackage = "de.jeanlucmakiola.agendula"
|
|
||||||
override fun packageDeclaring(authority: String): String? = installed[authority]
|
override fun packageDeclaring(authority: String): String? = installed[authority]
|
||||||
override fun isGranted(permission: String): Boolean = permission in granted
|
override fun isGranted(permission: String): Boolean = permission in granted
|
||||||
}
|
}
|
||||||
@@ -39,26 +37,16 @@ class ProviderResolverTest {
|
|||||||
private val openTasksGranted = setOf(openTasks.readPermission, openTasks.writePermission)
|
private val openTasksGranted = setOf(openTasks.readPermission, openTasks.writePermission)
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
inner class OwnProvider {
|
inner class OwnStore {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `needs no permission grant`() {
|
fun `resolves to no provider at all`() {
|
||||||
// The bypass the whole Local mode rests on: our provider runs under our
|
// Room has no authority and no ContentResolver, so there is nothing
|
||||||
// own uid, so there is nothing to grant and nothing to ask for.
|
// here to resolve — which is the point. Callers that need to tell this
|
||||||
val resolver = resolver()
|
// apart from "External, none installed" ask mode().
|
||||||
assertThat(resolver.hasPermission(resolver.own)).isTrue()
|
val resolver = resolver(mode = StorageMode.OWN)
|
||||||
}
|
assertThat(resolver.resolve()).isNull()
|
||||||
|
assertThat(resolver.mode()).isEqualTo(StorageMode.OWN)
|
||||||
@Test
|
|
||||||
fun `is always resolvable because it ships in the APK`() {
|
|
||||||
assertThat(resolver(mode = StorageMode.LOCAL).resolve()).isNotNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `does not use a dmfs authority`() {
|
|
||||||
// Squatting org.dmfs.tasks would make Agendula and OpenTasks mutually
|
|
||||||
// uninstallable. Guards against a careless resync of the vendored module.
|
|
||||||
assertThat(resolver().own.authority).isEqualTo("de.jeanlucmakiola.agendula.tasks")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +92,12 @@ class ProviderResolverTest {
|
|||||||
fun `overrides the automatic answer in both directions`() {
|
fun `overrides the automatic answer in both directions`() {
|
||||||
val wouldBeExternal = FakeEnvironment(openTasksInstalled, openTasksGranted)
|
val wouldBeExternal = FakeEnvironment(openTasksInstalled, openTasksGranted)
|
||||||
|
|
||||||
val forcedLocal = ProviderResolver(wouldBeExternal).apply { storageMode = StorageMode.LOCAL }
|
val forcedOwn = ProviderResolver(wouldBeExternal).apply { storageMode = StorageMode.OWN }
|
||||||
assertThat(forcedLocal.resolve()?.isOwn).isTrue()
|
assertThat(forcedOwn.mode()).isEqualTo(StorageMode.OWN)
|
||||||
|
assertThat(forcedOwn.resolve()).isNull()
|
||||||
|
|
||||||
val forcedExternal = ProviderResolver(FakeEnvironment()).apply { storageMode = StorageMode.EXTERNAL }
|
val forcedExternal = ProviderResolver(FakeEnvironment()).apply { storageMode = StorageMode.EXTERNAL }
|
||||||
|
assertThat(forcedExternal.mode()).isEqualTo(StorageMode.EXTERNAL)
|
||||||
assertThat(forcedExternal.resolve()).isNull()
|
assertThat(forcedExternal.resolve()).isNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +142,14 @@ class ProviderResolverTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `never include our own provider`() {
|
fun `never name an authority of ours`() {
|
||||||
// EXTERNAL must mean "somebody else's store". If ours ever leaked into
|
// EXTERNAL must mean "somebody else's store", and Agendula publishes no
|
||||||
// this list, choosing External would silently keep using it.
|
// provider at all any more.
|
||||||
assertThat(ProviderResolver.EXTERNAL_CANDIDATES.none { it.isOwn }).isTrue()
|
assertThat(
|
||||||
|
ProviderResolver.EXTERNAL_CANDIDATES.none {
|
||||||
|
it.authority.startsWith("de.jeanlucmakiola")
|
||||||
|
},
|
||||||
|
).isTrue()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -250,3 +250,35 @@ point of sequencing it last.
|
|||||||
mode arguably already serves the second. Undecided.
|
mode arguably already serves the second. Undecided.
|
||||||
- **The Room estimate is mine, not measured.** Instance expansion is the item
|
- **The Room estimate is mine, not measured.** Instance expansion is the item
|
||||||
that could overrun; everything else is well-bounded.
|
that could overrun; everything else is well-bounded.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Postscript: the fork existed, and how it ended
|
||||||
|
|
||||||
|
`provider/PROVENANCE.md` recorded the vendored dmfs task provider in detail.
|
||||||
|
Both are gone; this is what is worth keeping.
|
||||||
|
|
||||||
|
The module was `opentasks-provider` plus `opentasks-contract` from
|
||||||
|
[dmfs/opentasks](https://github.com/dmfs/opentasks) **1.4.2**, commit
|
||||||
|
`49ebf80b1eeee52a611e5a22f24f849852a6255f` (2021-03-21), Apache-2.0, database
|
||||||
|
version 23. It was vendored in-tree rather than pulled as an artifact because the
|
||||||
|
permission names are hardcoded in the upstream AAR's manifest, and shipping under
|
||||||
|
dmfs's own names would have made Agendula and OpenTasks mutually uninstallable
|
||||||
|
(`INSTALL_FAILED_DUPLICATE_PERMISSION`). In-tree also satisfied F-Droid's
|
||||||
|
from-source requirement.
|
||||||
|
|
||||||
|
It was deleted in the `feat/own-store` work (`docs/OWN-STORE.md` phase 5) once
|
||||||
|
Room was the default and every v0.3.x install had been imported. 14,555 lines of
|
||||||
|
Java left with it.
|
||||||
|
|
||||||
|
**What survives, and why.** `lib-recur` (Apache-2.0, dmfs) is still a direct
|
||||||
|
dependency — it is what expands recurrences — so dmfs's attribution is still
|
||||||
|
owed, now through an ordinary third-party dependency rather than vendored source.
|
||||||
|
`TasksContract.kt` and the External-mode mappers also stay: they describe
|
||||||
|
*somebody else's* schema, which is exactly what they were always right for.
|
||||||
|
|
||||||
|
**One detail the fork's provenance file carried that still binds us.** DB 23 is
|
||||||
|
the first to have `is_recurring`; tasks.org's fork is DB 22 and lacks it. That is
|
||||||
|
why `TaskMapper.task` derives recurrence from `rrule`/`rdate` rather than trusting
|
||||||
|
that column, and it must keep doing so for as long as External mode supports
|
||||||
|
tasks.org.
|
||||||
|
|||||||
202
provider/LICENSE
202
provider/LICENSE
@@ -1,202 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
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.
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
OpenTasks - Open Source Task App for Android
|
|
||||||
Copyright 2012-2015 Marten Gajda
|
|
||||||
@@ -1,192 +0,0 @@
|
|||||||
# `:provider` — provenance
|
|
||||||
|
|
||||||
This module is **not our code**. It is the dmfs task provider, vendored, with
|
|
||||||
its namespace renamed to ours and a short list of changes recorded below.
|
|
||||||
|
|
||||||
| | |
|
|
||||||
|---|---|
|
|
||||||
| Upstream | [dmfs/opentasks](https://github.com/dmfs/opentasks) |
|
|
||||||
| Module taken | `opentasks-provider`, plus `opentasks-contract` (see [Why the contract came along](#why-the-contract-came-along)) |
|
|
||||||
| Version | `1.4.2` |
|
|
||||||
| Commit | `49ebf80b1eeee52a611e5a22f24f849852a6255f` (2021-03-21) |
|
|
||||||
| License | Apache-2.0 — see [`LICENSE`](LICENSE) and [`NOTICE`](NOTICE), both upstream's, unmodified |
|
|
||||||
| Database version | **23** |
|
|
||||||
|
|
||||||
Agendula itself is MIT. Apache-2.0 into MIT is fine in that direction, but this
|
|
||||||
module keeps its own `LICENSE`, `NOTICE`, and per-file Apache headers, and those
|
|
||||||
must survive any future edit here.
|
|
||||||
|
|
||||||
**1.4.2 specifically, for the database version.** DB 23 is the first to carry
|
|
||||||
`is_recurring`. tasks.org's fork is DB 22 and lacks it — which is why
|
|
||||||
`TaskMapper.task` on the app side derives recurrence from `rrule`/`rdate`
|
|
||||||
instead of trusting that column, and why it must keep doing so as long as
|
|
||||||
External mode supports tasks.org.
|
|
||||||
|
|
||||||
## Why vendored at all
|
|
||||||
|
|
||||||
Recorded properly in [`docs/STORAGE-AND-SYNC.md`](../docs/STORAGE-AND-SYNC.md);
|
|
||||||
in one line: **the permission names are hardcoded in the upstream AAR's
|
|
||||||
manifest.** No prebuilt artifact — Maven Central, JitPack, anything — can have
|
|
||||||
them renamed without `tools:` node surgery, and shipping under dmfs's own
|
|
||||||
permission names would make Agendula and OpenTasks mutually uninstallable
|
|
||||||
(`INSTALL_FAILED_DUPLICATE_PERMISSION`). In-tree also satisfies F-Droid's
|
|
||||||
from-source requirement, which a JitPack artifact would not.
|
|
||||||
|
|
||||||
In-tree rather than a git submodule, unlike floret-kit: we co-develop the kit,
|
|
||||||
whereas this is a fork we expect to resync from upstream approximately never.
|
|
||||||
|
|
||||||
## The namespace rename
|
|
||||||
|
|
||||||
Everything in this table is a rename and nothing more. The **contract shape is
|
|
||||||
untouched** — same tables, same column names, same URI paths — because that
|
|
||||||
shape is what our data layer, and every CalDAV engine, already speaks. We own
|
|
||||||
the namespace it lives in, not the schema.
|
|
||||||
|
|
||||||
| | Upstream | Ours |
|
|
||||||
|---|---|---|
|
|
||||||
| Authority | `org.dmfs.tasks` | `de.jeanlucmakiola.agendula.tasks` |
|
|
||||||
| Read permission | `org.dmfs.permission.READ_TASKS` | `de.jeanlucmakiola.agendula.permission.READ_TASKS` |
|
|
||||||
| Write permission | `org.dmfs.permission.WRITE_TASKS` | `de.jeanlucmakiola.agendula.permission.WRITE_TASKS` |
|
|
||||||
| Permission group | `org.dmfs.tasks.permissiongroup.Tasks` | `de.jeanlucmakiola.agendula.permissiongroup.Tasks` |
|
|
||||||
| Notification-alarm action | `org.dmfs.tasks.provider.NOTIFICATION_ALARM` | `de.jeanlucmakiola.agendula.provider.NOTIFICATION_ALARM` |
|
|
||||||
| Resource prefix | `opentasks_*` | `agendula_*` |
|
|
||||||
| R class | `org.dmfs.tasks.provider.R` | `de.jeanlucmakiola.agendula.provider.R` |
|
|
||||||
|
|
||||||
### What was deliberately *not* renamed
|
|
||||||
|
|
||||||
- **Java package names** stay `org.dmfs.provider.tasks` / `org.dmfs.tasks.contract`.
|
|
||||||
They are not a registered namespace — two apps may share them freely — and
|
|
||||||
keeping them means the diff against upstream stays legible. Only the AGP
|
|
||||||
`namespace` (which decides where `R` lands) is ours.
|
|
||||||
- **`TaskContract.LOCAL_ACCOUNT_TYPE`** stays `"org.dmfs.account.LOCAL"`. It is a
|
|
||||||
value stored in the database and recognised by dmfs-contract providers
|
|
||||||
generally, so the *same* app code has to write it whether it is talking to our
|
|
||||||
provider or, in External mode, to OpenTasks. Renaming it would fork the write
|
|
||||||
path in two for no gain.
|
|
||||||
- **`ACTION_BROADCAST_TASK_DUE` / `…_TASK_STARTING` / `ACTION_DATABASE_INITIALIZED`.**
|
|
||||||
Every send site calls `setPackage()` on its own package first, so these never
|
|
||||||
cross app boundaries and cannot collide with an installed OpenTasks.
|
|
||||||
|
|
||||||
## Changes to upstream source
|
|
||||||
|
|
||||||
Four files under `src/main/java`, one under `src/test/java`. Each edit is marked
|
|
||||||
with an `AGENDULA CHANGE` comment at the site, so this list and the code cannot
|
|
||||||
drift apart. Keep that convention.
|
|
||||||
|
|
||||||
### Behavioural
|
|
||||||
|
|
||||||
1. **`Utils.cleanUpLists` — only prune account types we authenticate ourselves.**
|
|
||||||
*The one change here that is about correctness rather than mechanics.*
|
|
||||||
|
|
||||||
Upstream holds `GET_ACCOUNTS` and enumerates every account on the device. We
|
|
||||||
dropped that permission (below), so `AccountManager` only ever reports
|
|
||||||
accounts of our own type. Upstream's cleanup deletes any task list whose
|
|
||||||
account is absent from that array — and an account we *cannot see* is
|
|
||||||
indistinguishable from one that has been *removed*. Left alone, the provider
|
|
||||||
would quietly delete synced lists. Not "sync stops": data disappears, with no
|
|
||||||
error anywhere.
|
|
||||||
|
|
||||||
Now a list is only prunable when its account type belongs to an authenticator
|
|
||||||
in **this package**. Agendula ships no authenticator yet, so the set is empty
|
|
||||||
and nothing is ever pruned; our sync adapter's type will join it on its own
|
|
||||||
when it lands, no edit needed here. Local lists were already exempt upstream.
|
|
||||||
|
|
||||||
2. **`TaskProvider.insert` — the same restriction for the stale-list signal.**
|
|
||||||
Upstream flags "list with unknown account" and broadcasts about it. With the
|
|
||||||
account cache holding only our own accounts, that fired on every insert into
|
|
||||||
any externally-synced list. Nothing listens today; the point is that whatever
|
|
||||||
listens tomorrow gets a signal that means something.
|
|
||||||
|
|
||||||
3. **`TaskProviderBroadcastReceiver.onReceive` — fall-through written out.**
|
|
||||||
Upstream's `switch` has no `break` in any branch, so `TIMEZONE_CHANGED` runs
|
|
||||||
all three content operations and `NOTIFICATION_ALARM` runs the last two. The
|
|
||||||
comment on the first branch ("don't trigger the notifications update yet")
|
|
||||||
describes breaks that were never written, so the code and the stated intent
|
|
||||||
contradict each other.
|
|
||||||
|
|
||||||
**Observed behaviour is preserved exactly**, just spelled out with `if`s
|
|
||||||
rather than reached by accident. A vendored fork is the wrong place to guess
|
|
||||||
at intent. ⚠️ **Open:** which of the two is the bug wants a device with a task
|
|
||||||
due across a timezone change to settle.
|
|
||||||
|
|
||||||
4. **`TaskProviderBroadcastReceiver.planNotificationUpdate` — inexact-alarm fallback.**
|
|
||||||
`setExact` throws `SecurityException` on API 31–32 when the user revokes
|
|
||||||
`SCHEDULE_EXACT_ALARM` — inside a receiver handling a system broadcast, so the
|
|
||||||
app would die on every timezone change. Falls back to `set` when exact alarms
|
|
||||||
aren't permitted. This alarm drives only the provider's own bookkeeping;
|
|
||||||
Agendula's user-visible reminders come from `ReminderScheduler`, which asks
|
|
||||||
for the permission properly.
|
|
||||||
|
|
||||||
### Required by modern Android (would not build or would crash otherwise)
|
|
||||||
|
|
||||||
5. **`PendingIntent.FLAG_IMMUTABLE`** added in `planNotificationUpdate`. Mandatory
|
|
||||||
since Android 12; throws `IllegalArgumentException` without it at targetSdk ≥ 31.
|
|
||||||
Upstream targets 29. Nothing mutates the intent later, so immutable is also
|
|
||||||
correct on the merits.
|
|
||||||
6. **`android:exported`** stated explicitly on the receiver. AGP hard-errors on an
|
|
||||||
intent-filtered component without it at targetSdk ≥ 31.
|
|
||||||
7. **`package=` attribute** removed from the manifest; AGP 8+ takes it from the
|
|
||||||
`namespace` in the build file.
|
|
||||||
8. **`<uses-sdk tools:overrideLibrary="org.dmfs.android.contenttestpal"/>`** removed
|
|
||||||
along with the androidTest sources it existed for (below).
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
|
|
||||||
9. **`android.permission.GET_ACCOUNTS` dropped.** We only ever need to see
|
|
||||||
accounts of our own type, and since API 26 an authenticator makes those
|
|
||||||
visible to its own package with no grant at all. Safe **only** in combination
|
|
||||||
with change 1 — the two must be read together.
|
|
||||||
|
|
||||||
### Build and test
|
|
||||||
|
|
||||||
10. **`build.gradle` → `build.gradle.kts`**, on the root version catalog. minSdk
|
|
||||||
21 → 29 (matching `:app`; the merger rejects a lower floor), Java 8 → 17.
|
|
||||||
11. **Test stack modernised**, sources otherwise untouched: JUnit 4.12 → 4.13.2,
|
|
||||||
Robolectric 3.5.1 → 4.16, Mockito 2.27 → 5.20, Hamcrest 1.3 → 3.0.
|
|
||||||
`org.dmfs:jems`, `rfc5545-datetime` and `lib-recur` stay on the versions
|
|
||||||
upstream pinned — all three resolve from Maven Central, so no new repository
|
|
||||||
was added (`settings.gradle.kts` is still `google()` + `mavenCentral()` under
|
|
||||||
`FAIL_ON_PROJECT_REPOS`).
|
|
||||||
12. **`ZippedTest.testAbsent`** — diamond `new Zipped<>` given an explicit type
|
|
||||||
argument. `absent()` pins no type, and javac 17 will not infer what javac 8
|
|
||||||
did. The assertion is unchanged.
|
|
||||||
13. **`src/test/resources/robolectric.properties` added** (`sdk=34`,
|
|
||||||
`conscryptMode=OFF`). A library module has no `targetSdk` for Robolectric to
|
|
||||||
read, and Robolectric installs Conscrypt unconditionally, whose uber jar has
|
|
||||||
no `linux-aarch_64` native — so without this the suite fails at setup on ARM64
|
|
||||||
machines while passing on x86_64 CI. See the file for the reasoning.
|
|
||||||
14. **`src/androidTest` dropped entirely.** It depends on `contentpal` /
|
|
||||||
`contenttestpal`, which are JitPack-only; adding JitPack would widen the
|
|
||||||
dependency trust surface for test-only code. ⚠️ This is the one place
|
|
||||||
vendoring lost coverage — those were the provider's *integration* tests
|
|
||||||
(recurrence, reparenting, instances, observers). The 56 JVM tests in
|
|
||||||
`src/test` all pass and are retained.
|
|
||||||
15. **`agendula_provider_changed_receivers` emptied.** Upstream notifies
|
|
||||||
`org.andstatus.todoagenda`, which listens for changes to the *dmfs* authority
|
|
||||||
and has never heard of ours. Anything re-added here also needs a `<queries>`
|
|
||||||
entry in the app manifest or package-visibility rules drop the broadcast.
|
|
||||||
16. **Translated `agendula_provider_label` overrides removed** (the other
|
|
||||||
translated strings are kept as upstream shipped them). The base label became
|
|
||||||
"Agendula tasks" so it is distinguishable from OpenTasks' own "Tasks" entry in
|
|
||||||
the system permission dialog; the inherited translations still said plain
|
|
||||||
"Tasks" in their language, which would have contradicted it.
|
|
||||||
|
|
||||||
## Resyncing from upstream
|
|
||||||
|
|
||||||
Unlikely to ever be worth it — upstream 1.4.2 is from 2021 — but if it is: the
|
|
||||||
`AGENDULA CHANGE` markers are the complete list of what to reapply, `git log` on
|
|
||||||
this directory is the audit trail, and the 56 JVM tests are the safety net.
|
|
||||||
Re-read change 1 before touching anything account-related.
|
|
||||||
|
|
||||||
## Known-unverified
|
|
||||||
|
|
||||||
Everything here is verified by the JVM test suite and a clean build. What is
|
|
||||||
**not** yet verified on a device with real data:
|
|
||||||
|
|
||||||
- The local-list path with **no account present at all** — the entirety of Local
|
|
||||||
mode. `cleanUpLists` exempts local lists explicitly and change 1 makes the
|
|
||||||
prunable set empty, so it should hold by construction; it is covered by
|
|
||||||
`ProviderAccountCleanupTest`, but that is Robolectric, not a device.
|
|
||||||
- The timezone-change behaviour in change 3.
|
|
||||||
- Any interaction with an external sync engine writing into our authority
|
|
||||||
(nothing does yet — that is the DAVx5 ask, step 4 of the sequencing).
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Agendula's own task store: the dmfs task provider (Apache-2.0), vendored.
|
|
||||||
//
|
|
||||||
// This module is a fork, not a dependency. What we changed and why is recorded
|
|
||||||
// in PROVENANCE.md; the short version is that the authority and the permission
|
|
||||||
// names had to become ours, and the permission names are hardcoded in the
|
|
||||||
// upstream AAR's manifest, so no prebuilt artifact could have been used.
|
|
||||||
//
|
|
||||||
// It stays Java, on upstream's `org.dmfs.*` package names, formatted the way
|
|
||||||
// upstream formats it. That is deliberate: every deviation from upstream is a
|
|
||||||
// line we have to re-reason about if we ever resync, so the diff is kept
|
|
||||||
// legible rather than idiomatic.
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.android.library)
|
|
||||||
}
|
|
||||||
|
|
||||||
android {
|
|
||||||
namespace = "de.jeanlucmakiola.agendula.provider"
|
|
||||||
compileSdk = 37
|
|
||||||
|
|
||||||
defaultConfig {
|
|
||||||
// Matches :app. Upstream ships minSdk 21 / targetSdk 29; targetSdk is set
|
|
||||||
// by the application module anyway, but the SDK floor here has to agree
|
|
||||||
// with :app's or the manifest merger rejects it.
|
|
||||||
minSdk = 29
|
|
||||||
|
|
||||||
consumerProguardFiles("proguard-rules.pro")
|
|
||||||
}
|
|
||||||
|
|
||||||
compileOptions {
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFeatures {
|
|
||||||
// The provider reads its own authority out of resources, so it needs R.
|
|
||||||
// It has no BuildConfig use at all.
|
|
||||||
buildConfig = false
|
|
||||||
}
|
|
||||||
|
|
||||||
testOptions {
|
|
||||||
unitTests {
|
|
||||||
isIncludeAndroidResources = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lint {
|
|
||||||
// Upstream's translations are inherited as-is and are partial — a missing
|
|
||||||
// string falls back to the English base at runtime. Same call as :app.
|
|
||||||
informational += listOf("MissingTranslation")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(libs.dmfs.jems)
|
|
||||||
implementation(libs.dmfs.rfc5545.datetime)
|
|
||||||
implementation(libs.dmfs.lib.recur)
|
|
||||||
|
|
||||||
// Upstream's own JVM test suite, on current versions of its stack. Note this
|
|
||||||
// module is JUnit 4 while :app is JUnit 5 — deliberately, see the version
|
|
||||||
// catalog. Do not add `useJUnitPlatform()` here.
|
|
||||||
testImplementation(libs.junit4)
|
|
||||||
testImplementation(libs.robolectric)
|
|
||||||
testImplementation(libs.hamcrest)
|
|
||||||
testImplementation(libs.mockito.core)
|
|
||||||
testImplementation(libs.dmfs.jems.testing)
|
|
||||||
}
|
|
||||||
25
provider/proguard-rules.pro
vendored
25
provider/proguard-rules.pro
vendored
@@ -1,25 +0,0 @@
|
|||||||
# Add project specific ProGuard rules here.
|
|
||||||
# By default, the flags in this file are appended to flags specified
|
|
||||||
# in /home/marten/.local/Android/Sdk/tools/proguard/proguard-android.txt
|
|
||||||
# You can edit the include path and order by changing the proguardFiles
|
|
||||||
# directive in build.gradle.
|
|
||||||
#
|
|
||||||
# For more details, see
|
|
||||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
|
||||||
|
|
||||||
# Add any project specific keep options here:
|
|
||||||
|
|
||||||
# If your project uses WebView with JS, uncomment the following
|
|
||||||
# and specify the fully qualified class name to the JavaScript interface
|
|
||||||
# class:
|
|
||||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
|
||||||
# public *;
|
|
||||||
#}
|
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
|
||||||
# debugging stack traces.
|
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
|
||||||
|
|
||||||
# If you keep the line number information, uncomment this to
|
|
||||||
# hide the original source file name.
|
|
||||||
#-renamesourcefileattribute SourceFile
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Agendula's own task store. This is the dmfs task provider (Apache-2.0)
|
|
||||||
vendored under *our* authority and *our* permission namespace — see
|
|
||||||
PROVENANCE.md. It coexists with OpenTasks / tasks.org rather than
|
|
||||||
replacing them: nothing here collides with `org.dmfs.*`, so both can be
|
|
||||||
installed at once.
|
|
||||||
|
|
||||||
`android.permission.GET_ACCOUNTS` is deliberately absent. We only ever need
|
|
||||||
to see accounts of our own type, and since API 26 an authenticator makes
|
|
||||||
those visible to its own package without any grant. Utils.cleanUpLists was
|
|
||||||
reworked to match — it must never prune a list whose account type we cannot
|
|
||||||
enumerate.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<permission-group
|
|
||||||
android:name="de.jeanlucmakiola.agendula.permissiongroup.Tasks"
|
|
||||||
android:label="@string/agendula_provider_label"
|
|
||||||
android:icon="@drawable/ic_24_agendula_tasks"
|
|
||||||
android:description="@string/agendula_permission_group_description_tasks"/>
|
|
||||||
|
|
||||||
<permission
|
|
||||||
android:name="de.jeanlucmakiola.agendula.permission.READ_TASKS"
|
|
||||||
android:description="@string/agendula_permission_description_read_tasks"
|
|
||||||
android:label="@string/agendula_permission_read_tasks"
|
|
||||||
android:permissionGroup="de.jeanlucmakiola.agendula.permissiongroup.Tasks"
|
|
||||||
android:protectionLevel="dangerous"/>
|
|
||||||
<permission
|
|
||||||
android:name="de.jeanlucmakiola.agendula.permission.WRITE_TASKS"
|
|
||||||
android:description="@string/agendula_permission_description_write_tasks"
|
|
||||||
android:label="@string/agendula_permission_write_tasks"
|
|
||||||
android:permissionGroup="de.jeanlucmakiola.agendula.permissiongroup.Tasks"
|
|
||||||
android:protectionLevel="dangerous"/>
|
|
||||||
|
|
||||||
<application>
|
|
||||||
<!--
|
|
||||||
Exported on purpose. Agendula itself reaches the provider same-uid and
|
|
||||||
needs no grant, but keeping it open under our own permissions is what
|
|
||||||
lets an external sync engine (DAVx5, once it knows our authority) write
|
|
||||||
into it — the whole point of owning the namespace rather than squatting
|
|
||||||
dmfs's. Third-party clients must hold the permissions declared above.
|
|
||||||
-->
|
|
||||||
<provider
|
|
||||||
android:name="org.dmfs.provider.tasks.TaskProvider"
|
|
||||||
android:authorities="@string/agendula_tasks_authority"
|
|
||||||
android:exported="true"
|
|
||||||
android:label="@string/agendula_provider_label"
|
|
||||||
android:multiprocess="false"
|
|
||||||
android:readPermission="de.jeanlucmakiola.agendula.permission.READ_TASKS"
|
|
||||||
android:writePermission="de.jeanlucmakiola.agendula.permission.WRITE_TASKS"/>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Exported because BOOT_COMPLETED / TIME_SET / TIMEZONE_CHANGED are system
|
|
||||||
broadcasts. Upstream left `exported` implicit, which AGP rejects outright
|
|
||||||
once targetSdk >= 31 — we're on 36, so it is stated here.
|
|
||||||
-->
|
|
||||||
<receiver
|
|
||||||
android:name="org.dmfs.provider.tasks.TaskProviderBroadcastReceiver"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
|
||||||
<action android:name="android.intent.action.QUICKBOOT_POWERON"/>
|
|
||||||
<action android:name="android.intent.action.TIME_SET"/>
|
|
||||||
<action android:name="android.intent.action.TIMEZONE_CHANGED"/>
|
|
||||||
<action android:name="android.intent.action.DATE_CHANGED"/>
|
|
||||||
</intent-filter>
|
|
||||||
</receiver>
|
|
||||||
|
|
||||||
</application>
|
|
||||||
|
|
||||||
</manifest>
|
|
||||||
@@ -1,168 +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.ngrams;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generator for N-grams from a given String.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class NGramGenerator
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* A {@link Pattern} that matches anything that doesn't belong to a word or number.
|
|
||||||
*/
|
|
||||||
private final static Pattern SEPARATOR_PATTERN = Pattern.compile("[^\\p{L}\\p{M}\\d]+");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Pattern} that matches anything that doesn't belong to a word.
|
|
||||||
*/
|
|
||||||
private final static Pattern SEPARATOR_PATTERN_NO_NUMBERS = Pattern.compile("[^\\p{L}\\p{M}]+");
|
|
||||||
|
|
||||||
private final int mN;
|
|
||||||
private final int mMinWordLen;
|
|
||||||
private boolean mAllLowercase = true;
|
|
||||||
private boolean mReturnNumbers = true;
|
|
||||||
private boolean mAddSpaceInFront = false;
|
|
||||||
private Locale mLocale = Locale.getDefault();
|
|
||||||
|
|
||||||
|
|
||||||
public NGramGenerator(int n)
|
|
||||||
{
|
|
||||||
this(n, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public NGramGenerator(int n, int minWordLen)
|
|
||||||
{
|
|
||||||
mN = n;
|
|
||||||
mMinWordLen = minWordLen;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether to convert all words to lower-case first.
|
|
||||||
*
|
|
||||||
* @param lowercase
|
|
||||||
* true to convert the test to lower case first.
|
|
||||||
*
|
|
||||||
* @return This instance.
|
|
||||||
*/
|
|
||||||
public NGramGenerator setAllLowercase(boolean lowercase)
|
|
||||||
{
|
|
||||||
mAllLowercase = lowercase;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set whether to index the beginning of a word with a space in front. This slightly raises the weight of word beginnings when searching.
|
|
||||||
*
|
|
||||||
* @param addSpace
|
|
||||||
* <code>true</code> to add a space in front of each word, <code>false</code> otherwise.
|
|
||||||
*
|
|
||||||
* @return This instance.
|
|
||||||
*/
|
|
||||||
public NGramGenerator setAddSpaceInFront(boolean addSpace)
|
|
||||||
{
|
|
||||||
mAddSpaceInFront = addSpace;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the {@link Locale} to use when converting the input string to lower case. This has no effect when {@link #setAllLowercase(boolean)} is called with
|
|
||||||
* <code>false</code>.
|
|
||||||
*
|
|
||||||
* @param locale
|
|
||||||
* The {@link Locale} to user for the conversion to lower case.
|
|
||||||
*
|
|
||||||
* @return This instance.
|
|
||||||
*/
|
|
||||||
public NGramGenerator setLocale(Locale locale)
|
|
||||||
{
|
|
||||||
mLocale = locale;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all N-grams contained in the given String.
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* The String to analyze.
|
|
||||||
*
|
|
||||||
* @return The {@link Set} containing the N-grams.
|
|
||||||
*/
|
|
||||||
public Set<String> getNgrams(String data)
|
|
||||||
{
|
|
||||||
if (data == null)
|
|
||||||
{
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mAllLowercase)
|
|
||||||
{
|
|
||||||
data = data.toLowerCase(mLocale);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] words = mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data);
|
|
||||||
|
|
||||||
Set<String> set = new HashSet<String>(128);
|
|
||||||
|
|
||||||
for (String word : words)
|
|
||||||
{
|
|
||||||
getNgrams(word, set);
|
|
||||||
}
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void getNgrams(String word, Set<String> ngrams)
|
|
||||||
{
|
|
||||||
final int len = word.length();
|
|
||||||
|
|
||||||
if (len < mMinWordLen)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int n = mN;
|
|
||||||
final int last = Math.max(1, len - n + 1);
|
|
||||||
|
|
||||||
for (int i = 0; i < last; ++i)
|
|
||||||
{
|
|
||||||
ngrams.add(word.substring(i, Math.min(i + n, len)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mAddSpaceInFront)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* Add another String with a space and the first n-1 characters of the word.
|
|
||||||
*/
|
|
||||||
ngrams.add(" " + word.substring(0, Math.min(len, n - 1)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import de.jeanlucmakiola.agendula.provider.R;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Access for the authority name of the tasks content provider.
|
|
||||||
*
|
|
||||||
* @author Gabor Keszthelyi
|
|
||||||
*/
|
|
||||||
// TODO Figure out better design or at least rename to TaskAuthority.get(context) (results in changes in many files)
|
|
||||||
public final class AuthorityUtil
|
|
||||||
{
|
|
||||||
private static String sCachedValue;
|
|
||||||
|
|
||||||
|
|
||||||
public static String taskAuthority(Context context)
|
|
||||||
{
|
|
||||||
if (sCachedValue == null)
|
|
||||||
{
|
|
||||||
sCachedValue = context.getString(R.string.agendula_tasks_authority);
|
|
||||||
}
|
|
||||||
return sCachedValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,419 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.SharedPreferences.Editor;
|
|
||||||
import android.content.UriMatcher;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesInstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.InstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.Instantiating;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Instances;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
public enum ContentOperation
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* When the local timezone has been changed we need to update the due and start sorting values. This handler will take care of running the appropriate
|
|
||||||
* update. In addition it fires an operation to update all notifications.
|
|
||||||
*/
|
|
||||||
UPDATE_TIMEZONE(new OperationHandler()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values)
|
|
||||||
{
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// request an update of all instance values
|
|
||||||
ContentValues vals = new ContentValues(1);
|
|
||||||
Instantiating.addUpdateRequest(vals);
|
|
||||||
|
|
||||||
// execute update that triggers a recalculation of all due and start sorting values
|
|
||||||
int count = context.getContentResolver().update(
|
|
||||||
TaskContract.Tasks.getContentUri(uri.getAuthority()).buildUpon().appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true").build(),
|
|
||||||
vals, null, null);
|
|
||||||
|
|
||||||
Log.i("TaskProvider", "time to update " + count + " tasks: " + (System.currentTimeMillis() - start) + " ms");
|
|
||||||
|
|
||||||
// now update alarms as well
|
|
||||||
UPDATE_NOTIFICATION_ALARM.fire(context, null);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes care of everything we need to send task start and task due broadcasts.
|
|
||||||
*/
|
|
||||||
POST_NOTIFICATIONS(new OperationHandler()
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values)
|
|
||||||
{
|
|
||||||
TimeZone localTimeZone = TimeZone.getDefault();
|
|
||||||
|
|
||||||
// the date-time of when the last notification was shown
|
|
||||||
DateTime lastAlarm = getLastAlarmTimestamp(context);
|
|
||||||
// the current time, we show all notifications between <set> and now
|
|
||||||
DateTime now = DateTime.nowAndHere();
|
|
||||||
|
|
||||||
String lastAlarmString = Long.toString(lastAlarm.getInstance());
|
|
||||||
String nowString = Long.toString(now.getInstance());
|
|
||||||
|
|
||||||
// load all tasks that have started or became due since the last time we've shown a notification.
|
|
||||||
Cursor instancesCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, "((" + TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and "
|
|
||||||
+ TaskContract.Instances.INSTANCE_DUE_SORTING + "<=?) or (" + TaskContract.Instances.INSTANCE_START_SORTING + ">? and "
|
|
||||||
+ TaskContract.Instances.INSTANCE_START_SORTING + "<=?)) and " + Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] {
|
|
||||||
lastAlarmString, nowString, lastAlarmString, nowString }, null, null, null);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (instancesCursor.moveToNext())
|
|
||||||
{
|
|
||||||
InstanceAdapter task = new CursorContentValuesInstanceAdapter(InstanceAdapter._ID.getFrom(instancesCursor), instancesCursor, null);
|
|
||||||
|
|
||||||
DateTime instanceDue = task.valueOf(InstanceAdapter.INSTANCE_DUE);
|
|
||||||
if (instanceDue != null && !instanceDue.isFloating())
|
|
||||||
{
|
|
||||||
// make sure we compare instances in local time
|
|
||||||
instanceDue = instanceDue.shiftTimeZone(localTimeZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime instanceStart = task.valueOf(InstanceAdapter.INSTANCE_START);
|
|
||||||
if (instanceStart != null && !instanceStart.isFloating())
|
|
||||||
{
|
|
||||||
// make sure we compare instances in local time
|
|
||||||
instanceStart = instanceStart.shiftTimeZone(localTimeZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instanceDue != null && lastAlarm.getInstance() < instanceDue.getInstance() && instanceDue.getInstance() <= now.getInstance())
|
|
||||||
{
|
|
||||||
// this task became due since the last alarm, send a due broadcast
|
|
||||||
sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_DUE, task.uri(uri.getAuthority()));
|
|
||||||
}
|
|
||||||
else if (instanceStart != null && lastAlarm.getInstance() < instanceStart.getInstance() && instanceStart.getInstance() <= now.getInstance())
|
|
||||||
{
|
|
||||||
// this task has started since the last alarm, send a start broadcast
|
|
||||||
sendBroadcast(context, TaskContract.ACTION_BROADCAST_TASK_STARTING, task.uri(uri.getAuthority()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
instancesCursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// all notifications up to now have been triggered
|
|
||||||
saveLastAlarmTime(context, now);
|
|
||||||
|
|
||||||
// set the alarm for the next notification
|
|
||||||
UPDATE_NOTIFICATION_ALARM.fire(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private void saveLastAlarmTime(Context context, DateTime time)
|
|
||||||
{
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
Editor editor = prefs.edit();
|
|
||||||
editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp());
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private DateTime getLastAlarmTimestamp(Context context)
|
|
||||||
{
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends a notification broadcast for a task instance that has started or became due.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* A {@link Context}.
|
|
||||||
* @param action
|
|
||||||
* The broadcast action.
|
|
||||||
* @param uri
|
|
||||||
* The task uri.
|
|
||||||
*/
|
|
||||||
private void sendBroadcast(Context context, String action, Uri uri)
|
|
||||||
{
|
|
||||||
Intent intent = new Intent(action);
|
|
||||||
intent.setData(uri);
|
|
||||||
// only notify our own package
|
|
||||||
intent.setPackage(context.getPackageName());
|
|
||||||
context.sendBroadcast(intent);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines the date-time of when the next task becomes due or starts (whatever happens first) and sets an alarm to trigger a notification.
|
|
||||||
*/
|
|
||||||
UPDATE_NOTIFICATION_ALARM(new OperationHandler()
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values)
|
|
||||||
{
|
|
||||||
TimeZone localTimeZone = TimeZone.getDefault();
|
|
||||||
DateTime lastAlarm = getLastAlarmTimestamp(context);
|
|
||||||
DateTime now = DateTime.nowAndHere();
|
|
||||||
|
|
||||||
if (now.before(lastAlarm))
|
|
||||||
{
|
|
||||||
// time went backwards, set last alarm time to now
|
|
||||||
lastAlarm = now;
|
|
||||||
saveLastAlarmTime(context, now);
|
|
||||||
}
|
|
||||||
|
|
||||||
String lastAlarmString = Long.toString(lastAlarm.getInstance());
|
|
||||||
|
|
||||||
DateTime nextAlarm = null;
|
|
||||||
|
|
||||||
// find the next task that starts
|
|
||||||
Cursor nextInstanceStartCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_START_SORTING + ">? and "
|
|
||||||
+ Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null,
|
|
||||||
TaskContract.Instances.INSTANCE_START_SORTING, "1");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (nextInstanceStartCursor.moveToNext())
|
|
||||||
{
|
|
||||||
TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceStartCursor),
|
|
||||||
nextInstanceStartCursor, null);
|
|
||||||
nextAlarm = task.valueOf(TaskAdapter.INSTANCE_START);
|
|
||||||
if (!nextAlarm.isFloating())
|
|
||||||
{
|
|
||||||
nextAlarm = nextAlarm.shiftTimeZone(localTimeZone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
nextInstanceStartCursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// find the next task that's due
|
|
||||||
Cursor nextInstanceDueCursor = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW, null, TaskContract.Instances.INSTANCE_DUE_SORTING + ">? and "
|
|
||||||
+ Instances.IS_CLOSED + " = 0 and " + Tasks._DELETED + "=0", new String[] { lastAlarmString }, null, null,
|
|
||||||
TaskContract.Instances.INSTANCE_DUE_SORTING, "1");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (nextInstanceDueCursor.moveToNext())
|
|
||||||
{
|
|
||||||
TaskAdapter task = new CursorContentValuesTaskAdapter(TaskAdapter.INSTANCE_TASK_ID.getFrom(nextInstanceDueCursor), nextInstanceDueCursor,
|
|
||||||
null);
|
|
||||||
DateTime nextDue = task.valueOf(TaskAdapter.INSTANCE_DUE);
|
|
||||||
if (!nextDue.isFloating())
|
|
||||||
{
|
|
||||||
nextDue = nextDue.shiftTimeZone(localTimeZone);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextAlarm == null || nextAlarm.getInstance() > nextDue.getInstance())
|
|
||||||
{
|
|
||||||
nextAlarm = nextDue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
nextInstanceDueCursor.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextAlarm != null)
|
|
||||||
{
|
|
||||||
TaskProviderBroadcastReceiver.planNotificationUpdate(context, nextAlarm);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
saveLastAlarmTime(context, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
private void saveLastAlarmTime(Context context, DateTime time)
|
|
||||||
{
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
Editor editor = prefs.edit();
|
|
||||||
editor.putLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, time.getTimestamp());
|
|
||||||
editor.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private DateTime getLastAlarmTimestamp(Context context)
|
|
||||||
{
|
|
||||||
SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
|
||||||
return new DateTime(TimeZone.getDefault(), prefs.getLong(PREFS_KEY_LAST_ALARM_TIMESTAMP, System.currentTimeMillis()));
|
|
||||||
}
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A lock object to serialize the execution of all incoming {@link ContentOperation}.
|
|
||||||
*/
|
|
||||||
private final static Object mLock = new Object();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The base path of the Uri to trigger content operations.
|
|
||||||
*/
|
|
||||||
private final static String BASE_PATH = "content_operation";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link OperationHandler} that handles this {@link ContentOperation}.
|
|
||||||
*/
|
|
||||||
private final OperationHandler mHandler;
|
|
||||||
|
|
||||||
private static final String PREFS_NAME = "org.dmfs.provider.tasks";
|
|
||||||
private static final String PREFS_KEY_LAST_ALARM_TIMESTAMP = "org.dmfs.provider.tasks.prefs.LAST_ALARM_TIMESTAMP";
|
|
||||||
|
|
||||||
|
|
||||||
ContentOperation(OperationHandler handler)
|
|
||||||
{
|
|
||||||
mHandler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute this {@link ContentOperation} with the given values.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* A {@link Context}.
|
|
||||||
* @param values
|
|
||||||
* Optional {@link ContentValues}, may be <code>null</code>.
|
|
||||||
*/
|
|
||||||
public void fire(Context context, ContentValues values)
|
|
||||||
{
|
|
||||||
context.getContentResolver().update(uri(AuthorityUtil.taskAuthority(context)), values == null ? new ContentValues() : values, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run the operation on the given handler.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* A {@link Context}.
|
|
||||||
* @param handler
|
|
||||||
* A {@link Handler} to run the operation on.
|
|
||||||
* @param uri
|
|
||||||
* The {@link Uri} that triggered this operation.
|
|
||||||
* @param db
|
|
||||||
* The database.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} that were supplied.
|
|
||||||
*/
|
|
||||||
void run(final Context context, Handler handler, final Uri uri, final SQLiteDatabase db, final ContentValues values)
|
|
||||||
{
|
|
||||||
handler.post(new Runnable()
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void run()
|
|
||||||
{
|
|
||||||
synchronized (mLock)
|
|
||||||
{
|
|
||||||
mHandler.handleOperation(context, uri, db, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link Uri} that triggers this {@link ContentOperation}.
|
|
||||||
*
|
|
||||||
* @param authority
|
|
||||||
* The authority of this provide.
|
|
||||||
*
|
|
||||||
* @return A {@link Uri}.
|
|
||||||
*/
|
|
||||||
private Uri uri(String authority)
|
|
||||||
{
|
|
||||||
return new Uri.Builder().scheme("content").authority(authority).path(BASE_PATH).appendPath(this.toString()).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the operations with the given {@link UriMatcher}.
|
|
||||||
*
|
|
||||||
* @param uriMatcher
|
|
||||||
* The {@link UriMatcher}.
|
|
||||||
* @param authority
|
|
||||||
* The authority of this TaskProvider.
|
|
||||||
* @param firstID
|
|
||||||
* Teh first Id to use for our Uris.
|
|
||||||
*/
|
|
||||||
public static void register(UriMatcher uriMatcher, String authority, int firstID)
|
|
||||||
{
|
|
||||||
for (ContentOperation op : values())
|
|
||||||
{
|
|
||||||
Uri uri = op.uri(authority);
|
|
||||||
uriMatcher.addURI(authority, uri.getPath().substring(1) /* remove leading slash */, firstID + op.ordinal());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a {@link ContentOperation} that belongs to the given id.
|
|
||||||
*
|
|
||||||
* @param id
|
|
||||||
* The id or the {@link ContentOperation}.
|
|
||||||
* @param firstId
|
|
||||||
* The first ID to use for Uris.
|
|
||||||
*
|
|
||||||
* @return The respective {@link ContentOperation} or <code>null</code> if none was found.
|
|
||||||
*/
|
|
||||||
public static ContentOperation get(int id, int firstId)
|
|
||||||
{
|
|
||||||
if (id < firstId)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id - firstId >= values().length)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return values()[id - firstId];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public interface OperationHandler
|
|
||||||
{
|
|
||||||
void handleOperation(Context context, Uri uri, SQLiteDatabase db, ContentValues values);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,630 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.iterable.decorators.Chunked;
|
|
||||||
import org.dmfs.ngrams.NGramGenerator;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Properties;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskColumns;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Supports the {@link TaskDatabaseHelper} in the matter of full-text-search.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class FTSDatabaseHelper
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* We search the ngram table in chunks of 500. This should be good enough for an average task but still well below
|
|
||||||
* the SQLITE expression length limit and the variable count limit.
|
|
||||||
*/
|
|
||||||
private final static int NGRAM_SEARCH_CHUNK_SIZE = 500;
|
|
||||||
|
|
||||||
private final static float SEARCH_RESULTS_MIN_SCORE = 0.33f;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Generator for 3-grams.
|
|
||||||
*/
|
|
||||||
private final static NGramGenerator TRIGRAM_GENERATOR = new NGramGenerator(3, 1).setAddSpaceInFront(true);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Generator for 4-grams.
|
|
||||||
*/
|
|
||||||
private final static NGramGenerator TETRAGRAM_GENERATOR = new NGramGenerator(4, 3 /* shorter words are fully covered by trigrams */).setAddSpaceInFront(
|
|
||||||
true);
|
|
||||||
private static final String PROPERTY_NGRAM_SELECTION = String.format("%s = ? AND %s = ? AND %s = ?", FTSContentColumns.TASK_ID, FTSContentColumns.TYPE,
|
|
||||||
FTSContentColumns.PROPERTY_ID);
|
|
||||||
private static final String NON_PROPERTY_NGRAM_SELECTION = String.format("%s = ? AND %s = ? AND %s is null", FTSContentColumns.TASK_ID,
|
|
||||||
FTSContentColumns.TYPE,
|
|
||||||
FTSContentColumns.PROPERTY_ID);
|
|
||||||
private static final String[] NGRAM_SYNC_COLUMNS = { "_rowid_", FTSContentColumns.NGRAM_ID };
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search content columns. Defines all the columns for the full text search
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface FTSContentColumns
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The row id of the belonging task.
|
|
||||||
*/
|
|
||||||
String TASK_ID = "fts_task_id";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The the property id of the searchable entry or <code>null</code> if the entry is not related to a property.
|
|
||||||
*/
|
|
||||||
String PROPERTY_ID = "fts_property_id";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The the type of the searchable entry
|
|
||||||
*/
|
|
||||||
String TYPE = "fts_type";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An n-gram for a task.
|
|
||||||
*/
|
|
||||||
String NGRAM_ID = "fts_ngram_id";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The columns of the N-gram table for the FTS search
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface NGramColumns
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The row id of the N-gram.
|
|
||||||
*/
|
|
||||||
String NGRAM_ID = "ngram_id";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The content of the N-gram
|
|
||||||
*/
|
|
||||||
String TEXT = "ngram_text";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static final String FTS_CONTENT_TABLE = "FTS_Content";
|
|
||||||
public static final String FTS_NGRAM_TABLE = "FTS_Ngram";
|
|
||||||
public static final String FTS_TASK_VIEW = "FTS_Task_View";
|
|
||||||
public static final String FTS_TASK_PROPERTY_VIEW = "FTS_Task_Property_View";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the table for full text search and contains relationships between ngrams and tasks
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_SEARCH_CONTENT_TABLE = "CREATE TABLE " + FTS_CONTENT_TABLE + "( " + FTSContentColumns.TASK_ID + " Integer, "
|
|
||||||
+ FTSContentColumns.NGRAM_ID + " Integer, " + FTSContentColumns.PROPERTY_ID + " Integer, " + FTSContentColumns.TYPE + " Integer, " + "FOREIGN KEY("
|
|
||||||
+ FTSContentColumns.TASK_ID + ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ")," + "FOREIGN KEY(" + FTSContentColumns.TASK_ID
|
|
||||||
+ ") REFERENCES " + Tables.TASKS + "(" + TaskColumns._ID + ") UNIQUE (" + FTSContentColumns.TASK_ID + ", " + FTSContentColumns.TYPE + ", "
|
|
||||||
+ FTSContentColumns.PROPERTY_ID + ") ON CONFLICT IGNORE )";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the table that stores the NGRAMS
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_NGRAM_TABLE = "CREATE TABLE " + FTS_NGRAM_TABLE + "( " + NGramColumns.NGRAM_ID
|
|
||||||
+ " Integer PRIMARY KEY AUTOINCREMENT, " + NGramColumns.TEXT + " Text)";
|
|
||||||
|
|
||||||
// FIXME: at present the minimum score is hard coded can we leave that decision to the caller?
|
|
||||||
private final static String SQL_RAW_QUERY_SEARCH_TASK = "SELECT %s " + ", (1.0*count(DISTINCT " + NGramColumns.NGRAM_ID + ")/?) as " + TaskContract.Tasks.SCORE + " from "
|
|
||||||
+ FTS_NGRAM_TABLE + " join " + FTS_CONTENT_TABLE + " on (" + FTS_NGRAM_TABLE + "." + NGramColumns.NGRAM_ID + "=" + FTS_CONTENT_TABLE + "."
|
|
||||||
+ FTSContentColumns.NGRAM_ID + ") join " + Tables.INSTANCE_VIEW + " on (" + Tables.INSTANCE_VIEW + "." + TaskContract.Instances.TASK_ID + " = " + FTS_CONTENT_TABLE + "."
|
|
||||||
+ FTSContentColumns.TASK_ID + ") where %s group by " + TaskContract.Instances.TASK_ID + " having " + TaskContract.Tasks.SCORE + " >= " + SEARCH_RESULTS_MIN_SCORE
|
|
||||||
+ " and " + Tasks.VISIBLE + " = 1 order by %s;";
|
|
||||||
|
|
||||||
private final static String SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION = Tables.INSTANCE_VIEW + ".* ," + FTS_NGRAM_TABLE + "." + NGramColumns.TEXT;
|
|
||||||
|
|
||||||
private final static String SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER = "CREATE TRIGGER search_task_delete_trigger AFTER DELETE ON " + Tables.TASKS + " BEGIN "
|
|
||||||
+ " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Tasks._ID + "; END";
|
|
||||||
|
|
||||||
private final static String SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER = "CREATE TRIGGER search_task_delete_property_trigger AFTER DELETE ON "
|
|
||||||
+ Tables.PROPERTIES + " BEGIN " + " DELETE FROM " + FTS_CONTENT_TABLE + " WHERE " + FTSContentColumns.TASK_ID + " = old." + Properties.TASK_ID
|
|
||||||
+ " AND " + FTSContentColumns.PROPERTY_ID + " = old." + Properties.PROPERTY_ID + "; END";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The different types of searchable entries for tasks linked to the <code>TYPE</code> column.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface SearchableTypes
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* This is an entry for the title of a task.
|
|
||||||
*/
|
|
||||||
int TITLE = 1;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an entry for the description of a task.
|
|
||||||
*/
|
|
||||||
int DESCRIPTION = 2;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an entry for the location of a task.
|
|
||||||
*/
|
|
||||||
int LOCATION = 3;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an entry for a property of a task.
|
|
||||||
*/
|
|
||||||
int PROPERTY = 4;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void onCreate(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
initializeFTS(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
|
||||||
{
|
|
||||||
if (oldVersion < 8)
|
|
||||||
{
|
|
||||||
initializeFTS(db);
|
|
||||||
initializeFTSContent(db);
|
|
||||||
}
|
|
||||||
if (oldVersion < 16)
|
|
||||||
{
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID,
|
|
||||||
FTSContentColumns.PROPERTY_ID));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the tables and triggers used in FTS.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
*/
|
|
||||||
private static void initializeFTS(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
db.execSQL(SQL_CREATE_SEARCH_CONTENT_TABLE);
|
|
||||||
db.execSQL(SQL_CREATE_NGRAM_TABLE);
|
|
||||||
db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_SEARCH_TASK_DELETE_PROPERTY_TRIGGER);
|
|
||||||
|
|
||||||
// create indices
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_NGRAM_TABLE, true, NGramColumns.TEXT));
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.NGRAM_ID));
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, false, FTSContentColumns.TASK_ID));
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.PROPERTY_ID, FTSContentColumns.TASK_ID,
|
|
||||||
FTSContentColumns.NGRAM_ID));
|
|
||||||
|
|
||||||
db.execSQL(TaskDatabaseHelper.createIndexString(FTS_CONTENT_TABLE, true, FTSContentColumns.TYPE, FTSContentColumns.TASK_ID,
|
|
||||||
FTSContentColumns.PROPERTY_ID));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the FTS entries for the existing tasks.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The writable {@link SQLiteDatabase}.
|
|
||||||
*/
|
|
||||||
private static void initializeFTSContent(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
String[] task_projection = new String[] { Tasks._ID, Tasks.TITLE, Tasks.DESCRIPTION, Tasks.LOCATION };
|
|
||||||
Cursor c = db.query(Tables.TASKS_PROPERTY_VIEW, task_projection, null, null, null, null, null);
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
insertTaskFTSEntries(db, c.getLong(0), c.getString(1), c.getString(2), c.getString(3));
|
|
||||||
}
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the searchable texts of the task in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The writable {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The row id of the task.
|
|
||||||
* @param title
|
|
||||||
* The title of the task.
|
|
||||||
* @param description
|
|
||||||
* The description of the task.
|
|
||||||
*/
|
|
||||||
private static void insertTaskFTSEntries(SQLiteDatabase db, long taskId, String title, String description, String location)
|
|
||||||
{
|
|
||||||
// title
|
|
||||||
if (title != null && title.length() > 0)
|
|
||||||
{
|
|
||||||
updateEntry(db, taskId, -1, SearchableTypes.TITLE, title);
|
|
||||||
}
|
|
||||||
|
|
||||||
// location
|
|
||||||
if (location != null && location.length() > 0)
|
|
||||||
{
|
|
||||||
updateEntry(db, taskId, -1, SearchableTypes.LOCATION, location);
|
|
||||||
}
|
|
||||||
|
|
||||||
// description
|
|
||||||
if (description != null && description.length() > 0)
|
|
||||||
{
|
|
||||||
updateEntry(db, taskId, -1, SearchableTypes.DESCRIPTION, description);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the existing searchables entries for the task.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The writable {@link SQLiteDatabase}.
|
|
||||||
* @param task
|
|
||||||
* The {@link TaskAdapter} containing the new values.
|
|
||||||
*/
|
|
||||||
public static void updateTaskFTSEntries(SQLiteDatabase db, TaskAdapter task)
|
|
||||||
{
|
|
||||||
// title
|
|
||||||
if (task.isUpdated(TaskAdapter.TITLE))
|
|
||||||
{
|
|
||||||
updateEntry(db, task.id(), -1, SearchableTypes.TITLE, task.valueOf(TaskAdapter.TITLE));
|
|
||||||
}
|
|
||||||
|
|
||||||
// location
|
|
||||||
if (task.isUpdated(TaskAdapter.LOCATION))
|
|
||||||
{
|
|
||||||
updateEntry(db, task.id(), -1, SearchableTypes.LOCATION, task.valueOf(TaskAdapter.LOCATION));
|
|
||||||
}
|
|
||||||
|
|
||||||
// description
|
|
||||||
if (task.isUpdated(TaskAdapter.DESCRIPTION))
|
|
||||||
{
|
|
||||||
updateEntry(db, task.id(), -1, SearchableTypes.DESCRIPTION, task.valueOf(TaskAdapter.DESCRIPTION));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates or creates the searchable entries for a property. Passing <code>null</code> as searchable text will remove the entry.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The writable {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* the row id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* the id of the property
|
|
||||||
* @param searchableText
|
|
||||||
* the searchable text value of the property
|
|
||||||
*/
|
|
||||||
public static void updatePropertyFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String searchableText)
|
|
||||||
{
|
|
||||||
updateEntry(db, taskId, propertyId, SearchableTypes.PROPERTY, searchableText);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the IDs of each of the provided ngrams, creating them in th database if necessary.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* A writable {@link SQLiteDatabase}.
|
|
||||||
* @param ngrams
|
|
||||||
* The NGrams.
|
|
||||||
*
|
|
||||||
* @return The ids of the ngrams in the given set.
|
|
||||||
*/
|
|
||||||
private static Set<Long> ngramIds(SQLiteDatabase db, Set<String> ngrams)
|
|
||||||
{
|
|
||||||
if (ngrams.size() == 0)
|
|
||||||
{
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> missingNgrams = new HashSet<>(ngrams);
|
|
||||||
Set<Long> ngramIds = new HashSet<>(ngrams.size() * 2);
|
|
||||||
|
|
||||||
for (Iterable<String> chunk : new Chunked<>(NGRAM_SEARCH_CHUNK_SIZE, ngrams))
|
|
||||||
{
|
|
||||||
// build selection and arguments for each chunk
|
|
||||||
// we can't do this in a single query because the length of sql statement and number of arguments is limited.
|
|
||||||
|
|
||||||
StringBuilder selection = new StringBuilder(NGramColumns.TEXT);
|
|
||||||
selection.append(" in (");
|
|
||||||
boolean first = true;
|
|
||||||
List<String> arguments = new ArrayList<>(NGRAM_SEARCH_CHUNK_SIZE);
|
|
||||||
for (String ngram : chunk)
|
|
||||||
{
|
|
||||||
if (first)
|
|
||||||
{
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selection.append(",");
|
|
||||||
}
|
|
||||||
selection.append("?");
|
|
||||||
arguments.add(ngram);
|
|
||||||
}
|
|
||||||
selection.append(" )");
|
|
||||||
|
|
||||||
try (Cursor c = db.query(FTS_NGRAM_TABLE, new String[] { NGramColumns.NGRAM_ID, NGramColumns.TEXT }, selection.toString(),
|
|
||||||
arguments.toArray(new String[0]), null, null, null))
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
// remove the ngrams we already have in the table
|
|
||||||
missingNgrams.remove(c.getString(1));
|
|
||||||
// remember its id
|
|
||||||
ngramIds.add(c.getLong(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
|
|
||||||
// now insert the missing ngrams and store their ids
|
|
||||||
for (String ngram : missingNgrams)
|
|
||||||
{
|
|
||||||
values.put(NGramColumns.TEXT, ngram);
|
|
||||||
ngramIds.add(db.insert(FTS_NGRAM_TABLE, null, values));
|
|
||||||
}
|
|
||||||
return ngramIds;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static void updateEntry(SQLiteDatabase db, long taskId, long propertyId, int type, String searchableText)
|
|
||||||
{
|
|
||||||
// generate nGrams
|
|
||||||
Set<String> propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText);
|
|
||||||
propertyNgrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchableText));
|
|
||||||
|
|
||||||
// get an ID for each of the Ngrams.
|
|
||||||
Set<Long> ngramIds = ngramIds(db, propertyNgrams);
|
|
||||||
|
|
||||||
// unlink unused ngrams from the task and get the missing ones we have to link to the tak
|
|
||||||
Set<Long> missing = syncNgrams(db, taskId, propertyId, type, ngramIds);
|
|
||||||
|
|
||||||
// insert ngram relations for all new ngrams
|
|
||||||
addNgrams(db, missing, taskId, propertyId, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts NGrams relations for a task entry.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* A writable {@link SQLiteDatabase}.
|
|
||||||
* @param ngramIds
|
|
||||||
* The set of NGram ids.
|
|
||||||
* @param taskId
|
|
||||||
* The row id of the task.
|
|
||||||
* @param propertyId
|
|
||||||
* The row id of the property.
|
|
||||||
*/
|
|
||||||
private static void addNgrams(SQLiteDatabase db, Set<Long> ngramIds, long taskId, Long propertyId, int contentType)
|
|
||||||
{
|
|
||||||
ContentValues values = new ContentValues(4);
|
|
||||||
for (Long ngramId : ngramIds)
|
|
||||||
{
|
|
||||||
values.put(FTSContentColumns.TASK_ID, taskId);
|
|
||||||
values.put(FTSContentColumns.NGRAM_ID, ngramId);
|
|
||||||
values.put(FTSContentColumns.TYPE, contentType);
|
|
||||||
if (contentType == SearchableTypes.PROPERTY)
|
|
||||||
{
|
|
||||||
values.put(FTSContentColumns.PROPERTY_ID, propertyId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(FTSContentColumns.PROPERTY_ID);
|
|
||||||
}
|
|
||||||
db.insert(FTS_CONTENT_TABLE, null, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronizes the NGram relations of a task
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The writable {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The task row id.
|
|
||||||
* @param propertyId
|
|
||||||
* The property row id, ignored if <code>contentType</code> is not {@link SearchableTypes#PROPERTY}.
|
|
||||||
* @param contentType
|
|
||||||
* The {@link SearchableTypes} type.
|
|
||||||
* @param ngramsIds
|
|
||||||
* The set of ngrams ids which should be linked to the task
|
|
||||||
*
|
|
||||||
* @return The number of deleted relations.
|
|
||||||
*/
|
|
||||||
private static Set<Long> syncNgrams(SQLiteDatabase db, long taskId, long propertyId, int contentType, Set<Long> ngramsIds)
|
|
||||||
{
|
|
||||||
String selection;
|
|
||||||
String[] selectionArgs;
|
|
||||||
if (SearchableTypes.PROPERTY == contentType)
|
|
||||||
{
|
|
||||||
selection = PROPERTY_NGRAM_SELECTION;
|
|
||||||
selectionArgs = new String[] { String.valueOf(taskId), String.valueOf(contentType), String.valueOf(propertyId) };
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selection = NON_PROPERTY_NGRAM_SELECTION;
|
|
||||||
selectionArgs = new String[] { String.valueOf(taskId), String.valueOf(contentType) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// In order to sync the ngrams, we go over each existing ngram and delete ngram relations not in the set of new ngrams
|
|
||||||
// Then we return the set of ngrams we didn't find
|
|
||||||
Set<Long> missing = new HashSet<>(ngramsIds);
|
|
||||||
try (Cursor c = db.query(FTS_CONTENT_TABLE, NGRAM_SYNC_COLUMNS, selection, selectionArgs, null, null, null))
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
Long ngramId = c.getLong(1);
|
|
||||||
if (!ngramsIds.contains(ngramId))
|
|
||||||
{
|
|
||||||
db.delete(FTS_CONTENT_TABLE, "_rowid_ = ?", new String[] { c.getString(0) });
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// this ngram wasn't missing
|
|
||||||
missing.remove(ngramId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return missing;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queries the task database to get a cursor with the search results.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param searchString
|
|
||||||
* The search query string.
|
|
||||||
* @param projection
|
|
||||||
* The database projection for the query.
|
|
||||||
* @param selection
|
|
||||||
* The selection for the query.
|
|
||||||
* @param selectionArgs
|
|
||||||
* The arguments for the query.
|
|
||||||
* @param sortOrder
|
|
||||||
* The sorting order of the query.
|
|
||||||
*
|
|
||||||
* @return A cursor of the task database with the search result.
|
|
||||||
*/
|
|
||||||
public static Cursor getTaskSearchCursor(SQLiteDatabase db, String searchString, String[] projection, String selection, String[] selectionArgs,
|
|
||||||
String sortOrder)
|
|
||||||
{
|
|
||||||
|
|
||||||
StringBuilder selectionBuilder = new StringBuilder(1024);
|
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(selection))
|
|
||||||
{
|
|
||||||
selectionBuilder.append(" (");
|
|
||||||
selectionBuilder.append(selection);
|
|
||||||
selectionBuilder.append(") AND (");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selectionBuilder.append(" (");
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<String> ngrams = TRIGRAM_GENERATOR.getNgrams(searchString);
|
|
||||||
ngrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchString));
|
|
||||||
|
|
||||||
String[] queryArgs;
|
|
||||||
|
|
||||||
if (searchString != null && searchString.length() > 1)
|
|
||||||
{
|
|
||||||
|
|
||||||
selectionBuilder.append(NGramColumns.TEXT);
|
|
||||||
selectionBuilder.append(" in (");
|
|
||||||
|
|
||||||
for (int i = 0, count = ngrams.size(); i < count; ++i)
|
|
||||||
{
|
|
||||||
if (i > 0)
|
|
||||||
{
|
|
||||||
selectionBuilder.append(",");
|
|
||||||
}
|
|
||||||
selectionBuilder.append("?");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// selection arguments
|
|
||||||
if (selectionArgs != null && selectionArgs.length > 0)
|
|
||||||
{
|
|
||||||
queryArgs = new String[selectionArgs.length + ngrams.size() + 1];
|
|
||||||
queryArgs[0] = String.valueOf(ngrams.size());
|
|
||||||
System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length);
|
|
||||||
String[] ngramArray = ngrams.toArray(new String[ngrams.size()]);
|
|
||||||
System.arraycopy(ngramArray, 0, queryArgs, selectionArgs.length + 1, ngramArray.length);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
String[] temp = ngrams.toArray(new String[ngrams.size()]);
|
|
||||||
|
|
||||||
queryArgs = new String[temp.length + 1];
|
|
||||||
queryArgs[0] = String.valueOf(ngrams.size());
|
|
||||||
System.arraycopy(temp, 0, queryArgs, 1, temp.length);
|
|
||||||
}
|
|
||||||
selectionBuilder.append(" ) ");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selectionBuilder.append(NGramColumns.TEXT);
|
|
||||||
selectionBuilder.append(" like ?");
|
|
||||||
|
|
||||||
// selection arguments
|
|
||||||
if (selectionArgs != null && selectionArgs.length > 0)
|
|
||||||
{
|
|
||||||
queryArgs = new String[selectionArgs.length + 2];
|
|
||||||
queryArgs[0] = String.valueOf(ngrams.size());
|
|
||||||
System.arraycopy(selectionArgs, 0, queryArgs, 1, selectionArgs.length);
|
|
||||||
queryArgs[queryArgs.length - 1] = " " + searchString + "%";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
queryArgs = new String[2];
|
|
||||||
queryArgs[0] = String.valueOf(ngrams.size());
|
|
||||||
queryArgs[1] = " " + searchString + "%";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionBuilder.append(") AND ");
|
|
||||||
selectionBuilder.append(Tasks._DELETED);
|
|
||||||
selectionBuilder.append(" = 0");
|
|
||||||
|
|
||||||
if (sortOrder == null)
|
|
||||||
{
|
|
||||||
sortOrder = Tasks.SCORE + " desc";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
sortOrder = Tasks.SCORE + " desc, " + sortOrder;
|
|
||||||
}
|
|
||||||
Cursor c = db.rawQueryWithFactory(null,
|
|
||||||
String.format(SQL_RAW_QUERY_SEARCH_TASK, SQL_RAW_QUERY_SEARCH_TASK_DEFAULT_PROJECTION, selectionBuilder.toString(), sortOrder), queryArgs,
|
|
||||||
null);
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +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.provider.tasks;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public enum ProviderOperation
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert operations.
|
|
||||||
*/
|
|
||||||
INSERT,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update operations.
|
|
||||||
*/
|
|
||||||
UPDATE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete operations.
|
|
||||||
*/
|
|
||||||
DELETE
|
|
||||||
}
|
|
||||||
@@ -1,364 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2009 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.provider.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentProviderOperation;
|
|
||||||
import android.content.ContentProviderResult;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.OperationApplicationException;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.SingletonIterable;
|
|
||||||
import org.dmfs.jems.fragile.Fragile;
|
|
||||||
import org.dmfs.jems.iterable.composite.Joined;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.provider.tasks.utils.Profiled;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* Changed by marten@dmfs.org:
|
|
||||||
*
|
|
||||||
* removed protected mDb field and replaced it by local fields. There is no reason to store the database if we get a new one for every transaction. Instead we
|
|
||||||
* also pass the database to the *InTransaction methods.
|
|
||||||
*
|
|
||||||
* update visibility of class and methods
|
|
||||||
*/
|
|
||||||
abstract class SQLiteContentProvider extends ContentProvider
|
|
||||||
{
|
|
||||||
|
|
||||||
interface TransactionEndTask
|
|
||||||
{
|
|
||||||
void execute(SQLiteDatabase database);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
private static final String TAG = "SQLiteContentProvider";
|
|
||||||
|
|
||||||
private SQLiteOpenHelper mOpenHelper;
|
|
||||||
private final Set<Uri> mChangedUris = new HashSet<>();
|
|
||||||
|
|
||||||
private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
|
|
||||||
private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum number of operations allowed in a batch between yield points.
|
|
||||||
*/
|
|
||||||
private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
|
|
||||||
|
|
||||||
private final Iterable<TransactionEndTask> mTransactionEndTasks;
|
|
||||||
|
|
||||||
|
|
||||||
protected SQLiteContentProvider(Iterable<TransactionEndTask> transactionEndTasks)
|
|
||||||
{
|
|
||||||
// append a task to set the transaction to successful
|
|
||||||
mTransactionEndTasks = new Joined<>(transactionEndTasks, new SingletonIterable<>(new SuccessfulTransactionEndTask()));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate()
|
|
||||||
{
|
|
||||||
mOpenHelper = getDatabaseHelper(getContext());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link SQLiteOpenHelper} that can open the database.
|
|
||||||
*/
|
|
||||||
protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of the {@link #insert} method, but invoked within a transaction.
|
|
||||||
*/
|
|
||||||
public abstract Uri insertInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, boolean callerIsSyncAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of the {@link #update} method, but invoked within a transaction.
|
|
||||||
*/
|
|
||||||
public abstract int updateInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs,
|
|
||||||
boolean callerIsSyncAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of the {@link #delete} method, but invoked within a transaction.
|
|
||||||
*/
|
|
||||||
public abstract int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this to add a URI to the list of URIs to be notified when the transaction is committed.
|
|
||||||
*/
|
|
||||||
protected void postNotifyUri(Uri uri)
|
|
||||||
{
|
|
||||||
synchronized (mChangedUris)
|
|
||||||
{
|
|
||||||
mChangedUris.add(uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isCallerSyncAdapter(Uri uri)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public SQLiteOpenHelper getDatabaseHelper()
|
|
||||||
{
|
|
||||||
return mOpenHelper;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean applyingBatch()
|
|
||||||
{
|
|
||||||
return mApplyingBatch.get() != null && mApplyingBatch.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(Uri uri, ContentValues values)
|
|
||||||
{
|
|
||||||
return new Profiled("Insert").run((Single<Uri>) () ->
|
|
||||||
{
|
|
||||||
Uri result;
|
|
||||||
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
|
|
||||||
boolean applyingBatch = applyingBatch();
|
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
||||||
if (!applyingBatch)
|
|
||||||
{
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
result = insertInTransaction(db, uri, values, callerIsSyncAdapter);
|
|
||||||
endTransaction(db);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
onEndTransaction(callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
result = insertInTransaction(db, uri, values, callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int bulkInsert(Uri uri, ContentValues[] values)
|
|
||||||
{
|
|
||||||
return new Profiled("BulkInsert").run((Single<Integer>) () ->
|
|
||||||
{
|
|
||||||
int numValues = values.length;
|
|
||||||
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
|
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
for (int i = 0; i < numValues; i++)
|
|
||||||
{
|
|
||||||
insertInTransaction(db, uri, values[i], callerIsSyncAdapter);
|
|
||||||
db.yieldIfContendedSafely();
|
|
||||||
}
|
|
||||||
endTransaction(db);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
onEndTransaction(callerIsSyncAdapter);
|
|
||||||
return numValues;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
|
|
||||||
{
|
|
||||||
return new Profiled("Update").run((Single<Integer>) () ->
|
|
||||||
{
|
|
||||||
int count;
|
|
||||||
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
|
|
||||||
boolean applyingBatch = applyingBatch();
|
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
||||||
if (!applyingBatch)
|
|
||||||
{
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter);
|
|
||||||
endTransaction(db);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
onEndTransaction(callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(Uri uri, String selection, String[] selectionArgs)
|
|
||||||
{
|
|
||||||
return new Profiled("Delete").run((Single<Integer>) () ->
|
|
||||||
{
|
|
||||||
int count;
|
|
||||||
boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
|
|
||||||
boolean applyingBatch = applyingBatch();
|
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
||||||
if (!applyingBatch)
|
|
||||||
{
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter);
|
|
||||||
endTransaction(db);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
onEndTransaction(callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
return count;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations) throws OperationApplicationException
|
|
||||||
{
|
|
||||||
return new Profiled(String.format(Locale.ENGLISH, "Batch of %d operations", operations.size())).run(
|
|
||||||
(Fragile<ContentProviderResult[], OperationApplicationException>) () ->
|
|
||||||
{
|
|
||||||
int ypCount = 0;
|
|
||||||
int opCount = 0;
|
|
||||||
boolean callerIsSyncAdapter = false;
|
|
||||||
SQLiteDatabase db = mOpenHelper.getWritableDatabase();
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
mApplyingBatch.set(true);
|
|
||||||
final int numOperations = operations.size();
|
|
||||||
final ContentProviderResult[] results = new ContentProviderResult[numOperations];
|
|
||||||
for (int i = 0; i < numOperations; i++)
|
|
||||||
{
|
|
||||||
if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT)
|
|
||||||
{
|
|
||||||
throw new OperationApplicationException("Too many content provider operations between yield points. "
|
|
||||||
+ "The maximum number of operations per yield point is " + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
|
|
||||||
}
|
|
||||||
final ContentProviderOperation operation = operations.get(i);
|
|
||||||
if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri()))
|
|
||||||
{
|
|
||||||
callerIsSyncAdapter = true;
|
|
||||||
}
|
|
||||||
if (i > 0 && operation.isYieldAllowed())
|
|
||||||
{
|
|
||||||
opCount = 0;
|
|
||||||
if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY))
|
|
||||||
{
|
|
||||||
ypCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
results[i] = operation.apply(this, results, i);
|
|
||||||
}
|
|
||||||
endTransaction(db);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
mApplyingBatch.set(false);
|
|
||||||
db.endTransaction();
|
|
||||||
onEndTransaction(callerIsSyncAdapter);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected void onEndTransaction(boolean callerIsSyncAdapter)
|
|
||||||
{
|
|
||||||
Set<Uri> changed;
|
|
||||||
synchronized (mChangedUris)
|
|
||||||
{
|
|
||||||
changed = new HashSet<Uri>(mChangedUris);
|
|
||||||
mChangedUris.clear();
|
|
||||||
}
|
|
||||||
ContentResolver resolver = getContext().getContentResolver();
|
|
||||||
for (Uri uri : changed)
|
|
||||||
{
|
|
||||||
boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
|
|
||||||
resolver.notifyChange(uri, null, syncToNetwork);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected boolean syncToNetwork(Uri uri)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void endTransaction(SQLiteDatabase database)
|
|
||||||
{
|
|
||||||
for (TransactionEndTask task : mTransactionEndTasks)
|
|
||||||
{
|
|
||||||
task.execute(database);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link TransactionEndTask} which sets the transaction to be successful.
|
|
||||||
*/
|
|
||||||
private static class SuccessfulTransactionEndTask implements TransactionEndTask
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public void execute(SQLiteDatabase database)
|
|
||||||
{
|
|
||||||
database.setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,895 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.database.sqlite.SQLiteOpenHelper;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.adapters.First;
|
|
||||||
import org.dmfs.jems.predicate.elementary.Equals;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.provider.tasks.processors.NoOpProcessor;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.Instantiating;
|
|
||||||
import org.dmfs.provider.tasks.utils.TableColumns;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Properties;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Alarm;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Category;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskLists;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Task database helper takes care of creating and updating the task database, including tables, indices and triggers.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class TaskDatabaseHelper extends SQLiteOpenHelper
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface of a listener that's called when the database has been created or migrated.
|
|
||||||
*/
|
|
||||||
public interface OnDatabaseOperationListener
|
|
||||||
{
|
|
||||||
void onDatabaseCreated(SQLiteDatabase db);
|
|
||||||
|
|
||||||
void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static final String TAG = "TaskDatabaseHelper";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name of our database file.
|
|
||||||
*/
|
|
||||||
private static final String DATABASE_NAME = "tasks.db";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The database version.
|
|
||||||
*/
|
|
||||||
private static final int DATABASE_VERSION = 23;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of all tables we provide.
|
|
||||||
*/
|
|
||||||
public interface Tables
|
|
||||||
{
|
|
||||||
String LISTS = "Lists";
|
|
||||||
|
|
||||||
String WRITEABLE_LISTS = "Writeable_Lists";
|
|
||||||
|
|
||||||
String TASKS = "Tasks";
|
|
||||||
|
|
||||||
String TASKS_VIEW = "Task_View";
|
|
||||||
|
|
||||||
String TASKS_PROPERTY_VIEW = "Task_Property_View";
|
|
||||||
|
|
||||||
String INSTANCES = "Instances";
|
|
||||||
|
|
||||||
String INSTANCE_VIEW = "Instance_View";
|
|
||||||
|
|
||||||
String INSTANCE_CLIENT_VIEW = "Instance_Client_View";
|
|
||||||
|
|
||||||
String INSTANCE_PROPERTY_VIEW = "Instance_Property_View";
|
|
||||||
|
|
||||||
String INSTANCE_CATEGORY_VIEW = "Instance_Cagetory_View";
|
|
||||||
|
|
||||||
String CATEGORIES = "Categories";
|
|
||||||
|
|
||||||
String CATEGORIES_MAPPING = "Categories_Mapping";
|
|
||||||
|
|
||||||
String PROPERTIES = "Properties";
|
|
||||||
|
|
||||||
String ALARMS = "Alarms";
|
|
||||||
|
|
||||||
String SYNCSTATE = "SyncState";
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Columns of internal table for the category mapping.
|
|
||||||
*/
|
|
||||||
public interface CategoriesMapping
|
|
||||||
{
|
|
||||||
String TASK_ID = "task_id";
|
|
||||||
|
|
||||||
String CATEGORY_ID = "category_id";
|
|
||||||
|
|
||||||
String PROPERTY_ID = "property_id";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines tasks with some data from the list they belong to.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASK_VIEW = "create view " + Tables.TASKS_VIEW + " as select " +
|
|
||||||
Tables.TASKS + ".*, " +
|
|
||||||
Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_OWNER + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_NAME + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_COLOR + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.VISIBLE +
|
|
||||||
" from " + Tables.TASKS + " join " + Tables.LISTS +
|
|
||||||
" on (" + Tables.TASKS + "." + Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskLists._ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines tasks with some data from the list they belong to.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASK_PROPERTY_VIEW = "create view " + Tables.TASKS_PROPERTY_VIEW + " as select " +
|
|
||||||
Tables.TASKS + ".*, " +
|
|
||||||
Tables.PROPERTIES + ".*, " +
|
|
||||||
Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_OWNER + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_NAME + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.LIST_COLOR + ", " +
|
|
||||||
Tables.LISTS + "." + Tasks.VISIBLE +
|
|
||||||
" from " + Tables.TASKS + " join " + Tables.LISTS +
|
|
||||||
" on (" + Tables.TASKS + "." + Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskLists._ID + ") " +
|
|
||||||
"left join " + Tables.PROPERTIES + " on (" + Tables.TASKS + "." + Tasks._ID + "=" + Tables.PROPERTIES + "." + Properties.TASK_ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to drop the task view.
|
|
||||||
*/
|
|
||||||
private final static String SQL_DROP_TASK_VIEW = "DROP VIEW " + Tables.TASKS_VIEW + ";";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines task instances with some data from the list they belong to.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_INSTANCE_VIEW = "CREATE VIEW " + Tables.INSTANCE_VIEW + " AS SELECT "
|
|
||||||
+ Tables.INSTANCES + ".*, "
|
|
||||||
+ Tables.TASKS + ".*, "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_OWNER + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_COLOR + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.VISIBLE
|
|
||||||
+ " FROM " + Tables.TASKS
|
|
||||||
+ " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")"
|
|
||||||
+ " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines task instances with some data from the list they belong to. This replaces the task DTSTART, DUE and
|
|
||||||
* ORIGINAL_INSTANCE_TIME values with respective values of the instance.
|
|
||||||
* <p>
|
|
||||||
* This is the instances view as seen by the content provider clients.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_INSTANCE_CLIENT_VIEW = "CREATE VIEW " + Tables.INSTANCE_CLIENT_VIEW + " AS SELECT "
|
|
||||||
+ Tables.INSTANCES + ".*, "
|
|
||||||
// override task due, start and original times with the instance values
|
|
||||||
+ Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_START + " as " + Tasks.DTSTART + ", "
|
|
||||||
+ Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_DUE + " as " + Tasks.DUE + ", "
|
|
||||||
+ Tables.INSTANCES + "." + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " as " + Tasks.ORIGINAL_INSTANCE_TIME + ", "
|
|
||||||
// override task duration with null, we already have a due
|
|
||||||
+ "null as " + Tasks.DURATION + ", "
|
|
||||||
// override recurrence values with null, instances themselves are not recurring
|
|
||||||
+ "null as " + Tasks.RRULE + ", "
|
|
||||||
+ "null as " + Tasks.RDATE + ", "
|
|
||||||
+ "null as " + Tasks.EXDATE + ", "
|
|
||||||
// this instance is part of a recurring task if either it has recurrence values or overrides an instance
|
|
||||||
+ "not (" + Tasks.RRULE + " is null and " + Tasks.RDATE + " is null and " + Tasks.ORIGINAL_INSTANCE_ID + " is null and " + Tasks.ORIGINAL_INSTANCE_SYNC_ID + " is null) as " + TaskContract.Instances.IS_RECURRING + ", "
|
|
||||||
+ Tables.TASKS + ".*, "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_OWNER + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_COLOR + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.VISIBLE
|
|
||||||
+ " FROM " + Tables.TASKS
|
|
||||||
+ " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.TaskLists._ID + ")"
|
|
||||||
+ " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines task instances view with the belonging properties.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_INSTANCE_PROPERTY_VIEW = "CREATE VIEW " + Tables.INSTANCE_PROPERTY_VIEW + " AS SELECT "
|
|
||||||
+ Tables.INSTANCES + ".*, "
|
|
||||||
+ Tables.PROPERTIES + ".*, "
|
|
||||||
+ Tables.TASKS + ".*, "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_OWNER + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_COLOR + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.VISIBLE
|
|
||||||
+ " FROM " + Tables.TASKS
|
|
||||||
+ " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")"
|
|
||||||
+ " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ")"
|
|
||||||
+ " LEFT JOIN " + Tables.PROPERTIES + " ON (" + Tables.TASKS + "." + Tasks._ID + "=" + Tables.PROPERTIES + "." + Properties.TASK_ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a view that combines task instances with some data from the list they belong to.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_INSTANCE_CATEGORY_VIEW = "CREATE VIEW " + Tables.INSTANCE_CATEGORY_VIEW + " AS SELECT "
|
|
||||||
+ Tables.INSTANCES + ".*, "
|
|
||||||
+ Tables.CATEGORIES_MAPPING + "." + CategoriesMapping.CATEGORY_ID + ", "
|
|
||||||
+ Tables.TASKS + ".*, "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.ACCOUNT_TYPE + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_OWNER + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_NAME + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_ACCESS_LEVEL + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.LIST_COLOR + ", "
|
|
||||||
+ Tables.LISTS + "." + Tasks.VISIBLE
|
|
||||||
+ " FROM " + Tables.TASKS
|
|
||||||
+ " JOIN " + Tables.LISTS + " ON (" + Tables.TASKS + "." + TaskContract.Tasks.LIST_ID + "=" + Tables.LISTS + "." + TaskContract.Tasks._ID + ")"
|
|
||||||
+ " JOIN " + Tables.INSTANCES + " ON (" + Tables.TASKS + "." + TaskContract.Tasks._ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ")"
|
|
||||||
+ " LEFT JOIN " + Tables.CATEGORIES_MAPPING + " ON (" + Tables.CATEGORIES_MAPPING + "." + CategoriesMapping.TASK_ID + "=" + Tables.INSTANCES + "." + TaskContract.Instances.TASK_ID + ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to drop the instance view.
|
|
||||||
*/
|
|
||||||
private final static String SQL_DROP_INSTANCE_VIEW = "DROP VIEW " + Tables.INSTANCE_VIEW + ";";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to drop the instance property view.
|
|
||||||
*/
|
|
||||||
//private final static String SQL_DROP_INSTANCE_PROPERTY_VIEW = "DROP VIEW " + Tables.INSTANCE_PROPERTY_VIEW + ";";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the instances table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_SYNCSTATE_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.SYNCSTATE + " ( " +
|
|
||||||
TaskContract.SyncState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
||||||
+ TaskContract.SyncState.ACCOUNT_NAME + " TEXT, "
|
|
||||||
+ TaskContract.SyncState.ACCOUNT_TYPE + " TEXT, "
|
|
||||||
+ TaskContract.SyncState.DATA + " TEXT "
|
|
||||||
+ ");";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the instances table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_INSTANCES_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.INSTANCES + " ( " +
|
|
||||||
TaskContract.Instances._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
||||||
+ TaskContract.Instances.TASK_ID + " INTEGER NOT NULL, " // NOT NULL
|
|
||||||
+ TaskContract.Instances.INSTANCE_START + " INTEGER, "
|
|
||||||
+ TaskContract.Instances.INSTANCE_DUE + " INTEGER, "
|
|
||||||
+ TaskContract.Instances.INSTANCE_START_SORTING + " INTEGER, "
|
|
||||||
+ TaskContract.Instances.INSTANCE_DUE_SORTING + " INTEGER, "
|
|
||||||
+ TaskContract.Instances.INSTANCE_DURATION + " INTEGER, "
|
|
||||||
+ TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " INTEGER DEFAULT 0, "
|
|
||||||
+ TaskContract.Instances.DISTANCE_FROM_CURRENT + " INTEGER DEFAULT 0);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to clean up data of removed tasks.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASKS_CLEANUP_TRIGGER =
|
|
||||||
"CREATE TRIGGER task_cleanup_trigger AFTER DELETE ON " + Tables.TASKS
|
|
||||||
+ " BEGIN "
|
|
||||||
+ " DELETE FROM " + Tables.PROPERTIES + " WHERE " + TaskContract.Properties.TASK_ID + "= old." + TaskContract.Tasks._ID + ";"
|
|
||||||
+ " DELETE FROM " + Tables.INSTANCES + " WHERE " + TaskContract.Instances.TASK_ID + "=old." + TaskContract.Tasks._ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to clean up data of removed lists.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_LISTS_CLEANUP_TRIGGER =
|
|
||||||
"CREATE TRIGGER list_cleanup_trigger AFTER DELETE ON " + Tables.LISTS
|
|
||||||
+ " BEGIN "
|
|
||||||
+ " DELETE FROM " + Tables.TASKS + " WHERE " + Tasks.LIST_ID + "= old." + TaskLists._ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to drop the clean up trigger.
|
|
||||||
*/
|
|
||||||
private final static String SQL_DROP_TASKS_CLEANUP_TRIGGER =
|
|
||||||
"DROP TRIGGER task_cleanup_trigger;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command that counts and sets the alarm on deletion
|
|
||||||
*/
|
|
||||||
private final static String SQL_COUNT_ALARMS_ON_DELETE =
|
|
||||||
" BEGIN UPDATE " + Tables.TASKS + " SET " + Tasks.HAS_ALARMS
|
|
||||||
+ " = (SELECT COUNT (*) FROM " + Tables.PROPERTIES
|
|
||||||
+ " WHERE " + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "' AND " + Alarm.ALARM_TYPE + " <> " + Alarm.ALARM_TYPE_NOTHING + " AND " + Properties.TASK_ID + " = OLD." + Properties.TASK_ID
|
|
||||||
+ ") WHERE " + Tasks._ID + " = OLD." + Properties.TASK_ID
|
|
||||||
+ "; END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command that counts and sets the alarm on insert and update
|
|
||||||
*/
|
|
||||||
private final static String SQL_COUNT_ALARMS =
|
|
||||||
" BEGIN UPDATE " + Tables.TASKS + " SET " + Tasks.HAS_ALARMS
|
|
||||||
+ " = (SELECT COUNT (*) FROM " + Tables.PROPERTIES
|
|
||||||
+ " WHERE " + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "' AND " + Alarm.ALARM_TYPE + " <> " + Alarm.ALARM_TYPE_NOTHING + " AND " + Properties.TASK_ID + " = NEW." + Properties.TASK_ID
|
|
||||||
+ ") WHERE " + Tasks._ID + " = NEW." + Properties.TASK_ID
|
|
||||||
+ "; END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger that counts the alarms for a task on create
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER =
|
|
||||||
"CREATE TRIGGER alarm_count_create_trigger AFTER INSERT ON " + Tables.PROPERTIES + " WHEN NEW." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'"
|
|
||||||
+ SQL_COUNT_ALARMS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger that counts the alarms for a task on update
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER =
|
|
||||||
"CREATE TRIGGER alarm_count_update_trigger AFTER UPDATE ON " + Tables.PROPERTIES + " WHEN NEW." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'"
|
|
||||||
+ SQL_COUNT_ALARMS;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger that counts the alarms for a task on delete
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER =
|
|
||||||
"CREATE TRIGGER alarm_count_delete_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'"
|
|
||||||
+ SQL_COUNT_ALARMS_ON_DELETE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to clean up data of removed property.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER =
|
|
||||||
"CREATE TRIGGER alarm_property_cleanup_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Alarm.CONTENT_ITEM_TYPE + "'"
|
|
||||||
+ " BEGIN "
|
|
||||||
+ " DELETE FROM " + Tables.ALARMS + " WHERE " + TaskContract.Alarms.ALARM_ID + "= OLD." + TaskContract.Properties.PROPERTY_ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to clean up data of removed property.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER =
|
|
||||||
"CREATE TRIGGER category_property_cleanup_trigger AFTER DELETE ON " + Tables.PROPERTIES + " WHEN OLD." + Properties.MIMETYPE + " = '" + Category.CONTENT_ITEM_TYPE + "'"
|
|
||||||
+ " BEGIN "
|
|
||||||
+ " DELETE FROM " + Tables.CATEGORIES_MAPPING + " WHERE " + CategoriesMapping.PROPERTY_ID + "= OLD." + TaskContract.Properties.PROPERTY_ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to clean up property data of removed task.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER =
|
|
||||||
"CREATE TRIGGER task_property_cleanup_trigger AFTER DELETE ON " + Tables.TASKS + " BEGIN "
|
|
||||||
+ " DELETE FROM " + Tables.PROPERTIES + " WHERE " + Properties.TASK_ID + "= OLD." + Tasks._ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create a trigger to increment task version number on every update.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASK_VERSION_TRIGGER =
|
|
||||||
"CREATE TRIGGER task_version_trigger BEFORE UPDATE ON " + Tables.TASKS + " BEGIN "
|
|
||||||
+ " UPDATE " + Tables.TASKS + " SET " + Tasks.VERSION + " = OLD." + Tasks.VERSION + " + 1 where " + Tasks._ID + " = NEW." + Tasks._ID + ";"
|
|
||||||
+ " END;";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the task list table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_LISTS_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.LISTS + " ( "
|
|
||||||
+ TaskContract.TaskLists._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
||||||
+ TaskContract.TaskLists.ACCOUNT_NAME + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.ACCOUNT_TYPE + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.LIST_NAME + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.LIST_COLOR + " INTEGER,"
|
|
||||||
+ TaskContract.TaskLists.ACCESS_LEVEL + " INTEGER,"
|
|
||||||
+ TaskContract.TaskLists.VISIBLE + " INTEGER,"
|
|
||||||
+ TaskContract.TaskLists.SYNC_ENABLED + " INTEGER,"
|
|
||||||
+ TaskContract.TaskLists.OWNER + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists._DIRTY + " INTEGER DEFAULT 0,"
|
|
||||||
+ TaskContract.TaskLists._SYNC_ID + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC_VERSION + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC1 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC2 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC3 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC4 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC5 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC6 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC7 + " TEXT,"
|
|
||||||
+ TaskContract.TaskLists.SYNC8 + " TEXT);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the task table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_TASKS_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.TASKS + " ( "
|
|
||||||
+ TaskContract.Tasks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
||||||
+ TaskContract.Tasks.VERSION + " INTEGER DEFAULT 0,"
|
|
||||||
+ TaskContract.Tasks.LIST_ID + " INTEGER NOT NULL, "
|
|
||||||
+ TaskContract.Tasks.TITLE + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.LOCATION + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.GEO + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.DESCRIPTION + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.URL + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.ORGANIZER + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.PRIORITY + " INTEGER, "
|
|
||||||
+ TaskContract.Tasks.TASK_COLOR + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.CLASSIFICATION + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.COMPLETED + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.COMPLETED_IS_ALLDAY + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.PERCENT_COMPLETE + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.STATUS + " INTEGER DEFAULT " + TaskContract.Tasks.STATUS_DEFAULT + ","
|
|
||||||
+ TaskContract.Tasks.IS_NEW + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.IS_CLOSED + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.DTSTART + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.CREATED + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.LAST_MODIFIED + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.IS_ALLDAY + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.TZ + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.DUE + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.DURATION + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.RDATE + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.EXDATE + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.RRULE + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.PARENT_ID + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.SORTING + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.HAS_ALARMS + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.HAS_PROPERTIES + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.PINNED + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.ORIGINAL_INSTANCE_TIME + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY + " INTEGER,"
|
|
||||||
+ TaskContract.Tasks._DIRTY + " INTEGER DEFAULT 1," // a new task is always dirty
|
|
||||||
+ TaskContract.Tasks._DELETED + " INTEGER DEFAULT 0," // new tasks are not deleted by default
|
|
||||||
+ TaskContract.Tasks._SYNC_ID + " TEXT,"
|
|
||||||
+ TaskContract.Tasks._UID + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC_VERSION + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC1 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC2 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC3 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC4 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC5 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC6 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC7 + " TEXT,"
|
|
||||||
+ TaskContract.Tasks.SYNC8 + " TEXT);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the categories table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_CATEGORIES_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.CATEGORIES
|
|
||||||
+ " ( " + TaskContract.Categories._ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
||||||
+ TaskContract.Categories.ACCOUNT_NAME + " TEXT,"
|
|
||||||
+ TaskContract.Categories.ACCOUNT_TYPE + " TEXT,"
|
|
||||||
+ TaskContract.Categories.NAME + " TEXT,"
|
|
||||||
+ TaskContract.Categories.COLOR + " INTEGER);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the categories table.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_CATEGORIES_MAPPING_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.CATEGORIES_MAPPING
|
|
||||||
+ " ( " + CategoriesMapping.TASK_ID + " INTEGER,"
|
|
||||||
+ CategoriesMapping.CATEGORY_ID + " INTEGER,"
|
|
||||||
+ CategoriesMapping.PROPERTY_ID + " INTEGER,"
|
|
||||||
+ "FOREIGN KEY (" + CategoriesMapping.TASK_ID + ") REFERENCES " + Tables.TASKS + "(" + TaskContract.Tasks._ID + "),"
|
|
||||||
+ "FOREIGN KEY (" + CategoriesMapping.PROPERTY_ID + ") REFERENCES " + Tables.PROPERTIES + "(" + TaskContract.Properties.PROPERTY_ID + "),"
|
|
||||||
+ "FOREIGN KEY (" + CategoriesMapping.CATEGORY_ID + ") REFERENCES " + Tables.CATEGORIES + "(" + TaskContract.Categories._ID + "));";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the alarms table the stores the already triggered alarms.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_ALARMS_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.ALARMS
|
|
||||||
+ " ( " + TaskContract.Alarms.ALARM_ID + " INTEGER,"
|
|
||||||
+ TaskContract.Alarms.LAST_TRIGGER + " TEXT,"
|
|
||||||
+ TaskContract.Alarms.NEXT_TRIGGER + " TEXT);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to create the table for extended properties.
|
|
||||||
*/
|
|
||||||
private final static String SQL_CREATE_PROPERTIES_TABLE =
|
|
||||||
"CREATE TABLE " + Tables.PROPERTIES + " ( "
|
|
||||||
+ TaskContract.Properties.PROPERTY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
|
|
||||||
+ TaskContract.Properties.TASK_ID + " INTEGER,"
|
|
||||||
+ TaskContract.Properties.MIMETYPE + " INTEGER,"
|
|
||||||
+ TaskContract.Properties.VERSION + " INTEGER,"
|
|
||||||
+ TaskContract.Properties.DATA0 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA1 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA2 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA3 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA4 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA5 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA6 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA7 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA8 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA9 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA10 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA11 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA12 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA13 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA14 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.DATA15 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC1 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC2 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC3 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC4 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC5 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC6 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC7 + " TEXT,"
|
|
||||||
+ TaskContract.Properties.SYNC8 + " TEXT);";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SQL command to drop the task view.
|
|
||||||
*/
|
|
||||||
private final static String SQL_DROP_PROPERTIES_TABLE = "DROP TABLE " + Tables.PROPERTIES + ";";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a string that creates an index on the given table for the given columns.
|
|
||||||
*
|
|
||||||
* @param table
|
|
||||||
* The table to create the index on.
|
|
||||||
* @param fields
|
|
||||||
* The fields to index.
|
|
||||||
*
|
|
||||||
* @return An SQL command string.
|
|
||||||
*/
|
|
||||||
public static String createIndexString(String table, boolean unique, String... fields)
|
|
||||||
{
|
|
||||||
if (fields == null || fields.length < 1)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("need at least one field to build an index!");
|
|
||||||
}
|
|
||||||
|
|
||||||
StringBuffer buffer = new StringBuffer();
|
|
||||||
|
|
||||||
// Index name is constructed like this: tablename_fields[0]_idx
|
|
||||||
buffer.append("CREATE ");
|
|
||||||
if (unique)
|
|
||||||
{
|
|
||||||
buffer.append(" UNIQUE ");
|
|
||||||
}
|
|
||||||
buffer.append("INDEX IF NOT EXISTS ");
|
|
||||||
buffer.append(table).append("_").append(fields[0]).append("_idx ON ");
|
|
||||||
buffer.append(table).append(" (");
|
|
||||||
buffer.append(fields[0]);
|
|
||||||
for (int i = 1; i < fields.length; i++)
|
|
||||||
{
|
|
||||||
buffer.append(", ").append(fields[i]);
|
|
||||||
}
|
|
||||||
buffer.append(");");
|
|
||||||
|
|
||||||
return buffer.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private final OnDatabaseOperationListener mListener;
|
|
||||||
|
|
||||||
|
|
||||||
TaskDatabaseHelper(Context context, OnDatabaseOperationListener listener)
|
|
||||||
{
|
|
||||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
|
||||||
mListener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the tables, views, triggers and indices.
|
|
||||||
* <p>
|
|
||||||
* TODO: move all strings to separate final static variables.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCreate(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
|
|
||||||
// create task list table
|
|
||||||
db.execSQL(SQL_CREATE_LISTS_TABLE);
|
|
||||||
|
|
||||||
// trigger that removes tasks of a list that has been removed
|
|
||||||
db.execSQL("CREATE TRIGGER task_list_cleanup_trigger AFTER DELETE ON " + Tables.LISTS + " BEGIN DELETE FROM " + Tables.TASKS + " WHERE "
|
|
||||||
+ TaskContract.Tasks.LIST_ID + "= old." + TaskContract.TaskLists._ID + "; END");
|
|
||||||
|
|
||||||
// create task table
|
|
||||||
db.execSQL(SQL_CREATE_TASKS_TABLE);
|
|
||||||
|
|
||||||
// trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted
|
|
||||||
db.execSQL("CREATE TRIGGER task_list_make_dirty_on_update AFTER UPDATE ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET "
|
|
||||||
+ TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new."
|
|
||||||
+ TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END");
|
|
||||||
|
|
||||||
// trigger that marks a list as dirty if a task in that list gets marked as dirty or deleted
|
|
||||||
db.execSQL("CREATE TRIGGER task_list_make_dirty_on_insert AFTER INSERT ON " + Tables.TASKS + " BEGIN UPDATE " + Tables.LISTS + " SET "
|
|
||||||
+ TaskContract.TaskLists._DIRTY + "=" + TaskContract.TaskLists._DIRTY + " + " + "new." + TaskContract.Tasks._DIRTY + " + " + "new."
|
|
||||||
+ TaskContract.Tasks._DELETED + " WHERE " + TaskContract.TaskLists._ID + "= new." + TaskContract.Tasks.LIST_ID + "; END");
|
|
||||||
|
|
||||||
// create task version update trigger
|
|
||||||
db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER);
|
|
||||||
|
|
||||||
// create instances table and view
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCES_TABLE);
|
|
||||||
|
|
||||||
// create categories table
|
|
||||||
db.execSQL(SQL_CREATE_CATEGORIES_TABLE);
|
|
||||||
|
|
||||||
// create categories mapping table
|
|
||||||
db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE);
|
|
||||||
|
|
||||||
// create alarms table
|
|
||||||
db.execSQL(SQL_CREATE_ALARMS_TABLE);
|
|
||||||
|
|
||||||
// create properties table
|
|
||||||
db.execSQL(SQL_CREATE_PROPERTIES_TABLE);
|
|
||||||
|
|
||||||
// create syncstate table
|
|
||||||
db.execSQL(SQL_CREATE_SYNCSTATE_TABLE);
|
|
||||||
|
|
||||||
// create views
|
|
||||||
db.execSQL(SQL_CREATE_TASK_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW);
|
|
||||||
|
|
||||||
// create indices
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.TASK_ID, TaskContract.Instances.INSTANCE_START,
|
|
||||||
TaskContract.Instances.INSTANCE_DUE));
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING));
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING));
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME));
|
|
||||||
db.execSQL(createIndexString(Tables.LISTS, false, TaskContract.TaskLists.ACCOUNT_NAME, // not sure if necessary
|
|
||||||
TaskContract.TaskLists.ACCOUNT_TYPE));
|
|
||||||
db.execSQL(createIndexString(Tables.TASKS, false, TaskContract.Tasks.STATUS, TaskContract.Tasks.LIST_ID, TaskContract.Tasks._SYNC_ID));
|
|
||||||
db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID));
|
|
||||||
db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID));
|
|
||||||
db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE,
|
|
||||||
TaskContract.Categories.NAME));
|
|
||||||
db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME));
|
|
||||||
db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE));
|
|
||||||
|
|
||||||
// trigger that removes properties of a task that has been removed
|
|
||||||
db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER);
|
|
||||||
|
|
||||||
// trigger that removes alarms when an alarm property was deleted
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER);
|
|
||||||
|
|
||||||
// trigger that removes tasks when a list was removed
|
|
||||||
db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER);
|
|
||||||
|
|
||||||
// trigger that counts the alarms for tasks
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER);
|
|
||||||
|
|
||||||
// add cleanup trigger for orphaned properties
|
|
||||||
db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER);
|
|
||||||
|
|
||||||
// initialize FTS
|
|
||||||
FTSDatabaseHelper.onCreate(db);
|
|
||||||
|
|
||||||
if (mListener != null)
|
|
||||||
{
|
|
||||||
mListener.onDatabaseCreated(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Manages the database schema migration.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)
|
|
||||||
{
|
|
||||||
Log.i(TAG, "updgrading db from " + oldVersion + " to " + newVersion);
|
|
||||||
if (oldVersion < 2)
|
|
||||||
{
|
|
||||||
// add IS_NEW and IS_CLOSED columns and update their values
|
|
||||||
db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_NEW + " INTEGER");
|
|
||||||
db.execSQL("ALTER TABLE " + Tables.TASKS + " ADD COLUMN " + TaskContract.Tasks.IS_CLOSED + " INTEGER");
|
|
||||||
db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 1 WHERE " + TaskContract.Tasks.STATUS + " = "
|
|
||||||
+ TaskContract.Tasks.STATUS_NEEDS_ACTION);
|
|
||||||
db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_NEW + " = 0 WHERE " + TaskContract.Tasks.STATUS + " != "
|
|
||||||
+ TaskContract.Tasks.STATUS_NEEDS_ACTION);
|
|
||||||
db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 1 WHERE " + TaskContract.Tasks.STATUS + " > "
|
|
||||||
+ TaskContract.Tasks.STATUS_IN_PROCESS);
|
|
||||||
db.execSQL("UPDATE " + Tables.TASKS + " SET " + TaskContract.Tasks.IS_CLOSED + " = 0 WHERE " + TaskContract.Tasks.STATUS + " <= "
|
|
||||||
+ TaskContract.Tasks.STATUS_IN_PROCESS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 3)
|
|
||||||
{
|
|
||||||
// add instance sortings
|
|
||||||
db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_START_SORTING + " INTEGER");
|
|
||||||
db.execSQL("ALTER TABLE " + Tables.INSTANCES + " ADD COLUMN " + TaskContract.Instances.INSTANCE_DUE_SORTING + " INTEGER");
|
|
||||||
db.execSQL("UPDATE " + Tables.INSTANCES + " SET " + TaskContract.Instances.INSTANCE_START_SORTING + " = " + TaskContract.Instances.INSTANCE_START
|
|
||||||
+ ", " + TaskContract.Instances.INSTANCE_DUE_SORTING + " = " + TaskContract.Instances.INSTANCE_DUE);
|
|
||||||
}
|
|
||||||
if (oldVersion < 4)
|
|
||||||
{
|
|
||||||
// drop old view before altering the schema
|
|
||||||
db.execSQL(SQL_DROP_TASK_VIEW);
|
|
||||||
db.execSQL(SQL_DROP_INSTANCE_VIEW);
|
|
||||||
|
|
||||||
// change property id column name to work with the left join in task view
|
|
||||||
db.execSQL(SQL_DROP_TASKS_CLEANUP_TRIGGER);
|
|
||||||
db.execSQL(SQL_DROP_PROPERTIES_TABLE);
|
|
||||||
db.execSQL(SQL_CREATE_PROPERTIES_TABLE);
|
|
||||||
db.execSQL(SQL_CREATE_TASKS_CLEANUP_TRIGGER);
|
|
||||||
|
|
||||||
// create categories mapping table
|
|
||||||
db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE);
|
|
||||||
|
|
||||||
// create alarms table
|
|
||||||
db.execSQL(SQL_CREATE_ALARMS_TABLE);
|
|
||||||
|
|
||||||
// update views
|
|
||||||
db.execSQL(SQL_CREATE_TASK_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_TASK_PROPERTY_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_PROPERTY_VIEW);
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_CATEGORY_VIEW);
|
|
||||||
|
|
||||||
// create Indices
|
|
||||||
db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.MIMETYPE, TaskContract.Properties.TASK_ID));
|
|
||||||
db.execSQL(createIndexString(Tables.PROPERTIES, false, TaskContract.Properties.TASK_ID));
|
|
||||||
db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.ACCOUNT_NAME, TaskContract.Categories.ACCOUNT_TYPE,
|
|
||||||
TaskContract.Categories.NAME));
|
|
||||||
db.execSQL(createIndexString(Tables.CATEGORIES, false, TaskContract.Categories.NAME));
|
|
||||||
|
|
||||||
// add new triggers
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_PROPERTY_CLEANUP_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_CREATE_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_UPDATE_TRIGGER);
|
|
||||||
db.execSQL(SQL_CREATE_ALARM_COUNT_DELETE_TRIGGER);
|
|
||||||
|
|
||||||
}
|
|
||||||
if (oldVersion < 6)
|
|
||||||
{
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PARENT_ID + " integer;");
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_ALARMS + " integer;");
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.SORTING + " text;");
|
|
||||||
}
|
|
||||||
if (oldVersion < 7)
|
|
||||||
{
|
|
||||||
db.execSQL(SQL_CREATE_LISTS_CLEANUP_TRIGGER);
|
|
||||||
}
|
|
||||||
if (oldVersion < 8)
|
|
||||||
{
|
|
||||||
// replace priority 0 by null. We need this to sort the widget properly. Since 0 is the default this is no problem when syncing.
|
|
||||||
db.execSQL("update " + Tables.TASKS + " set " + Tasks.PRIORITY + "=null where " + Tasks.PRIORITY + "=0;");
|
|
||||||
}
|
|
||||||
if (oldVersion < 9)
|
|
||||||
{
|
|
||||||
// add missing column _UID
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks._UID + " integer;");
|
|
||||||
// add cleanup trigger for orphaned properties
|
|
||||||
db.execSQL(SQL_CREATE_TASK_PROPERTY_CLEANUP_TRIGGER);
|
|
||||||
}
|
|
||||||
if (oldVersion < 10)
|
|
||||||
{
|
|
||||||
// add property column to categories_mapping table. Since adding a constraint is not supported by SQLite we have to remove and recreate the entire
|
|
||||||
// table
|
|
||||||
db.execSQL("drop table " + Tables.CATEGORIES_MAPPING);
|
|
||||||
db.execSQL(SQL_CREATE_CATEGORIES_MAPPING_TABLE);
|
|
||||||
db.execSQL(SQL_CREATE_CATEGORY_PROPERTY_CLEANUP_TRIGGER);
|
|
||||||
}
|
|
||||||
if (oldVersion < 11)
|
|
||||||
{
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.PINNED + " integer;");
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.HAS_PROPERTIES + " integer;");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 12)
|
|
||||||
{
|
|
||||||
// rename the local account type
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
values.put(TaskLists.ACCOUNT_TYPE, TaskContract.LOCAL_ACCOUNT_TYPE);
|
|
||||||
db.update(Tables.LISTS, values, TaskLists.ACCOUNT_TYPE + "=?", new String[] { "LOCAL" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 13)
|
|
||||||
{
|
|
||||||
db.execSQL(SQL_CREATE_SYNCSTATE_TABLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 14)
|
|
||||||
{
|
|
||||||
// create a unique index for account name and account type on the sync state table
|
|
||||||
db.execSQL(createIndexString(Tables.SYNCSTATE, true, TaskContract.SyncState.ACCOUNT_NAME, TaskContract.SyncState.ACCOUNT_TYPE));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 16)
|
|
||||||
{
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_START_SORTING));
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_DUE_SORTING));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 17)
|
|
||||||
{
|
|
||||||
db.execSQL("alter table " + Tables.INSTANCES + " add column " + TaskContract.Instances.INSTANCE_ORIGINAL_TIME + " integer default 0;");
|
|
||||||
db.execSQL(createIndexString(Tables.INSTANCES, false, TaskContract.Instances.INSTANCE_ORIGINAL_TIME));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 18)
|
|
||||||
{
|
|
||||||
db.execSQL("alter table " + Tables.INSTANCES + " add column " + TaskContract.Instances.DISTANCE_FROM_CURRENT + " integer default 0;");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 19)
|
|
||||||
{
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 22)
|
|
||||||
{
|
|
||||||
// create version column, unless it already exists
|
|
||||||
if (!new First<>(new TableColumns(Tables.TASKS).value(db), new Equals<>(Tasks.VERSION)).isPresent())
|
|
||||||
{
|
|
||||||
// create task version column and update trigger
|
|
||||||
db.execSQL("alter table " + Tables.TASKS + " add column " + Tasks.VERSION + " Integer default 0;");
|
|
||||||
db.execSQL(SQL_CREATE_TASK_VERSION_TRIGGER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 22)
|
|
||||||
{
|
|
||||||
db.beginTransaction();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// make sure we upgrade the instances of every recurring task
|
|
||||||
EntityProcessor<TaskAdapter> processor = new Instantiating(new NoOpProcessor<>());
|
|
||||||
try (Cursor c = db.query(Tables.TASKS,
|
|
||||||
new String[] {
|
|
||||||
TaskContract.Tasks._ID, Tasks.ORIGINAL_INSTANCE_ID, Tasks.DTSTART, Tasks.DUE, Tasks.DURATION, Tasks.IS_CLOSED, Tasks.TZ,
|
|
||||||
Tasks.IS_ALLDAY, Tasks.RRULE, Tasks.RDATE, Tasks.EXDATE, Tasks.ORIGINAL_INSTANCE_TIME, Tasks.ORIGINAL_INSTANCE_ALLDAY },
|
|
||||||
String.format(Locale.ENGLISH, "%s is null", TaskContract.Tasks.ORIGINAL_INSTANCE_ID),
|
|
||||||
null, null, null, null))
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
Instantiating.addUpdateRequest(values);
|
|
||||||
TaskAdapter adapter = new CursorContentValuesTaskAdapter(c, values);
|
|
||||||
processor.update(db, adapter, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 23)
|
|
||||||
{
|
|
||||||
db.execSQL("drop view " + Tables.INSTANCE_CLIENT_VIEW + ";");
|
|
||||||
db.execSQL(SQL_CREATE_INSTANCE_CLIENT_VIEW);
|
|
||||||
}
|
|
||||||
|
|
||||||
// upgrade FTS
|
|
||||||
FTSDatabaseHelper.onUpgrade(db, oldVersion, newVersion);
|
|
||||||
|
|
||||||
if (mListener != null)
|
|
||||||
{
|
|
||||||
mListener.onDatabaseUpdate(db, oldVersion, newVersion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,134 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Build;
|
|
||||||
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A receiver for all task provider related broadcasts. This receiver merely forwards all incoming broadcasts to the provider, so they can be handled
|
|
||||||
* asynchronously in the provider context.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class TaskProviderBroadcastReceiver extends BroadcastReceiver
|
|
||||||
{
|
|
||||||
private final static int REQUEST_CODE_ALARM = 1337;
|
|
||||||
|
|
||||||
// AGENDULA CHANGE: renamed into our namespace. Only ever used for a PendingIntent we address to
|
|
||||||
// this very class, so it collides with nothing — but a device may have OpenTasks installed too,
|
|
||||||
// and identical action strings across two apps are the kind of thing that is very confusing to
|
|
||||||
// read in a bug report.
|
|
||||||
private final static String ACTION_NOTIFICATION_ALARM = "de.jeanlucmakiola.agendula.provider.NOTIFICATION_ALARM";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a system alarm to update notifications at a specific time.
|
|
||||||
*
|
|
||||||
* @param context
|
|
||||||
* A Context.
|
|
||||||
* @param updateTime
|
|
||||||
* When to fire the alarm.
|
|
||||||
*/
|
|
||||||
// AGENDULA CHANGE: MissingPermission added. Lint cannot see that the setExact call below is already
|
|
||||||
// guarded by canScheduleExactAlarms(), with a set() fallback when it returns false.
|
|
||||||
@SuppressLint({ "NewApi", "MissingPermission" })
|
|
||||||
static void planNotificationUpdate(Context context, DateTime updateTime)
|
|
||||||
{
|
|
||||||
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
|
|
||||||
Intent alarmIntent = new Intent(context, TaskProviderBroadcastReceiver.class);
|
|
||||||
alarmIntent.setAction(ACTION_NOTIFICATION_ALARM);
|
|
||||||
|
|
||||||
// AGENDULA CHANGE: FLAG_IMMUTABLE added. Since Android 12 a PendingIntent must state its
|
|
||||||
// mutability, and this call throws IllegalArgumentException without it once targetSdk >= 31.
|
|
||||||
// Upstream targets 29, so it never hit this; we target 36. Nothing fills the intent in later,
|
|
||||||
// so immutable is also the correct choice on the merits.
|
|
||||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(
|
|
||||||
context, REQUEST_CODE_ALARM, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
|
||||||
|
|
||||||
// cancel any previous alarm
|
|
||||||
am.cancel(pendingIntent);
|
|
||||||
|
|
||||||
if (updateTime.isFloating())
|
|
||||||
{
|
|
||||||
// convert floating times to absolute times
|
|
||||||
updateTime = new DateTime(TimeZone.getDefault(), updateTime.getYear(), updateTime.getMonth(), updateTime.getDayOfMonth(), updateTime.getHours(),
|
|
||||||
updateTime.getMinutes(), updateTime.getSeconds());
|
|
||||||
}
|
|
||||||
|
|
||||||
// AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time anymore
|
|
||||||
//
|
|
||||||
// AGENDULA CHANGE: fall back to an inexact alarm when exact ones aren't permitted. On API
|
|
||||||
// 31-32 SCHEDULE_EXACT_ALARM is revocable, and setExact then throws SecurityException — here,
|
|
||||||
// inside a receiver handling a system broadcast, which means the app dies every time the
|
|
||||||
// timezone changes. This alarm only re-runs the provider's own bookkeeping, so a few minutes
|
|
||||||
// of drift costs nothing; Agendula's user-visible due reminders are armed by ReminderScheduler,
|
|
||||||
// which asks for the permission properly.
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || am.canScheduleExactAlarms())
|
|
||||||
{
|
|
||||||
am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
am.set(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent)
|
|
||||||
{
|
|
||||||
String action = intent.getAction();
|
|
||||||
if (action == null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// AGENDULA CHANGE: the cases below are upstream's, written out rather than reached by
|
|
||||||
// fall-through. Upstream's switch has no `break` anywhere, so TIMEZONE_CHANGED already runs
|
|
||||||
// all three operations and ACTION_NOTIFICATION_ALARM runs the last two — which is what this
|
|
||||||
// does, unchanged. It is spelled out because the comment upstream attaches to the first case
|
|
||||||
// ("don't trigger the notifications update yet") describes breaks that were never written,
|
|
||||||
// so the code and the stated intent disagree and only one of them can be preserved.
|
|
||||||
//
|
|
||||||
// Behaviour wins: a vendored fork is the wrong place to act on a guess. Whether the missing
|
|
||||||
// breaks are the bug, or the comment is, wants a device with a task due across a timezone
|
|
||||||
// change to settle — see provider/PROVENANCE.md.
|
|
||||||
if (Intent.ACTION_TIMEZONE_CHANGED.equals(action))
|
|
||||||
{
|
|
||||||
// the local timezone has been changed, notify the provider to take the necessary steps.
|
|
||||||
ContentOperation.UPDATE_TIMEZONE.fire(context, null);
|
|
||||||
}
|
|
||||||
if (Intent.ACTION_TIMEZONE_CHANGED.equals(action) || ACTION_NOTIFICATION_ALARM.equals(action))
|
|
||||||
{
|
|
||||||
// it's time for the next notification
|
|
||||||
ContentOperation.POST_NOTIFICATIONS.fire(context, null);
|
|
||||||
}
|
|
||||||
// at this time all other actions trigger an update of the notification alarm
|
|
||||||
ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(context, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +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.provider.tasks;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AuthenticatorDescription;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.SingletonIterable;
|
|
||||||
import org.dmfs.jems.iterable.composite.Joined;
|
|
||||||
import org.dmfs.jems.iterable.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.procedure.composite.Batch;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables;
|
|
||||||
import org.dmfs.provider.tasks.utils.ResourceArray;
|
|
||||||
import org.dmfs.provider.tasks.utils.With;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Instances;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.SyncState;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskListColumns;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskListSyncColumns;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskLists;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
import de.jeanlucmakiola.agendula.provider.R;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Class Utils.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class Utils
|
|
||||||
{
|
|
||||||
private static final AtomicReference<Set<String>> sOwnAccountTypes = new AtomicReference<>(null);
|
|
||||||
|
|
||||||
|
|
||||||
public static void sendActionProviderChangedBroadCast(Context context, String authority)
|
|
||||||
{
|
|
||||||
// TODO: Using the TaskContract content uri results in a "Unknown URI content" error message. Using the Tasks content uri instead will break the
|
|
||||||
// broadcast receiver. We have to find away around this
|
|
||||||
// TODO: coalesce fast consecutive broadcasts, a delay of up to 1 second should be acceptable
|
|
||||||
|
|
||||||
new With<>(new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority)))
|
|
||||||
.process(providerChangedIntent ->
|
|
||||||
new Batch<Intent>(context::sendBroadcast)
|
|
||||||
.process(new Mapped<>(
|
|
||||||
packageName -> new Intent(providerChangedIntent).setPackage(packageName),
|
|
||||||
// TODO: fow now we hard code 3rd party package names, this should be replaced by some sort or registry
|
|
||||||
// see https://github.com/dmfs/opentasks/issues/824
|
|
||||||
new Joined<>(
|
|
||||||
new SingletonIterable<>(context.getPackageName()),
|
|
||||||
new ResourceArray(context, R.array.agendula_provider_changed_receivers)))));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The account types this package owns an authenticator for.
|
|
||||||
* <p>
|
|
||||||
* AGENDULA CHANGE. Upstream held {@code android.permission.GET_ACCOUNTS} and so could enumerate every account on the device; we dropped it, because
|
|
||||||
* since API 26 an authenticator already makes its own accounts visible to its own package and nothing else concerns us.
|
|
||||||
* <p>
|
|
||||||
* That deletion is only safe together with {@link #cleanUpLists}: an account we cannot see is indistinguishable from an account that has been removed,
|
|
||||||
* and the upstream cleanup treats the latter as a licence to delete the lists hanging off it. Restricting the cleanup to types in this set makes that
|
|
||||||
* failure impossible by construction rather than by care — the provider can only ever prune accounts it is authoritative about.
|
|
||||||
* <p>
|
|
||||||
* While Agendula ships no sync adapter this set is empty and no list is ever pruned. Our own account type joins it automatically the moment the
|
|
||||||
* authenticator is declared, with no further change here.
|
|
||||||
*
|
|
||||||
* @return the account types authenticated by this very package, possibly empty — never null.
|
|
||||||
*/
|
|
||||||
static Set<String> ownAccountTypes(Context context)
|
|
||||||
{
|
|
||||||
Set<String> cached = sOwnAccountTypes.get();
|
|
||||||
if (cached != null)
|
|
||||||
{
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
String ownPackage = context.getPackageName();
|
|
||||||
Set<String> types = new HashSet<>();
|
|
||||||
for (AuthenticatorDescription description : AccountManager.get(context).getAuthenticatorTypes())
|
|
||||||
{
|
|
||||||
if (ownPackage.equals(description.packageName))
|
|
||||||
{
|
|
||||||
types.add(description.type);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Which authenticators our own package declares is fixed at install time, so this cannot go
|
|
||||||
// stale within a process. Worth caching: isOwnAccountType runs on every task-list insert and
|
|
||||||
// getAuthenticatorTypes is a binder round trip.
|
|
||||||
Set<String> result = Collections.unmodifiableSet(types);
|
|
||||||
sOwnAccountTypes.compareAndSet(null, result);
|
|
||||||
return sOwnAccountTypes.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether {@code accountType} is authenticated by this package. See {@link #ownAccountTypes}.
|
|
||||||
*/
|
|
||||||
static boolean isOwnAccountType(Context context, String accountType)
|
|
||||||
{
|
|
||||||
return ownAccountTypes(context).contains(accountType);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Drops the {@link #ownAccountTypes} cache. Tests only — in a real process the answer is fixed at install time, which is the entire reason it is cached.
|
|
||||||
*/
|
|
||||||
static void clearOwnAccountTypesCache()
|
|
||||||
{
|
|
||||||
sOwnAccountTypes.set(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public static void cleanUpLists(Context context, SQLiteDatabase db, Account[] accounts, String authority)
|
|
||||||
{
|
|
||||||
// make a list of the accounts array
|
|
||||||
List<Account> accountList = Arrays.asList(accounts);
|
|
||||||
// AGENDULA CHANGE — see ownAccountTypes: only types we authenticate ourselves may be pruned.
|
|
||||||
Set<String> prunableTypes = ownAccountTypes(context);
|
|
||||||
|
|
||||||
db.beginTransaction();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Cursor c = db.query(Tables.LISTS, new String[] { TaskListColumns._ID, TaskListSyncColumns.ACCOUNT_NAME, TaskListSyncColumns.ACCOUNT_TYPE }, null,
|
|
||||||
null, null, null, null);
|
|
||||||
|
|
||||||
// build a list of all task list ids that no longer have an account
|
|
||||||
List<Long> obsoleteLists = new ArrayList<Long>();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
String accountType = c.getString(2);
|
|
||||||
// mark list for removal if it is non-local, of a type we authenticate
|
|
||||||
// ourselves, and the account is not in accountList
|
|
||||||
if (!TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType) && prunableTypes.contains(accountType))
|
|
||||||
{
|
|
||||||
Account account = new Account(c.getString(1), accountType);
|
|
||||||
if (!accountList.contains(account))
|
|
||||||
{
|
|
||||||
obsoleteLists.add(c.getLong(0));
|
|
||||||
|
|
||||||
// remove syncstate for this account right away
|
|
||||||
db.delete(Tables.SYNCSTATE, SyncState.ACCOUNT_NAME + "=? and " + SyncState.ACCOUNT_TYPE + "=?", new String[] {
|
|
||||||
account.name,
|
|
||||||
account.type });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (obsoleteLists.size() == 0)
|
|
||||||
{
|
|
||||||
// nothing to do here
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all accounts in the list
|
|
||||||
for (Long id : obsoleteLists)
|
|
||||||
{
|
|
||||||
if (id != null)
|
|
||||||
{
|
|
||||||
db.delete(Tables.LISTS, TaskListColumns._ID + "=" + id, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db.setTransactionSuccessful();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
db.endTransaction();
|
|
||||||
}
|
|
||||||
// notify all observers
|
|
||||||
|
|
||||||
ContentResolver cr = context.getContentResolver();
|
|
||||||
cr.notifyChange(TaskLists.getContentUri(authority), null);
|
|
||||||
cr.notifyChange(Tasks.getContentUri(authority), null);
|
|
||||||
cr.notifyChange(Instances.getContentUri(authority), null);
|
|
||||||
|
|
||||||
Utils.sendActionProviderChangedBroadCast(context, authority);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,133 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to handle alarm property values during database transactions.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class AlarmHandler extends PropertyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
// private static final String[] ALARM_ID_PROJECTION = { Alarms.ALARM_ID };
|
|
||||||
// private static final String ALARM_SELECTION = Alarms.ALARM_ID + " =?";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the content of the alarm prior to insert and update transactions.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property if <code>isNew</code> is <code>false</code>. If <code>isNew</code> is <code>true</code> this value is ignored.
|
|
||||||
* @param isNew
|
|
||||||
* Indicates that the content is new and not an update.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to validate.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The valid {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException
|
|
||||||
* if the {@link ContentValues} are invalid.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// row id can not be changed or set manually
|
|
||||||
if (values.containsKey(Property.Alarm.PROPERTY_ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("_ID can not be set manually");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.containsKey(Property.Alarm.MINUTES_BEFORE))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("alarm property requires a time offset");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.containsKey(Property.Alarm.REFERENCE) || values.getAsInteger(Property.Alarm.REFERENCE) < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("alarm property requires a valid reference date ");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.containsKey(Property.Alarm.ALARM_TYPE))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("alarm property requires an alarm type");
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the alarm into the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task the new property belongs to.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to insert.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The row id of the new alarm as <code>long</code>
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
values = validateValues(db, taskId, -1, true, values, isSyncAdapter);
|
|
||||||
return super.insert(db, taskId, values, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the alarm in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to update.
|
|
||||||
* @param oldValues
|
|
||||||
* A {@link Cursor} pointing to the old values in the database.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The number of rows affected.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter);
|
|
||||||
return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,277 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper.CategoriesMapping;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Categories;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Properties;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Category;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to handle category property values during database transactions.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class CategoryHandler extends PropertyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
private static final String[] CATEGORY_ID_PROJECTION = { Categories._ID, Categories.NAME, Categories.COLOR };
|
|
||||||
|
|
||||||
private static final String CATEGORY_ID_SELECTION = Categories._ID + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?";
|
|
||||||
private static final String CATEGORY_NAME_SELECTION = Categories.NAME + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?";
|
|
||||||
|
|
||||||
public static final String IS_NEW_CATEGORY = "is_new_category";
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the content of the category prior to insert and update transactions.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property if <code>isNew</code> is <code>false</code>. If <code>isNew</code> is <code>true</code> this value is ignored.
|
|
||||||
* @param isNew
|
|
||||||
* Indicates that the content is new and not an update.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to validate.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The valid {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException
|
|
||||||
* if the {@link ContentValues} are invalid.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// the category requires a name or an id
|
|
||||||
if (!values.containsKey(Category.CATEGORY_ID) && !values.containsKey(Category.CATEGORY_NAME))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Neiter an id nor a category name was supplied for the category property.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the matching task & account for the property
|
|
||||||
if (!values.containsKey(Properties.TASK_ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("No task id was supplied for the category property");
|
|
||||||
}
|
|
||||||
String[] queryArgs = { values.getAsString(Properties.TASK_ID) };
|
|
||||||
String[] queryProjection = { Tasks.ACCOUNT_NAME, Tasks.ACCOUNT_TYPE };
|
|
||||||
String querySelection = Tasks._ID + "=?";
|
|
||||||
Cursor taskCursor = db.query(Tables.TASKS_VIEW, queryProjection, querySelection, queryArgs, null, null, null);
|
|
||||||
|
|
||||||
String accountName = null;
|
|
||||||
String accountType = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (taskCursor.moveToNext())
|
|
||||||
{
|
|
||||||
accountName = taskCursor.getString(0);
|
|
||||||
accountType = taskCursor.getString(1);
|
|
||||||
|
|
||||||
values.put(Categories.ACCOUNT_NAME, accountName);
|
|
||||||
values.put(Categories.ACCOUNT_TYPE, accountType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (taskCursor != null)
|
|
||||||
{
|
|
||||||
taskCursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accountName != null && accountType != null)
|
|
||||||
{
|
|
||||||
// search for matching categories
|
|
||||||
String[] categoryArgs;
|
|
||||||
Cursor cursor;
|
|
||||||
|
|
||||||
if (values.containsKey(Categories._ID))
|
|
||||||
{
|
|
||||||
// serach by ID
|
|
||||||
categoryArgs = new String[] { values.getAsString(Category.CATEGORY_ID), accountName, accountType };
|
|
||||||
cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_ID_SELECTION, categoryArgs, null, null, null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// search by name
|
|
||||||
categoryArgs = new String[] { values.getAsString(Category.CATEGORY_NAME), accountName, accountType };
|
|
||||||
cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_NAME_SELECTION, categoryArgs, null, null, null);
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (cursor != null && cursor.getCount() == 1)
|
|
||||||
{
|
|
||||||
cursor.moveToNext();
|
|
||||||
Long categoryID = cursor.getLong(0);
|
|
||||||
String categoryName = cursor.getString(1);
|
|
||||||
int color = cursor.getInt(2);
|
|
||||||
|
|
||||||
values.put(Category.CATEGORY_ID, categoryID);
|
|
||||||
values.put(Category.CATEGORY_NAME, categoryName);
|
|
||||||
values.put(Category.CATEGORY_COLOR, color);
|
|
||||||
values.put(IS_NEW_CATEGORY, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.put(IS_NEW_CATEGORY, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (cursor != null)
|
|
||||||
{
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the category into the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task the new property belongs to.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to insert.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The row id of the new category as <code>long</code>
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
values = validateValues(db, taskId, -1, true, values, isSyncAdapter);
|
|
||||||
values = getOrInsertCategory(db, values);
|
|
||||||
|
|
||||||
// insert property row and create relation
|
|
||||||
long id = super.insert(db, taskId, values, isSyncAdapter);
|
|
||||||
insertRelation(db, taskId, values.getAsLong(Category.CATEGORY_ID), id);
|
|
||||||
|
|
||||||
// update FTS entry with category name
|
|
||||||
updateFTSEntry(db, taskId, id, values.getAsString(Category.CATEGORY_NAME));
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the category in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to update.
|
|
||||||
* @param oldValues
|
|
||||||
* A {@link Cursor} pointing to the old values in the database.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The number of rows affected.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter);
|
|
||||||
values = getOrInsertCategory(db, values);
|
|
||||||
|
|
||||||
if (values.containsKey(Category.CATEGORY_NAME))
|
|
||||||
{
|
|
||||||
// update FTS entry with new category name
|
|
||||||
updateFTSEntry(db, taskId, propertyId, values.getAsString(Category.CATEGORY_NAME));
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a category with matching {@link ContentValues} exists and returns the existing category or creates a new category in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} of the category.
|
|
||||||
*
|
|
||||||
* @return The {@link ContentValues} of the existing or new category.
|
|
||||||
*/
|
|
||||||
private ContentValues getOrInsertCategory(SQLiteDatabase db, ContentValues values)
|
|
||||||
{
|
|
||||||
if (values.getAsBoolean(IS_NEW_CATEGORY))
|
|
||||||
{
|
|
||||||
// insert new category in category table
|
|
||||||
ContentValues newCategoryValues = new ContentValues(4);
|
|
||||||
newCategoryValues.put(Categories.ACCOUNT_NAME, values.getAsString(Categories.ACCOUNT_NAME));
|
|
||||||
newCategoryValues.put(Categories.ACCOUNT_TYPE, values.getAsString(Categories.ACCOUNT_TYPE));
|
|
||||||
newCategoryValues.put(Categories.NAME, values.getAsString(Category.CATEGORY_NAME));
|
|
||||||
newCategoryValues.put(Categories.COLOR, values.getAsInteger(Category.CATEGORY_COLOR));
|
|
||||||
|
|
||||||
long categoryID = db.insert(Tables.CATEGORIES, "", newCategoryValues);
|
|
||||||
values.put(Category.CATEGORY_ID, categoryID);
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove redundant values
|
|
||||||
values.remove(IS_NEW_CATEGORY);
|
|
||||||
values.remove(Categories.ACCOUNT_NAME);
|
|
||||||
values.remove(Categories.ACCOUNT_TYPE);
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a relation entry in the database to link task and category.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The row id of the task.
|
|
||||||
* @param categoryId
|
|
||||||
* The row id of the category.
|
|
||||||
*
|
|
||||||
* @return The row id of the inserted relation.
|
|
||||||
*/
|
|
||||||
private long insertRelation(SQLiteDatabase db, long taskId, long categoryId, long propertyId)
|
|
||||||
{
|
|
||||||
ContentValues relationValues = new ContentValues(3);
|
|
||||||
relationValues.put(CategoriesMapping.TASK_ID, taskId);
|
|
||||||
relationValues.put(CategoriesMapping.CATEGORY_ID, categoryId);
|
|
||||||
relationValues.put(CategoriesMapping.PROPERTY_ID, propertyId);
|
|
||||||
return db.insert(Tables.CATEGORIES_MAPPING, "", relationValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class is used to handle properties with unknown / unsupported mime-types.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class DefaultPropertyHandler extends PropertyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the content of the alarm prior to insert and update transactions.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param isNew
|
|
||||||
* Indicates that the content is new and not an update.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to validate.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The valid {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException
|
|
||||||
* if the {@link ContentValues} are invalid.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,155 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.FTSDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Properties;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abstract class that is used as template for specific property handlers.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public abstract class PropertyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the content of the property prior to insert and update transactions.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property if <code>isNew</code> is <code>false</code>. If <code>isNew</code> is <code>true</code> this value is ignored.
|
|
||||||
* @param isNew
|
|
||||||
* Indicates that the content is new and not an update.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to validate.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The valid {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @throws IllegalArgumentException
|
|
||||||
* if the {@link ContentValues} are invalid.
|
|
||||||
*/
|
|
||||||
public abstract ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts the property {@link ContentValues} into the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task the new property belongs to.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to insert.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The row id of the new property as <code>long</code>
|
|
||||||
*/
|
|
||||||
public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return db.insert(Tables.PROPERTIES, "", values);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the property {@link ContentValues} in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to update.
|
|
||||||
* @param oldValues
|
|
||||||
* A {@link Cursor} pointing to the old values in the database.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return The number of rows affected.
|
|
||||||
*/
|
|
||||||
public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return db.update(Tables.PROPERTIES, values, Properties.PROPERTY_ID + "=" + propertyId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the property in the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The belonging database.
|
|
||||||
* @param taskId
|
|
||||||
* The id of the task this property belongs to.
|
|
||||||
* @param propertyId
|
|
||||||
* The id of the property.
|
|
||||||
* @param oldValues
|
|
||||||
* A {@link Cursor} pointing to the old values in the database.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* Indicates that the transaction was triggered from a SyncAdapter.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return db.delete(Tables.PROPERTIES, Properties.PROPERTY_ID + "=" + propertyId, null);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Method hook to insert FTS entries on database migration.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}.
|
|
||||||
* @param taskId
|
|
||||||
* the row id of the task this property belongs to
|
|
||||||
* @param propertyId
|
|
||||||
* the id of the property
|
|
||||||
* @param text
|
|
||||||
* the searchable text of the property. If the property has multiple text snippets to search in, concat them separated by a space.
|
|
||||||
*/
|
|
||||||
protected void updateFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String text)
|
|
||||||
{
|
|
||||||
FTSDatabaseHelper.updatePropertyFTSEntry(db, taskId, propertyId, text);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValues cloneForNewTask(long newTaskId, ContentValues values)
|
|
||||||
{
|
|
||||||
ContentValues newValues = new ContentValues(values);
|
|
||||||
newValues.remove(Properties.PROPERTY_ID);
|
|
||||||
newValues.put(Properties.TASK_ID, newTaskId);
|
|
||||||
return newValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
;
|
|
||||||
}
|
|
||||||
@@ -1,61 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Alarm;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Category;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Relation;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A factory that creates the matching {@link PropertyHandler} for the given mimetype.
|
|
||||||
*
|
|
||||||
* @author Tobias Reinsch <tobias@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class PropertyHandlerFactory
|
|
||||||
{
|
|
||||||
private final static PropertyHandler CATEGORY_HANDLER = new CategoryHandler();
|
|
||||||
private final static PropertyHandler ALARM_HANDLER = new AlarmHandler();
|
|
||||||
private final static PropertyHandler RELATION_HANDLER = new RelationHandler();
|
|
||||||
private final static PropertyHandler DEFAULT_PROPERTY_HANDLER = new DefaultPropertyHandler();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a specific {@link PropertyHandler}.
|
|
||||||
*
|
|
||||||
* @param mimeType
|
|
||||||
* The mimetype of the property.
|
|
||||||
*
|
|
||||||
* @return The matching {@link PropertyHandler} for the given mimetype or <code>null</code>
|
|
||||||
*/
|
|
||||||
public static PropertyHandler get(String mimeType)
|
|
||||||
{
|
|
||||||
if (Category.CONTENT_ITEM_TYPE.equals(mimeType))
|
|
||||||
{
|
|
||||||
return CATEGORY_HANDLER;
|
|
||||||
}
|
|
||||||
if (Alarm.CONTENT_ITEM_TYPE.equals(mimeType))
|
|
||||||
{
|
|
||||||
return ALARM_HANDLER;
|
|
||||||
}
|
|
||||||
if (Relation.CONTENT_ITEM_TYPE.equals(mimeType))
|
|
||||||
{
|
|
||||||
return RELATION_HANDLER;
|
|
||||||
}
|
|
||||||
return DEFAULT_PROPERTY_HANDLER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,276 +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.provider.tasks.handler;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Property.Relation;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles any inserts, updates and deletes on the relations table.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class RelationHandler extends PropertyHandler
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (values.containsKey(Relation.RELATED_CONTENT_URI))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("setting of RELATED_CONTENT_URI not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Long id = values.getAsLong(Relation.RELATED_ID);
|
|
||||||
String uid = values.getAsString(Relation.RELATED_UID);
|
|
||||||
|
|
||||||
if (id == null && uid != null)
|
|
||||||
{
|
|
||||||
values.putNull(Relation.RELATED_ID);
|
|
||||||
}
|
|
||||||
else if (id != null && uid == null)
|
|
||||||
{
|
|
||||||
values.putNull(Relation.RELATED_UID);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("exactly one of RELATED_ID, RELATED_UID and RELATED_URI must be non-null");
|
|
||||||
}
|
|
||||||
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
validateValues(db, taskId, -1, true, values, isSyncAdapter);
|
|
||||||
resolveFields(db, values);
|
|
||||||
updateParentId(db, taskId, values, null);
|
|
||||||
return super.insert(db, taskId, values, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues cloneForNewTask(long newTaskId, ContentValues values)
|
|
||||||
{
|
|
||||||
ContentValues newValues = super.cloneForNewTask(newTaskId, values);
|
|
||||||
newValues.remove(Relation.RELATED_CONTENT_URI);
|
|
||||||
return newValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
validateValues(db, taskId, propertyId, false, values, isSyncAdapter);
|
|
||||||
resolveFields(db, values);
|
|
||||||
updateParentId(db, taskId, values, oldValues);
|
|
||||||
return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
clearParentId(db, taskId, oldValues);
|
|
||||||
return super.delete(db, taskId, propertyId, oldValues, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve <code>_id</code> or <code>_uid</code>, depending of which value is given.
|
|
||||||
* <p>
|
|
||||||
* TODO: store links into the calendar provider if we find an event that matches the UID.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The task database.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues}.
|
|
||||||
*/
|
|
||||||
private void resolveFields(SQLiteDatabase db, ContentValues values)
|
|
||||||
{
|
|
||||||
Long id = values.getAsLong(Relation.RELATED_ID);
|
|
||||||
String uid = values.getAsString(Relation.RELATED_UID);
|
|
||||||
|
|
||||||
if (id != null)
|
|
||||||
{
|
|
||||||
values.put(Relation.RELATED_UID, resolveTaskStringField(db, Tasks._ID, id.toString(), Tasks._UID));
|
|
||||||
}
|
|
||||||
else if (uid != null)
|
|
||||||
{
|
|
||||||
values.put(Relation.RELATED_ID, resolveTaskLongField(db, Tasks._UID, uid, Tasks._ID));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Long resolveTaskLongField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField)
|
|
||||||
{
|
|
||||||
String result = resolveTaskStringField(db, selectionField, selectionValue, resultField);
|
|
||||||
if (result != null)
|
|
||||||
{
|
|
||||||
return Long.parseLong(result);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private String resolveTaskStringField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField)
|
|
||||||
{
|
|
||||||
Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, new String[] { resultField }, selectionField + "=?", new String[] { selectionValue }, null, null,
|
|
||||||
null);
|
|
||||||
if (c != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (c.moveToNext())
|
|
||||||
{
|
|
||||||
return c.getString(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update {@link Tasks#PARENT_ID} when a parent is assigned to a child.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* @param taskId
|
|
||||||
* @param values
|
|
||||||
* @param oldValues
|
|
||||||
*/
|
|
||||||
// AGENDULA CHANGE: lint's Range check flags getInt(getColumnIndex(...)), since getColumnIndex returns -1 for an absent column. Both callers pass a cursor
|
|
||||||
// over a property row selected with a projection that contains RELATED_TYPE, so the index is never -1 in practice. Suppressed rather than "fixed":
|
|
||||||
// inventing a fallback value would change what the provider does on a path upstream chose to let fail loudly, and this is a fork, not our design.
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private void updateParentId(SQLiteDatabase db, long taskId, ContentValues values, Cursor oldValues)
|
|
||||||
{
|
|
||||||
int type;
|
|
||||||
if (values.containsKey(Relation.RELATED_TYPE))
|
|
||||||
{
|
|
||||||
type = values.getAsInteger(Relation.RELATED_TYPE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == Relation.RELTYPE_PARENT)
|
|
||||||
{
|
|
||||||
// this is a link to the parent, we need to update the PARENT_ID of this task, if we can
|
|
||||||
|
|
||||||
if (values.containsKey(Relation.RELATED_ID))
|
|
||||||
{
|
|
||||||
ContentValues taskValues = new ContentValues(1);
|
|
||||||
taskValues.put(Tasks.PARENT_ID, values.getAsLong(Relation.RELATED_ID));
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null);
|
|
||||||
}
|
|
||||||
// else: the parent task is probably not synced yet, we have to fix this in RelationUpdaterHook
|
|
||||||
}
|
|
||||||
else if (type == Relation.RELTYPE_CHILD)
|
|
||||||
{
|
|
||||||
// this is a link to a child, we need to update the PARENT_ID of the linked task
|
|
||||||
|
|
||||||
if (values.getAsLong(Relation.RELATED_ID) != null)
|
|
||||||
{
|
|
||||||
ContentValues taskValues = new ContentValues(1);
|
|
||||||
taskValues.put(Tasks.PARENT_ID, taskId);
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + values.getAsLong(Relation.RELATED_ID), null);
|
|
||||||
}
|
|
||||||
// else: the child task is probably not synced yet, we have to fix this in RelationUpdaterHook
|
|
||||||
}
|
|
||||||
else if (type == Relation.RELTYPE_SIBLING)
|
|
||||||
{
|
|
||||||
// this is a link to a sibling, we need to copy the PARENT_ID of the linked task to this task
|
|
||||||
if (values.getAsLong(Relation.RELATED_ID) != null)
|
|
||||||
{
|
|
||||||
// get the parent of the other task first
|
|
||||||
Long otherParent = resolveTaskLongField(db, Tasks._ID, values.getAsString(Relation.RELATED_ID), Tasks.PARENT_ID);
|
|
||||||
|
|
||||||
ContentValues taskValues = new ContentValues(1);
|
|
||||||
taskValues.put(Tasks.PARENT_ID, otherParent);
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null);
|
|
||||||
}
|
|
||||||
// else: the sibling task is probably not synced yet, we have to fix this in RelationUpdaterHook
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear {@link Tasks#PARENT_ID} if a link is removed.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* @param taskId
|
|
||||||
* @param oldValues
|
|
||||||
*/
|
|
||||||
// AGENDULA CHANGE: see updateParentId — same Range suppression, same reason.
|
|
||||||
@SuppressLint("Range")
|
|
||||||
private void clearParentId(SQLiteDatabase db, long taskId, Cursor oldValues)
|
|
||||||
{
|
|
||||||
int type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This is more complicated than it may sound. We don't know the order in which relations are created, updated or removed. So it's possible that a new
|
|
||||||
* parent relationship has been created and the old one is removed afterwards. In that case we can not simply clear the PARENT_ID.
|
|
||||||
*
|
|
||||||
* FIXME: For now we ignore that fact. But we should fix it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
if (type == Relation.RELTYPE_PARENT)
|
|
||||||
{
|
|
||||||
// this was a link to the parent, we're orphaned now, so clear PARENT_ID of this task
|
|
||||||
|
|
||||||
ContentValues taskValues = new ContentValues(1);
|
|
||||||
taskValues.putNull(Tasks.PARENT_ID);
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null);
|
|
||||||
}
|
|
||||||
else if (type == Relation.RELTYPE_CHILD)
|
|
||||||
{
|
|
||||||
// this was a link to a child, the child is orphaned now, clear its PARENT_ID
|
|
||||||
|
|
||||||
int relIdCol = oldValues.getColumnIndex(Relation.RELATED_ID);
|
|
||||||
if (!oldValues.isNull(relIdCol))
|
|
||||||
{
|
|
||||||
ContentValues taskValues = new ContentValues(1);
|
|
||||||
taskValues.putNull(Tasks.PARENT_ID);
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + oldValues.getLong(relIdCol), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// else if (type == Relation.RELTYPE_SIBLING)
|
|
||||||
// {
|
|
||||||
/*
|
|
||||||
* This was a link to a sibling, since it's no longer our sibling either it or we're orphaned now We won't know unless we check all relations.
|
|
||||||
*
|
|
||||||
* FIXME: properly handle this case
|
|
||||||
*/
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract implementation of a {@link InstanceAdapter} to server as the base for more concrete adapters.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public abstract class AbstractInstanceAdapter implements InstanceAdapter
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public final Uri uri(String authority)
|
|
||||||
{
|
|
||||||
return ContentUris.withAppendedId(TaskContract.Instances.getContentUri(authority), id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract implementation of a {@link ListAdapter} to server as the base for more concrete adapters.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public abstract class AbstractListAdapter implements ListAdapter
|
|
||||||
{
|
|
||||||
private final ContentValues mState = new ContentValues(10);
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri uri(String authority)
|
|
||||||
{
|
|
||||||
return ContentUris.withAppendedId(TaskContract.TaskLists.getContentUri(authority), id());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getState(FieldAdapter<T, ListAdapter> stateFieldAdater)
|
|
||||||
{
|
|
||||||
return stateFieldAdater.getFrom(mState);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void setState(FieldAdapter<T, ListAdapter> stateFieldAdater, T value)
|
|
||||||
{
|
|
||||||
stateFieldAdater.setIn(mState, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,71 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentUris;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract implementation of a {@link TaskAdapter} to server as the base for more concrete adapters.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public abstract class AbstractTaskAdapter implements TaskAdapter
|
|
||||||
{
|
|
||||||
private final ContentValues mState = new ContentValues(10);
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri uri(String authority)
|
|
||||||
{
|
|
||||||
return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isRecurring()
|
|
||||||
{
|
|
||||||
// recurring tasks must have an RRULE or RDATEs and at least one of DTSTART and DUE date
|
|
||||||
return (valueOf(RRULE) != null || valueOf(RDATE).iterator().hasNext()) && (valueOf(DTSTART) != null || valueOf(DUE) != null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean recurrenceUpdated()
|
|
||||||
{
|
|
||||||
return isUpdated(RRULE) || isUpdated(DTSTART) || isUpdated(DUE) || isUpdated(DURATION) || isUpdated(RDATE) || isUpdated(EXDATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getState(FieldAdapter<T, TaskAdapter> stateFieldAdater)
|
|
||||||
{
|
|
||||||
return stateFieldAdater.getFrom(mState);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void setState(FieldAdapter<T, TaskAdapter> stateFieldAdater, T value)
|
|
||||||
{
|
|
||||||
stateFieldAdater.setIn(mState, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,161 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.jems.single.elementary.Reduced;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link TaskAdapter} for tasks that are stored in a {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class ContentValuesInstanceAdapter extends AbstractInstanceAdapter
|
|
||||||
{
|
|
||||||
private long mId;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesInstanceAdapter(ContentValues values)
|
|
||||||
{
|
|
||||||
this(-1L, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesInstanceAdapter(long id, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.isSetIn(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, InstanceAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, InstanceAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mId < 0)
|
|
||||||
{
|
|
||||||
mId = db.insert(TaskDatabaseHelper.Tables.TASKS, null, mValues);
|
|
||||||
return mId > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getState(FieldAdapter<T, InstanceAdapter> stateFieldAdater)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void setState(FieldAdapter<T, InstanceAdapter> stateFieldAdater, T value)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter duplicate()
|
|
||||||
{
|
|
||||||
return new ContentValuesInstanceAdapter(new ContentValues(mValues));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter taskAdapter()
|
|
||||||
{
|
|
||||||
// make sure we remove any instance fields
|
|
||||||
return new ContentValuesTaskAdapter(new Reduced<String, ContentValues>(
|
|
||||||
() -> new ContentValues(mValues),
|
|
||||||
(contentValues, column) -> {
|
|
||||||
contentValues.remove(column);
|
|
||||||
return contentValues;
|
|
||||||
},
|
|
||||||
INSTANCE_COLUMN_NAMES).value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class ContentValuesListAdapter extends AbstractListAdapter
|
|
||||||
{
|
|
||||||
private long mId;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesListAdapter(ContentValues values)
|
|
||||||
{
|
|
||||||
this(-1L, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesListAdapter(long id, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.isSetIn(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, ListAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, ListAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mId < 0)
|
|
||||||
{
|
|
||||||
mId = db.insert(TaskDatabaseHelper.Tables.LISTS, null, mValues);
|
|
||||||
return mId > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter duplicate()
|
|
||||||
{
|
|
||||||
return new ContentValuesListAdapter(new ContentValues(mValues));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link TaskAdapter} for tasks that are stored in a {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class ContentValuesTaskAdapter extends AbstractTaskAdapter
|
|
||||||
{
|
|
||||||
private long mId;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesTaskAdapter(ContentValues values)
|
|
||||||
{
|
|
||||||
this(-1L, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public ContentValuesTaskAdapter(long id, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.isSetIn(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues.size() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, TaskAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, TaskAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mId < 0)
|
|
||||||
{
|
|
||||||
mId = db.insert(TaskDatabaseHelper.Tables.TASKS, null, mValues);
|
|
||||||
return mId > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter duplicate()
|
|
||||||
{
|
|
||||||
return new ContentValuesTaskAdapter(new ContentValues(mValues));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,212 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.decorators.Sieved;
|
|
||||||
import org.dmfs.iterables.elementary.Seq;
|
|
||||||
import org.dmfs.jems.iterable.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.single.elementary.Collected;
|
|
||||||
import org.dmfs.jems.single.elementary.Reduced;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.utils.ContainsValues;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link InstanceAdapter} that adapts a {@link Cursor} and a {@link ContentValues} instance. All changes are written to the {@link ContentValues} and can be
|
|
||||||
* stored in the database with {@link #commit(SQLiteDatabase)}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class CursorContentValuesInstanceAdapter extends AbstractInstanceAdapter
|
|
||||||
{
|
|
||||||
private final long mId;
|
|
||||||
private final Cursor mCursor;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public CursorContentValuesInstanceAdapter(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
if (cursor == null && !_ID.existsIn(values))
|
|
||||||
{
|
|
||||||
mId = -1L;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mId = _ID.getFrom(cursor);
|
|
||||||
}
|
|
||||||
mCursor = cursor;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public CursorContentValuesInstanceAdapter(long id, Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mCursor = cursor;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
if (mValues == null)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor);
|
|
||||||
}
|
|
||||||
return fieldAdapter.getFrom(mCursor, mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, InstanceAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
if (mValues == null || !fieldAdapter.isSetIn(mValues))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Object oldValue = fieldAdapter.getFrom(mCursor);
|
|
||||||
Object newValue = fieldAdapter.getFrom(mValues);
|
|
||||||
|
|
||||||
return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return mValues != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, InstanceAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, InstanceAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T getState(FieldAdapter<T, InstanceAdapter> stateFieldAdater)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void setState(FieldAdapter<T, InstanceAdapter> stateFieldAdater, T value)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter duplicate()
|
|
||||||
{
|
|
||||||
ContentValues newValues = new ContentValues(mValues);
|
|
||||||
|
|
||||||
// copy all columns (except _ID) that are not in the values yet
|
|
||||||
for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i)
|
|
||||||
{
|
|
||||||
String column = mCursor.getColumnName(i);
|
|
||||||
if (!newValues.containsKey(column) && !TaskContract.Instances._ID.equals(column))
|
|
||||||
{
|
|
||||||
newValues.put(column, mCursor.getString(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ContentValuesInstanceAdapter(newValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter taskAdapter()
|
|
||||||
{
|
|
||||||
// make sure we remove any instance fields
|
|
||||||
ContentValues values = new Reduced<String, ContentValues>(
|
|
||||||
() -> new ContentValues(mValues),
|
|
||||||
(contentValues, column) -> {
|
|
||||||
contentValues.remove(column);
|
|
||||||
return contentValues;
|
|
||||||
},
|
|
||||||
INSTANCE_COLUMN_NAMES).value();
|
|
||||||
|
|
||||||
// create a new cursor which doesn't contain the instance columns
|
|
||||||
String[] cursorColumns = new Collected<>(
|
|
||||||
ArrayList::new,
|
|
||||||
new Sieved<>(col -> !INSTANCE_COLUMN_NAMES.contains(col), new Seq<>(mCursor.getColumnNames())))
|
|
||||||
.value().toArray(new String[0]);
|
|
||||||
MatrixCursor cursor = new MatrixCursor(cursorColumns);
|
|
||||||
cursor.addRow(
|
|
||||||
new Mapped<>(
|
|
||||||
column -> mCursor.getType(column) == Cursor.FIELD_TYPE_BLOB ? mCursor.getBlob(column) : mCursor.getString(column),
|
|
||||||
new Mapped<>(
|
|
||||||
mCursor::getColumnIndex,
|
|
||||||
new Seq<>(cursorColumns))));
|
|
||||||
cursor.moveToFirst();
|
|
||||||
return new CursorContentValuesTaskAdapter(valueOf(InstanceAdapter.TASK_ID), cursor, values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,139 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.utils.ContainsValues;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class CursorContentValuesListAdapter extends AbstractListAdapter
|
|
||||||
{
|
|
||||||
private final long mId;
|
|
||||||
private final Cursor mCursor;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public CursorContentValuesListAdapter(long id, Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mCursor = cursor;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor, mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, ListAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
if (mValues == null || !fieldAdapter.isSetIn(mValues))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Object oldValue = fieldAdapter.getFrom(mCursor);
|
|
||||||
Object newValue = fieldAdapter.getFrom(mValues);
|
|
||||||
|
|
||||||
return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, ListAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, ListAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter duplicate()
|
|
||||||
{
|
|
||||||
ContentValues newValues = new ContentValues(mValues);
|
|
||||||
|
|
||||||
// copy all columns (except _ID) that are not in the values yet
|
|
||||||
for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i)
|
|
||||||
{
|
|
||||||
String column = mCursor.getColumnName(i);
|
|
||||||
if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column))
|
|
||||||
{
|
|
||||||
newValues.put(column, mCursor.getString(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ContentValuesListAdapter(newValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,169 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.utils.ContainsValues;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link TaskAdapter} that adapts a {@link Cursor} and a {@link ContentValues} instance. All changes are written to the {@link ContentValues} and can be
|
|
||||||
* stored in the database with {@link #commit(SQLiteDatabase)}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public class CursorContentValuesTaskAdapter extends AbstractTaskAdapter
|
|
||||||
{
|
|
||||||
private final long mId;
|
|
||||||
private final Cursor mCursor;
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public CursorContentValuesTaskAdapter(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
if (cursor == null && !_ID.existsIn(values))
|
|
||||||
{
|
|
||||||
mId = -1L;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
mId = _ID.getFrom(cursor);
|
|
||||||
}
|
|
||||||
mCursor = cursor;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public CursorContentValuesTaskAdapter(long id, Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mCursor = cursor;
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id()
|
|
||||||
{
|
|
||||||
return mId;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T valueOf(FieldAdapter<T, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
if (mValues == null)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor);
|
|
||||||
}
|
|
||||||
return fieldAdapter.getFrom(mCursor, mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> T oldValueOf(FieldAdapter<T, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
return fieldAdapter.getFrom(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isUpdated(FieldAdapter<?, TaskAdapter> fieldAdapter)
|
|
||||||
{
|
|
||||||
if (mValues == null || !fieldAdapter.isSetIn(mValues))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Object oldValue = fieldAdapter.existsIn(mCursor) ? fieldAdapter.getFrom(mCursor) : null;
|
|
||||||
Object newValue = fieldAdapter.getFrom(mValues);
|
|
||||||
// we need to special case RRULE, because RecurrenceRule doesn't support `equals`
|
|
||||||
if (fieldAdapter != TaskAdapter.RRULE)
|
|
||||||
{
|
|
||||||
return oldValue == null && newValue != null || oldValue != null && !oldValue.equals(newValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// in case of RRULE we compare the String values.
|
|
||||||
return oldValue == null && newValue != null || oldValue != null && (newValue == null || !oldValue.toString().equals(newValue.toString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isWriteable()
|
|
||||||
{
|
|
||||||
return mValues != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasUpdates()
|
|
||||||
{
|
|
||||||
return mValues != null && mValues.size() > 0 && !new ContainsValues(mValues).satisfiedBy(mCursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public <T> void set(FieldAdapter<T, TaskAdapter> fieldAdapter, T value) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.setIn(mValues, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unset(FieldAdapter<?, TaskAdapter> fieldAdapter) throws IllegalStateException
|
|
||||||
{
|
|
||||||
fieldAdapter.removeFrom(mValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int commit(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
if (mValues.size() == 0)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter duplicate()
|
|
||||||
{
|
|
||||||
ContentValues newValues = new ContentValues(mValues);
|
|
||||||
|
|
||||||
// copy all columns (except _ID) that are not in the values yet
|
|
||||||
for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i)
|
|
||||||
{
|
|
||||||
String column = mCursor.getColumnName(i);
|
|
||||||
if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column))
|
|
||||||
{
|
|
||||||
newValues.put(column, mCursor.getString(i));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ContentValuesTaskAdapter(newValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,151 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter to read values of a specific entity type from primitive data sets like {@link Cursor}s or {@link ContentValues}s.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface EntityAdapter<EntityType>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Returns the row id of the entity or <code>-1</code> if the entity has not been stored yet.
|
|
||||||
*
|
|
||||||
* @return The entity row id or <code>-1</code>.
|
|
||||||
*/
|
|
||||||
long id();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the {@link Uri} of the entity using the given authority.
|
|
||||||
*
|
|
||||||
* @param authority
|
|
||||||
* The authority of this provider.
|
|
||||||
*
|
|
||||||
* @return A {@link Uri} or <code>null</code> if this entity has not been stored yet.
|
|
||||||
*/
|
|
||||||
Uri uri(String authority);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the value identified by the given {@link FieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldAdapter
|
|
||||||
* The {@link FieldAdapter} of the value to return.
|
|
||||||
*
|
|
||||||
* @return The value, maybe be <code>null</code>.
|
|
||||||
*/
|
|
||||||
<T> T valueOf(FieldAdapter<T, EntityType> fieldAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the old value identified by the given {@link FieldAdapter}. This will be equal to the value returned by {@link #valueOf(FieldAdapter)} unless it
|
|
||||||
* has been overridden, in which case this returns the former value.
|
|
||||||
*
|
|
||||||
* @param fieldAdapter
|
|
||||||
* The {@link FieldAdapter} of the value to return.
|
|
||||||
*
|
|
||||||
* @return The value, maybe be <code>null</code>.
|
|
||||||
*/
|
|
||||||
<T> T oldValueOf(FieldAdapter<T, EntityType> fieldAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the given field has been overridden or not.
|
|
||||||
*
|
|
||||||
* @param fieldAdapter
|
|
||||||
* The {@link FieldAdapter} of the field to check.
|
|
||||||
*
|
|
||||||
* @return <code>true</code> if the field has been overridden, <code>false</code> otherwise.
|
|
||||||
*/
|
|
||||||
boolean isUpdated(FieldAdapter<?, EntityType> fieldAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether this adapter supports modifying values.
|
|
||||||
*
|
|
||||||
* @return <code>true</code> if the task values can be changed by this adapter, false otherwise.
|
|
||||||
*/
|
|
||||||
boolean isWriteable();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether any value has been modified.
|
|
||||||
*
|
|
||||||
* @return <code>true</code> if there are modified values, false otherwise.
|
|
||||||
*/
|
|
||||||
boolean hasUpdates();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a value of the adapted entity. The value is identified by a {@link FieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldAdapter
|
|
||||||
* The {@link FieldAdapter} of the value to set.
|
|
||||||
* @param value
|
|
||||||
* The new value.
|
|
||||||
*/
|
|
||||||
<T> void set(FieldAdapter<T, EntityType> fieldAdapter, T value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value from the change set. In effect the respective field will keep it's old value.
|
|
||||||
*
|
|
||||||
* @param fieldAdapter
|
|
||||||
* The {@link FieldAdapter} of the field to un-set.
|
|
||||||
*/
|
|
||||||
void unset(FieldAdapter<?, EntityType> fieldAdapter);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Commit all changes to the database.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* A writable database.
|
|
||||||
*
|
|
||||||
* @return The number of entries affected. This may be <code>0</code> if no fields have been changed.
|
|
||||||
*/
|
|
||||||
int commit(SQLiteDatabase db);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the value of a temporary state field. The state of an entity is not committed to the database, it's only bound to the instances of this
|
|
||||||
* {@link EntityAdapter} and will be lost once it gets garbage collected.
|
|
||||||
*
|
|
||||||
* @param stateFieldAdater
|
|
||||||
* The {@link FieldAdapter} of a state field.
|
|
||||||
*
|
|
||||||
* @return The value of the state field.
|
|
||||||
*/
|
|
||||||
<T> T getState(FieldAdapter<T, EntityType> stateFieldAdater);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the value of a state field. This value is not stored in the database. Instead it only exists as long as this {@link EntityAdapter} exists.
|
|
||||||
*
|
|
||||||
* @param stateFieldAdater
|
|
||||||
* The {@link FieldAdapter} of the state field to set.
|
|
||||||
* @param value
|
|
||||||
* The new state value.
|
|
||||||
*/
|
|
||||||
<T> void setState(FieldAdapter<T, EntityType> stateFieldAdater, T value);
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Creates a {@link EntityAdapter} for a new entity initialized with the values of this entity (except for _ID).
|
|
||||||
*
|
|
||||||
* @return A new {@link EntityAdapter} having the same values.
|
|
||||||
*/
|
|
||||||
EntityAdapter<EntityType> duplicate();
|
|
||||||
}
|
|
||||||
@@ -1,109 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.DateTimeFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Instances;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter to read instance values from primitive data sets like {@link Cursor}s or {@link ContentValues}s.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface InstanceAdapter extends EntityAdapter<InstanceAdapter>
|
|
||||||
{
|
|
||||||
|
|
||||||
Collection<String> INSTANCE_COLUMN_NAMES = new HashSet<>(asList(
|
|
||||||
TaskContract.Instances.INSTANCE_START,
|
|
||||||
TaskContract.Instances.INSTANCE_START_SORTING,
|
|
||||||
TaskContract.Instances.INSTANCE_DUE,
|
|
||||||
TaskContract.Instances.INSTANCE_DUE_SORTING,
|
|
||||||
TaskContract.Instances.INSTANCE_DURATION,
|
|
||||||
TaskContract.Instances.INSTANCE_ORIGINAL_TIME,
|
|
||||||
TaskContract.Instances.TASK_ID,
|
|
||||||
TaskContract.Instances.DISTANCE_FROM_CURRENT,
|
|
||||||
"_id:1"));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of a task instance.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<InstanceAdapter> _ID = new LongFieldAdapter<InstanceAdapter>(Instances._ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the due date of a task instance.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<InstanceAdapter> INSTANCE_DUE = new DateTimeFieldAdapter<>(Instances.INSTANCE_DUE, Tasks.TZ, Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the start date of a task instance.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<InstanceAdapter> INSTANCE_START = new DateTimeFieldAdapter<>(Instances.INSTANCE_START, Tasks.TZ, Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the start sorting of a task instance.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<InstanceAdapter> INSTANCE_START_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_START_SORTING);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the due sorting of a task instance.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<InstanceAdapter> INSTANCE_DUE_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_DUE_SORTING);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the original time of a task instance.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<InstanceAdapter> INSTANCE_ORIGINAL_TIME = new DateTimeFieldAdapter<>(Instances.INSTANCE_ORIGINAL_TIME, Tasks.TZ, Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the distance of a task instance from the current instance.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<InstanceAdapter> DISTANCE_FROM_CURRENT = new IntegerFieldAdapter<>(Instances.DISTANCE_FROM_CURRENT);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the title of a task instance.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<InstanceAdapter> TITLE = new StringFieldAdapter<>(Tasks.TITLE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of the task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<InstanceAdapter> TASK_ID = new LongFieldAdapter<InstanceAdapter>(Instances.TASK_ID);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
InstanceAdapter duplicate();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a {@link TaskAdapter} for the task component of the instanced view.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
TaskAdapter taskAdapter();
|
|
||||||
}
|
|
||||||
@@ -1,82 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.TaskLists;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter to read list values from primitive data sets like {@link Cursor}s or {@link ContentValues}s.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface ListAdapter extends EntityAdapter<ListAdapter>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of a task list.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<ListAdapter> _ID = new LongFieldAdapter<ListAdapter>(TaskLists._ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the _sync_id of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> SYNC_ID = new StringFieldAdapter<ListAdapter>(TaskLists._SYNC_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the sync version of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> SYNC_VERSION = new StringFieldAdapter<ListAdapter>(TaskLists.SYNC_VERSION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the account name of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> ACCOUNT_NAME = new StringFieldAdapter<ListAdapter>(TaskLists.ACCOUNT_NAME);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the account type of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> ACCOUNT_TYPE = new StringFieldAdapter<ListAdapter>(TaskLists.ACCOUNT_TYPE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the owner of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> OWNER = new StringFieldAdapter<ListAdapter>(TaskLists.OWNER);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the name of a list.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<ListAdapter> LIST_NAME = new StringFieldAdapter<ListAdapter>(TaskLists.LIST_NAME);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the color of a list.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<ListAdapter> LIST_COLOR = new IntegerFieldAdapter<ListAdapter>(TaskLists.LIST_COLOR);
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Creates a {@link ListAdapter} for a new task initialized with the values of this task (except for _ID).
|
|
||||||
*
|
|
||||||
* @return A new task having the same values.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
ListAdapter duplicate();
|
|
||||||
}
|
|
||||||
@@ -1,362 +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.provider.tasks.model;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.BinaryFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.DateTimeFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.DateTimeIterableFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.DurationFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.RRuleFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.UrlFieldAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Instances;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract.Tasks;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter to read task values from primitive data sets like {@link Cursor}s or {@link ContentValues}s.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface TaskAdapter extends EntityAdapter<TaskAdapter>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> _ID = new LongFieldAdapter<TaskAdapter>(Tasks._ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the version of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> VERSION = new LongFieldAdapter<>(Tasks.VERSION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the task row id of as instance.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> INSTANCE_TASK_ID = new LongFieldAdapter<TaskAdapter>(Instances.TASK_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of the list of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> LIST_ID = new LongFieldAdapter<TaskAdapter>(Tasks.LIST_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the owner of the list of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> LIST_OWNER = new StringFieldAdapter<TaskAdapter>(Tasks.LIST_OWNER);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the row id of original instance of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> ORIGINAL_INSTANCE_ID = new LongFieldAdapter<TaskAdapter>(Tasks.ORIGINAL_INSTANCE_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the sync_id of original instance of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> ORIGINAL_INSTANCE_SYNC_ID = new StringFieldAdapter<TaskAdapter>(Tasks.ORIGINAL_INSTANCE_SYNC_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the original instance all day flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> ORIGINAL_INSTANCE_ALLDAY = new BooleanFieldAdapter<TaskAdapter>(Tasks.ORIGINAL_INSTANCE_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the parent_id of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> PARENT_ID = new LongFieldAdapter<TaskAdapter>(Tasks.PARENT_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the all day flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> IS_ALLDAY = new BooleanFieldAdapter<TaskAdapter>(Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the percent complete value of a task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> PERCENT_COMPLETE = new IntegerFieldAdapter<TaskAdapter>(Tasks.PERCENT_COMPLETE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the status of a task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> STATUS = new IntegerFieldAdapter<TaskAdapter>(Tasks.STATUS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the priority value of a task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> PRIORITY = new IntegerFieldAdapter<TaskAdapter>(Tasks.PRIORITY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the classification value of a task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> CLASSIFICATION = new IntegerFieldAdapter<TaskAdapter>(Tasks.CLASSIFICATION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the list name of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> LIST_NAME = new StringFieldAdapter<TaskAdapter>(Tasks.LIST_NAME);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the account name of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> ACCOUNT_NAME = new StringFieldAdapter<TaskAdapter>(Tasks.ACCOUNT_NAME);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the account type of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> ACCOUNT_TYPE = new StringFieldAdapter<TaskAdapter>(Tasks.ACCOUNT_TYPE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the title of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> TITLE = new StringFieldAdapter<TaskAdapter>(Tasks.TITLE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the location of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> LOCATION = new StringFieldAdapter<TaskAdapter>(Tasks.LOCATION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the description of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> DESCRIPTION = new StringFieldAdapter<TaskAdapter>(Tasks.DESCRIPTION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the start date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> DTSTART = new DateTimeFieldAdapter<TaskAdapter>(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the original date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> ORIGINAL_INSTANCE_TIME = new DateTimeFieldAdapter<TaskAdapter>(Tasks.ORIGINAL_INSTANCE_TIME, Tasks.TZ,
|
|
||||||
Tasks.ORIGINAL_INSTANCE_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the raw start date timestamp of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> DTSTART_RAW = new LongFieldAdapter<TaskAdapter>(Tasks.DTSTART);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the due date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> DUE = new DateTimeFieldAdapter<TaskAdapter>(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the raw due date timestamp of a task.
|
|
||||||
*/
|
|
||||||
LongFieldAdapter<TaskAdapter> DUE_RAW = new LongFieldAdapter<TaskAdapter>(Tasks.DUE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the start date of a task.
|
|
||||||
*/
|
|
||||||
DurationFieldAdapter<TaskAdapter> DURATION = new DurationFieldAdapter<TaskAdapter>(Tasks.DURATION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the dirty flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> _DIRTY = new BooleanFieldAdapter<TaskAdapter>(Tasks._DIRTY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the deleted flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> _DELETED = new BooleanFieldAdapter<TaskAdapter>(Tasks._DELETED);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the completed date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> COMPLETED = new DateTimeFieldAdapter<TaskAdapter>(Tasks.COMPLETED, null, null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the created date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> CREATED = new DateTimeFieldAdapter<TaskAdapter>(Tasks.CREATED, null, null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the last modified date of a task.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> LAST_MODIFIED = new DateTimeFieldAdapter<TaskAdapter>(Tasks.LAST_MODIFIED, null, null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the URL of a task.
|
|
||||||
*/
|
|
||||||
UrlFieldAdapter<TaskAdapter> URL = new UrlFieldAdapter<TaskAdapter>(TaskContract.Tasks.URL);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the UID of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> _UID = new StringFieldAdapter<TaskAdapter>(TaskContract.Tasks._UID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the raw time zone of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> TIMEZONE_RAW = new StringFieldAdapter<TaskAdapter>(TaskContract.Tasks.TZ);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the Color of the task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> LIST_COLOR = new IntegerFieldAdapter<TaskAdapter>(TaskContract.Tasks.LIST_COLOR);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the access level of the task list.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> LIST_ACCESS_LEVEL = new IntegerFieldAdapter<TaskAdapter>(TaskContract.Tasks.LIST_ACCESS_LEVEL);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the visibility setting of the task list.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> LIST_VISIBLE = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.VISIBLE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adpater for the ID of the task.
|
|
||||||
*/
|
|
||||||
IntegerFieldAdapter<TaskAdapter> TASK_ID = new IntegerFieldAdapter<TaskAdapter>(TaskContract.Tasks._ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the IS_CLOSED flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> IS_CLOSED = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.IS_CLOSED);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the IS_NEW flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> IS_NEW = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.IS_NEW);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the PINNED flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> PINNED = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.PINNED);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the HAS_ALARMS flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> HAS_ALARMS = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.HAS_ALARMS);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the HAS_PROPERTIES flag of a task.
|
|
||||||
*/
|
|
||||||
BooleanFieldAdapter<TaskAdapter> HAS_PROPERTIES = new BooleanFieldAdapter<TaskAdapter>(TaskContract.Tasks.HAS_PROPERTIES);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the RRULE of a task.
|
|
||||||
*/
|
|
||||||
RRuleFieldAdapter<TaskAdapter> RRULE = new RRuleFieldAdapter<TaskAdapter>(TaskContract.Tasks.RRULE);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the RDATE of a task.
|
|
||||||
*/
|
|
||||||
DateTimeIterableFieldAdapter<TaskAdapter> RDATE = new DateTimeIterableFieldAdapter<TaskAdapter>(TaskContract.Tasks.RDATE,
|
|
||||||
TaskContract.Tasks.TZ);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the EXDATE of a task.
|
|
||||||
*/
|
|
||||||
DateTimeIterableFieldAdapter<TaskAdapter> EXDATE = new DateTimeIterableFieldAdapter<TaskAdapter>(TaskContract.Tasks.EXDATE,
|
|
||||||
TaskContract.Tasks.TZ);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC1 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC1 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC1);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC2 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC2 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC2);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC3 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC3 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC3);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC4 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC4 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC4);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC5 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC5 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC5);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC6 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC6 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC6);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC7 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC7 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC7);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC8 field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC8 = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC8);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC_VERSION field of a task.
|
|
||||||
*/
|
|
||||||
BinaryFieldAdapter<TaskAdapter> SYNC_VERSION = new BinaryFieldAdapter<TaskAdapter>(TaskContract.Tasks.SYNC_VERSION);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the SYNC_ID field of a task.
|
|
||||||
*/
|
|
||||||
StringFieldAdapter<TaskAdapter> SYNC_ID = new StringFieldAdapter<TaskAdapter>(TaskContract.Tasks._SYNC_ID);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the due date of a task instance.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> INSTANCE_DUE = new DateTimeFieldAdapter<TaskAdapter>(Instances.INSTANCE_DUE, Tasks.TZ,
|
|
||||||
Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter for the start date of a task instance.
|
|
||||||
*/
|
|
||||||
DateTimeFieldAdapter<TaskAdapter> INSTANCE_START = new DateTimeFieldAdapter<TaskAdapter>(Instances.INSTANCE_START, Tasks.TZ,
|
|
||||||
Tasks.IS_ALLDAY);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the adapted task is recurring.
|
|
||||||
*
|
|
||||||
* @return <code>true</code> if the task is recurring, <code>false</code> otherwise.
|
|
||||||
*/
|
|
||||||
boolean isRecurring();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether any value that's relevant for recurrence has been modified thought this adapter. This returns true if any of
|
|
||||||
* {@link TaskContract.TaskColumns#DTSTART}, {@link TaskContract.TaskColumns#DUE},{@link TaskContract.TaskColumns#DURATION},
|
|
||||||
* {@link TaskContract.TaskColumns#RRULE}, {@link TaskContract.TaskColumns#RDATE} or {@link TaskContract.TaskColumns#EXDATE} has been modified.
|
|
||||||
*
|
|
||||||
* @return <code>true</code> if the recurrence set has changed, <code>false</code> otherwise.
|
|
||||||
*/
|
|
||||||
boolean recurrenceUpdated();
|
|
||||||
|
|
||||||
/***
|
|
||||||
* Creates a {@link TaskAdapter} for a new task initialized with the values of this task (except for _ID).
|
|
||||||
*
|
|
||||||
* @return A new task having the same values.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
TaskAdapter duplicate();
|
|
||||||
}
|
|
||||||
@@ -1,95 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a binary value from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class BinaryFieldAdapter<EntityType> extends SimpleFieldAdapter<byte[], EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link BinaryFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public BinaryFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.getAsByteArray(mFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public byte[] getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return cursor.isNull(columnIdx) ? null : cursor.getBlob(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, byte[] value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,94 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a {@link Boolean} value from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
* <p/>
|
|
||||||
* Implementation detail:
|
|
||||||
* <p/>
|
|
||||||
* The values are loaded and stored as <code>0</code> (for <code>false</code>) and <code>1</code> (for <code>true</code>).
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class BooleanFieldAdapter<EntityType> extends SimpleFieldAdapter<Boolean, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link BooleanFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public BooleanFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Boolean getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
Integer value = values.getAsInteger(mFieldName);
|
|
||||||
|
|
||||||
return value != null && value > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Boolean getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return !cursor.isNull(columnIdx) && cursor.getInt(columnIdx) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Boolean value)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value ? 1 : 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,243 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store {@link DateTime} values from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
* <p>
|
|
||||||
* {@link DateTime} values are stored as three separate values:
|
|
||||||
* <ul>
|
|
||||||
* <li>a timestamp in milliseconds since the epoch</li>
|
|
||||||
* <li>a time zone</li>
|
|
||||||
* <li>an allday flag</li>
|
|
||||||
* </ul>
|
|
||||||
* <p>
|
|
||||||
* This adapter combines those three fields to a {@link DateTime} value. If the time zone field is <code>null</code> the time zone is always set to UTC.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class DateTimeFieldAdapter<EntityType> extends SimpleFieldAdapter<DateTime, EntityType>
|
|
||||||
{
|
|
||||||
private final String mTimestampField;
|
|
||||||
private final String mTzField;
|
|
||||||
private final String mAllDayField;
|
|
||||||
private final boolean mAllDayDefault;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link DateTimeFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param timestampField
|
|
||||||
* The name of the field that holds the time stamp in milliseconds.
|
|
||||||
* @param tzField
|
|
||||||
* The name of the field that holds the time zone (as Olson ID). If the field name is <code>null</code> the time is always set to UTC.
|
|
||||||
* @param alldayField
|
|
||||||
* The name of the field that indicated that this time is a date not a date-time. If this fieldName is <code>null</code> all loaded values are
|
|
||||||
* non-allday.
|
|
||||||
*/
|
|
||||||
public DateTimeFieldAdapter(String timestampField, String tzField, String alldayField)
|
|
||||||
{
|
|
||||||
if (timestampField == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("timestampField must not be null");
|
|
||||||
}
|
|
||||||
mTimestampField = timestampField;
|
|
||||||
mTzField = tzField;
|
|
||||||
mAllDayField = alldayField;
|
|
||||||
mAllDayDefault = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mTimestampField;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DateTime getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
Long timestamp = values.getAsLong(mTimestampField);
|
|
||||||
if (timestamp == null)
|
|
||||||
{
|
|
||||||
// if the time stamp is null we return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
String timezone = mTzField == null ? null : values.getAsString(mTzField);
|
|
||||||
DateTime value = new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp);
|
|
||||||
|
|
||||||
// cache mAlldayField locally
|
|
||||||
String allDayField = mAllDayField;
|
|
||||||
|
|
||||||
// set the allday flag appropriately
|
|
||||||
Integer allDayInt = allDayField == null ? null : values.getAsInteger(allDayField);
|
|
||||||
|
|
||||||
if ((allDayInt != null && allDayInt != 0) || (allDayField == null && mAllDayDefault))
|
|
||||||
{
|
|
||||||
value = value.toAllDay();
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DateTime getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int tsIdx = cursor.getColumnIndex(mTimestampField);
|
|
||||||
int tzIdx = mTzField == null ? -1 : cursor.getColumnIndex(mTzField);
|
|
||||||
int adIdx = mAllDayField == null ? -1 : cursor.getColumnIndex(mAllDayField);
|
|
||||||
|
|
||||||
if (tsIdx < 0 || (mTzField != null && tzIdx < 0) || (mAllDayField != null && adIdx < 0))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("At least one column is missing in cursor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.isNull(tsIdx))
|
|
||||||
{
|
|
||||||
// if the time stamp is null we return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Long timestamp = cursor.getLong(tsIdx);
|
|
||||||
|
|
||||||
String timezone = mTzField == null ? null : cursor.getString(tzIdx);
|
|
||||||
DateTime value = new DateTime(timezone == null ? null : TimeZone.getTimeZone(timezone), timestamp);
|
|
||||||
|
|
||||||
// set the allday flag appropriately
|
|
||||||
Integer allDayInt = adIdx < 0 ? null : cursor.getInt(adIdx);
|
|
||||||
|
|
||||||
if ((allDayInt != null && allDayInt != 0) || (mAllDayField == null && mAllDayDefault))
|
|
||||||
{
|
|
||||||
value = value.toAllDay();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DateTime getFrom(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
int tsIdx;
|
|
||||||
int tzIdx;
|
|
||||||
int adIdx;
|
|
||||||
long timestamp;
|
|
||||||
String timeZoneId = null;
|
|
||||||
Integer allDay = 0;
|
|
||||||
|
|
||||||
if (values != null && values.containsKey(mTimestampField))
|
|
||||||
{
|
|
||||||
if (values.getAsLong(mTimestampField) == null)
|
|
||||||
{
|
|
||||||
// if the time stamp is null we return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
timestamp = values.getAsLong(mTimestampField);
|
|
||||||
}
|
|
||||||
else if (cursor != null && (tsIdx = cursor.getColumnIndex(mTimestampField)) >= 0)
|
|
||||||
{
|
|
||||||
if (cursor.isNull(tsIdx))
|
|
||||||
{
|
|
||||||
// if the time stamp is null we return null
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
timestamp = cursor.getLong(tsIdx);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Missing timestamp column.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mTzField != null)
|
|
||||||
{
|
|
||||||
if (values != null && values.containsKey(mTzField))
|
|
||||||
{
|
|
||||||
timeZoneId = values.getAsString(mTzField);
|
|
||||||
}
|
|
||||||
else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTzField)) >= 0)
|
|
||||||
{
|
|
||||||
timeZoneId = cursor.getString(tzIdx);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Missing timezone column.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mAllDayField != null)
|
|
||||||
{
|
|
||||||
if (values != null && values.containsKey(mAllDayField))
|
|
||||||
{
|
|
||||||
allDay = values.getAsInteger(mAllDayField);
|
|
||||||
}
|
|
||||||
else if (cursor != null && (adIdx = cursor.getColumnIndex(mAllDayField)) >= 0)
|
|
||||||
{
|
|
||||||
allDay = cursor.getInt(adIdx);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Missing timezone column.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime value = new DateTime(timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId), timestamp);
|
|
||||||
|
|
||||||
if (allDay != 0)
|
|
||||||
{
|
|
||||||
value = value.toAllDay();
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, DateTime value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
// just store all three parts separately
|
|
||||||
values.put(mTimestampField, value.getTimestamp());
|
|
||||||
|
|
||||||
if (mTzField != null)
|
|
||||||
{
|
|
||||||
TimeZone timezone = value.getTimeZone();
|
|
||||||
values.put(mTzField, timezone == null ? null : timezone.getID());
|
|
||||||
}
|
|
||||||
if (mAllDayField != null)
|
|
||||||
{
|
|
||||||
values.put(mAllDayField, value.isAllDay() ? 1 : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// write timestamp only, other fields may still use allday and timezone
|
|
||||||
values.put(mTimestampField, (Long) null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,198 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.EmptyIterable;
|
|
||||||
import org.dmfs.iterables.Split;
|
|
||||||
import org.dmfs.iterables.decorators.DelegatingIterable;
|
|
||||||
import org.dmfs.jems.iterable.decorators.Mapped;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store {@link Iterable}s of {@link DateTime} values from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class DateTimeIterableFieldAdapter<EntityType> extends SimpleFieldAdapter<Iterable<DateTime>, EntityType>
|
|
||||||
{
|
|
||||||
private final String mDateTimeListFieldName;
|
|
||||||
private final String mTimeZoneFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link DateTimeIterableFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param datetimeListFieldName
|
|
||||||
* The name of the field that holds the {@link DateTime} list.
|
|
||||||
* @param timezoneFieldName
|
|
||||||
* The name of the field that holds the time zone name.
|
|
||||||
*/
|
|
||||||
public DateTimeIterableFieldAdapter(String datetimeListFieldName, String timezoneFieldName)
|
|
||||||
{
|
|
||||||
if (datetimeListFieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("datetimeListFieldName must not be null");
|
|
||||||
}
|
|
||||||
mDateTimeListFieldName = datetimeListFieldName;
|
|
||||||
mTimeZoneFieldName = timezoneFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mDateTimeListFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterable<DateTime> getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
String datetimeList = values.getAsString(mDateTimeListFieldName);
|
|
||||||
if (datetimeList == null)
|
|
||||||
{
|
|
||||||
// no list, return an empty Iterable
|
|
||||||
return EmptyIterable.instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new TimeZone for the given time zone string
|
|
||||||
String timezoneString = mTimeZoneFieldName == null ? null : values.getAsString(mTimeZoneFieldName);
|
|
||||||
TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString);
|
|
||||||
|
|
||||||
return new DateTimeList(timeZone, datetimeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterable<DateTime> getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int tdLIdx = cursor.getColumnIndex(mDateTimeListFieldName);
|
|
||||||
int tzIdx = mTimeZoneFieldName == null ? -1 : cursor.getColumnIndex(mTimeZoneFieldName);
|
|
||||||
|
|
||||||
if (tdLIdx < 0 || (mTimeZoneFieldName != null && tzIdx < 0))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("At least one column is missing in cursor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.isNull(tdLIdx))
|
|
||||||
{
|
|
||||||
// if the time stamp list is null we return an empty Iterable
|
|
||||||
return EmptyIterable.instance();
|
|
||||||
}
|
|
||||||
|
|
||||||
String datetimeList = cursor.getString(tdLIdx);
|
|
||||||
|
|
||||||
// create a new TimeZone for the given time zone string
|
|
||||||
String timezoneString = mTimeZoneFieldName == null ? null : cursor.getString(tzIdx);
|
|
||||||
TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString);
|
|
||||||
|
|
||||||
return new DateTimeList(timeZone, datetimeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterable<DateTime> getFrom(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
int tsIdx;
|
|
||||||
int tzIdx;
|
|
||||||
String datetimeList;
|
|
||||||
String timeZoneId = null;
|
|
||||||
|
|
||||||
if (values != null && values.containsKey(mDateTimeListFieldName))
|
|
||||||
{
|
|
||||||
if (values.getAsString(mDateTimeListFieldName) == null)
|
|
||||||
{
|
|
||||||
// the date times are null, so we return null
|
|
||||||
return EmptyIterable.instance();
|
|
||||||
}
|
|
||||||
datetimeList = values.getAsString(mDateTimeListFieldName);
|
|
||||||
}
|
|
||||||
else if (cursor != null && (tsIdx = cursor.getColumnIndex(mDateTimeListFieldName)) >= 0)
|
|
||||||
{
|
|
||||||
if (cursor.isNull(tsIdx))
|
|
||||||
{
|
|
||||||
// the date times are null, so we return an empty Iterable.
|
|
||||||
return EmptyIterable.instance();
|
|
||||||
}
|
|
||||||
datetimeList = cursor.getString(tsIdx);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Missing date time list column.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mTimeZoneFieldName != null)
|
|
||||||
{
|
|
||||||
if (values != null && values.containsKey(mTimeZoneFieldName))
|
|
||||||
{
|
|
||||||
timeZoneId = values.getAsString(mTimeZoneFieldName);
|
|
||||||
}
|
|
||||||
else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTimeZoneFieldName)) >= 0)
|
|
||||||
{
|
|
||||||
timeZoneId = cursor.getString(tzIdx);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Missing timezone column.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new TimeZone for the given time zone string
|
|
||||||
TimeZone timeZone = timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId);
|
|
||||||
|
|
||||||
return new DateTimeList(timeZone, datetimeList);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Iterable<DateTime> value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
String stringValue = TextUtils.join(",", new Mapped<>(dt -> dt.isFloating() ? dt : dt.shiftTimeZone(DateTime.UTC), value));
|
|
||||||
values.put(mDateTimeListFieldName, stringValue.isEmpty() ? null : stringValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.put(mDateTimeListFieldName, (String) null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private final class DateTimeList extends DelegatingIterable<DateTime>
|
|
||||||
{
|
|
||||||
|
|
||||||
public DateTimeList(TimeZone timeZone, String dateTimeList)
|
|
||||||
{
|
|
||||||
super(new Mapped<>(
|
|
||||||
datetime -> !datetime.isFloating() && timeZone != null ? datetime.shiftTimeZone(timeZone) : datetime,
|
|
||||||
new Mapped<CharSequence, DateTime>(
|
|
||||||
charSequence -> DateTime.parse(timeZone, charSequence.toString()),
|
|
||||||
new Split(dateTimeList, ','))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.rfc5545.Duration;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store {@link Duration} values from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class DurationFieldAdapter<EntityType> extends SimpleFieldAdapter<Duration, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link DurationFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param urlField
|
|
||||||
* The field name that holds the {@link Duration}.
|
|
||||||
*/
|
|
||||||
public DurationFieldAdapter(String urlField)
|
|
||||||
{
|
|
||||||
if (urlField == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("urlField must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = urlField;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Duration getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
String rawValue = values.getAsString(mFieldName);
|
|
||||||
if (rawValue == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Duration.parse(rawValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Duration getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cursor.isNull(columnIdx))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Duration.parse(cursor.getString(columnIdx));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Duration value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value.toString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,148 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a specific field from or to {@link ContentValues} or from {@link Cursor}s.
|
|
||||||
*
|
|
||||||
* @param <FieldType>
|
|
||||||
* The type of the value this adapter stores.
|
|
||||||
* @param <EntitType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public interface FieldAdapter<FieldType, EntitType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is present and non-<code>null</code> in the given {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to check.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean existsIn(ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is present (may be <code>null</code>) in the given {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to check.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean isSetIn(ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value from the given {@link ContentValues}
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} that contain the value to return.
|
|
||||||
*
|
|
||||||
* @return The value.
|
|
||||||
*/
|
|
||||||
FieldType getFrom(ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is present and non-<code>null</code> in the given {@link Cursor}.
|
|
||||||
*
|
|
||||||
* @param cursor
|
|
||||||
* The {@link Cursor} that contains the value to check.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean existsIn(Cursor cursor);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value from the given {@link Cursor}
|
|
||||||
*
|
|
||||||
* @param cursor
|
|
||||||
* The {@link Cursor} that contain the value to return.
|
|
||||||
*
|
|
||||||
* @return The value.
|
|
||||||
*/
|
|
||||||
FieldType getFrom(Cursor cursor);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a value is present and non-<code>null</code> in the given {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param cursor
|
|
||||||
* The {@link Cursor} that contains the value to check.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} that contains the value to check.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
boolean existsIn(Cursor cursor, ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the value from the given {@link Cursor} or {@link ContentValues}, with the {@link ContentValues} taking precedence over the cursor values.
|
|
||||||
*
|
|
||||||
* @param cursor
|
|
||||||
* The {@link Cursor} that contains the value to return.
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} that contains the value to return.
|
|
||||||
*
|
|
||||||
* @return The value.
|
|
||||||
*/
|
|
||||||
FieldType getFrom(Cursor cursor, ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a value in the given {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to store the new value in.
|
|
||||||
* @param value
|
|
||||||
* The new value to store.
|
|
||||||
*/
|
|
||||||
void setIn(ContentValues values, FieldType value);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a value from the given {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} from which to remove the value.
|
|
||||||
*/
|
|
||||||
void removeFrom(ContentValues values);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the value from a {@link Cursor} to the given {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* The {@link Cursor} that contains the value to copy.
|
|
||||||
* @param dest
|
|
||||||
* The {@link ContentValues} to receive the value.
|
|
||||||
*/
|
|
||||||
void copyValue(Cursor source, ContentValues dest);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the value from {@link ContentValues} to another {@link ContentValues} object.
|
|
||||||
*
|
|
||||||
* @param source
|
|
||||||
* The {@link ContentValues} that contains the value to copy.
|
|
||||||
* @param dest
|
|
||||||
* The {@link ContentValues} to receive the value.
|
|
||||||
*/
|
|
||||||
void copyValue(ContentValues source, ContentValues dest);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,95 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a {@link Float} value from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class FloatFieldAdapter<EntityType> extends SimpleFieldAdapter<Float, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link FloatFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public FloatFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Float getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.getAsFloat(mFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Float getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return cursor.isNull(columnIdx) ? null : cursor.getFloat(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Float value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,96 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store an {@link Integer} from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class IntegerFieldAdapter<EntityType> extends SimpleFieldAdapter<Integer, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link IntegerFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public IntegerFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
// return the value as Integer
|
|
||||||
return values.getAsInteger(mFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Integer getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return cursor.isNull(columnIdx) ? null : cursor.getInt(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Integer value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,94 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a {@link Long} value from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class LongFieldAdapter<EntityType> extends SimpleFieldAdapter<Long, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link LongFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public LongFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.getAsLong(mFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return cursor.isNull(columnIdx) ? null : cursor.getLong(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, Long value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,122 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException;
|
|
||||||
import org.dmfs.rfc5545.recur.RecurrenceRule;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a {@link RecurrenceRule} from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class RRuleFieldAdapter<EntityType> extends SimpleFieldAdapter<RecurrenceRule, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link RRuleFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public RRuleFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecurrenceRule getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
String rrule = values.getAsString(mFieldName);
|
|
||||||
if (rrule == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new RecurrenceRule(rrule);
|
|
||||||
}
|
|
||||||
catch (InvalidRecurrenceRuleException e)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("can not parse RRULE '" + rrule + "'", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public RecurrenceRule getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
if (cursor.isNull(columnIdx))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new RecurrenceRule(cursor.getString(columnIdx));
|
|
||||||
}
|
|
||||||
catch (InvalidRecurrenceRuleException e)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("can not parse RRULE '" + cursor.getString(columnIdx) + "'", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, RecurrenceRule value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value.toString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,100 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An abstract {@link FieldAdapter} that implements a couple of methods as used by most simple FieldAdapters.
|
|
||||||
*
|
|
||||||
* @param <FieldType>
|
|
||||||
* The Type of the field this adapter handles.
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public abstract class SimpleFieldAdapter<FieldType, EntityType> implements FieldAdapter<FieldType, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the sole field name of this adapter.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
abstract String fieldName();
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsIn(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.get(fieldName()) != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isSetIn(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.containsKey(fieldName());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsIn(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(fieldName());
|
|
||||||
return columnIdx >= 0 && !cursor.isNull(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public FieldType getFrom(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
return values.containsKey(fieldName()) ? getFrom(values) : getFrom(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean existsIn(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
return existsIn(values) || existsIn(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void removeFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
values.remove(fieldName());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void copyValue(Cursor cursor, ContentValues values)
|
|
||||||
{
|
|
||||||
setIn(values, getFrom(cursor));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void copyValue(ContentValues oldValues, ContentValues newValues)
|
|
||||||
{
|
|
||||||
setIn(newValues, getFrom(oldValues));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,95 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store a {@link String} value from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class StringFieldAdapter<EntityType> extends SimpleFieldAdapter<String, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The field name this adapter uses to store the values.
|
|
||||||
*/
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link StringFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param fieldName
|
|
||||||
* The name of the field to use when loading or storing the value.
|
|
||||||
*/
|
|
||||||
public StringFieldAdapter(String fieldName)
|
|
||||||
{
|
|
||||||
if (fieldName == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("fieldName must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = fieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
// return the value as String
|
|
||||||
return values.getAsString(mFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
return cursor.getString(columnIdx);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, String value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +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.provider.tasks.model.adapters;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URL;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Knows how to load and store {@link URL} values from a {@link Cursor} or {@link ContentValues}.
|
|
||||||
*
|
|
||||||
* @param <EntityType>
|
|
||||||
* The type of the entity the field belongs to.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda <marten@dmfs.org>
|
|
||||||
*/
|
|
||||||
public final class UrlFieldAdapter<EntityType> extends SimpleFieldAdapter<URI, EntityType>
|
|
||||||
{
|
|
||||||
|
|
||||||
private final String mFieldName;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor for a new {@link UrlFieldAdapter}.
|
|
||||||
*
|
|
||||||
* @param urlField
|
|
||||||
* The field name that holds the URL.
|
|
||||||
*/
|
|
||||||
public UrlFieldAdapter(String urlField)
|
|
||||||
{
|
|
||||||
if (urlField == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("urlField must not be null");
|
|
||||||
}
|
|
||||||
mFieldName = urlField;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
String fieldName()
|
|
||||||
{
|
|
||||||
return mFieldName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public URI getFrom(ContentValues values)
|
|
||||||
{
|
|
||||||
return values.get(mFieldName) == null ? null : URI.create(values.getAsString(mFieldName));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public URI getFrom(Cursor cursor)
|
|
||||||
{
|
|
||||||
int columnIdx = cursor.getColumnIndex(mFieldName);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return cursor.isNull(columnIdx) ? null : URI.create(cursor.getString(columnIdx));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setIn(ContentValues values, URI value)
|
|
||||||
{
|
|
||||||
if (value != null)
|
|
||||||
{
|
|
||||||
values.put(mFieldName, value.toASCIIString());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
values.putNull(mFieldName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +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.provider.tasks.processors;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.EntityAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public interface EntityProcessor<T extends EntityAdapter<T>>
|
|
||||||
{
|
|
||||||
T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter);
|
|
||||||
|
|
||||||
T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter);
|
|
||||||
|
|
||||||
void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,67 +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.provider.tasks.processors;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.EntityAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Logging<T extends EntityAdapter<T>> implements EntityProcessor<T>
|
|
||||||
{
|
|
||||||
public static final String TAG = "Logging EntityProcessor";
|
|
||||||
private final EntityProcessor<T> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Logging(EntityProcessor<T> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
Log.d(TAG, "before insert");
|
|
||||||
T result = mDelegate.insert(db, entityAdapter, isSyncAdapter);
|
|
||||||
Log.d(TAG, "after insert on " + entityAdapter.id());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
Log.d(TAG, "before update of " + entityAdapter.id());
|
|
||||||
T result = mDelegate.update(db, entityAdapter, isSyncAdapter);
|
|
||||||
Log.d(TAG, "after update of " + entityAdapter.id());
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
Log.d(TAG, "before delete of " + entityAdapter.id());
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
Log.d(TAG, "after delete of " + entityAdapter.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.provider.tasks.processors;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.EntityAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple No-Op {@link EntityProcessor}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class NoOpProcessor<T extends EntityAdapter<T>> implements EntityProcessor<T>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public T insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return entityAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return entityAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,337 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.processors.instances;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.SingletonIterable;
|
|
||||||
import org.dmfs.iterables.decorators.Sieved;
|
|
||||||
import org.dmfs.jems.iterable.composite.Joined;
|
|
||||||
import org.dmfs.jems.optional.adapters.FirstPresent;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.predicate.composite.AnyOf;
|
|
||||||
import org.dmfs.jems.predicate.composite.Not;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesInstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.InstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.provider.tasks.utils.Timestamps;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.rfc5545.Duration;
|
|
||||||
import org.dmfs.rfc5545.recur.RecurrenceRule;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceList;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An instance {@link EntityProcessor} detaches completed instances at the start of a recurring task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Detaching implements EntityProcessor<InstanceAdapter>
|
|
||||||
{
|
|
||||||
|
|
||||||
private final EntityProcessor<InstanceAdapter> mDelegate;
|
|
||||||
private final EntityProcessor<TaskAdapter> mTaskDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Detaching(EntityProcessor<InstanceAdapter> delegate, EntityProcessor<TaskAdapter> taskDelegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
mTaskDelegate = taskDelegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// just delegate for now
|
|
||||||
// if we ever support inserting instances, we'll have to make sure that inserting a completed instance results in a detached task
|
|
||||||
return mDelegate.insert(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detach the given instance if all of the following conditions are met
|
|
||||||
* <p>
|
|
||||||
* - The instance is a recurrence instance (INSTANCE_ORIGINAL_TIME != null)
|
|
||||||
* - and the task has been closed (IS_CLOSED != 0)
|
|
||||||
* - and the instance is the first non-closed instance (DISTANCE_FROM_CURRENT==0).
|
|
||||||
* <p>
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (entityAdapter.valueOf(InstanceAdapter.DISTANCE_FROM_CURRENT) != 0 // not the first open task
|
|
||||||
|
|
||||||
// not closed, note we can't use IS_CLOSED at this point because its not updated yet
|
|
||||||
|| (!new HashSet<>(asList(TaskContract.Tasks.STATUS_COMPLETED, TaskContract.Tasks.STATUS_CANCELLED)).contains(
|
|
||||||
entityAdapter.valueOf(new IntegerFieldAdapter<>(TaskContract.Tasks.STATUS))))
|
|
||||||
|
|
||||||
// not recurring
|
|
||||||
|| entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME) == null)
|
|
||||||
{
|
|
||||||
// not a detachable instance
|
|
||||||
return mDelegate.update(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
// update instance accordingly and detach it
|
|
||||||
return detachAll(db, mDelegate.update(db, entityAdapter, isSyncAdapter));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// just delegate
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detach all closed instances preceding the given one.
|
|
||||||
* <p>
|
|
||||||
* TODO: this method needs some refactoring
|
|
||||||
*/
|
|
||||||
private InstanceAdapter detachAll(SQLiteDatabase db, InstanceAdapter entityAdapter)
|
|
||||||
{
|
|
||||||
// keep some values for later
|
|
||||||
long masterId = new FirstPresent<>(
|
|
||||||
new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.ORIGINAL_INSTANCE_ID))),
|
|
||||||
new NullSafe<>(entityAdapter.valueOf(new LongFieldAdapter<>(TaskContract.Instances.TASK_ID)))).value();
|
|
||||||
DateTime instanceOriginalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME);
|
|
||||||
|
|
||||||
// detach instances which are completed
|
|
||||||
try (Cursor instances = db.query(TaskDatabaseHelper.Tables.INSTANCE_VIEW,
|
|
||||||
null,
|
|
||||||
String.format("%s < 0 and %s == ?", TaskContract.Instances.DISTANCE_FROM_CURRENT, TaskContract.Instances.ORIGINAL_INSTANCE_ID),
|
|
||||||
new String[] { String.valueOf(masterId) },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null))
|
|
||||||
{
|
|
||||||
while (instances.moveToNext())
|
|
||||||
{
|
|
||||||
detachSingle(db, new CursorContentValuesInstanceAdapter(instances, new ContentValues()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// move the master to the first incomplete task
|
|
||||||
try (Cursor task = db.query(TaskDatabaseHelper.Tables.TASKS_VIEW,
|
|
||||||
null,
|
|
||||||
String.format("%s == ?", TaskContract.Tasks._ID),
|
|
||||||
new String[] { String.valueOf(masterId) },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null))
|
|
||||||
{
|
|
||||||
if (task.moveToFirst())
|
|
||||||
{
|
|
||||||
TaskAdapter masterTask = new CursorContentValuesTaskAdapter(task, new ContentValues());
|
|
||||||
DateTime oldStart = new FirstPresent<>(
|
|
||||||
new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)),
|
|
||||||
new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value();
|
|
||||||
|
|
||||||
// assume we have no instances left
|
|
||||||
boolean noInstances = true;
|
|
||||||
|
|
||||||
// update RRULE, if existent
|
|
||||||
RecurrenceRule rule = masterTask.valueOf(TaskAdapter.RRULE);
|
|
||||||
int count = 0;
|
|
||||||
if (rule != null)
|
|
||||||
{
|
|
||||||
RecurrenceSet ruleSet = new RecurrenceSet();
|
|
||||||
ruleSet.addInstances(new RecurrenceRuleAdapter(rule));
|
|
||||||
if (rule.getCount() == null)
|
|
||||||
{
|
|
||||||
// rule has no count limit, allowing us to exclude exdates
|
|
||||||
ruleSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value()));
|
|
||||||
}
|
|
||||||
RecurrenceSetIterator ruleIterator = ruleSet.iterator(
|
|
||||||
oldStart.getTimeZone(),
|
|
||||||
oldStart.getTimestamp());
|
|
||||||
|
|
||||||
// move DTSTART to next RRULE instance which is > instanceOriginalTime
|
|
||||||
// reduce COUNT by the number of skipped instances, if present
|
|
||||||
while (count < 1000 && ruleIterator.hasNext())
|
|
||||||
{
|
|
||||||
DateTime inst = new DateTime(oldStart.getTimeZone(), ruleIterator.next());
|
|
||||||
if (instanceOriginalTime.before(inst))
|
|
||||||
{
|
|
||||||
updateStart(masterTask, inst);
|
|
||||||
noInstances = false; // just found another instance
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (noInstances)
|
|
||||||
{
|
|
||||||
// remove the RRULE but keep a mask for the old start
|
|
||||||
masterTask.set(TaskAdapter.EXDATE,
|
|
||||||
new Joined<>(new SingletonIterable<>(oldStart), new Sieved<>(new Not<>(oldStart::equals), masterTask.valueOf(TaskAdapter.EXDATE))));
|
|
||||||
masterTask.set(TaskAdapter.RRULE, null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// adjust COUNT if present
|
|
||||||
if (rule.getCount() != null)
|
|
||||||
{
|
|
||||||
rule.setCount(rule.getCount() - count);
|
|
||||||
masterTask.set(TaskAdapter.RRULE, rule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime newStart = new FirstPresent<>(
|
|
||||||
new NullSafe<>(masterTask.valueOf(TaskAdapter.DTSTART)),
|
|
||||||
new NullSafe<>(masterTask.valueOf(TaskAdapter.DUE))).value();
|
|
||||||
|
|
||||||
// update RDATE and EXDATE
|
|
||||||
masterTask.set(TaskAdapter.RDATE, new Sieved<>(instanceOriginalTime::before, masterTask.valueOf(TaskAdapter.RDATE)));
|
|
||||||
masterTask.set(TaskAdapter.EXDATE,
|
|
||||||
new Sieved<>(new AnyOf<>(instanceOriginalTime::before, newStart::equals), masterTask.valueOf(TaskAdapter.EXDATE)));
|
|
||||||
|
|
||||||
// First check if we still have any RDATE instances left
|
|
||||||
// TODO: 6 lines for something we should be able to express in one simple expression, we need to straighten lib-recur!!
|
|
||||||
RecurrenceSet rdateSet = new RecurrenceSet();
|
|
||||||
rdateSet.addInstances(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.RDATE)).value()));
|
|
||||||
rdateSet.addExceptions(new RecurrenceList(new Timestamps(masterTask.valueOf(TaskAdapter.EXDATE)).value()));
|
|
||||||
RecurrenceSetIterator iterator = rdateSet.iterator(DateTime.UTC, Long.MIN_VALUE);
|
|
||||||
iterator.fastForward(Long.MIN_VALUE + 1); // skip bogus start
|
|
||||||
noInstances &= !iterator.hasNext();
|
|
||||||
|
|
||||||
if (noInstances)
|
|
||||||
{
|
|
||||||
// no more instances left, remove the master
|
|
||||||
mTaskDelegate.delete(db, masterTask, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (masterTask.valueOf(TaskAdapter.RRULE) == null)
|
|
||||||
{
|
|
||||||
// we don't have any RRULE, allowing us to adjust DTSTART/DUE to the first RDATE
|
|
||||||
DateTime start = new DateTime(iterator.next());
|
|
||||||
if (masterTask.valueOf(TaskAdapter.IS_ALLDAY))
|
|
||||||
{
|
|
||||||
start = start.toAllDay();
|
|
||||||
}
|
|
||||||
else if (masterTask.valueOf(TaskAdapter.TIMEZONE_RAW) != null)
|
|
||||||
{
|
|
||||||
start = start.shiftTimeZone(TimeZone.getTimeZone(masterTask.valueOf(TaskAdapter.TIMEZONE_RAW)));
|
|
||||||
}
|
|
||||||
updateStart(masterTask, start);
|
|
||||||
}
|
|
||||||
|
|
||||||
// we still have instances, update the database
|
|
||||||
mTaskDelegate.update(db, masterTask, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return entityAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updateStart(TaskAdapter task, DateTime newStart)
|
|
||||||
{
|
|
||||||
// this new instance becomes the new start (or due if we don't have a start)
|
|
||||||
if (task.valueOf(TaskAdapter.DTSTART) != null)
|
|
||||||
{
|
|
||||||
DateTime oldStart = task.valueOf(TaskAdapter.DTSTART);
|
|
||||||
task.set(TaskAdapter.DTSTART, newStart);
|
|
||||||
if (task.valueOf(TaskAdapter.DUE) != null)
|
|
||||||
{
|
|
||||||
long duration = task.valueOf(TaskAdapter.DUE).getTimestamp() - oldStart.getTimestamp();
|
|
||||||
task.set(TaskAdapter.DUE,
|
|
||||||
newStart.addDuration(
|
|
||||||
new Duration(1, (int) (duration / (3600 * 24 * 1000)), (int) (duration % (3600 * 24 * 1000)) / 1000)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.DUE, newStart);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detach the given instance.
|
|
||||||
* <p>
|
|
||||||
* - clone the override into a new deleted task (set _DELETED == 1)
|
|
||||||
* - detach the original override by removing the ORIGINAL_INSTANCE_ID, ORIGINAL_INSTANCE_SYNC_ID, ORIGINAL_INSTANCE_START and ORIGINAL_INSTANCE_ALLDAY
|
|
||||||
* (i.e. all columns which relate this to the original)
|
|
||||||
* - wipe _SYNC_ID, _UID and all sync columns (make this an unsynced task)
|
|
||||||
*/
|
|
||||||
private void detachSingle(SQLiteDatabase db, InstanceAdapter entityAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter original = entityAdapter.taskAdapter();
|
|
||||||
TaskAdapter cloneAdapter = original.duplicate();
|
|
||||||
|
|
||||||
// first prepare the original to resemble the same instance but as a new, detached task
|
|
||||||
original.set(TaskAdapter.SYNC_ID, null);
|
|
||||||
original.set(TaskAdapter.SYNC_VERSION, null);
|
|
||||||
original.set(TaskAdapter.SYNC1, null);
|
|
||||||
original.set(TaskAdapter.SYNC2, null);
|
|
||||||
original.set(TaskAdapter.SYNC3, null);
|
|
||||||
original.set(TaskAdapter.SYNC4, null);
|
|
||||||
original.set(TaskAdapter.SYNC5, null);
|
|
||||||
original.set(TaskAdapter.SYNC6, null);
|
|
||||||
original.set(TaskAdapter.SYNC7, null);
|
|
||||||
original.set(TaskAdapter.SYNC8, null);
|
|
||||||
original.set(TaskAdapter._UID, null);
|
|
||||||
original.set(TaskAdapter._DIRTY, true);
|
|
||||||
original.set(TaskAdapter.ORIGINAL_INSTANCE_ID, null);
|
|
||||||
original.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null);
|
|
||||||
original.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, null);
|
|
||||||
original.unset(TaskAdapter.COMPLETED);
|
|
||||||
original.commit(db);
|
|
||||||
|
|
||||||
// wipe INSTANCE_ORIGINAL_TIME from instances entry
|
|
||||||
ContentValues noOriginalTime = new ContentValues();
|
|
||||||
noOriginalTime.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
db.update(TaskDatabaseHelper.Tables.INSTANCES, noOriginalTime, "_ID = ?", new String[] { String.valueOf(entityAdapter.id()) });
|
|
||||||
|
|
||||||
// reset the clone to be a deleted instance
|
|
||||||
cloneAdapter.set(TaskAdapter._DELETED, true);
|
|
||||||
// remove joined field values
|
|
||||||
cloneAdapter.unset(TaskAdapter.LIST_ACCESS_LEVEL);
|
|
||||||
cloneAdapter.unset(TaskAdapter.LIST_COLOR);
|
|
||||||
cloneAdapter.unset(TaskAdapter.LIST_NAME);
|
|
||||||
cloneAdapter.unset(TaskAdapter.LIST_OWNER);
|
|
||||||
cloneAdapter.unset(TaskAdapter.LIST_VISIBLE);
|
|
||||||
cloneAdapter.unset(TaskAdapter.ACCOUNT_NAME);
|
|
||||||
cloneAdapter.unset(TaskAdapter.ACCOUNT_TYPE);
|
|
||||||
cloneAdapter.commit(db);
|
|
||||||
|
|
||||||
// note, we don't have to create an instance for the clone because it's deleted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,284 +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.provider.tasks.processors.instances;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.decorators.Filtered;
|
|
||||||
import org.dmfs.iterables.elementary.Seq;
|
|
||||||
import org.dmfs.iterators.filters.NoneOf;
|
|
||||||
import org.dmfs.jems.iterable.composite.Joined;
|
|
||||||
import org.dmfs.jems.optional.adapters.FirstPresent;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.handler.PropertyHandler;
|
|
||||||
import org.dmfs.provider.tasks.handler.PropertyHandlerFactory;
|
|
||||||
import org.dmfs.provider.tasks.model.ContentValuesInstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.InstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An instance {@link EntityProcessor} which delegates to the appropriate task {@link EntityProcessor}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TaskValueDelegate implements EntityProcessor<InstanceAdapter>
|
|
||||||
{
|
|
||||||
private final static Iterable<FieldAdapter<?, TaskAdapter>> SPECIAL_FIELD_ADAPTERS = new Seq<>(
|
|
||||||
TaskAdapter.SYNC1,
|
|
||||||
TaskAdapter.SYNC2,
|
|
||||||
TaskAdapter.SYNC3,
|
|
||||||
TaskAdapter.SYNC4,
|
|
||||||
TaskAdapter.SYNC5,
|
|
||||||
TaskAdapter.SYNC6,
|
|
||||||
TaskAdapter.SYNC7,
|
|
||||||
TaskAdapter.SYNC8,
|
|
||||||
TaskAdapter.SYNC_ID,
|
|
||||||
TaskAdapter.SYNC_VERSION,
|
|
||||||
// unset any list and read-only fields
|
|
||||||
TaskAdapter.VERSION,
|
|
||||||
TaskAdapter.ACCOUNT_NAME,
|
|
||||||
TaskAdapter.ACCOUNT_TYPE,
|
|
||||||
TaskAdapter.LIST_VISIBLE,
|
|
||||||
TaskAdapter.LIST_COLOR,
|
|
||||||
TaskAdapter.LIST_NAME,
|
|
||||||
TaskAdapter.LIST_ACCESS_LEVEL,
|
|
||||||
TaskAdapter.LIST_OWNER,
|
|
||||||
TaskAdapter._DELETED,
|
|
||||||
TaskAdapter._DIRTY,
|
|
||||||
TaskAdapter.IS_NEW,
|
|
||||||
TaskAdapter.IS_CLOSED,
|
|
||||||
TaskAdapter.HAS_PROPERTIES,
|
|
||||||
TaskAdapter.HAS_ALARMS,
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, /* this will be resolved automatically */
|
|
||||||
// also unset any recurrence fields
|
|
||||||
TaskAdapter.RRULE,
|
|
||||||
TaskAdapter.RDATE,
|
|
||||||
TaskAdapter.EXDATE,
|
|
||||||
TaskAdapter.CREATED,
|
|
||||||
TaskAdapter.LAST_MODIFIED
|
|
||||||
);
|
|
||||||
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public TaskValueDelegate(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter taskAdapter = entityAdapter.taskAdapter();
|
|
||||||
Long masterTaskId = null;
|
|
||||||
if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null)
|
|
||||||
{
|
|
||||||
// this is going to be an override to an existing task - make sure we add an RDATE first
|
|
||||||
masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID);
|
|
||||||
DateTime originalTime = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME);
|
|
||||||
// get the master and add an rdate
|
|
||||||
try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null /* all */, TaskContract.Tasks._ID + "=" + masterTaskId, null, null, null, null))
|
|
||||||
{
|
|
||||||
if (c.moveToFirst())
|
|
||||||
{
|
|
||||||
TaskAdapter masterTaskAdapter = new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues());
|
|
||||||
if (masterTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Can't add an instance to an override instance");
|
|
||||||
}
|
|
||||||
DateTime masterDate = new Backed<DateTime>(new FirstPresent<>(new Seq<>(
|
|
||||||
new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DTSTART)),
|
|
||||||
new NullSafe<>(masterTaskAdapter.valueOf(TaskAdapter.DUE)))), () -> null).value();
|
|
||||||
if (!masterTaskAdapter.isRecurring() && masterDate != null)
|
|
||||||
{
|
|
||||||
// master is not recurring yet, also add its start as an RDATE
|
|
||||||
appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, masterDate);
|
|
||||||
}
|
|
||||||
// TODO: should we throw if the new master has no DTSTART?
|
|
||||||
appendDate(masterTaskAdapter, TaskAdapter.RDATE, TaskAdapter.EXDATE, originalTime);
|
|
||||||
mDelegate.update(db, masterTaskAdapter, false);
|
|
||||||
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "No task with _ID %d found", masterTaskId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// move on with inserting the instance
|
|
||||||
TaskAdapter taskResult = mDelegate.insert(db, entityAdapter.taskAdapter(), false);
|
|
||||||
|
|
||||||
if (masterTaskId != null)
|
|
||||||
{
|
|
||||||
// we just cloned the master task into a new instance, we need to copy the properties as well
|
|
||||||
copyProperties(db, masterTaskId, taskResult.id());
|
|
||||||
}
|
|
||||||
|
|
||||||
try (Cursor c = db.query(TaskDatabaseHelper.Tables.INSTANCES, new String[] { TaskContract.Instances._ID },
|
|
||||||
TaskContract.Instances.TASK_ID + "=" + taskResult.id(), null, null, null, null))
|
|
||||||
{
|
|
||||||
// the cursor should contain exactly one row after this operation
|
|
||||||
c.moveToFirst();
|
|
||||||
return new ContentValuesInstanceAdapter(c.getLong(0), new ContentValues());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// if this is the master of a recurring task, we create a new instance or update an existing one for this override, otherwise we just delegate
|
|
||||||
TaskAdapter taskAdapter = entityAdapter.taskAdapter();
|
|
||||||
if (taskAdapter.isRecurring())
|
|
||||||
{
|
|
||||||
// clone the task to create an unsynced override
|
|
||||||
InstanceAdapter newInstanceAdapter = entityAdapter.duplicate();
|
|
||||||
TaskAdapter override = newInstanceAdapter.taskAdapter();
|
|
||||||
override.set(TaskAdapter.ORIGINAL_INSTANCE_ID, entityAdapter.valueOf(InstanceAdapter.TASK_ID));
|
|
||||||
override.set(TaskAdapter.ORIGINAL_INSTANCE_TIME, entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME));
|
|
||||||
// unset all fields which have special meaning
|
|
||||||
for (FieldAdapter<?, TaskAdapter> specialFieldAdapter : SPECIAL_FIELD_ADAPTERS)
|
|
||||||
{
|
|
||||||
override.unset(specialFieldAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure we update DTSTART and DUE to match the instance values (unless they are set explicitly)
|
|
||||||
if (!taskAdapter.isUpdated(TaskAdapter.DTSTART))
|
|
||||||
{
|
|
||||||
// set DTSTART to the instance start
|
|
||||||
override.set(TaskAdapter.DTSTART, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_START));
|
|
||||||
}
|
|
||||||
if (!taskAdapter.isUpdated(TaskAdapter.DUE) && !taskAdapter.isUpdated(TaskAdapter.DURATION))
|
|
||||||
{
|
|
||||||
// set DUE to the effective instance DUE and wipe any duration
|
|
||||||
override.set(TaskAdapter.DUE, newInstanceAdapter.valueOf(InstanceAdapter.INSTANCE_DUE));
|
|
||||||
override.set(TaskAdapter.DURATION, null);
|
|
||||||
}
|
|
||||||
// copy original instance allday flag
|
|
||||||
override.set(TaskAdapter.ORIGINAL_INSTANCE_ALLDAY, taskAdapter.valueOf(TaskAdapter.IS_ALLDAY));
|
|
||||||
|
|
||||||
TaskAdapter newTask = mDelegate.insert(db, override, false);
|
|
||||||
|
|
||||||
copyProperties(db, taskAdapter.id(), newTask.id());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// this is a non-recurring task or it's already an override, just delegate the update
|
|
||||||
mDelegate.update(db, taskAdapter, false);
|
|
||||||
}
|
|
||||||
return entityAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// deleted instances are converted to deleted tasks (for non-recurring tasks) or exdates (for recurring tasks).
|
|
||||||
TaskAdapter taskAdapter = entityAdapter.taskAdapter();
|
|
||||||
|
|
||||||
if (taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null)
|
|
||||||
{
|
|
||||||
/* this is an override - we have to:
|
|
||||||
* - mark it deleted
|
|
||||||
* - add an exclusion to the master task
|
|
||||||
*
|
|
||||||
* TODO: if this instance was added by an RDATE, just remove the RDATE
|
|
||||||
* TODO: if this is the first instance, consider moving the recurrence start instead of adding an exdate
|
|
||||||
* TODO: if this is the last instance of a finite task, consider just setting a new recurrence end
|
|
||||||
*/
|
|
||||||
long masterTaskId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID);
|
|
||||||
DateTime originalTime = entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME);
|
|
||||||
|
|
||||||
// delete the override
|
|
||||||
mDelegate.delete(db, taskAdapter, false);
|
|
||||||
|
|
||||||
// get the master and add an exdate
|
|
||||||
try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null /* all */, TaskContract.Tasks._ID + "=" + masterTaskId, null, null, null, null))
|
|
||||||
{
|
|
||||||
if (c.moveToFirst())
|
|
||||||
{
|
|
||||||
TaskAdapter masterTaskAdapter = new CursorContentValuesTaskAdapter(masterTaskId, c, new ContentValues());
|
|
||||||
appendDate(masterTaskAdapter, TaskAdapter.EXDATE, TaskAdapter.RDATE, originalTime);
|
|
||||||
mDelegate.update(db, masterTaskAdapter, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (taskAdapter.isRecurring())
|
|
||||||
{
|
|
||||||
// TODO: if this is the first instance, consider moving the recurrence start instead of adding an exdate
|
|
||||||
// TODO: if this is the last instance of a finite task, consider just setting a new recurrence end
|
|
||||||
appendDate(taskAdapter, TaskAdapter.EXDATE, TaskAdapter.RDATE, entityAdapter.valueOf(InstanceAdapter.INSTANCE_ORIGINAL_TIME));
|
|
||||||
mDelegate.update(db, taskAdapter, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// task is non-recurring, delete it as a non-sync-adapter (effectively setting the _deleted flag)
|
|
||||||
mDelegate.delete(db, taskAdapter, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void appendDate(TaskAdapter taskAdapter, FieldAdapter<Iterable<DateTime>, TaskAdapter> addfieldAdapter, FieldAdapter<Iterable<DateTime>, TaskAdapter> removefieldAdapter, DateTime dateTime)
|
|
||||||
{
|
|
||||||
taskAdapter.set(addfieldAdapter, new Joined<>(new Filtered<>(taskAdapter.valueOf(addfieldAdapter), new NoneOf<>(dateTime)), new Seq<>(dateTime)));
|
|
||||||
taskAdapter.set(removefieldAdapter, new Filtered<>(taskAdapter.valueOf(removefieldAdapter), new NoneOf<>(dateTime)));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copy the properties from the give original task to the new task.
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* The {@link SQLiteDatabase}
|
|
||||||
* @param originalId
|
|
||||||
* The ID of the task of which to copy the properties
|
|
||||||
* @param newId
|
|
||||||
* The ID of the task to copy the properties to.
|
|
||||||
*/
|
|
||||||
private void copyProperties(SQLiteDatabase db, long originalId, long newId)
|
|
||||||
{
|
|
||||||
// for each property of the original task
|
|
||||||
try (Cursor c = db.query(TaskDatabaseHelper.Tables.PROPERTIES, null /* all */,
|
|
||||||
String.format(Locale.ENGLISH, "%s = %d", TaskContract.Properties.TASK_ID, originalId), null, null, null, null))
|
|
||||||
{
|
|
||||||
// load the property and insert it for the new task
|
|
||||||
ContentValues values = new ContentValues(c.getColumnCount());
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
values.clear();
|
|
||||||
DatabaseUtils.cursorRowToContentValues(c, values);
|
|
||||||
PropertyHandler ph = PropertyHandlerFactory.get(values.getAsString(TaskContract.Properties.MIMETYPE));
|
|
||||||
ph.insert(db, newId, ph.cloneForNewTask(newId, values), false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +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.provider.tasks.processors.instances;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.iterables.decorators.Sieved;
|
|
||||||
import org.dmfs.iterables.elementary.Seq;
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.adapters.First;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.InstanceAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.FieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link EntityProcessor} which validates the instance data.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Validating implements EntityProcessor<InstanceAdapter>
|
|
||||||
{
|
|
||||||
private final static Iterable<FieldAdapter<?, InstanceAdapter>> INSTANCE_FIELD_ADAPTERS = new Seq<>(
|
|
||||||
InstanceAdapter._ID,
|
|
||||||
InstanceAdapter.INSTANCE_START,
|
|
||||||
InstanceAdapter.INSTANCE_START_SORTING,
|
|
||||||
InstanceAdapter.INSTANCE_DUE,
|
|
||||||
InstanceAdapter.INSTANCE_DUE_SORTING,
|
|
||||||
InstanceAdapter.INSTANCE_ORIGINAL_TIME,
|
|
||||||
InstanceAdapter.DISTANCE_FROM_CURRENT,
|
|
||||||
InstanceAdapter.TASK_ID);
|
|
||||||
|
|
||||||
private final static Iterable<FieldAdapter<?, TaskAdapter>> RECURRENCE_FIELD_ADAPTERS = new Seq<>(
|
|
||||||
TaskAdapter.RRULE,
|
|
||||||
TaskAdapter.RDATE,
|
|
||||||
TaskAdapter.EXDATE);
|
|
||||||
|
|
||||||
private static final Iterable<FieldAdapter<?, TaskAdapter>> ORIGINAL_INSTANCE_FIELD_ADAPTERS = new Seq<>(
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_ID,
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_TIME,
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_ALLDAY,
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID);
|
|
||||||
|
|
||||||
private final EntityProcessor<InstanceAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Validating(EntityProcessor<InstanceAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter insert(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
validateIsSyncAdapter(isSyncAdapter);
|
|
||||||
validateValues(entityAdapter);
|
|
||||||
validateInstanceIsNew(db, entityAdapter);
|
|
||||||
|
|
||||||
return mDelegate.insert(db, entityAdapter, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public InstanceAdapter update(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
validateIsSyncAdapter(isSyncAdapter);
|
|
||||||
validateValues(entityAdapter);
|
|
||||||
validateOriginalInstanceValues(entityAdapter);
|
|
||||||
return mDelegate.update(db, entityAdapter, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, InstanceAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
validateIsSyncAdapter(isSyncAdapter);
|
|
||||||
mDelegate.delete(db, entityAdapter, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateIsSyncAdapter(boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (isSyncAdapter)
|
|
||||||
{
|
|
||||||
throw new UnsupportedOperationException("Sync adapters are not expected to write to the instances table.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateInstanceIsNew(SQLiteDatabase db, InstanceAdapter entityAdapter)
|
|
||||||
{
|
|
||||||
Optional<Long> instanceId = new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID));
|
|
||||||
Optional<DateTime> instanceTime = new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME));
|
|
||||||
|
|
||||||
// check if ORIGINAL_INSTANCE_ID and ORIGINAL_INSTANCE_TIME are both present/absent at the same time
|
|
||||||
if (instanceId.isPresent() != instanceTime.isPresent())
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException(String.format("%s and %s must either be both absent or both present",
|
|
||||||
TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks.ORIGINAL_INSTANCE_TIME));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instanceId.isPresent())
|
|
||||||
{
|
|
||||||
String timeStampString = Long.toString(instanceTime.value().getTimestamp());
|
|
||||||
// Make sure there is no instance at the given time already
|
|
||||||
try (Cursor c = db.query(
|
|
||||||
TaskDatabaseHelper.Tables.INSTANCE_VIEW,
|
|
||||||
new String[] { TaskContract.Instances._ID },
|
|
||||||
// find any instance which refers to the given original ID and has the same instance time
|
|
||||||
// for recurring tasks this matches the INSTANCE_ORIGINAL_TIME, for non-recurring tasks this matches start or due (whichever is present).
|
|
||||||
String.format("(%1$s == ? or %2$s == ?) and (%3$s == ? or %3$s is null and %4$s == ? or %3$s is null and %4$s is null and %5$s == ?) ",
|
|
||||||
TaskContract.Instances.TASK_ID,
|
|
||||||
TaskContract.Instances.ORIGINAL_INSTANCE_ID,
|
|
||||||
TaskContract.Instances.INSTANCE_ORIGINAL_TIME,
|
|
||||||
TaskContract.Instances.INSTANCE_START,
|
|
||||||
TaskContract.Instances.INSTANCE_DUE),
|
|
||||||
new String[] {
|
|
||||||
instanceId.value().toString(),
|
|
||||||
instanceId.value().toString(),
|
|
||||||
timeStampString,
|
|
||||||
timeStampString,
|
|
||||||
timeStampString },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null))
|
|
||||||
{
|
|
||||||
if (c.getCount() > 0)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException(String.format(Locale.ENGLISH, "Instance %s of task %d already exists",
|
|
||||||
entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME).toString(), instanceId.value()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateValues(InstanceAdapter instanceAdapter)
|
|
||||||
{
|
|
||||||
// actually, no instance value can be changed, the instance table only allows for updating task values
|
|
||||||
if (new First<>(new Sieved<>(instanceAdapter::isUpdated, INSTANCE_FIELD_ADAPTERS)).isPresent())
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Instance columns are read-only.");
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskAdapter taskAdapter = instanceAdapter.taskAdapter();
|
|
||||||
// By definition, single instances don't have a recurrence set on their own, hence changes to the recurrence fields are not allowed.
|
|
||||||
if (new First<>(new Sieved<>(taskAdapter::isUpdated, RECURRENCE_FIELD_ADAPTERS)).isPresent())
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Recurrence values can not be modified through the instances table.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void validateOriginalInstanceValues(InstanceAdapter instanceAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter taskAdapter = instanceAdapter.taskAdapter();
|
|
||||||
// Updates of ORIGINAL_INSTANCE_* fields are not allowed
|
|
||||||
if (new First<>(new Sieved<>(taskAdapter::isUpdated, ORIGINAL_INSTANCE_FIELD_ADAPTERS)).isPresent())
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ORIGINAL_INSTANCE_* fields can not be updated through the instances table.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +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.provider.tasks.processors.lists;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.ListAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor that performs the actual operations on task lists.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class ListCommitProcessor implements EntityProcessor<ListAdapter>
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter insert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
list.commit(db);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter update(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
list.commit(db);
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
db.delete(TaskDatabaseHelper.Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +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.provider.tasks.processors.lists;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.model.ListAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor to validate the values of a task list.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Validating implements EntityProcessor<ListAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<ListAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Validating(EntityProcessor<ListAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter insert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_NAME)))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_NAME is required on INSERT");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_TYPE)))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_TYPE is required on INSERT");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyCommon(list, isSyncAdapter);
|
|
||||||
return mDelegate.insert(db, list, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ListAdapter update(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (list.isUpdated(ListAdapter.ACCOUNT_NAME))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_NAME is write-once");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.isUpdated(ListAdapter.ACCOUNT_TYPE))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_TYPE is write-once");
|
|
||||||
}
|
|
||||||
|
|
||||||
verifyCommon(list, isSyncAdapter);
|
|
||||||
return mDelegate.update(db, list, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, ListAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists");
|
|
||||||
}
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs tests that are common to insert an update operations.
|
|
||||||
*
|
|
||||||
* @param list
|
|
||||||
* The {@link ListAdapter} to verify.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* <code>true</code> if the caller is a sync adapter, false otherwise.
|
|
||||||
*/
|
|
||||||
private void verifyCommon(ListAdapter list, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// row id can not be changed or set manually
|
|
||||||
if (list.isUpdated(ListAdapter._ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("_ID can not be set manually");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSyncAdapter)
|
|
||||||
{
|
|
||||||
// sync adapters may do all the stuff below
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.isUpdated(ListAdapter.LIST_COLOR))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only sync adapters can change the LIST_COLOR.");
|
|
||||||
}
|
|
||||||
if (list.isUpdated(ListAdapter.LIST_NAME))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only sync adapters can change the LIST_NAME.");
|
|
||||||
}
|
|
||||||
if (list.isUpdated(ListAdapter.SYNC_ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only sync adapters can change the _SYNC_ID.");
|
|
||||||
}
|
|
||||||
if (list.isUpdated(ListAdapter.SYNC_VERSION))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only sync adapters can change SYNC_VERSION.");
|
|
||||||
}
|
|
||||||
if (list.isUpdated(ListAdapter.OWNER))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only sync adapters can change the list OWNER.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,210 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor to adjust some task values automatically.
|
|
||||||
* <p/>
|
|
||||||
* Other then recurrence exceptions no relations are handled by this code. Relation specific changes go to {@link Relating}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class AutoCompleting implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
private static final String[] TASK_ID_PROJECTION = { TaskContract.Tasks._ID };
|
|
||||||
private static final String[] TASK_SYNC_ID_PROJECTION = { TaskContract.Tasks._SYNC_ID };
|
|
||||||
|
|
||||||
private static final String SYNC_ID_SELECTION = TaskContract.Tasks._SYNC_ID + "=?";
|
|
||||||
private static final String TASK_ID_SELECTION = TaskContract.Tasks._ID + "=?";
|
|
||||||
|
|
||||||
|
|
||||||
public AutoCompleting(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
updateFields(db, task, isSyncAdapter);
|
|
||||||
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
// set created date for tasks created on the device
|
|
||||||
task.set(TaskAdapter.CREATED, DateTime.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
|
|
||||||
if (isSyncAdapter && result.isRecurring())
|
|
||||||
{
|
|
||||||
// task is recurring, update ORIGINAL_INSTANCE_ID of all exceptions that may already exists
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_ID.setIn(values, result.id());
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + "=? and "
|
|
||||||
+ TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " is null", new String[] { result.valueOf(TaskAdapter.SYNC_ID) });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
updateFields(db, task, isSyncAdapter);
|
|
||||||
TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
|
|
||||||
if (isSyncAdapter && result.isRecurring() && result.isUpdated(TaskAdapter.SYNC_ID))
|
|
||||||
{
|
|
||||||
// task is recurring, update ORIGINAL_INSTANCE_SYNC_ID of all exceptions that may already exists
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID.setIn(values, result.valueOf(TaskAdapter.SYNC_ID));
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + result.id(), null);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter._DIRTY, true);
|
|
||||||
task.set(TaskAdapter.LAST_MODIFIED, DateTime.now());
|
|
||||||
|
|
||||||
// set proper STATUS if task has been completed
|
|
||||||
if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS))
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.STATUS, TaskContract.Tasks.STATUS_COMPLETED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.isUpdated(TaskAdapter.PRIORITY))
|
|
||||||
{
|
|
||||||
Integer priority = task.valueOf(TaskAdapter.PRIORITY);
|
|
||||||
if (priority != null && priority == 0)
|
|
||||||
{
|
|
||||||
// replace priority 0 by null, it's the default and we need that for proper sorting
|
|
||||||
task.set(TaskAdapter.PRIORITY, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find corresponding ORIGINAL_INSTANCE_ID
|
|
||||||
if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID))
|
|
||||||
{
|
|
||||||
String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) };
|
|
||||||
try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null))
|
|
||||||
{
|
|
||||||
if (cursor.moveToNext())
|
|
||||||
{
|
|
||||||
Long originalId = cursor.getLong(0);
|
|
||||||
task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID
|
|
||||||
{
|
|
||||||
String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) };
|
|
||||||
try (Cursor cursor = db.query(TaskDatabaseHelper.Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null))
|
|
||||||
{
|
|
||||||
if (cursor.moveToNext())
|
|
||||||
{
|
|
||||||
String originalSyncId = cursor.getString(0);
|
|
||||||
task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that PERCENT_COMPLETE is an Integer between 0 and 100 if supplied also update status and completed accordingly
|
|
||||||
if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE))
|
|
||||||
{
|
|
||||||
Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE);
|
|
||||||
|
|
||||||
if (!isSyncAdapter && percent != null && percent == 100)
|
|
||||||
{
|
|
||||||
if (!task.isUpdated(TaskAdapter.STATUS))
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.STATUS, TaskContract.Tasks.STATUS_COMPLETED);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!task.isUpdated(TaskAdapter.COMPLETED))
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!isSyncAdapter && percent != null)
|
|
||||||
{
|
|
||||||
if (!task.isUpdated(TaskAdapter.COMPLETED))
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.COMPLETED, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate STATUS and set IS_NEW and IS_CLOSED accordingly
|
|
||||||
if (task.isUpdated(TaskAdapter.STATUS) || task.id() < 0 /* this is true when the task is new */)
|
|
||||||
{
|
|
||||||
Integer status = task.valueOf(TaskAdapter.STATUS);
|
|
||||||
if (status == null)
|
|
||||||
{
|
|
||||||
status = TaskContract.Tasks.STATUS_DEFAULT;
|
|
||||||
task.set(TaskAdapter.STATUS, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
task.set(TaskAdapter.IS_NEW, status == TaskContract.Tasks.STATUS_NEEDS_ACTION);
|
|
||||||
task.set(TaskAdapter.IS_CLOSED, status == TaskContract.Tasks.STATUS_COMPLETED || status == TaskContract.Tasks.STATUS_CANCELLED);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Update PERCENT_COMPLETE and COMPLETED (if not given). Sync adapters should know what they're doing, so don't update anything if caller is a sync
|
|
||||||
* adapter.
|
|
||||||
*/
|
|
||||||
if (status == TaskContract.Tasks.STATUS_COMPLETED && !isSyncAdapter)
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.PERCENT_COMPLETE, 100);
|
|
||||||
if (!task.isUpdated(TaskAdapter.COMPLETED) || task.valueOf(TaskAdapter.COMPLETED) == null)
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
task.set(TaskAdapter.COMPLETED, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,397 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.jems.function.elementary.DiffMap;
|
|
||||||
import org.dmfs.jems.iterable.composite.Diff;
|
|
||||||
import org.dmfs.jems.iterable.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.pair.Pair;
|
|
||||||
import org.dmfs.jems.pair.elementary.RightSidedPair;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.provider.tasks.utils.InstanceValuesIterable;
|
|
||||||
import org.dmfs.provider.tasks.utils.Limited;
|
|
||||||
import org.dmfs.provider.tasks.utils.OverrideValuesFunction;
|
|
||||||
import org.dmfs.provider.tasks.utils.Range;
|
|
||||||
import org.dmfs.provider.tasks.utils.RowIterator;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import static org.dmfs.provider.tasks.model.TaskAdapter.IS_CLOSED;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor that creates or updates the instance values of a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Instantiating implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Projection we use to read the overrides of a task
|
|
||||||
*/
|
|
||||||
private final static String[] OVERRIDE_PROJECTION = {
|
|
||||||
TaskContract.Tasks._ID,
|
|
||||||
TaskContract.Tasks.DTSTART,
|
|
||||||
TaskContract.Tasks.DUE,
|
|
||||||
TaskContract.Tasks.DURATION,
|
|
||||||
TaskContract.Tasks.TZ,
|
|
||||||
TaskContract.Tasks.IS_ALLDAY,
|
|
||||||
TaskContract.Tasks.IS_CLOSED,
|
|
||||||
TaskContract.Tasks.ORIGINAL_INSTANCE_TIME,
|
|
||||||
TaskContract.Tasks.ORIGINAL_INSTANCE_ALLDAY };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a field adapter for a pseudo column to indicate that the instances may need an update, even if no relevant value has changed. This is useful to
|
|
||||||
* force an update of the sorting values when the local timezone has been changed.
|
|
||||||
* <p>
|
|
||||||
* TODO: get rid of it
|
|
||||||
*/
|
|
||||||
private final static BooleanFieldAdapter<TaskAdapter> UPDATE_REQUESTED = new BooleanFieldAdapter<TaskAdapter>(
|
|
||||||
"org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED");
|
|
||||||
|
|
||||||
// for now we only expand the next upcoming instance
|
|
||||||
private final static int UPCOMING_INSTANCE_COUNT_LIMIT = 1;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a pseudo column to the given {@link ContentValues} to request an instances update, even if no time value has changed.
|
|
||||||
*
|
|
||||||
* @param values
|
|
||||||
* The {@link ContentValues} to add the pseudo column to.
|
|
||||||
*/
|
|
||||||
public static void addUpdateRequest(ContentValues values)
|
|
||||||
{
|
|
||||||
UPDATE_REQUESTED.setIn(values, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Instantiating(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null)
|
|
||||||
{
|
|
||||||
// an override was created, insert a single task
|
|
||||||
updateOverrideInstance(db, result, result.id());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// update the recurring instances, there may already be overrides, so we use the update method
|
|
||||||
updateMasterInstances(db, result, result.id());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// TODO: get rid if this mechanism
|
|
||||||
boolean updateRequested = task.isUpdated(UPDATE_REQUESTED) ? task.valueOf(UPDATE_REQUESTED) : false;
|
|
||||||
task.unset(UPDATE_REQUESTED);
|
|
||||||
|
|
||||||
TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
|
|
||||||
if (!result.isUpdated(TaskAdapter.DTSTART) && !result.isUpdated(TaskAdapter.DUE) && !result.isUpdated(TaskAdapter.DURATION)
|
|
||||||
&& !result.isUpdated(TaskAdapter.STATUS) && !result.isUpdated(TaskAdapter.RDATE) && !result.isUpdated(TaskAdapter.RRULE) && !result.isUpdated(
|
|
||||||
TaskAdapter.EXDATE) && !result.isUpdated(IS_CLOSED) && !updateRequested)
|
|
||||||
{
|
|
||||||
// date values didn't change and update not requested -> no need to update the instances table
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) == null)
|
|
||||||
{
|
|
||||||
updateMasterInstances(db, result, result.id());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
updateOverrideInstance(db, result, result.id());
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// Note: there is a database trigger which cleans the instances table automatically when a task is deleted
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the instance of an override.
|
|
||||||
* <p>
|
|
||||||
* TODO: take instance overrides into account
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* an {@link SQLiteDatabase}.
|
|
||||||
* @param taskAdapter
|
|
||||||
* the {@link TaskAdapter} of the task to insert.
|
|
||||||
* @param id
|
|
||||||
* the row id of the new task.
|
|
||||||
*/
|
|
||||||
private void updateOverrideInstance(SQLiteDatabase db, TaskAdapter taskAdapter, long id)
|
|
||||||
{
|
|
||||||
long origId = taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID);
|
|
||||||
int count = 0;
|
|
||||||
if (!taskAdapter.isUpdated(IS_CLOSED))
|
|
||||||
{
|
|
||||||
// task status was not updated, we can take the shortcut and only update any existing instance values
|
|
||||||
for (Single<ContentValues> values : new InstanceValuesIterable(id, taskAdapter))
|
|
||||||
{
|
|
||||||
if (count++ > 1)
|
|
||||||
{
|
|
||||||
throw new RuntimeException("more than one instance returned for task instance which was supposed to have exactly one");
|
|
||||||
}
|
|
||||||
ContentValues contentValues = values.value();
|
|
||||||
// we don't know the current distance, but it for sure hasn't changed either, so just make sure we don't change it
|
|
||||||
contentValues.remove(TaskContract.Instances.DISTANCE_FROM_CURRENT);
|
|
||||||
// TASK_ID hasn't changed either
|
|
||||||
contentValues.remove(TaskContract.Instances.TASK_ID);
|
|
||||||
|
|
||||||
db.update(TaskDatabaseHelper.Tables.INSTANCES,
|
|
||||||
contentValues,
|
|
||||||
String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances.TASK_ID, id),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
if (count == 0)
|
|
||||||
{
|
|
||||||
throw new RuntimeException("no instance returned for task which was supposed to have exactly one");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// task status was updated, this might affect other instances, update them all
|
|
||||||
// ensure the distance from current is set properly for all sibling instances
|
|
||||||
try (Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null,
|
|
||||||
String.format(Locale.ENGLISH, "(%s = %d)", TaskContract.Tasks._ID, origId), null, null, null, null))
|
|
||||||
{
|
|
||||||
if (c.moveToFirst())
|
|
||||||
{
|
|
||||||
TaskAdapter ta = new CursorContentValuesTaskAdapter(c, new ContentValues());
|
|
||||||
updateMasterInstances(db, ta, ta.id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the instances of an existing task
|
|
||||||
*
|
|
||||||
* @param db
|
|
||||||
* An {@link SQLiteDatabase}.
|
|
||||||
* @param taskAdapter
|
|
||||||
* the {@link TaskAdapter} of the task to update
|
|
||||||
* @param id
|
|
||||||
* the row id of the new task
|
|
||||||
*/
|
|
||||||
private void updateMasterInstances(SQLiteDatabase db, TaskAdapter taskAdapter, long id)
|
|
||||||
{
|
|
||||||
try (Cursor existingInstances = db.query(
|
|
||||||
TaskDatabaseHelper.Tables.INSTANCE_VIEW,
|
|
||||||
new String[] {
|
|
||||||
TaskContract.Instances._ID,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_ORIGINAL_TIME,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_START,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_START_SORTING,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_DUE,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_DUE_SORTING,
|
|
||||||
TaskContract.InstanceColumns.INSTANCE_DURATION,
|
|
||||||
TaskContract.InstanceColumns.TASK_ID,
|
|
||||||
TaskContract.InstanceColumns.DISTANCE_FROM_CURRENT,
|
|
||||||
TaskContract.Instances.IS_CLOSED },
|
|
||||||
String.format(Locale.ENGLISH, "%s = ? or %s = ?", TaskContract.Instances.TASK_ID, TaskContract.Instances.ORIGINAL_INSTANCE_ID),
|
|
||||||
new String[] { Long.toString(id), Long.toString(id) },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
Cursor overrides = db.query(
|
|
||||||
TaskDatabaseHelper.Tables.TASKS,
|
|
||||||
OVERRIDE_PROJECTION,
|
|
||||||
String.format("%s = ? AND %s != 1", TaskContract.Tasks.ORIGINAL_INSTANCE_ID, TaskContract.Tasks._DELETED),
|
|
||||||
new String[] { Long.toString(id) },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
TaskContract.Tasks.ORIGINAL_INSTANCE_TIME);)
|
|
||||||
{
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The goal of the code below is to update existing instances in place (as opposed to delete and recreate all instances). We do this for two reasons:
|
|
||||||
* 1) efficiency, in most cases existing instances don't change, deleting and recreating them would be overly expensive
|
|
||||||
* 2) stable row ids, deleting and recreating instances would change their id and void any existing URIs to them
|
|
||||||
*/
|
|
||||||
final int idIdx = existingInstances.getColumnIndex(TaskContract.Instances._ID);
|
|
||||||
final int startIdx = existingInstances.getColumnIndex(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
final int taskIdIdx = existingInstances.getColumnIndex(TaskContract.Instances.TASK_ID);
|
|
||||||
final int isClosedIdx = existingInstances.getColumnIndex(TaskContract.Instances.IS_CLOSED);
|
|
||||||
final int distanceIdx = existingInstances.getColumnIndex(TaskContract.Instances.DISTANCE_FROM_CURRENT);
|
|
||||||
|
|
||||||
// get an Iterator of all expected instances
|
|
||||||
// for very long or even infinite series we need to stop iterating at some point.
|
|
||||||
|
|
||||||
Iterable<Pair<Optional<ContentValues>, Optional<Integer>>> diff = new Diff<>(
|
|
||||||
new Mapped<>(Single::value, new Limited<>(10000 /* hard limit for infinite rules*/,
|
|
||||||
new Mapped<>(
|
|
||||||
new DiffMap<>(
|
|
||||||
(original, override) -> override, // we have both, a regular instance and an override -> take the override
|
|
||||||
original -> original,
|
|
||||||
override -> override // we only have an override :-o, not really valid but tolerated
|
|
||||||
),
|
|
||||||
new Diff<>(
|
|
||||||
new InstanceValuesIterable(id, taskAdapter),
|
|
||||||
new Mapped<>(
|
|
||||||
cursor ->
|
|
||||||
new OverrideValuesFunction()
|
|
||||||
.value(new CursorContentValuesTaskAdapter(cursor, new ContentValues())),
|
|
||||||
() -> new RowIterator(overrides)),
|
|
||||||
(left, right) -> {
|
|
||||||
Long leftLong = left.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
Long rightLong = right.value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
// null is always smaller
|
|
||||||
if (leftLong == null)
|
|
||||||
{
|
|
||||||
return rightLong == null ? 0 : -1;
|
|
||||||
}
|
|
||||||
if (rightLong == null)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
long ldiff = leftLong - rightLong;
|
|
||||||
return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0);
|
|
||||||
})))),
|
|
||||||
new Range(existingInstances.getCount()),
|
|
||||||
(newInstanceValues, cursorRow) ->
|
|
||||||
{
|
|
||||||
existingInstances.moveToPosition(cursorRow);
|
|
||||||
long ldiff = new Backed<>(new NullSafe<>(newInstanceValues.getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME)), 0L).value()
|
|
||||||
- existingInstances.getLong(startIdx);
|
|
||||||
return ldiff < 0 ? -1 : (ldiff > 0 ? 1 : 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
int distance = -1;
|
|
||||||
// sync the instances table with the new instances
|
|
||||||
for (Pair<Optional<ContentValues>, Optional<Integer>> next : diff)
|
|
||||||
{
|
|
||||||
if (distance >= UPCOMING_INSTANCE_COUNT_LIMIT - 1)
|
|
||||||
{
|
|
||||||
// we already expanded enough instances
|
|
||||||
if (!next.right().isPresent())
|
|
||||||
{
|
|
||||||
// if no further instances exist, stop here
|
|
||||||
Long original = next.left().value().getAsLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
if (original != null && existingInstances.moveToLast() && existingInstances.getLong(startIdx) < original)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we may have to delete a few future instances
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
next = new RightSidedPair<>(next.right());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!next.left().isPresent())
|
|
||||||
{
|
|
||||||
// there is no new instance for this old one, remove it
|
|
||||||
existingInstances.moveToPosition(next.right().value());
|
|
||||||
db.delete(TaskDatabaseHelper.Tables.INSTANCES,
|
|
||||||
String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)), null);
|
|
||||||
}
|
|
||||||
else if (!next.right().isPresent())
|
|
||||||
{
|
|
||||||
// there is no old instance for this new one, add it
|
|
||||||
ContentValues values = next.left().value();
|
|
||||||
if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0)
|
|
||||||
{
|
|
||||||
distance += 1;
|
|
||||||
}
|
|
||||||
values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
|
|
||||||
db.insert(TaskDatabaseHelper.Tables.INSTANCES, "", values);
|
|
||||||
}
|
|
||||||
else // both sides are present
|
|
||||||
{
|
|
||||||
// update this instance
|
|
||||||
existingInstances.moveToPosition(next.right().value());
|
|
||||||
ContentValues values = next.left().value();
|
|
||||||
if (distance >= 0 || values.getAsLong(TaskContract.Instances.DISTANCE_FROM_CURRENT) >= 0)
|
|
||||||
{
|
|
||||||
// the distance needs to be updated
|
|
||||||
distance += 1;
|
|
||||||
values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentValues updates = updatedOnly(values, existingInstances);
|
|
||||||
if (updates.size() > 0)
|
|
||||||
{
|
|
||||||
db.update(TaskDatabaseHelper.Tables.INSTANCES,
|
|
||||||
updates,
|
|
||||||
String.format(Locale.ENGLISH, "%s = %d", TaskContract.Instances._ID, existingInstances.getLong(idIdx)),
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private static ContentValues updatedOnly(ContentValues newValues, Cursor oldValues)
|
|
||||||
{
|
|
||||||
ContentValues result = new ContentValues(newValues);
|
|
||||||
for (String key : newValues.keySet())
|
|
||||||
{
|
|
||||||
int columnIdx = oldValues.getColumnIndex(key);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
throw new RuntimeException("Missing column " + key + " in Cursor ");
|
|
||||||
}
|
|
||||||
if (oldValues.isNull(columnIdx) && newValues.get(key) == null)
|
|
||||||
{
|
|
||||||
result.remove(key);
|
|
||||||
}
|
|
||||||
else if (!oldValues.isNull(columnIdx) && newValues.get(key) != null && oldValues.getLong(columnIdx) == newValues.getAsLong(key))
|
|
||||||
{
|
|
||||||
result.remove(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,205 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This processor makes sure that changing the list a task belongs is properly handled by sync adapters. This is achieved by emulating an atomic copy & delete
|
|
||||||
* operation.
|
|
||||||
* <p/>
|
|
||||||
* TODO: at present we only move recurrence exceptions based on the original row id. We should consider to move exceptions based on the original SYNC_ID as well
|
|
||||||
* to support moving exception sets of tasks without known master instance.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Moving implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Moving(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (isSyncAdapter)
|
|
||||||
{
|
|
||||||
// sync-adapters have to implement the move logic themselves
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!task.isUpdated(TaskAdapter.LIST_ID))
|
|
||||||
{
|
|
||||||
// list has not been changed
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
long oldList = task.oldValueOf(TaskAdapter.LIST_ID);
|
|
||||||
long newList = task.valueOf(TaskAdapter.LIST_ID);
|
|
||||||
|
|
||||||
if (oldList == newList)
|
|
||||||
{
|
|
||||||
// list has not been changed
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
Long newMasterId;
|
|
||||||
Long deletedMasterId = null;
|
|
||||||
|
|
||||||
if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null)
|
|
||||||
{
|
|
||||||
// this is an exception, move the master first
|
|
||||||
newMasterId = task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID);
|
|
||||||
if (newMasterId != null)
|
|
||||||
{
|
|
||||||
// find the master task
|
|
||||||
Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks._ID + "=" + newMasterId, null, null, null, null);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (c.moveToFirst())
|
|
||||||
{
|
|
||||||
// move the master task
|
|
||||||
deletedMasterId = moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, null, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// now move this exception, make sure we link the deleted exception to the deleted master
|
|
||||||
moveTask(db, task, oldList, newList, deletedMasterId, false);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
newMasterId = task.id();
|
|
||||||
// move the task to the new list
|
|
||||||
deletedMasterId = moveTask(db, task, oldList, newList, null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.isRecurring() || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null)
|
|
||||||
{
|
|
||||||
// This task is recurring and may have exceptions or it's an exception itself. Move all (other) exceptions to the new list.
|
|
||||||
Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + newMasterId + " and "
|
|
||||||
+ TaskContract.Tasks._ID + "!=" + task.id(), null, null, null, null);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, deletedMasterId, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
mDelegate.delete(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private Long moveTask(SQLiteDatabase db, TaskAdapter task, long oldList, long newList, Long deletedOriginalId, boolean commitTask)
|
|
||||||
{
|
|
||||||
/*
|
|
||||||
* The task has been moved to a different list. Sync adapters are not expected to support this (especially since the new list may belong to a completely
|
|
||||||
* different account or even account-type), so we emulate a copy & delete operation.
|
|
||||||
*
|
|
||||||
* All sync adapter fields of the task are cleared, so it looks like a new task. In addition we create a new deleted task in the old list having the old
|
|
||||||
* sync adapter field values. This means that the _ID field of the "deleted" task will not equal the _ID field f the original task. Sync adapters should
|
|
||||||
* handle that correctly.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Long result = null;
|
|
||||||
|
|
||||||
// create a deleted task for the old one, unless the task has not been synced yet (which is always true for tasks in the local account)
|
|
||||||
if (task.valueOf(TaskAdapter.SYNC_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null
|
|
||||||
|| task.valueOf(TaskAdapter.SYNC_VERSION) != null)
|
|
||||||
{
|
|
||||||
TaskAdapter deletedTask = task.duplicate();
|
|
||||||
deletedTask.set(TaskAdapter.LIST_ID, oldList);
|
|
||||||
deletedTask.set(TaskAdapter.ORIGINAL_INSTANCE_ID, deletedOriginalId);
|
|
||||||
deletedTask.set(TaskAdapter._DELETED, true);
|
|
||||||
|
|
||||||
// make sure we unset any values that do not exist in the tasks table
|
|
||||||
deletedTask.unset(TaskAdapter.LIST_COLOR);
|
|
||||||
deletedTask.unset(TaskAdapter.LIST_NAME);
|
|
||||||
deletedTask.unset(TaskAdapter.ACCOUNT_NAME);
|
|
||||||
deletedTask.unset(TaskAdapter.ACCOUNT_TYPE);
|
|
||||||
deletedTask.unset(TaskAdapter.LIST_OWNER);
|
|
||||||
deletedTask.unset(TaskAdapter.LIST_ACCESS_LEVEL);
|
|
||||||
deletedTask.unset(TaskAdapter.LIST_VISIBLE);
|
|
||||||
|
|
||||||
// create the deleted task
|
|
||||||
deletedTask.commit(db);
|
|
||||||
|
|
||||||
result = deletedTask.id();
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear all sync fields to convert the existing task to a new task
|
|
||||||
task.set(TaskAdapter.LIST_ID, newList);
|
|
||||||
task.set(TaskAdapter._DIRTY, true);
|
|
||||||
task.set(TaskAdapter.SYNC1, null);
|
|
||||||
task.set(TaskAdapter.SYNC2, null);
|
|
||||||
task.set(TaskAdapter.SYNC3, null);
|
|
||||||
task.set(TaskAdapter.SYNC4, null);
|
|
||||||
task.set(TaskAdapter.SYNC5, null);
|
|
||||||
task.set(TaskAdapter.SYNC6, null);
|
|
||||||
task.set(TaskAdapter.SYNC7, null);
|
|
||||||
task.set(TaskAdapter.SYNC8, null);
|
|
||||||
task.set(TaskAdapter.SYNC_ID, null);
|
|
||||||
task.set(TaskAdapter.SYNC_VERSION, null);
|
|
||||||
task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null);
|
|
||||||
if (commitTask)
|
|
||||||
{
|
|
||||||
task.commit(db);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link EntityProcessor} which updates the {@link TaskContract.Tasks#ORIGINAL_INSTANCE_ID} of any overrides when a master is inserted which has the
|
|
||||||
* matching {@link TaskContract.Tasks#ORIGINAL_INSTANCE_SYNC_ID}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Originating implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Originating(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
String syncId = result.valueOf(TaskAdapter.SYNC_ID);
|
|
||||||
if (syncId != null)
|
|
||||||
{
|
|
||||||
// A master task with a syncId has been inserted.
|
|
||||||
// Update original ID of any existing overrides.
|
|
||||||
ContentValues values = new ContentValues(1);
|
|
||||||
values.put(TaskContract.Tasks.ORIGINAL_INSTANCE_ID, result.id());
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, values, String.format(Locale.ENGLISH, "%s = ?", TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID),
|
|
||||||
new String[] { syncId });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
mDelegate.delete(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor that updates relations for new tasks.
|
|
||||||
* <p>
|
|
||||||
* In general there is no guarantee that a related task is already in the database when a task is
|
|
||||||
* inserted. In such a case we can not set the {@link TaskContract.Property.Relation#RELATED_ID} value. This processor updates the {@link
|
|
||||||
* TaskContract.Property.Relation#RELATED_ID} when a task is inserted.
|
|
||||||
* <p>
|
|
||||||
* It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks
|
|
||||||
* is synced the first time and a UID has been set.
|
|
||||||
* <p>
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Relating implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Relating(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
// A new task has been inserted by the sync adapter. Update all relations that point to this task.
|
|
||||||
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
// the task was created on the device, so it doesn't have a UID
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String uid = result.valueOf(TaskAdapter._UID);
|
|
||||||
|
|
||||||
if (uid != null)
|
|
||||||
{
|
|
||||||
ContentValues v = new ContentValues(1);
|
|
||||||
v.put(TaskContract.Property.Relation.RELATED_ID, result.id());
|
|
||||||
|
|
||||||
int updates = db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
|
|
||||||
TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_UID + "=?", new String[] {
|
|
||||||
TaskContract.Property.Relation.CONTENT_ITEM_TYPE, uid });
|
|
||||||
|
|
||||||
if (updates > 0)
|
|
||||||
{
|
|
||||||
// there were other relations pointing towards this task, update PARENT_IDs if necessary
|
|
||||||
ContentValues parentIdValues = new ContentValues(1);
|
|
||||||
parentIdValues.put(TaskContract.Tasks.PARENT_ID, result.id());
|
|
||||||
// iterate over all tasks which refer to this as their parent and update their PARENT_ID
|
|
||||||
try (Cursor c = db.query(
|
|
||||||
TaskDatabaseHelper.Tables.PROPERTIES, new String[] { TaskContract.Property.Relation.TASK_ID },
|
|
||||||
String.format("%s = ? and %s = ? and %s = ?",
|
|
||||||
TaskContract.Property.Relation.MIMETYPE,
|
|
||||||
TaskContract.Property.Relation.RELATED_ID,
|
|
||||||
TaskContract.Property.Relation.RELATED_TYPE),
|
|
||||||
new String[] {
|
|
||||||
TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
|
|
||||||
String.valueOf(result.id()),
|
|
||||||
String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT) },
|
|
||||||
null, null, null))
|
|
||||||
{
|
|
||||||
while (c.moveToNext())
|
|
||||||
{
|
|
||||||
db.update(TaskDatabaseHelper.Tables.TASKS, parentIdValues, TaskContract.Tasks._ID + " = ?", new String[] { c.getString(0) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO, way also may have to do this for all the siblings of these tasks.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
// A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task.
|
|
||||||
// in this case we don't need to update any PARENT_ID because it should already be set.
|
|
||||||
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
// only sync adapters may assign a UID
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
String uid = result.valueOf(TaskAdapter._UID);
|
|
||||||
|
|
||||||
if (uid != null)
|
|
||||||
{
|
|
||||||
ContentValues v = new ContentValues(1);
|
|
||||||
v.put(TaskContract.Property.Relation.RELATED_UID, uid);
|
|
||||||
|
|
||||||
db.update(TaskDatabaseHelper.Tables.PROPERTIES, v,
|
|
||||||
TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_ID + "=?", new String[] {
|
|
||||||
TaskContract.Property.Relation.CONTENT_ITEM_TYPE, Long.toString(result.id()) });
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
mDelegate.delete(db, task, isSyncAdapter);
|
|
||||||
|
|
||||||
if (!isSyncAdapter)
|
|
||||||
{
|
|
||||||
// remove once the deletion is final, which is when the sync adapter removes it
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
db.delete(TaskDatabaseHelper.Tables.PROPERTIES, TaskContract.Property.Relation.MIMETYPE + "= ? AND " + TaskContract.Property.Relation.RELATED_ID + "=?",
|
|
||||||
new String[] {
|
|
||||||
TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
|
|
||||||
Long.toString(task.id()) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link EntityProcessor} which updates a task's parent-child relations when its {@link TaskContract.Tasks#PARENT_ID} is updated.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Reparenting implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Reparenting(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.insert(db, entityAdapter, isSyncAdapter);
|
|
||||||
if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID))
|
|
||||||
{
|
|
||||||
linkParent(db, result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
if (entityAdapter.isUpdated(TaskAdapter.PARENT_ID))
|
|
||||||
{
|
|
||||||
unlinkParent(db, entityAdapter);
|
|
||||||
TaskAdapter result = mDelegate.update(db, entityAdapter, isSyncAdapter);
|
|
||||||
linkParent(db, entityAdapter);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return mDelegate.update(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
unlinkParent(db, entityAdapter);
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void unlinkParent(SQLiteDatabase db, TaskAdapter taskAdapter)
|
|
||||||
{
|
|
||||||
if (taskAdapter.oldValueOf(TaskAdapter.PARENT_ID) != null)
|
|
||||||
{
|
|
||||||
// delete any parent, child or sibling relation with this task
|
|
||||||
db.delete(TaskDatabaseHelper.Tables.PROPERTIES,
|
|
||||||
String.format("%s = ? AND (%s = ? and %s in (?, ?) or %s = ? and %s in (?, ?))",
|
|
||||||
TaskContract.Property.Relation.MIMETYPE,
|
|
||||||
TaskContract.Property.Relation.TASK_ID,
|
|
||||||
TaskContract.Property.Relation.RELATED_TYPE,
|
|
||||||
TaskContract.Property.Relation.RELATED_ID,
|
|
||||||
TaskContract.Property.Relation.RELATED_TYPE),
|
|
||||||
new String[] {
|
|
||||||
TaskContract.Property.Relation.CONTENT_ITEM_TYPE,
|
|
||||||
String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)),
|
|
||||||
String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING),
|
|
||||||
String.valueOf(TaskContract.Property.Relation.RELTYPE_PARENT),
|
|
||||||
String.valueOf(taskAdapter.valueOf(TaskAdapter._ID)),
|
|
||||||
String.valueOf(TaskContract.Property.Relation.RELTYPE_SIBLING),
|
|
||||||
String.valueOf(TaskContract.Property.Relation.RELTYPE_CHILD) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void linkParent(SQLiteDatabase db, TaskAdapter taskAdapter)
|
|
||||||
{
|
|
||||||
if (taskAdapter.valueOf(TaskAdapter.PARENT_ID) != null)
|
|
||||||
{
|
|
||||||
ContentValues values = new ContentValues();
|
|
||||||
values.put(TaskContract.Property.Relation.MIMETYPE, TaskContract.Property.Relation.CONTENT_ITEM_TYPE);
|
|
||||||
values.put(TaskContract.Property.Relation.TASK_ID, taskAdapter.id());
|
|
||||||
values.put(TaskContract.Property.Relation.RELATED_TYPE, TaskContract.Property.Relation.RELTYPE_PARENT);
|
|
||||||
values.put(TaskContract.Property.Relation.RELATED_ID, taskAdapter.valueOf(TaskAdapter.PARENT_ID));
|
|
||||||
values.put(TaskContract.Property.Relation.RELATED_UID, taskAdapter.valueOf(TaskAdapter._UID));
|
|
||||||
db.insert(TaskDatabaseHelper.Tables.PROPERTIES, "", values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,66 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.FTSDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.provider.tasks.utils.Profiled;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link EntityProcessor} to update the fast text search table when inserting or updating a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Searchable implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Searchable(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
new Profiled("InsertFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
TaskAdapter result = mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
new Profiled("UpdateFTS").run(() -> FTSDatabaseHelper.updateTaskFTSEntries(db, task));
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
new Profiled("DeleteFTS").run(() -> mDelegate.delete(db, entityAdapter, isSyncAdapter));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor that performs the actual operations on tasks.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TaskCommitProcessor implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
task.commit(db);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
task.commit(db);
|
|
||||||
return task;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
String accountType = task.valueOf(TaskAdapter.ACCOUNT_TYPE);
|
|
||||||
|
|
||||||
if (isSyncAdapter || TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType))
|
|
||||||
{
|
|
||||||
// this is a local task or it's removed by a sync adapter, in either case we delete it right away
|
|
||||||
db.delete(TaskDatabaseHelper.Tables.TASKS, TaskContract.TaskColumns._ID + "=" + task.id(), null);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// just set the deleted flag otherwise
|
|
||||||
task.set(TaskAdapter._DELETED, true);
|
|
||||||
task.commit(db);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,278 +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.provider.tasks.processors.tasks;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.provider.tasks.TaskDatabaseHelper;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.EntityProcessor;
|
|
||||||
import org.dmfs.rfc5545.Duration;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A processor that validates the values of a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Validating implements EntityProcessor<TaskAdapter>
|
|
||||||
{
|
|
||||||
private static final String[] TASKLIST_ID_PROJECTION = { TaskContract.TaskLists._ID };
|
|
||||||
private static final String TASKLISTS_ID_SELECTION = TaskContract.TaskLists._ID + "=";
|
|
||||||
|
|
||||||
private final EntityProcessor<TaskAdapter> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Validating(EntityProcessor<TaskAdapter> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter insert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
verifyCommon(task, isSyncAdapter);
|
|
||||||
|
|
||||||
// LIST_ID must be present and refer to an existing TaskList row id
|
|
||||||
Long listId = task.valueOf(TaskAdapter.LIST_ID);
|
|
||||||
if (listId == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("LIST_ID is required on INSERT");
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: get rid of this query and use a cache instead
|
|
||||||
// TODO: ensure that the list is writable unless the caller is a sync adapter
|
|
||||||
Cursor cursor = db.query(TaskDatabaseHelper.Tables.LISTS, TASKLIST_ID_PROJECTION, TASKLISTS_ID_SELECTION + listId, null, null, null, null);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (cursor == null || cursor.getCount() != 1)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("LIST_ID must refer to an existing TaskList");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (cursor != null)
|
|
||||||
{
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mDelegate.insert(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public TaskAdapter update(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
verifyCommon(task, isSyncAdapter);
|
|
||||||
|
|
||||||
// only sync adapters can modify original sync id and original instance id of an existing task
|
|
||||||
if (!isSyncAdapter && (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID) || task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID can be modified by sync adapters only");
|
|
||||||
}
|
|
||||||
|
|
||||||
// only sync adapters are allowed to change the UID of existing tasks
|
|
||||||
if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of _UID is not allowed to non-sync adapters");
|
|
||||||
}
|
|
||||||
|
|
||||||
return mDelegate.update(db, task, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void delete(SQLiteDatabase db, TaskAdapter entityAdapter, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
mDelegate.delete(db, entityAdapter, isSyncAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Performs tests that are common to insert an update operations.
|
|
||||||
*
|
|
||||||
* @param task
|
|
||||||
* The {@link TaskAdapter} to verify.
|
|
||||||
* @param isSyncAdapter
|
|
||||||
* <code>true</code> if the caller is a sync adapter, false otherwise.
|
|
||||||
*/
|
|
||||||
private void verifyCommon(TaskAdapter task, boolean isSyncAdapter)
|
|
||||||
{
|
|
||||||
// row id can not be changed or set manually
|
|
||||||
if (task.isUpdated(TaskAdapter._ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("_ID can not be set manually");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.isUpdated(TaskAdapter.VERSION))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("VERSION can not be set manually");
|
|
||||||
}
|
|
||||||
|
|
||||||
// account name can not be set on a tasks
|
|
||||||
if (task.isUpdated(TaskAdapter.ACCOUNT_NAME))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_NAME can not be set on a tasks");
|
|
||||||
}
|
|
||||||
|
|
||||||
// account type can not be set on a tasks
|
|
||||||
if (task.isUpdated(TaskAdapter.ACCOUNT_TYPE))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ACCOUNT_TYPE can not be set on a tasks");
|
|
||||||
}
|
|
||||||
|
|
||||||
// list color is read only for tasks
|
|
||||||
if (task.isUpdated(TaskAdapter.LIST_COLOR))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("LIST_COLOR can not be set on a tasks");
|
|
||||||
}
|
|
||||||
|
|
||||||
// no one can undelete a task!
|
|
||||||
if (task.isUpdated(TaskAdapter._DELETED))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of _DELETE is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// only sync adapters are allowed to remove the dirty flag
|
|
||||||
if (!isSyncAdapter && task.isUpdated(TaskAdapter._DIRTY))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of _DIRTY is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// only sync adapters are allowed to set creation time
|
|
||||||
if (!isSyncAdapter && task.isUpdated(TaskAdapter.CREATED))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of CREATED is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// IS_NEW is set automatically
|
|
||||||
if (task.isUpdated(TaskAdapter.IS_NEW))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of IS_NEW is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// IS_CLOSED is set automatically
|
|
||||||
if (task.isUpdated(TaskAdapter.IS_CLOSED))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of IS_CLOSED is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// HAS_PROPERTIES is set automatically
|
|
||||||
if (task.isUpdated(TaskAdapter.HAS_PROPERTIES))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of HAS_PROPERTIES is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// HAS_ALARMS is set automatically
|
|
||||||
if (task.isUpdated(TaskAdapter.HAS_ALARMS))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of HAS_ALARMS is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
// only sync adapters are allowed to set modification time
|
|
||||||
if (!isSyncAdapter && task.isUpdated(TaskAdapter.LAST_MODIFIED))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("modification of MODIFICATION_TIME is not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) && task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID must not be specified at the same time");
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that CLASSIFICATION is an Integer between 0 and 2 if given
|
|
||||||
if (task.isUpdated(TaskAdapter.CLASSIFICATION))
|
|
||||||
{
|
|
||||||
Integer classification = task.valueOf(TaskAdapter.CLASSIFICATION);
|
|
||||||
if (classification != null && (classification < 0 || classification > 2))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("CLASSIFICATION must be an integer between 0 and 2");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that PRIORITY is an Integer between 0 and 9 if given
|
|
||||||
if (task.isUpdated(TaskAdapter.PRIORITY))
|
|
||||||
{
|
|
||||||
Integer priority = task.valueOf(TaskAdapter.PRIORITY);
|
|
||||||
if (priority != null && (priority < 0 || priority > 9))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("PRIORITY must be an integer between 0 and 9");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check that PERCENT_COMPLETE is an Integer between 0 and 100
|
|
||||||
if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE))
|
|
||||||
{
|
|
||||||
Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE);
|
|
||||||
if (percent != null && (percent < 0 || percent > 100))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("PERCENT_COMPLETE must be null or an integer between 0 and 100");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate STATUS
|
|
||||||
if (task.isUpdated(TaskAdapter.STATUS))
|
|
||||||
{
|
|
||||||
Integer status = task.valueOf(TaskAdapter.STATUS);
|
|
||||||
if (status != null && (status < TaskContract.Tasks.STATUS_NEEDS_ACTION || status > TaskContract.Tasks.STATUS_CANCELLED))
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("invalid STATUS: " + status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure that DUE and DURATION are set properly if DTSTART is given
|
|
||||||
Long dtStart = task.valueOf(TaskAdapter.DTSTART_RAW);
|
|
||||||
Long due = task.valueOf(TaskAdapter.DUE_RAW);
|
|
||||||
Duration duration = task.valueOf(TaskAdapter.DURATION);
|
|
||||||
|
|
||||||
if (dtStart != null)
|
|
||||||
{
|
|
||||||
if (due != null && duration != null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("Only one of DUE or DURATION must be supplied.");
|
|
||||||
}
|
|
||||||
else if (due != null)
|
|
||||||
{
|
|
||||||
if (due < dtStart)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("DUE must not be < DTSTART");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (duration != null)
|
|
||||||
{
|
|
||||||
if (duration.getSign() == -1)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("DURATION must not be negative");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (duration != null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("DURATION must not be supplied without DTSTART");
|
|
||||||
}
|
|
||||||
|
|
||||||
// if one of DTSTART or DUE is given, TZ must not be null unless it's an all-day task
|
|
||||||
if ((dtStart != null || due != null) && !task.valueOf(TaskAdapter.IS_ALLDAY) && task.valueOf(TaskAdapter.TIMEZONE_RAW) == null)
|
|
||||||
{
|
|
||||||
throw new IllegalArgumentException("TIMEZONE must be supplied if one of DTSTART or DUE is not null and not all-day");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.decorators.DelegatingSingle;
|
|
||||||
import org.dmfs.provider.tasks.utils.Zipped;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Single} of date and time {@link ContentValues} of an instance.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Dated extends DelegatingSingle<ContentValues>
|
|
||||||
{
|
|
||||||
|
|
||||||
public Dated(Optional<DateTime> dateTime, String timeStampColumn, String sortingColumn, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
super(new Zipped<>(
|
|
||||||
dateTime,
|
|
||||||
delegate,
|
|
||||||
(dateTime1, values) ->
|
|
||||||
{
|
|
||||||
// add timestamp and sorting
|
|
||||||
values.put(timeStampColumn, dateTime1.getTimestamp());
|
|
||||||
values.put(sortingColumn, dateTime1.isAllDay() ? dateTime1.getInstance() : dateTime1.shiftTimeZone(TimeZone.getDefault()).getInstance());
|
|
||||||
return values;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.decorators.DelegatingSingle;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Single} of the instance distance {@link ContentValues} of an instance.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Distant extends DelegatingSingle<ContentValues>
|
|
||||||
{
|
|
||||||
|
|
||||||
public Distant(int distance, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
super(() ->
|
|
||||||
{
|
|
||||||
ContentValues values = delegate.value();
|
|
||||||
values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance);
|
|
||||||
return values;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.decorators.DelegatingSingle;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator to a {@link Single} of {@link ContentValues} adding due data.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class DueDated extends DelegatingSingle<ContentValues>
|
|
||||||
{
|
|
||||||
public DueDated(Optional<DateTime> due, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
super(new Dated(due, TaskContract.Instances.INSTANCE_DUE, TaskContract.Instances.INSTANCE_DUE_SORTING, delegate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.composite.Zipped;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_DURATION} field based on the
|
|
||||||
* already populated {@link TaskContract.Instances#INSTANCE_START} and {@link TaskContract.Instances#INSTANCE_DUE} fields.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Enduring implements Single<ContentValues>
|
|
||||||
{
|
|
||||||
private final Single<ContentValues> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Enduring(Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues value()
|
|
||||||
{
|
|
||||||
ContentValues values = mDelegate.value();
|
|
||||||
// just store the difference between due and start, if both are present, otherwise store null
|
|
||||||
values.put(TaskContract.Instances.INSTANCE_DURATION,
|
|
||||||
new Backed<Long>(
|
|
||||||
new Zipped<>(
|
|
||||||
new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_START)),
|
|
||||||
new NullSafe<>(values.getAsLong(TaskContract.Instances.INSTANCE_DUE)),
|
|
||||||
(start, due) -> due - start),
|
|
||||||
() -> null).value());
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.optional.elementary.Present;
|
|
||||||
import org.dmfs.jems.procedure.composite.ForEach;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator for {@link Single}s of Instance {@link ContentValues} which populates the {@link TaskContract.Instances#INSTANCE_ORIGINAL_TIME} field based on
|
|
||||||
* the given {@link Optional} original start.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Overridden implements Single<ContentValues>
|
|
||||||
{
|
|
||||||
private final Optional<DateTime> mOriginalTime;
|
|
||||||
private final Single<ContentValues> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Overridden(DateTime originalTime, ContentValues delegate)
|
|
||||||
{
|
|
||||||
this(new Present<>(originalTime), () -> delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Overridden(Optional<DateTime> originalTime, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
mOriginalTime = originalTime;
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues value()
|
|
||||||
{
|
|
||||||
ContentValues values = mDelegate.value();
|
|
||||||
new ForEach<>(new Mapped<>(DateTime::getTimestamp, mOriginalTime)).process(time -> values.put(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, time));
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.decorators.DelegatingSingle;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator to a {@link Single} of {@link ContentValues} adding start data.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class StartDated extends DelegatingSingle<ContentValues>
|
|
||||||
{
|
|
||||||
public StartDated(Optional<DateTime> start, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
super(new Dated(start, TaskContract.Instances.INSTANCE_START, TaskContract.Instances.INSTANCE_START_SORTING, delegate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A decorator to {@link Single}s of {@link ContentValues} adding a {@link TaskContract.Instances#TASK_ID} to the data.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TaskRelated implements Single<ContentValues>
|
|
||||||
{
|
|
||||||
private final long mTaskId;
|
|
||||||
private final Single<ContentValues> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public TaskRelated(TaskAdapter taskAdapter, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
this(taskAdapter.id(), delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public TaskRelated(long taskId, Single<ContentValues> delegate)
|
|
||||||
{
|
|
||||||
mTaskId = taskId;
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ContentValues value()
|
|
||||||
{
|
|
||||||
ContentValues values = mDelegate.value();
|
|
||||||
values.put(TaskContract.Instances.TASK_ID, mTaskId);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +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.provider.tasks.processors.tasks.instancedata;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.tasks.contract.TaskContract;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Single} of instance data {@link ContentValues}. It initializes most columns with {@code null} values, except for {@link
|
|
||||||
* TaskContract.Instances#TASK_ID} which is left out and {@link TaskContract.Instances#DISTANCE_FROM_CURRENT} which is initialized with {@code 0} as well.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class VanillaInstanceData implements Single<ContentValues>
|
|
||||||
{
|
|
||||||
@Override
|
|
||||||
public ContentValues value()
|
|
||||||
{
|
|
||||||
ContentValues values = new ContentValues(10);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_START);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_START_SORTING);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_DUE);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_DUE_SORTING);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_DURATION);
|
|
||||||
values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, 0);
|
|
||||||
values.putNull(TaskContract.Instances.INSTANCE_ORIGINAL_TIME);
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.jems.predicate.Predicate;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Predicate} which determines whether all values of a ContentValues object are present in a {@link Cursor}.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class ContainsValues implements Predicate<Cursor>
|
|
||||||
{
|
|
||||||
private final ContentValues mValues;
|
|
||||||
|
|
||||||
|
|
||||||
public ContainsValues(ContentValues values)
|
|
||||||
{
|
|
||||||
mValues = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean satisfiedBy(Cursor testedInstance)
|
|
||||||
{
|
|
||||||
for (String key : mValues.keySet())
|
|
||||||
{
|
|
||||||
int columnIdx = testedInstance.getColumnIndex(key);
|
|
||||||
if (columnIdx < 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (testedInstance.getType(columnIdx) == Cursor.FIELD_TYPE_BLOB)
|
|
||||||
{
|
|
||||||
if (!Arrays.equals(mValues.getAsByteArray(key), testedInstance.getBlob(columnIdx)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
String stringValue = mValues.getAsString(key);
|
|
||||||
if (stringValue != null && !stringValue.equals(testedInstance.getString(columnIdx)) || stringValue == null && !testedInstance.isNull(columnIdx))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,120 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.iterators.SingletonIterator;
|
|
||||||
import org.dmfs.jems.iterable.elementary.Seq;
|
|
||||||
import org.dmfs.jems.iterator.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.adapters.FirstPresent;
|
|
||||||
import org.dmfs.jems.optional.composite.Zipped;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.optional.elementary.Present;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Distant;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.DueDated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Enduring;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Overridden;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.StartDated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.rfc5545.Duration;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} of {@link Single} {@link ContentValues} of the instances of a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
// TODO: replace Single with Generator
|
|
||||||
public final class InstanceValuesIterable implements Iterable<Single<ContentValues>>
|
|
||||||
{
|
|
||||||
private final long mId;
|
|
||||||
private final TaskAdapter mTaskAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
public InstanceValuesIterable(long id, TaskAdapter taskAdapter)
|
|
||||||
{
|
|
||||||
mId = id;
|
|
||||||
mTaskAdapter = taskAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<Single<ContentValues>> iterator()
|
|
||||||
{
|
|
||||||
Optional<DateTime> start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART));
|
|
||||||
// effective due is either the actual due, start + duration or absent
|
|
||||||
Optional<DateTime> effectiveDue = new FirstPresent<>(
|
|
||||||
new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)),
|
|
||||||
new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration));
|
|
||||||
|
|
||||||
Single<ContentValues> baseData = new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
|
|
||||||
new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(mId, new VanillaInstanceData())))));
|
|
||||||
|
|
||||||
if (!mTaskAdapter.isRecurring())
|
|
||||||
{
|
|
||||||
return new SingletonIterator<>(
|
|
||||||
// apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME
|
|
||||||
new org.dmfs.provider.tasks.utils.Zipped<>(
|
|
||||||
new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)),
|
|
||||||
baseData,
|
|
||||||
(DateTime time, ContentValues data) -> new Overridden(time, data).value()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (start.isPresent())
|
|
||||||
{
|
|
||||||
Optional<Duration> effectiveDuration = new FirstPresent<>(
|
|
||||||
new Seq<>(
|
|
||||||
new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)),
|
|
||||||
new Zipped<>(start, effectiveDue,
|
|
||||||
(dtStart, due) -> new Duration(1, 0, (int) ((due.getTimestamp() - dtStart.getTimestamp()) / 1000)))));
|
|
||||||
|
|
||||||
return new Mapped<>(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
|
|
||||||
new Overridden(new Present<>(dateTime),
|
|
||||||
new Enduring(
|
|
||||||
new DueDated(new Zipped<>(new Present<>(dateTime), effectiveDuration, this::addDuration),
|
|
||||||
new StartDated(new Present<>(dateTime),
|
|
||||||
new TaskRelated(mId, new VanillaInstanceData())))))),
|
|
||||||
new TaskInstanceIterable(mTaskAdapter).iterator());
|
|
||||||
}
|
|
||||||
|
|
||||||
// special treatment for recurring tasks without a DTSTART:
|
|
||||||
return new Mapped<>(dateTime -> new Distant(mTaskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
|
|
||||||
new Overridden(new Present<>(dateTime),
|
|
||||||
new DueDated(new Present<>(dateTime), new TaskRelated(mId, new VanillaInstanceData())))),
|
|
||||||
new TaskInstanceIterable(mTaskAdapter).iterator());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private DateTime addDuration(DateTime dt, Duration dur)
|
|
||||||
{
|
|
||||||
if (dt.isAllDay() && dur.getSecondsOfDay() != 0)
|
|
||||||
{
|
|
||||||
dur = new Duration(1, dur.getWeeks() * 7 + dur.getDays() + dur.getSecondsOfDay() / (3600 * 24), 0);
|
|
||||||
}
|
|
||||||
return dt.addDuration(dur);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,49 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} which limits the number of elements.
|
|
||||||
* <p>
|
|
||||||
* TODO: move to jems
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public final class Limited<T> implements Iterable<T>
|
|
||||||
{
|
|
||||||
private final int mCount;
|
|
||||||
private final Iterable<T> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public Limited(int count, Iterable<T> delegate)
|
|
||||||
{
|
|
||||||
mCount = count;
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<T> iterator()
|
|
||||||
{
|
|
||||||
return new LimitedIterator<>(mCount, mDelegate.iterator());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.iterators.AbstractBaseIterator;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterator} which limits the number elements.
|
|
||||||
* TODO: move to jems
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
* @deprecated
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public final class LimitedIterator<T> extends AbstractBaseIterator<T>
|
|
||||||
{
|
|
||||||
private int mCount;
|
|
||||||
private final Iterator<T> mDelegate;
|
|
||||||
|
|
||||||
|
|
||||||
public LimitedIterator(int count, Iterator<T> delegate)
|
|
||||||
{
|
|
||||||
mCount = count;
|
|
||||||
mDelegate = delegate;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNext()
|
|
||||||
{
|
|
||||||
return mCount > 0 && mDelegate.hasNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public T next()
|
|
||||||
{
|
|
||||||
if (!hasNext())
|
|
||||||
{
|
|
||||||
throw new NoSuchElementException("No more elements to iterate");
|
|
||||||
}
|
|
||||||
mCount--;
|
|
||||||
return mDelegate.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.content.ContentValues;
|
|
||||||
|
|
||||||
import org.dmfs.jems.function.Function;
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.adapters.FirstPresent;
|
|
||||||
import org.dmfs.jems.optional.composite.Zipped;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Distant;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.DueDated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Enduring;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.Overridden;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.StartDated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.TaskRelated;
|
|
||||||
import org.dmfs.provider.tasks.processors.tasks.instancedata.VanillaInstanceData;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} of {@link Single} {@link ContentValues} of the overrides of a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class OverrideValuesFunction implements Function<TaskAdapter, Single<ContentValues>>
|
|
||||||
{
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Single<ContentValues> value(TaskAdapter taskAdapter)
|
|
||||||
{
|
|
||||||
Optional<DateTime> start = new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DTSTART));
|
|
||||||
// effective due is either the actual due, start + duration or absent
|
|
||||||
Optional<DateTime> effectiveDue = new FirstPresent<>(
|
|
||||||
new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DUE)),
|
|
||||||
new Zipped<>(start, new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration));
|
|
||||||
|
|
||||||
Single<ContentValues> baseData = new Distant(taskAdapter.valueOf(TaskAdapter.IS_CLOSED) ? -1 : 0,
|
|
||||||
new Enduring(new DueDated(effectiveDue, new StartDated(start, new TaskRelated(taskAdapter, new VanillaInstanceData())))));
|
|
||||||
|
|
||||||
// apply the Overridden decorator only if this task has an ORIGINAL_INSTANCE_TIME
|
|
||||||
return new org.dmfs.provider.tasks.utils.Zipped<>(
|
|
||||||
new NullSafe<>(taskAdapter.valueOf(TaskAdapter.ORIGINAL_INSTANCE_TIME)),
|
|
||||||
baseData,
|
|
||||||
(DateTime time, ContentValues data) -> new Overridden(time, data).value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.dmfs.jems.fragile.Fragile;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple class to measure the execution time of a given piece of code.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Profiled
|
|
||||||
{
|
|
||||||
private final String mSubject;
|
|
||||||
|
|
||||||
|
|
||||||
public Profiled(String subject)
|
|
||||||
{
|
|
||||||
mSubject = subject;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void run(Runnable runnable)
|
|
||||||
{
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
runnable.run();
|
|
||||||
Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public <V> V run(Single<V> runnable)
|
|
||||||
{
|
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return runnable.value();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public <V, E extends Exception> V run(Fragile<V, E> runnable) throws E
|
|
||||||
{
|
|
||||||
|
|
||||||
long start = System.currentTimeMillis();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return runnable.value();
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Log.d("Profiled", String.format(Locale.ENGLISH, "Time spent in %s: %d milliseconds", mSubject, System.currentTimeMillis() - start));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,56 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.iterator.generators.IntSequenceGenerator;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} which iterates a range of numbers.
|
|
||||||
* <p>
|
|
||||||
* TODO: implement in jems
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public final class Range implements Iterable<Integer>
|
|
||||||
{
|
|
||||||
private final int mStart;
|
|
||||||
private final int mEnd;
|
|
||||||
|
|
||||||
|
|
||||||
public Range(int end)
|
|
||||||
{
|
|
||||||
this(0, end);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Range(int start, int end)
|
|
||||||
{
|
|
||||||
mStart = start;
|
|
||||||
mEnd = end;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<Integer> iterator()
|
|
||||||
{
|
|
||||||
return new LimitedIterator<>(mEnd - mStart, new IntSequenceGenerator(mStart));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import org.dmfs.iterators.elementary.Seq;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} of a string array resource.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class ResourceArray implements Iterable<String>
|
|
||||||
{
|
|
||||||
private final Context mContext;
|
|
||||||
private final int mResource;
|
|
||||||
|
|
||||||
|
|
||||||
public ResourceArray(Context context, int resource)
|
|
||||||
{
|
|
||||||
mContext = context;
|
|
||||||
mResource = resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<String> iterator()
|
|
||||||
{
|
|
||||||
return new Seq<>(mContext.getResources().getStringArray(mResource));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2020 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
|
|
||||||
import org.dmfs.iterators.AbstractBaseIterator;
|
|
||||||
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class RowIterator extends AbstractBaseIterator<Cursor>
|
|
||||||
{
|
|
||||||
private final Cursor mCursor;
|
|
||||||
|
|
||||||
|
|
||||||
public RowIterator(Cursor cursor)
|
|
||||||
{
|
|
||||||
mCursor = cursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNext()
|
|
||||||
{
|
|
||||||
return mCursor.getCount() > 0 && !mCursor.isClosed() && !mCursor.isLast();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor next()
|
|
||||||
{
|
|
||||||
if (!hasNext())
|
|
||||||
{
|
|
||||||
throw new NoSuchElementException("No other rows to iterate.");
|
|
||||||
}
|
|
||||||
mCursor.moveToNext();
|
|
||||||
return mCursor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
|
|
||||||
import org.dmfs.jems.function.Function;
|
|
||||||
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Function} which returns all column names of a specific table on a given database.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TableColumns implements Function<SQLiteDatabase, Iterable<String>>
|
|
||||||
{
|
|
||||||
private final String mTableName;
|
|
||||||
|
|
||||||
|
|
||||||
public TableColumns(String tableName)
|
|
||||||
{
|
|
||||||
mTableName = tableName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterable<String> value(SQLiteDatabase db)
|
|
||||||
{
|
|
||||||
try (Cursor cursor = db.rawQuery(String.format("PRAGMA table_info(%s)", DatabaseUtils.sqlEscapeString(mTableName)), null))
|
|
||||||
{
|
|
||||||
int nameIdx = cursor.getColumnIndexOrThrow("name");
|
|
||||||
|
|
||||||
List<String> result = new LinkedList<>();
|
|
||||||
while (cursor.moveToNext())
|
|
||||||
{
|
|
||||||
result.add(cursor.getString(nameIdx));
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,80 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.provider.tasks.model.TaskAdapter;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.rfc5545.recur.RecurrenceRule;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceList;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterable} of all the instances of a task.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TaskInstanceIterable implements Iterable<DateTime>
|
|
||||||
{
|
|
||||||
private final TaskAdapter mTaskAdapter;
|
|
||||||
|
|
||||||
|
|
||||||
public TaskInstanceIterable(TaskAdapter taskAdapter)
|
|
||||||
{
|
|
||||||
mTaskAdapter = taskAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Iterator<DateTime> iterator()
|
|
||||||
{
|
|
||||||
DateTime dtstart = new Backed<DateTime>(new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)), () -> mTaskAdapter.valueOf(TaskAdapter.DUE)).value();
|
|
||||||
|
|
||||||
RecurrenceSet set = new RecurrenceSet();
|
|
||||||
RecurrenceRule rule = mTaskAdapter.valueOf(TaskAdapter.RRULE);
|
|
||||||
if (rule != null)
|
|
||||||
{
|
|
||||||
if (rule.getUntil() != null && dtstart.isFloating() != rule.getUntil().isFloating())
|
|
||||||
{
|
|
||||||
// rule UNTIL date mismatches start. This is merely a workaround for existing users. In future we should make sure
|
|
||||||
// such tasks don't exist
|
|
||||||
if (dtstart.isFloating())
|
|
||||||
{
|
|
||||||
// make until floating too by making it floating in the current time zone
|
|
||||||
rule.setUntil(rule.getUntil().shiftTimeZone(TimeZone.getDefault()).swapTimeZone(null));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// anchor UNTIL in the current time zone
|
|
||||||
rule.setUntil(new DateTime(null, rule.getUntil().getTimestamp()).swapTimeZone(TimeZone.getDefault()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set.addInstances(new RecurrenceRuleAdapter(rule));
|
|
||||||
}
|
|
||||||
|
|
||||||
set.addInstances(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.RDATE)).value()));
|
|
||||||
set.addExceptions(new RecurrenceList(new Timestamps(mTaskAdapter.valueOf(TaskAdapter.EXDATE)).value()));
|
|
||||||
|
|
||||||
return new TaskInstanceIterator(dtstart, set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.iterators.AbstractBaseIterator;
|
|
||||||
import org.dmfs.jems.optional.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.optional.elementary.NullSafe;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceSet;
|
|
||||||
import org.dmfs.rfc5545.recurrenceset.RecurrenceSetIterator;
|
|
||||||
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.NoSuchElementException;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An {@link Iterator} of instances as returned by a {@link RecurrenceSetIterator}.
|
|
||||||
* <p>
|
|
||||||
* TODO: this should go to lib-recur
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class TaskInstanceIterator extends AbstractBaseIterator<DateTime>
|
|
||||||
{
|
|
||||||
private final DateTime mStart;
|
|
||||||
private final RecurrenceSetIterator mSetIterator;
|
|
||||||
private final String mTimezone;
|
|
||||||
|
|
||||||
|
|
||||||
TaskInstanceIterator(DateTime start, RecurrenceSet set)
|
|
||||||
{
|
|
||||||
this(start, set.iterator(start.getTimeZone(), start.getTimestamp()),
|
|
||||||
new Backed<>(new Mapped<>(TimeZone::getID, new NullSafe<>(start.getTimeZone())), () -> null).value());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
TaskInstanceIterator(DateTime start, RecurrenceSetIterator setIterator, String timezone)
|
|
||||||
{
|
|
||||||
mStart = start;
|
|
||||||
mSetIterator = setIterator;
|
|
||||||
mTimezone = timezone;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasNext()
|
|
||||||
{
|
|
||||||
return mSetIterator.hasNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DateTime next()
|
|
||||||
{
|
|
||||||
if (!hasNext())
|
|
||||||
{
|
|
||||||
throw new NoSuchElementException("No more elements to iterate");
|
|
||||||
}
|
|
||||||
DateTime result = new DateTime(mStart.getTimeZone(), mSetIterator.next());
|
|
||||||
return mStart.isAllDay() ? result.toAllDay() : mTimezone == null ? result.swapTimeZone(null) : result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.rfc5545.DateTime;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A {@link Single} of an array of timestamp values of a given {@link Iterable} of {@link DateTime}s.
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
public final class Timestamps implements Single<long[]>
|
|
||||||
{
|
|
||||||
private final Iterable<DateTime> mDateTimes;
|
|
||||||
|
|
||||||
|
|
||||||
public Timestamps(Iterable<DateTime> dateTimes)
|
|
||||||
{
|
|
||||||
mDateTimes = dateTimes;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long[] value()
|
|
||||||
{
|
|
||||||
int count = 0;
|
|
||||||
for (DateTime ignored : mDateTimes)
|
|
||||||
{
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
long[] timeStamps = new long[count];
|
|
||||||
int i = 0;
|
|
||||||
for (DateTime dt : mDateTimes)
|
|
||||||
{
|
|
||||||
timeStamps[i++] = dt.getTimestamp();
|
|
||||||
}
|
|
||||||
return timeStamps;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2019 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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.adapters.SinglePresent;
|
|
||||||
import org.dmfs.jems.procedure.Procedure;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Experiemental Procedure which calls another procedure with a given value.
|
|
||||||
* <p>
|
|
||||||
* TODO move to jems if this works out well
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public final class With<T> implements Procedure<Procedure<T>>
|
|
||||||
{
|
|
||||||
private final Optional<T> mValue;
|
|
||||||
|
|
||||||
|
|
||||||
public With(T value)
|
|
||||||
{
|
|
||||||
this(() -> value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public With(Single<T> value)
|
|
||||||
{
|
|
||||||
this(new SinglePresent<>(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public With(Optional<T> value)
|
|
||||||
{
|
|
||||||
mValue = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void process(Procedure<T> delegate)
|
|
||||||
{
|
|
||||||
if (mValue.isPresent())
|
|
||||||
{
|
|
||||||
delegate.process(mValue.value());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +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.provider.tasks.utils;
|
|
||||||
|
|
||||||
import org.dmfs.jems.function.BiFunction;
|
|
||||||
import org.dmfs.jems.optional.Optional;
|
|
||||||
import org.dmfs.jems.optional.decorators.Mapped;
|
|
||||||
import org.dmfs.jems.single.Single;
|
|
||||||
import org.dmfs.jems.single.combined.Backed;
|
|
||||||
import org.dmfs.jems.single.decorators.DelegatingSingle;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Experimental {@link Single} which applies a {@link BiFunction} based on the presence of an {@link Optional}.
|
|
||||||
* <p>
|
|
||||||
* TODO: maybe a more appropriate name?
|
|
||||||
* <p>
|
|
||||||
* TODO: move to jems
|
|
||||||
*
|
|
||||||
* @author Marten Gajda
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public final class Zipped<T> extends DelegatingSingle<T>
|
|
||||||
{
|
|
||||||
public <V> Zipped(Optional<V> optionalValue, Single<T> delegate, BiFunction<V, T, T> function)
|
|
||||||
{
|
|
||||||
super(new Backed<T>(new Mapped<>(from -> function.value(from, delegate.value()), optionalValue), delegate));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user