diff --git a/README.md b/README.md index bd88bbd..86bea13 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ Open standards, no account required.

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 -speaks the **dmfs `TaskContract`** — the same shape DAVx5 (and SmoothSync, -DecSync, …) syncs your CalDAV `VTODO` tasks into. +keeps its own store, designed around RFC 5545's `VTODO` — the same tasks DAVx5 +(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 “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 | |---|---|---|---| -| **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 | -Agendula carries its own copy of the Apache-2.0 dmfs task provider, under its own -name — so it **coexists with OpenTasks rather than replacing it**, and installing -one never breaks the other. It is a fork of a proven schema, not a database -written from scratch, which is why every CalDAV engine already understands it. +Agendula's own store is an ordinary app database — nothing is published to other +apps, so there is no authority to clash over and no permission to grant. It +**coexists with OpenTasks rather than replacing it**: installing one never breaks +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 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 > 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 @@ -50,7 +56,7 @@ data you can't take with you isn't really yours. ## 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 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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7473816..b846f2f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -145,11 +145,6 @@ ksp { } dependencies { - // Agendula's own task store — the dmfs provider vendored under our authority. - // Contributes a 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.appcompat) implementation(libs.androidx.lifecycle.runtime.ktx) @@ -170,10 +165,9 @@ dependencies { implementation(libs.androidx.navigation.compose) ksp(libs.hilt.compiler) - // RFC 5545 recurrence expansion, in-process. Pulled in directly rather than - // through :provider: the own store expands occurrences itself. Pinned at - // 0.12.2 — 0.16.0 removed RecurrenceSet. rfc5545-datetime comes with it and - // is part of its API surface, so it isn't declared separately. + // RFC 5545 recurrence expansion, in-process. Pinned at 0.12.2 — 0.16.0 + // removed RecurrenceSet. rfc5545-datetime comes with it and is part of its + // API surface, so it isn't declared separately. implementation(libs.dmfs.lib.recur) implementation(libs.androidx.room.runtime) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 130f919..a50a376 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,9 +8,9 @@ runtime by the permission flow, and only once the user has actually selected External mode. Both are dangerous-level. - Agendula's own bundled provider needs NO entry here: it runs under our uid - and a same-uid caller bypasses a provider's permission checks outright. - Its permissions are declared by the :provider module, for other apps. --> + StorageMode.OWN needs nothing here: it is a Room database in our own data + directory. Agendula publishes no ContentProvider and declares no + permissions of its own. --> @@ -80,16 +80,16 @@ - + - diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt index 48c9401..2702a6c 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/prefs/SettingsPrefs.kt @@ -95,13 +95,15 @@ class SettingsPrefs @Inject constructor( * upgrading Posture A user pointed at the provider that holds their data. */ val storageMode: Flow = dataStore.data.map { p -> - p[STORAGE_MODE] - ?.let { runCatching { StorageMode.valueOf(it) }.getOrNull() } - // A stored LOCAL means "the bundled dmfs provider", and after the - // one-shot import that store no longer holds the user's tasks — its - // file has been renamed away. Read it as OWN so someone who chose - // local storage explicitly lands on the store their data is now in. - ?.let { if (it == StorageMode.LOCAL) StorageMode.OWN else it } + when (val stored = p[STORAGE_MODE]) { + null -> null + // 0.3.x's value for the bundled dmfs provider. That store is gone and + // its data was imported into OWN, so read it as OWN rather than + // letting it fall through to autoMode — someone who chose local + // storage explicitly would otherwise be sent to an external provider. + "LOCAL" -> StorageMode.OWN + else -> runCatching { StorageMode.valueOf(stored) }.getOrNull() + } } suspend fun setStorageMode(mode: StorageMode) = dataStore.edit { it[STORAGE_MODE] = mode.name } diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ModeRoutingTasksDataSource.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ModeRoutingTasksDataSource.kt index 4220dab..22120ac 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ModeRoutingTasksDataSource.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ModeRoutingTasksDataSource.kt @@ -16,8 +16,7 @@ import kotlin.time.Instant * [ProviderResolver] without rebuilding the object graph. Both delegates are * singletons, so this chooses between two existing objects. * - * The [StorageMode.LOCAL] branch disappears with the `:provider` module; after - * that this is just Room versus a third-party ContentProvider. + * Room versus a third-party ContentProvider — nothing else. */ class ModeRoutingTasksDataSource( private val resolver: ProviderResolver, @@ -26,9 +25,9 @@ class ModeRoutingTasksDataSource( ) : TasksDataSource { private fun active(): TasksDataSource = - when (resolver.storageMode ?: resolver.autoMode()) { + when (resolver.mode()) { StorageMode.OWN -> room.get() - StorageMode.LOCAL, StorageMode.EXTERNAL -> external.get() + StorageMode.EXTERNAL -> external.get() } override fun taskLists(): List = active().taskLists() diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderEnvironment.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderEnvironment.kt index 4bf4c9e..4cfca57 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderEnvironment.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderEnvironment.kt @@ -8,7 +8,7 @@ import javax.inject.Inject 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 * user lands on is decided by [ProviderResolver.autoMode], getting it wrong shows @@ -18,12 +18,6 @@ import javax.inject.Singleton */ 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. */ fun packageDeclaring(authority: String): String? @@ -36,13 +30,6 @@ class AndroidProviderEnvironment @Inject constructor( @ApplicationContext private val context: Context, ) : 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? = context.packageManager.resolveContentProvider(authority, 0)?.packageName diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolver.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolver.kt index 10e6f86..207755f 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolver.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolver.kt @@ -4,39 +4,26 @@ import javax.inject.Inject import javax.inject.Singleton /** - * A tasks provider Agendula can talk to. The same dmfs `TaskProvider` backs every - * candidate — ours included, since `:provider` *is* that provider vendored — so - * the [TasksContract] columns apply regardless of which is active. + * An external tasks provider Agendula can talk to. Every candidate runs the same + * dmfs `TaskProvider`, so the [TasksContract] columns apply to either. */ data class TaskProvider( val authority: String, val readPermission: String, val writePermission: String, 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 - * supported; it stopped being the default and became a user choice. - * - **Posture B** — our own bundled provider, under our **own** authority. It - * coexists with everything and replaces nothing. + * This used to be the A/B seam between an external provider and one Agendula + * bundled itself. That second half is gone: [StorageMode.OWN] is a Room database + * with no authority, no ContentResolver and nothing to permit, so there is + * 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 - * "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 + * Which store is active comes from [storageMode]; how that gets decided when the * user has not chosen is [autoMode]. */ @Singleton @@ -54,23 +41,17 @@ class ProviderResolver @Inject constructor( @Volatile var storageMode: StorageMode? = null - /** Agendula's own provider. Always present — it ships inside the APK. */ - val own: TaskProvider by lazy { - TaskProvider( - authority = environment.ownAuthority, - readPermission = OWN_READ_PERMISSION, - writePermission = OWN_WRITE_PERMISSION, - packageName = environment.ownPackage, - isOwn = true, - ) - } + /** The active store, resolving the undecided case through [autoMode]. */ + fun mode(): StorageMode = storageMode ?: autoMode() - /** The active provider, or `null` when [StorageMode.EXTERNAL] is chosen and none is installed. */ - fun resolve(): TaskProvider? = when (storageMode ?: autoMode()) { - StorageMode.LOCAL -> own - // Room has no authority, no ContentResolver and nothing to permit. The - // provider entry stands in so ProviderStatus stays READY; nothing queries it. - StorageMode.OWN -> own + /** + * The provider to query, or `null` — either because [StorageMode.OWN] is + * active and there is no provider involved at all, or because + * [StorageMode.EXTERNAL] is and none is installed. Callers that need to tell + * those apart ask [mode]. + */ + fun resolve(): TaskProvider? = when (mode()) { + StorageMode.OWN -> null 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 * 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 - * 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 * 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 = - // Same uid, same process: there is nothing to grant, and asking would put a - // 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)) + environment.isGranted(provider.readPermission) && environment.isGranted(provider.writePermission) 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 * `org.dmfs.provider.tasks.TaskProvider`, guarded by `org.tasks.permission.*` diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/StorageMode.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/StorageMode.kt index d60e45c..6ca9963 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/StorageMode.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/StorageMode.kt @@ -1,46 +1,26 @@ 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 - * third store**: it is [LOCAL] with an account attached, so it is derived state - * (does an account of ours exist?) rather than something the user picks. Adding - * a `SYNCED` constant here would imply otherwise. - * - * ⚠️ 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. + * Only two values, though `docs/STORAGE-AND-SYNC.md` describes three modes. + * **Synced is not a third store**: it is [OWN] with an account attached to a + * list, which is derived state rather than something the user picks. Attaching + * 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. */ enum class StorageMode { /** - * Agendula's own bundled provider (the `:provider` module). Always available — - * it ships in the APK — and needs no permission grant at all, because - * 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. + * Agendula's own Room database. The default, and always available: there is + * no authority, no ContentResolver and no permission to grant. */ OWN, /** * 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 - * Posture A, still fully supported, but now a choice rather than the only way. - * Requires that provider's runtime read/write permissions. + * whatever that provider's engine is — DAVx5 and friends. Still fully + * supported, now a choice rather than the only way. Requires that provider's + * runtime read/write permissions. */ EXTERNAL, } diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/TasksRepositoryImpl.kt b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/TasksRepositoryImpl.kt index 8bdd073..03850ad 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/TasksRepositoryImpl.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/data/tasks/TasksRepositoryImpl.kt @@ -127,6 +127,10 @@ class TasksRepositoryImpl @Inject constructor( withContext(io) { dataSource.createLocalList(name, color) } 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 return if (providerResolver.hasPermission(provider)) ProviderStatus.READY else ProviderStatus.NEEDS_PERMISSION diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/permission/PermissionViewModel.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/permission/PermissionViewModel.kt index 9186e16..4cdd0cf 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/permission/PermissionViewModel.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/permission/PermissionViewModel.kt @@ -41,13 +41,8 @@ class PermissionViewModel @Inject constructor( val provider = providerResolver.resolve() _state.value = PermissionUiState( status = repository.providerStatus(), - // Never our own provider's permissions. They are declared for *other* - // 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. + // Null in OWN mode, where there is no provider and nothing to grant. permissionsToRequest = provider - ?.takeUnless { it.isOwn } ?.let { listOf(it.readPermission, it.writePermission) } .orEmpty(), ) diff --git a/app/src/test/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolverTest.kt b/app/src/test/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolverTest.kt index fd6a171..da2eab5 100644 --- a/app/src/test/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolverTest.kt +++ b/app/src/test/java/de/jeanlucmakiola/agendula/data/tasks/ProviderResolverTest.kt @@ -21,8 +21,6 @@ class ProviderResolverTest { val installed: Map = emptyMap(), val granted: Set = emptySet(), ) : ProviderEnvironment { - override val ownAuthority = "de.jeanlucmakiola.agendula.tasks" - override val ownPackage = "de.jeanlucmakiola.agendula" override fun packageDeclaring(authority: String): String? = installed[authority] override fun isGranted(permission: String): Boolean = permission in granted } @@ -39,26 +37,16 @@ class ProviderResolverTest { private val openTasksGranted = setOf(openTasks.readPermission, openTasks.writePermission) @Nested - inner class OwnProvider { + inner class OwnStore { @Test - fun `needs no permission grant`() { - // The bypass the whole Local mode rests on: our provider runs under our - // own uid, so there is nothing to grant and nothing to ask for. - val resolver = resolver() - assertThat(resolver.hasPermission(resolver.own)).isTrue() - } - - @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") + fun `resolves to no provider at all`() { + // Room has no authority and no ContentResolver, so there is nothing + // here to resolve — which is the point. Callers that need to tell this + // apart from "External, none installed" ask mode(). + val resolver = resolver(mode = StorageMode.OWN) + assertThat(resolver.resolve()).isNull() + assertThat(resolver.mode()).isEqualTo(StorageMode.OWN) } } @@ -104,10 +92,12 @@ class ProviderResolverTest { fun `overrides the automatic answer in both directions`() { val wouldBeExternal = FakeEnvironment(openTasksInstalled, openTasksGranted) - val forcedLocal = ProviderResolver(wouldBeExternal).apply { storageMode = StorageMode.LOCAL } - assertThat(forcedLocal.resolve()?.isOwn).isTrue() + val forcedOwn = ProviderResolver(wouldBeExternal).apply { storageMode = StorageMode.OWN } + assertThat(forcedOwn.mode()).isEqualTo(StorageMode.OWN) + assertThat(forcedOwn.resolve()).isNull() val forcedExternal = ProviderResolver(FakeEnvironment()).apply { storageMode = StorageMode.EXTERNAL } + assertThat(forcedExternal.mode()).isEqualTo(StorageMode.EXTERNAL) assertThat(forcedExternal.resolve()).isNull() } @@ -152,10 +142,14 @@ class ProviderResolverTest { } @Test - fun `never include our own provider`() { - // EXTERNAL must mean "somebody else's store". If ours ever leaked into - // this list, choosing External would silently keep using it. - assertThat(ProviderResolver.EXTERNAL_CANDIDATES.none { it.isOwn }).isTrue() + fun `never name an authority of ours`() { + // EXTERNAL must mean "somebody else's store", and Agendula publishes no + // provider at all any more. + assertThat( + ProviderResolver.EXTERNAL_CANDIDATES.none { + it.authority.startsWith("de.jeanlucmakiola") + }, + ).isTrue() } } } diff --git a/docs/STORAGE-DECISION.md b/docs/STORAGE-DECISION.md index 13491e0..8bddcc0 100644 --- a/docs/STORAGE-DECISION.md +++ b/docs/STORAGE-DECISION.md @@ -250,3 +250,35 @@ point of sequencing it last. mode arguably already serves the second. Undecided. - **The Room estimate is mine, not measured.** Instance expansion is the item 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. diff --git a/provider/LICENSE b/provider/LICENSE deleted file mode 100644 index d645695..0000000 --- a/provider/LICENSE +++ /dev/null @@ -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. diff --git a/provider/NOTICE b/provider/NOTICE deleted file mode 100644 index 6a0d99d..0000000 --- a/provider/NOTICE +++ /dev/null @@ -1,2 +0,0 @@ -OpenTasks - Open Source Task App for Android -Copyright 2012-2015 Marten Gajda \ No newline at end of file diff --git a/provider/PROVENANCE.md b/provider/PROVENANCE.md deleted file mode 100644 index 6cfcefe..0000000 --- a/provider/PROVENANCE.md +++ /dev/null @@ -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. **``** 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 `` - 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). diff --git a/provider/build.gradle.kts b/provider/build.gradle.kts deleted file mode 100644 index 3206e9c..0000000 --- a/provider/build.gradle.kts +++ /dev/null @@ -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) -} diff --git a/provider/proguard-rules.pro b/provider/proguard-rules.pro deleted file mode 100644 index 6a525b0..0000000 --- a/provider/proguard-rules.pro +++ /dev/null @@ -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 diff --git a/provider/src/main/AndroidManifest.xml b/provider/src/main/AndroidManifest.xml deleted file mode 100644 index 8cd8305..0000000 --- a/provider/src/main/AndroidManifest.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java b/provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java deleted file mode 100644 index 58e3064..0000000 --- a/provider/src/main/java/org/dmfs/ngrams/NGramGenerator.java +++ /dev/null @@ -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 - */ -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 - * true to add a space in front of each word, false 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 - * false. - * - * @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 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 set = new HashSet(128); - - for (String word : words) - { - getNgrams(word, set); - } - - return set; - } - - - private void getNgrams(String word, Set 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))); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java b/provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java deleted file mode 100644 index 369cc57..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/AuthorityUtil.java +++ /dev/null @@ -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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java b/provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java deleted file mode 100644 index 622533c..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/ContentOperation.java +++ /dev/null @@ -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 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 null. - */ - 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 null 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); - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java b/provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java deleted file mode 100644 index 1093a4f..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/FTSDatabaseHelper.java +++ /dev/null @@ -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 - * @author Marten Gajda - */ -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 - */ - 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 null 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 - */ - 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 TYPE column. - * - * @author Tobias Reinsch - * @author Marten Gajda - */ - 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 null 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 ngramIds(SQLiteDatabase db, Set ngrams) - { - if (ngrams.size() == 0) - { - return Collections.emptySet(); - } - - Set missingNgrams = new HashSet<>(ngrams); - Set ngramIds = new HashSet<>(ngrams.size() * 2); - - for (Iterable 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 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 propertyNgrams = TRIGRAM_GENERATOR.getNgrams(searchableText); - propertyNgrams.addAll(TETRAGRAM_GENERATOR.getNgrams(searchableText)); - - // get an ID for each of the Ngrams. - Set ngramIds = ngramIds(db, propertyNgrams); - - // unlink unused ngrams from the task and get the missing ones we have to link to the tak - Set 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 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 contentType 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 syncNgrams(SQLiteDatabase db, long taskId, long propertyId, int contentType, Set 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 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 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java b/provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java deleted file mode 100644 index 2d50bfd..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/ProviderOperation.java +++ /dev/null @@ -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 - */ -public enum ProviderOperation -{ - - /** - * Insert operations. - */ - INSERT, - - /** - * Update operations. - */ - UPDATE, - - /** - * Delete operations. - */ - DELETE -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java b/provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java deleted file mode 100644 index 21c54ae..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/SQLiteContentProvider.java +++ /dev/null @@ -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 mChangedUris = new HashSet<>(); - - private final ThreadLocal mApplyingBatch = new ThreadLocal(); - 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 mTransactionEndTasks; - - - protected SQLiteContentProvider(Iterable 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 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) () -> - { - 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) () -> - { - 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) () -> - { - 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 operations) throws OperationApplicationException - { - return new Profiled(String.format(Locale.ENGLISH, "Batch of %d operations", operations.size())).run( - (Fragile) () -> - { - 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 changed; - synchronized (mChangedUris) - { - changed = new HashSet(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(); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java b/provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java deleted file mode 100644 index a78d8c7..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/TaskDatabaseHelper.java +++ /dev/null @@ -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 - * @author Tobias Reinsch - */ -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. - *

- * 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. - *

- * 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 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java b/provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java deleted file mode 100644 index 6bddccd..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/TaskProvider.java +++ /dev/null @@ -1,1408 +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.OnAccountsUpdateListener; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.UriMatcher; -import android.database.Cursor; -import android.database.DatabaseUtils; -import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; -import android.text.TextUtils; -import android.util.Log; - -import org.dmfs.iterables.EmptyIterable; -import org.dmfs.provider.tasks.TaskDatabaseHelper.OnDatabaseOperationListener; -import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; -import org.dmfs.provider.tasks.handler.PropertyHandler; -import org.dmfs.provider.tasks.handler.PropertyHandlerFactory; -import org.dmfs.provider.tasks.model.ContentValuesListAdapter; -import org.dmfs.provider.tasks.model.ContentValuesTaskAdapter; -import org.dmfs.provider.tasks.model.CursorContentValuesInstanceAdapter; -import org.dmfs.provider.tasks.model.CursorContentValuesListAdapter; -import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; -import org.dmfs.provider.tasks.model.InstanceAdapter; -import org.dmfs.provider.tasks.model.ListAdapter; -import org.dmfs.provider.tasks.model.TaskAdapter; -import org.dmfs.provider.tasks.processors.EntityProcessor; -import org.dmfs.provider.tasks.processors.instances.Detaching; -import org.dmfs.provider.tasks.processors.instances.TaskValueDelegate; -import org.dmfs.provider.tasks.processors.lists.ListCommitProcessor; -import org.dmfs.provider.tasks.processors.tasks.AutoCompleting; -import org.dmfs.provider.tasks.processors.tasks.Instantiating; -import org.dmfs.provider.tasks.processors.tasks.Moving; -import org.dmfs.provider.tasks.processors.tasks.Originating; -import org.dmfs.provider.tasks.processors.tasks.Relating; -import org.dmfs.provider.tasks.processors.tasks.Reparenting; -import org.dmfs.provider.tasks.processors.tasks.Searchable; -import org.dmfs.provider.tasks.processors.tasks.TaskCommitProcessor; -import org.dmfs.provider.tasks.processors.tasks.Validating; -import org.dmfs.tasks.contract.TaskContract; -import org.dmfs.tasks.contract.TaskContract.Alarms; -import org.dmfs.tasks.contract.TaskContract.Categories; -import org.dmfs.tasks.contract.TaskContract.CategoriesColumns; -import org.dmfs.tasks.contract.TaskContract.Instances; -import org.dmfs.tasks.contract.TaskContract.Properties; -import org.dmfs.tasks.contract.TaskContract.PropertyColumns; -import org.dmfs.tasks.contract.TaskContract.SyncState; -import org.dmfs.tasks.contract.TaskContract.TaskColumns; -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 java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; - - -/** - * The provider for tasks. - *

- * TODO: add support for recurring tasks - *

- * TODO: add support for reminders - *

- * TODO: add support for attendees - *

- * TODO: refactor the selection stuff - * - * @author Marten Gajda - * @author Tobias Reinsch - */ -public final class TaskProvider extends SQLiteContentProvider implements OnAccountsUpdateListener, OnDatabaseOperationListener -{ - - private static final int LISTS = 1; - private static final int LIST_ID = 2; - private static final int TASKS = 101; - private static final int TASK_ID = 102; - private static final int INSTANCES = 103; - private static final int INSTANCE_ID = 104; - private static final int CATEGORIES = 1001; - private static final int CATEGORY_ID = 1002; - private static final int PROPERTIES = 1003; - private static final int PROPERTY_ID = 1004; - private static final int ALARMS = 1005; - private static final int ALARM_ID = 1006; - private static final int SEARCH = 1007; - private static final int SYNCSTATE = 1008; - private static final int SYNCSTATE_ID = 1009; - - private static final int OPERATIONS = 100000; - - private final static Set TASK_LIST_SYNC_COLUMNS = new HashSet(Arrays.asList(TaskLists.SYNC_ADAPTER_COLUMNS)); - private static final String TAG = "TaskProvider"; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the instances table. - */ - private EntityProcessor mInstanceProcessorChain; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the tasks table. - */ - private EntityProcessor mTaskProcessorChain; - - /** - * A list of {@link EntityProcessor}s to execute when doing operations on the task lists table. - */ - private EntityProcessor mListProcessorChain; - - /** - * Our authority. - */ - String mAuthority; - - /** - * The {@link UriMatcher} we use. - */ - private UriMatcher mUriMatcher; - - /** - * A handler to execute asynchronous jobs. - */ - Handler mAsyncHandler; - - /** - * Boolean to track if there are changes within a transaction. - *

- * This can be shared by multiple threads, hence the {@link AtomicBoolean}. - */ - private AtomicBoolean mChanged = new AtomicBoolean(false); - - /** - * This is a per transaction/thread flag which indicates whether new lists with an unknown account have been added. - * If this holds true at the end of a transaction a window should be shown to ask the user for access to that account. - */ - private ThreadLocal mStaleListCreated = new ThreadLocal<>(); - - /** - * The currently known accounts. This may be accessed from various threads, hence the AtomicReference. - * By statring with an empty set, we can always guarantee a non-null reference. - */ - private AtomicReference> mAccountCache = new AtomicReference<>(Collections.emptySet()); - - - public TaskProvider() - { - // for now we don't have anything specific to execute before the transaction ends. - super(EmptyIterable.instance()); - } - - - @Override - public boolean onCreate() - { - mAuthority = AuthorityUtil.taskAuthority(getContext()); - - mTaskProcessorChain = new Validating( - new AutoCompleting(new Relating(new Reparenting(new Instantiating(new Searchable(new Moving(new Originating(new TaskCommitProcessor())))))))); - - mListProcessorChain = new org.dmfs.provider.tasks.processors.lists.Validating(new ListCommitProcessor()); - - mInstanceProcessorChain = new org.dmfs.provider.tasks.processors.instances.Validating( - new Detaching(new TaskValueDelegate(mTaskProcessorChain), mTaskProcessorChain)); - - mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH); - mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH, LISTS); - - mUriMatcher.addURI(mAuthority, TaskContract.TaskLists.CONTENT_URI_PATH + "/#", LIST_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH, TASKS); - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.CONTENT_URI_PATH + "/#", TASK_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH, INSTANCES); - mUriMatcher.addURI(mAuthority, TaskContract.Instances.CONTENT_URI_PATH + "/#", INSTANCE_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH, PROPERTIES); - mUriMatcher.addURI(mAuthority, TaskContract.Properties.CONTENT_URI_PATH + "/#", PROPERTY_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH, CATEGORIES); - mUriMatcher.addURI(mAuthority, TaskContract.Categories.CONTENT_URI_PATH + "/#", CATEGORY_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH, ALARMS); - mUriMatcher.addURI(mAuthority, TaskContract.Alarms.CONTENT_URI_PATH + "/#", ALARM_ID); - - mUriMatcher.addURI(mAuthority, TaskContract.Tasks.SEARCH_URI_PATH, SEARCH); - - mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH, SYNCSTATE); - mUriMatcher.addURI(mAuthority, TaskContract.SyncState.CONTENT_URI_PATH + "/#", SYNCSTATE_ID); - - ContentOperation.register(mUriMatcher, mAuthority, OPERATIONS); - - boolean result = super.onCreate(); - - // create a HandlerThread to perform async operations - HandlerThread thread = new HandlerThread("backgroundHandler"); - thread.start(); - mAsyncHandler = new Handler(thread.getLooper()); - - AccountManager accountManager = AccountManager.get(getContext()); - accountManager.addOnAccountsUpdatedListener(this, mAsyncHandler, true); - - updateNotifications(); - - return result; - } - - - /** - * Return true if the caller is a sync adapter (i.e. if the Uri contains the query parameter {@link TaskContract#CALLER_IS_SYNCADAPTER} and its value is - * true). - * - * @param uri - * The {@link Uri} to check. - * - * @return true if the caller pretends to be a sync adapter, false otherwise. - */ - @Override - public boolean isCallerSyncAdapter(Uri uri) - { - String param = uri.getQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER); - return param != null && !"false".equals(param); - } - - - /** - * Return true if the URI indicates to a load extended properties with {@link TaskContract#LOAD_PROPERTIES}. - * - * @param uri - * The {@link Uri} to check. - * - * @return true if the URI requests to load extended properties, false otherwise. - */ - public boolean shouldLoadProperties(Uri uri) - { - String param = uri.getQueryParameter(TaskContract.LOAD_PROPERTIES); - return param != null && !"false".equals(param); - } - - - /** - * Get the account name from the given {@link Uri}. - * - * @param uri - * The Uri to check. - * - * @return The account name or null if no account name has been specified. - */ - protected String getAccountName(Uri uri) - { - return uri.getQueryParameter(TaskContract.ACCOUNT_NAME); - } - - - /** - * Get the account type from the given {@link Uri}. - * - * @param uri - * The Uri to check. - * - * @return The account type or null if no account type has been specified. - */ - protected String getAccountType(Uri uri) - { - return uri.getQueryParameter(TaskContract.ACCOUNT_TYPE); - } - - - /** - * Get any id from the given {@link Uri}. - * - * @param uri - * The Uri. - * - * @return The last path segment (which should contain the id). - */ - private long getId(Uri uri) - { - return Long.parseLong(uri.getPathSegments().get(1)); - } - - - /** - * Build a selection string that selects the account specified in uri. - * - * @param uri - * A {@link Uri} that specifies an account. - * - * @return A {@link StringBuilder} with a selection string for the account. - */ - protected StringBuilder selectAccount(Uri uri) - { - StringBuilder sb = new StringBuilder(256); - return selectAccount(sb, uri); - } - - - /** - * Append the selection of the account specified in uri to the {@link StringBuilder} sb. - * - * @param sb - * A {@link StringBuilder} that the selection is appended to. - * @param uri - * A {@link Uri} that specifies an account. - * - * @return sb. - */ - protected StringBuilder selectAccount(StringBuilder sb, Uri uri) - { - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - if (accountName != null || accountType != null) - { - - if (accountName != null) - { - if (sb.length() > 0) - { - sb.append(" AND "); - } - - sb.append(TaskListSyncColumns.ACCOUNT_NAME); - sb.append("="); - DatabaseUtils.appendEscapedSQLString(sb, accountName); - } - if (accountType != null) - { - - if (sb.length() > 0) - { - sb.append(" AND "); - } - - sb.append(TaskListSyncColumns.ACCOUNT_TYPE); - sb.append("="); - DatabaseUtils.appendEscapedSQLString(sb, accountType); - } - } - return sb; - } - - - /** - * Append the selection of the account specified in uri to the an {@link SQLiteQueryBuilder}. - * - * @param sqlBuilder - * A {@link SQLiteQueryBuilder} that the selection is appended to. - * @param uri - * A {@link Uri} that specifies an account. - */ - protected void selectAccount(SQLiteQueryBuilder sqlBuilder, Uri uri) - { - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - if (accountName != null) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_NAME); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhereEscapeString(accountName); - } - if (accountType != null) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(TaskListSyncColumns.ACCOUNT_TYPE); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhereEscapeString(accountType); - } - } - - - private StringBuilder _selectId(StringBuilder sb, long id, String key) - { - if (sb.length() > 0) - { - sb.append(" AND "); - } - sb.append(key); - sb.append("="); - sb.append(id); - return sb; - } - - - protected StringBuilder selectId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectId(sb, uri); - } - - - protected StringBuilder selectId(StringBuilder sb, Uri uri) - { - return _selectId(sb, getId(uri), TaskListColumns._ID); - } - - - protected StringBuilder selectTaskId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, uri); - } - - - protected StringBuilder selectTaskId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectTaskId(sb, id); - } - - - protected StringBuilder selectTaskId(StringBuilder sb, Uri uri) - { - return selectTaskId(sb, getId(uri)); - } - - - protected StringBuilder selectTaskId(StringBuilder sb, long id) - { - return _selectId(sb, id, Instances.TASK_ID); - - } - - - protected StringBuilder selectPropertyId(Uri uri) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, uri); - } - - - protected StringBuilder selectPropertyId(StringBuilder sb, Uri uri) - { - return selectPropertyId(sb, getId(uri)); - } - - - protected StringBuilder selectPropertyId(long id) - { - StringBuilder sb = new StringBuilder(128); - return selectPropertyId(sb, id); - } - - - protected StringBuilder selectPropertyId(StringBuilder sb, long id) - { - return _selectId(sb, id, PropertyColumns.PROPERTY_ID); - } - - - /** - * Add a selection by ID to the given {@link SQLiteQueryBuilder}. The id is taken from the given Uri. - * - * @param sqlBuilder - * The {@link SQLiteQueryBuilder} to append the selection to. - * @param idColumn - * The column that must match the id. - * @param uri - * An {@link Uri} that contains the id. - */ - protected void selectId(SQLiteQueryBuilder sqlBuilder, String idColumn, Uri uri) - { - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(idColumn); - sqlBuilder.appendWhere("="); - sqlBuilder.appendWhere(String.valueOf(getId(uri))); - } - - - /** - * Append any arbitrary selection string to the selection in sb - * - * @param sb - * A {@link StringBuilder} that already contains a selection string. - * @param selection - * A valid SQL selection string. - * - * @return A string with the final selection. - */ - protected String updateSelection(StringBuilder sb, String selection) - { - if (selection != null) - { - if (sb.length() > 0) - { - sb.append(" AND ( ").append(selection).append(" ) "); - } - else - { - sb.append(" ( ").append(selection).append(" ) "); - } - } - return sb.toString(); - } - - - @Override - public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) - { - final SQLiteDatabase db = getDatabaseHelper().getWritableDatabase(); - SQLiteQueryBuilder sqlBuilder = new SQLiteQueryBuilder(); - // initialize appendWhere, this allows us to append all other selections with a preceding "AND" - sqlBuilder.appendWhere(" 1=1 "); - boolean isSyncAdapter = isCallerSyncAdapter(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.SYNCSTATE); - break; - } - case LISTS: - // add account to selection if any - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.LISTS); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; - } - break; - - case LIST_ID: - // add account to selection if any - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.LISTS); - selectId(sqlBuilder, TaskListColumns._ID, uri); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.TaskLists.DEFAULT_SORT_ORDER; - } - break; - - case TASKS: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to task view that includes these properties - sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.TASKS_VIEW); - } - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; - } - break; - - case TASK_ID: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to task view that includes these properties - sqlBuilder.setTables(Tables.TASKS_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.TASKS_VIEW); - } - selectId(sqlBuilder, TaskColumns._ID, uri); - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Tasks.DEFAULT_SORT_ORDER; - } - break; - - case INSTANCES: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to instance view that includes these properties - sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.INSTANCE_CLIENT_VIEW); - } - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; - } - break; - - case INSTANCE_ID: - if (shouldLoadProperties(uri)) - { - // extended properties were requested, therefore change to instance view that includes these properties - sqlBuilder.setTables(Tables.INSTANCE_PROPERTY_VIEW); - } - else - { - sqlBuilder.setTables(Tables.INSTANCE_CLIENT_VIEW); - } - selectId(sqlBuilder, Instances._ID, uri); - if (!isSyncAdapter) - { - // do not return deleted rows if caller is not a sync adapter - sqlBuilder.appendWhere(" AND "); - sqlBuilder.appendWhere(Tasks._DELETED); - sqlBuilder.appendWhere("=0"); - } - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Instances.DEFAULT_SORT_ORDER; - } - break; - - case CATEGORIES: - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.CATEGORIES); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; - } - break; - - case CATEGORY_ID: - selectAccount(sqlBuilder, uri); - sqlBuilder.setTables(Tables.CATEGORIES); - selectId(sqlBuilder, CategoriesColumns._ID, uri); - if (sortOrder == null || sortOrder.length() == 0) - { - sortOrder = TaskContract.Categories.DEFAULT_SORT_ORDER; - } - break; - - case PROPERTIES: - sqlBuilder.setTables(Tables.PROPERTIES); - break; - - case PROPERTY_ID: - sqlBuilder.setTables(Tables.PROPERTIES); - selectId(sqlBuilder, PropertyColumns.PROPERTY_ID, uri); - break; - - case SEARCH: - String searchString = uri.getQueryParameter(Tasks.SEARCH_QUERY_PARAMETER); - searchString = Uri.decode(searchString); - Cursor searchCursor = FTSDatabaseHelper.getTaskSearchCursor(db, searchString, projection, selection, selectionArgs, sortOrder); - if (searchCursor != null) - { - // attach tasks uri for notifications, that way the search results are updated when a task changes - searchCursor.setNotificationUri(getContext().getContentResolver(), Tasks.getContentUri(mAuthority)); - } - return searchCursor; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - Cursor c = sqlBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder); - - if (c != null) - { - c.setNotificationUri(getContext().getContentResolver(), uri); - } - return c; - } - - - @Override - public int deleteInTransaction(final SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, final boolean isSyncAdapter) - { - int count = 0; - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - if (TextUtils.isEmpty(getAccountName(uri)) || TextUtils.isEmpty(getAccountType(uri))) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - selection = updateSelection(selectAccount(uri), selection); - count = db.delete(Tables.SYNCSTATE, selection, selectionArgs); - break; - } - /* - * Deleting task lists is only allowed to sync adapters. They must provide ACCOUNT_NAME and ACCOUNT_TYPE. - */ - case LIST_ID: - // add _id to selection and fall through - selection = updateSelection(selectId(uri), selection); - case LISTS: - { - if (isSyncAdapter) - { - if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) - { - throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); - } - } - - // iterate over all lists that match the selection - final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - final ListAdapter list = new CursorContentValuesListAdapter(ListAdapter._ID.getFrom(cursor), cursor, new ContentValues()); - - mListProcessorChain.delete(db, list, isSyncAdapter); - mChanged.set(true); - count++; - } - } - finally - { - cursor.close(); - } - - break; - - } - /* - * Task won't be removed, just marked as deleted if the caller isn't a sync adapter. Sync adapters can remove tasks immediately. - */ - case TASK_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case TASKS: - { - // TODO: filter by account name and type if present in uri. - - if (isSyncAdapter) - { - if (TextUtils.isEmpty(accountType) || TextUtils.isEmpty(accountName)) - { - throw new IllegalArgumentException("Sync adapters must specify an account and account type: " + uri); - } - } - - // iterate over all tasks that match the selection - final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, new ContentValues()); - - mTaskProcessorChain.delete(db, task, isSyncAdapter); - - mChanged.set(true); - count++; - } - } - finally - { - cursor.close(); - } - - break; - } - - case INSTANCE_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case INSTANCES: - { - // iterate over all instances that match the selection - try (Cursor cursor = db.query(Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) - { - while (cursor.moveToNext()) - { - mInstanceProcessorChain.delete(db, new CursorContentValuesInstanceAdapter(cursor, new ContentValues()), isSyncAdapter); - mChanged.set(true); - count++; - } - } - - break; - } - - case ALARM_ID: - // add id to selection and fall through - selection = updateSelection(selectId(uri), selection); - - case ALARMS: - - count = db.delete(Tables.ALARMS, selection, selectionArgs); - break; - - case PROPERTY_ID: - selection = updateSelection(selectPropertyId(uri), selection); - - case PROPERTIES: - // fetch all properties that match the selection - Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); - - try - { - int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); - int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); - int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); - while (cursor.moveToNext()) - { - long propertyId = cursor.getLong(propIdCol); - long taskId = cursor.getLong(taskIdCol); - String mimeType = cursor.getString(mimeTypeCol); - if (mimeType != null) - { - PropertyHandler handler = PropertyHandlerFactory.get(mimeType); - count += handler.delete(db, taskId, propertyId, cursor, isSyncAdapter); - } - } - } - finally - { - cursor.close(); - } - postNotifyUri(Properties.getContentUri(mAuthority)); - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - if (count > 0) - { - postNotifyUri(uri); - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - return count; - } - - - @Override - public Uri insertInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, final boolean isSyncAdapter) - { - long rowId; - Uri result_uri; - - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - values.put(SyncState.ACCOUNT_NAME, accountName); - values.put(SyncState.ACCOUNT_TYPE, accountType); - rowId = db.replace(Tables.SYNCSTATE, null, values); - result_uri = TaskContract.SyncState.getContentUri(mAuthority); - break; - } - case LISTS: - { - final ListAdapter list = new ContentValuesListAdapter(values); - list.set(ListAdapter.ACCOUNT_NAME, accountName); - list.set(ListAdapter.ACCOUNT_TYPE, accountType); - - mListProcessorChain.insert(db, list, isSyncAdapter); - mChanged.set(true); - - rowId = list.id(); - result_uri = TaskContract.TaskLists.getContentUri(mAuthority); - // if the account is unknown we need to ask the user - // - // AGENDULA CHANGE: also require the type to be one we authenticate ourselves, for - // the same reason Utils.cleanUpLists does. Without GET_ACCOUNTS the cache only ever - // holds our own accounts, so upstream's test would call *every* externally-synced - // list stale and fire a broadcast about it on each insert. Nothing listens today; - // the point is that whatever listens tomorrow gets a signal that means something. - if (Build.VERSION.SDK_INT >= 26 && - !TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType) && - Utils.isOwnAccountType(getContext(), accountType) && - !mAccountCache.get().contains(new Account(accountName, accountType))) - { - // store the fact that we have an unknown account in this transaction - mStaleListCreated.set(true); - Log.d(TAG, String.format("List with unknown account %s inserted.", new Account(accountName, accountType))); - } - break; - } - case TASKS: - final TaskAdapter task = new ContentValuesTaskAdapter(values); - - mTaskProcessorChain.insert(db, task, isSyncAdapter); - - mChanged.set(true); - - rowId = task.id(); - result_uri = TaskContract.Tasks.getContentUri(mAuthority); - - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - - break; - - // inserting instances is currently disabled because we only expand one instance, - // so even though a new task (exception) would be created, no instance might show up - // we need to resolve this discrepancy. Until then this feature remains disabled. -// case INSTANCES: -// { -// InstanceAdapter instance = mInstanceProcessorChain.insert(db, new ContentValuesInstanceAdapter(values), isSyncAdapter); -// rowId = instance.id(); -// result_uri = TaskContract.Instances.getContentUri(mAuthority); -// -// postNotifyUri(Instances.getContentUri(mAuthority)); -// postNotifyUri(Tasks.getContentUri(mAuthority)); -// -// break; -// } - case PROPERTIES: - String mimetype = values.getAsString(Properties.MIMETYPE); - - if (mimetype == null) - { - throw new IllegalArgumentException("missing mimetype in property values"); - } - - Long taskId = values.getAsLong(Properties.TASK_ID); - if (taskId == null) - { - throw new IllegalArgumentException("missing task id in property values"); - } - - if (values.containsKey(Properties.PROPERTY_ID)) - { - throw new IllegalArgumentException("property id can not be written"); - } - - PropertyHandler handler = PropertyHandlerFactory.get(mimetype); - rowId = handler.insert(db, taskId, values, isSyncAdapter); - result_uri = TaskContract.Properties.getContentUri(mAuthority); - if (rowId >= 0) - { - postNotifyUri(Tasks.getContentUri(mAuthority)); - postNotifyUri(Instances.getContentUri(mAuthority)); - } - break; - - default: - throw new IllegalArgumentException("Unknown URI " + uri); - } - - if (rowId > 0 && result_uri != null) - { - result_uri = ContentUris.withAppendedId(result_uri, rowId); - postNotifyUri(result_uri); - postNotifyUri(uri); - return result_uri; - } - throw new SQLException("Failed to insert row into " + uri); - } - - - @Override - public int updateInTransaction(final SQLiteDatabase db, Uri uri, final ContentValues values, String selection, String[] selectionArgs, - final boolean isSyncAdapter) - { - int count = 0; - boolean dataChanged = false; - switch (mUriMatcher.match(uri)) - { - case SYNCSTATE_ID: - // the id is ignored, we only match by account type and name given in the Uri - case SYNCSTATE: - { - if (!isSyncAdapter) - { - throw new IllegalAccessError("only sync adapters may access syncstate"); - } - - String accountName = getAccountName(uri); - String accountType = getAccountType(uri); - if (TextUtils.isEmpty(accountName) || TextUtils.isEmpty(accountType)) - { - throw new IllegalArgumentException("uri must contain an account when accessing syncstate"); - } - - if (values.size() == 0) - { - // we're done - break; - } - - values.put(SyncState.ACCOUNT_NAME, accountName); - values.put(SyncState.ACCOUNT_TYPE, accountType); - - long id = db.replace(Tables.SYNCSTATE, null, values); - if (id >= 0) - { - count = 1; - } - break; - } - case LIST_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case LISTS: - { - // iterate over all task lists that match the selection - final Cursor cursor = db.query(Tables.LISTS, null, selection, selectionArgs, null, null, null, null); - - int idCol = cursor.getColumnIndex(TaskContract.TaskLists._ID); - - try - { - while (cursor.moveToNext()) - { - final long listId = cursor.getLong(idCol); - - // clone list values if we have more than one list to update - // we need this, because the processors may change the values - final ListAdapter list = new CursorContentValuesListAdapter(listId, cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); - - if (list.hasUpdates()) - { - mListProcessorChain.update(db, list, isSyncAdapter); - dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); - } - // note we still count the row even if no update was necessary - count++; - } - } - finally - { - cursor.close(); - } - break; - } - case TASK_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case TASKS: - { - // iterate over all tasks that match the selection - final Cursor cursor = db.query(Tables.TASKS_VIEW, null, selection, selectionArgs, null, null, null, null); - - try - { - while (cursor.moveToNext()) - { - // clone task values if we have more than one task to update - // we need this, because the processors may change the values - final TaskAdapter task = new CursorContentValuesTaskAdapter(cursor, cursor.getCount() > 1 ? new ContentValues(values) : values); - - if (task.hasUpdates()) - { - mTaskProcessorChain.update(db, task, isSyncAdapter); - dataChanged |= !TASK_LIST_SYNC_COLUMNS.containsAll(values.keySet()); - } - // note we still count the row even if no update was necessary - count++; - } - } - finally - { - cursor.close(); - } - - if (dataChanged) - { - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - break; - } - - case INSTANCE_ID: - // update selection and fall through - selection = updateSelection(selectId(uri), selection); - - case INSTANCES: - { - // iterate over all instances that match the selection - - try (Cursor cursor = db.query(Tables.INSTANCE_VIEW, null, selection, selectionArgs, null, null, null, null)) - { - while (cursor.moveToNext()) - { - // clone task values if we have more than one task to update - // we need this, because the processors may change the values - final InstanceAdapter instance = new CursorContentValuesInstanceAdapter(cursor, - cursor.getCount() > 1 ? new ContentValues(values) : values); - - if (instance.hasUpdates()) - { - mInstanceProcessorChain.update(db, instance, isSyncAdapter); - dataChanged = true; - } - // note we still count the row even if no update was necessary - count++; - } - } - - if (dataChanged) - { - postNotifyUri(Instances.getContentUri(mAuthority)); - postNotifyUri(Tasks.getContentUri(mAuthority)); - } - break; - } - case PROPERTY_ID: - selection = updateSelection(selectPropertyId(uri), selection); - - case PROPERTIES: - if (values.containsKey(Properties.MIMETYPE)) - { - throw new IllegalArgumentException("property mimetypes can not be modified"); - } - - if (values.containsKey(Properties.TASK_ID)) - { - throw new IllegalArgumentException("task id can not be changed"); - } - - if (values.containsKey(Properties.PROPERTY_ID)) - { - throw new IllegalArgumentException("property id can not be changed"); - } - - // fetch all properties that match the selection - Cursor cursor = db.query(Tables.PROPERTIES, null, selection, selectionArgs, null, null, null); - - try - { - int propIdCol = cursor.getColumnIndex(Properties.PROPERTY_ID); - int taskIdCol = cursor.getColumnIndex(Properties.TASK_ID); - int mimeTypeCol = cursor.getColumnIndex(Properties.MIMETYPE); - while (cursor.moveToNext()) - { - long propertyId = cursor.getLong(propIdCol); - long taskId = cursor.getLong(taskIdCol); - String mimeType = cursor.getString(mimeTypeCol); - if (mimeType != null) - { - PropertyHandler handler = PropertyHandlerFactory.get(mimeType); - count += handler.update(db, taskId, propertyId, values, cursor, isSyncAdapter); - } - } - } - finally - { - cursor.close(); - } - postNotifyUri(Properties.getContentUri(mAuthority)); - break; - - case CATEGORY_ID: - String newCategorySelection = updateSelection(selectId(uri), selection); - validateCategoryValues(values, false, isSyncAdapter); - count = db.update(Tables.CATEGORIES, values, newCategorySelection, selectionArgs); - break; - case ALARM_ID: - String newAlarmSelection = updateSelection(selectId(uri), selection); - validateAlarmValues(values, false, isSyncAdapter); - count = db.update(Tables.ALARMS, values, newAlarmSelection, selectionArgs); - break; - default: - ContentOperation operation = ContentOperation.get(mUriMatcher.match(uri), OPERATIONS); - - if (operation == null) - { - throw new IllegalArgumentException("Unknown URI " + uri); - } - - operation.run(getContext(), mAsyncHandler, uri, db, values); - } - - if (dataChanged) - { - // send notifications, because non-sync columns have been updated - postNotifyUri(uri); - mChanged.set(true); - } - - return count; - } - - - /** - * Update task due and task start notifications. - */ - private void updateNotifications() - { - mAsyncHandler.post(new Runnable() - { - - @Override - public void run() - { - ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(getContext(), null); - } - }); - } - - - /** - * Validate the given category values. - * - * @param values - * The category properties to validate. - * - * @throws IllegalArgumentException - * if any of the values is invalid. - */ - private void validateCategoryValues(ContentValues values, boolean isNew, boolean isSyncAdapter) - { - // row id can not be changed or set manually - if (values.containsKey(Categories._ID)) - { - throw new IllegalArgumentException("_ID can not be set manually"); - } - - if (isNew != values.containsKey(Categories.ACCOUNT_NAME) && (!isNew || values.get(Categories.ACCOUNT_NAME) != null)) - { - throw new IllegalArgumentException("ACCOUNT_NAME is write-once and required on INSERT"); - } - - if (isNew != values.containsKey(Categories.ACCOUNT_TYPE) && (!isNew || values.get(Categories.ACCOUNT_TYPE) != null)) - { - throw new IllegalArgumentException("ACCOUNT_TYPE is write-once and required on INSERT"); - } - } - - - /** - * Validate the given alarm values. - * - * @param values - * The alarm values to validate - * - * @throws IllegalArgumentException - * if any of the values is invalid. - */ - private void validateAlarmValues(ContentValues values, boolean isNew, boolean isSyncAdapter) - { - if (values.containsKey(Alarms.ALARM_ID)) - { - throw new IllegalArgumentException("ALARM_ID can not be set manually"); - } - } - - - @Override - public String getType(Uri uri) - { - switch (mUriMatcher.match(uri)) - { - case LISTS: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; - case LIST_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + TaskLists.CONTENT_URI_PATH; - case TASKS: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; - case TASK_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + Tasks.CONTENT_URI_PATH; - case INSTANCES: - return ContentResolver.CURSOR_DIR_BASE_TYPE + "/org.dmfs.tasks." + Instances.CONTENT_URI_PATH; - case INSTANCE_ID: - return ContentResolver.CURSOR_ITEM_BASE_TYPE + "/org.dmfs.tasks." + Instances.CONTENT_URI_PATH; - default: - throw new IllegalArgumentException("Unsupported URI: " + uri); - } - } - - - @Override - protected void onEndTransaction(boolean callerIsSyncAdapter) - { - super.onEndTransaction(callerIsSyncAdapter); - if (mChanged.compareAndSet(true, false)) - { - updateNotifications(); - Utils.sendActionProviderChangedBroadCast(getContext(), mAuthority); - } - - if (Boolean.TRUE.equals(mStaleListCreated.get())) - { - // notify UI about the stale lists, it's up the UI to deal with this, either by showing a notification or an instant popup. - Intent visbilityRequest = new Intent("org.dmfs.tasks.action.STALE_LIST_BROADCAST").setPackage(getContext().getPackageName()); - getContext().sendBroadcast(visbilityRequest); - } - } - - - @Override - public SQLiteOpenHelper getDatabaseHelper(Context context) - { - TaskDatabaseHelper helper = new TaskDatabaseHelper(context, this); - - return helper; - } - - - @Override - public void onDatabaseCreated(SQLiteDatabase db) - { - // notify listeners that the database has been created - Intent dbInitializedIntent = new Intent(TaskContract.ACTION_DATABASE_INITIALIZED); - dbInitializedIntent.setDataAndType(TaskContract.getContentUri(mAuthority), TaskContract.MIMETYPE_AUTHORITY); - // Android SDK 26 doesn't allow us to send implicit broadcasts, this particular brodcast is only for internal use, so just make it explicit by setting our package name - dbInitializedIntent.setPackage(getContext().getPackageName()); - getContext().sendBroadcast(dbInitializedIntent); - } - - - @Override - public void onDatabaseUpdate(SQLiteDatabase db, int oldVersion, int newVersion) - { - if (oldVersion < 15) - { - mAsyncHandler.post(() -> ContentOperation.UPDATE_TIMEZONE.fire(getContext(), null)); - } - } - - - @Override - protected boolean syncToNetwork(Uri uri) - { - return true; - } - - - @Override - public void onAccountsUpdated(Account[] accounts) - { - // cache the known accounts so we can check whether we know accounts for which new lists are added - mAccountCache.set(new HashSet<>(Arrays.asList(accounts))); - // TODO: we probably can move the cleanup code here and get rid of the Utils class - Utils.cleanUpLists(getContext(), getDatabaseHelper().getWritableDatabase(), accounts, mAuthority); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java b/provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java deleted file mode 100644 index e9b6a07..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java +++ /dev/null @@ -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 - */ -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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/Utils.java b/provider/src/main/java/org/dmfs/provider/tasks/Utils.java deleted file mode 100644 index febcd52..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/Utils.java +++ /dev/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 - * @author Marten Gajda - */ -public class Utils -{ - private static final AtomicReference> 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(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. - *

- * 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. - *

- * 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. - *

- * 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 ownAccountTypes(Context context) - { - Set cached = sOwnAccountTypes.get(); - if (cached != null) - { - return cached; - } - String ownPackage = context.getPackageName(); - Set 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 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 accountList = Arrays.asList(accounts); - // AGENDULA CHANGE — see ownAccountTypes: only types we authenticate ourselves may be pruned. - Set 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 obsoleteLists = new ArrayList(); - 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); - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java deleted file mode 100644 index 4882d21..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/AlarmHandler.java +++ /dev/null @@ -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 - */ -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 isNew is false. If isNew is true 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 long - */ - @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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java deleted file mode 100644 index 8c129cb..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/CategoryHandler.java +++ /dev/null @@ -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 - */ -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 isNew is false. If isNew is true 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 long - */ - @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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java deleted file mode 100644 index 52d32d3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java +++ /dev/null @@ -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 - */ -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; - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java deleted file mode 100644 index a01e39d..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandler.java +++ /dev/null @@ -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 - */ -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 isNew is false. If isNew is true 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 long - */ - 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; - } - - - ; -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java deleted file mode 100644 index 0e19463..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java +++ /dev/null @@ -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 - */ -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 null - */ - 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java b/provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java deleted file mode 100644 index 8f896c5..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/handler/RelationHandler.java +++ /dev/null @@ -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 - */ -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 _id or _uid, depending of which value is given. - *

- * TODO: store links into the calendar provider if we find an event that matches the UID. - *

- * - * @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 - */ - // } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java deleted file mode 100644 index 4cae1c8..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractInstanceAdapter.java +++ /dev/null @@ -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 - */ -public abstract class AbstractInstanceAdapter implements InstanceAdapter -{ - @Override - public final Uri uri(String authority) - { - return ContentUris.withAppendedId(TaskContract.Instances.getContentUri(authority), id()); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java deleted file mode 100644 index 80478e3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractListAdapter.java +++ /dev/null @@ -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 - */ -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 getState(FieldAdapter stateFieldAdater) - { - return stateFieldAdater.getFrom(mState); - } - - - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - stateFieldAdater.setIn(mState, value); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java deleted file mode 100644 index 1f23f77..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java +++ /dev/null @@ -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 - */ -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 getState(FieldAdapter stateFieldAdater) - { - return stateFieldAdater.getFrom(mState); - } - - - @Override - public void setState(FieldAdapter stateFieldAdater, T value) - { - stateFieldAdater.setIn(mState, value); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java deleted file mode 100644 index 58a7e54..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesInstanceAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; - } - - - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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 getState(FieldAdapter stateFieldAdater) - { - return null; - } - - - @Override - public void setState(FieldAdapter 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( - () -> new ContentValues(mValues), - (contentValues, column) -> { - contentValues.remove(column); - return contentValues; - }, - INSTANCE_COLUMN_NAMES).value()); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java deleted file mode 100644 index d441848..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; - } - - - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java deleted file mode 100644 index c8a3b8d..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return null; - } - - - @Override - public boolean isUpdated(FieldAdapter fieldAdapter) - { - return fieldAdapter.isSetIn(mValues); - } - - - @Override - public boolean isWriteable() - { - return true; - } - - - @Override - public boolean hasUpdates() - { - return mValues.size() > 0; - } - - - @Override - public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java deleted file mode 100644 index 6213cb3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesInstanceAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - if (mValues == null) - { - return fieldAdapter.getFrom(mCursor); - } - return fieldAdapter.getFrom(mCursor, mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter 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 void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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 getState(FieldAdapter stateFieldAdater) - { - return null; - } - - - @Override - public void setState(FieldAdapter 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( - () -> 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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java deleted file mode 100644 index 4bdffb5..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor, mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter 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 void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java deleted file mode 100644 index 0ed20df..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java +++ /dev/null @@ -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 - */ -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 valueOf(FieldAdapter fieldAdapter) - { - if (mValues == null) - { - return fieldAdapter.getFrom(mCursor); - } - return fieldAdapter.getFrom(mCursor, mValues); - } - - - @Override - public T oldValueOf(FieldAdapter fieldAdapter) - { - return fieldAdapter.getFrom(mCursor); - } - - - @Override - public boolean isUpdated(FieldAdapter 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 void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException - { - fieldAdapter.setIn(mValues, value); - } - - - @Override - public void unset(FieldAdapter 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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java deleted file mode 100644 index b3d6c7e..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/EntityAdapter.java +++ /dev/null @@ -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 - */ -public interface EntityAdapter -{ - /** - * Returns the row id of the entity or -1 if the entity has not been stored yet. - * - * @return The entity row id or -1. - */ - 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 null 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 null. - */ - T valueOf(FieldAdapter 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 null. - */ - T oldValueOf(FieldAdapter fieldAdapter); - - /** - * Returns whether the given field has been overridden or not. - * - * @param fieldAdapter - * The {@link FieldAdapter} of the field to check. - * - * @return true if the field has been overridden, false otherwise. - */ - boolean isUpdated(FieldAdapter fieldAdapter); - - /** - * Returns whether this adapter supports modifying values. - * - * @return true if the task values can be changed by this adapter, false otherwise. - */ - boolean isWriteable(); - - /** - * Returns whether any value has been modified. - * - * @return true 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. - */ - void set(FieldAdapter 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 fieldAdapter); - - /** - * Commit all changes to the database. - * - * @param db - * A writable database. - * - * @return The number of entries affected. This may be 0 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 getState(FieldAdapter 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. - */ - void setState(FieldAdapter 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 duplicate(); -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java deleted file mode 100644 index b2c9de3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/InstanceAdapter.java +++ /dev/null @@ -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 - */ -public interface InstanceAdapter extends EntityAdapter -{ - - Collection 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 _ID = new LongFieldAdapter(Instances._ID); - - /** - * Adapter for the due date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter<>(Instances.INSTANCE_DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the start date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_START = new DateTimeFieldAdapter<>(Instances.INSTANCE_START, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the start sorting of a task instance. - */ - LongFieldAdapter INSTANCE_START_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_START_SORTING); - - /** - * Adapter for the due sorting of a task instance. - */ - LongFieldAdapter INSTANCE_DUE_SORTING = new LongFieldAdapter<>(Instances.INSTANCE_DUE_SORTING); - - /** - * Adapter for the original time of a task instance. - */ - DateTimeFieldAdapter 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 DISTANCE_FROM_CURRENT = new IntegerFieldAdapter<>(Instances.DISTANCE_FROM_CURRENT); - - /** - * Adapter for the title of a task instance. - */ - StringFieldAdapter TITLE = new StringFieldAdapter<>(Tasks.TITLE); - - /** - * Adapter for the row id of the task. - */ - LongFieldAdapter TASK_ID = new LongFieldAdapter(Instances.TASK_ID); - - @Override - InstanceAdapter duplicate(); - - /** - * Returns a {@link TaskAdapter} for the task component of the instanced view. - * - * @return - */ - TaskAdapter taskAdapter(); -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java deleted file mode 100644 index d72a673..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/ListAdapter.java +++ /dev/null @@ -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 - */ -public interface ListAdapter extends EntityAdapter -{ - /** - * Adapter for the row id of a task list. - */ - LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); - - /** - * Adapter for the _sync_id of a list. - */ - StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); - - /** - * Adapter for the sync version of a list. - */ - StringFieldAdapter SYNC_VERSION = new StringFieldAdapter(TaskLists.SYNC_VERSION); - - /** - * Adapter for the account name of a list. - */ - StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(TaskLists.ACCOUNT_NAME); - - /** - * Adapter for the account type of a list. - */ - StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); - - /** - * Adapter for the owner of a list. - */ - StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); - - /** - * Adapter for the name of a list. - */ - StringFieldAdapter LIST_NAME = new StringFieldAdapter(TaskLists.LIST_NAME); - - /** - * Adapter for the color of a list. - */ - IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(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(); -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java deleted file mode 100644 index 4668cce..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/TaskAdapter.java +++ /dev/null @@ -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 - */ -public interface TaskAdapter extends EntityAdapter -{ - /** - * Adapter for the row id of a task. - */ - LongFieldAdapter _ID = new LongFieldAdapter(Tasks._ID); - - /** - * Adapter for the version of a task. - */ - LongFieldAdapter VERSION = new LongFieldAdapter<>(Tasks.VERSION); - - /** - * Adapter for the task row id of as instance. - */ - LongFieldAdapter INSTANCE_TASK_ID = new LongFieldAdapter(Instances.TASK_ID); - - /** - * Adapter for the row id of the list of a task. - */ - LongFieldAdapter LIST_ID = new LongFieldAdapter(Tasks.LIST_ID); - - /** - * Adapter for the owner of the list of a task. - */ - StringFieldAdapter LIST_OWNER = new StringFieldAdapter(Tasks.LIST_OWNER); - - /** - * Adapter for the row id of original instance of a task. - */ - LongFieldAdapter ORIGINAL_INSTANCE_ID = new LongFieldAdapter(Tasks.ORIGINAL_INSTANCE_ID); - - /** - * Adapter for the sync_id of original instance of a task. - */ - StringFieldAdapter ORIGINAL_INSTANCE_SYNC_ID = new StringFieldAdapter(Tasks.ORIGINAL_INSTANCE_SYNC_ID); - - /** - * Adapter for the original instance all day flag of a task. - */ - BooleanFieldAdapter ORIGINAL_INSTANCE_ALLDAY = new BooleanFieldAdapter(Tasks.ORIGINAL_INSTANCE_ALLDAY); - - /** - * Adapter for the parent_id of a task. - */ - LongFieldAdapter PARENT_ID = new LongFieldAdapter(Tasks.PARENT_ID); - - /** - * Adapter for the all day flag of a task. - */ - BooleanFieldAdapter IS_ALLDAY = new BooleanFieldAdapter(Tasks.IS_ALLDAY); - - /** - * Adapter for the percent complete value of a task. - */ - IntegerFieldAdapter PERCENT_COMPLETE = new IntegerFieldAdapter(Tasks.PERCENT_COMPLETE); - - /** - * Adapter for the status of a task. - */ - IntegerFieldAdapter STATUS = new IntegerFieldAdapter(Tasks.STATUS); - - /** - * Adapter for the priority value of a task. - */ - IntegerFieldAdapter PRIORITY = new IntegerFieldAdapter(Tasks.PRIORITY); - - /** - * Adapter for the classification value of a task. - */ - IntegerFieldAdapter CLASSIFICATION = new IntegerFieldAdapter(Tasks.CLASSIFICATION); - - /** - * Adapter for the list name of a task. - */ - StringFieldAdapter LIST_NAME = new StringFieldAdapter(Tasks.LIST_NAME); - - /** - * Adapter for the account name of a task. - */ - StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(Tasks.ACCOUNT_NAME); - - /** - * Adapter for the account type of a task. - */ - StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(Tasks.ACCOUNT_TYPE); - - /** - * Adapter for the title of a task. - */ - StringFieldAdapter TITLE = new StringFieldAdapter(Tasks.TITLE); - - /** - * Adapter for the location of a task. - */ - StringFieldAdapter LOCATION = new StringFieldAdapter(Tasks.LOCATION); - - /** - * Adapter for the description of a task. - */ - StringFieldAdapter DESCRIPTION = new StringFieldAdapter(Tasks.DESCRIPTION); - - /** - * Adapter for the start date of a task. - */ - DateTimeFieldAdapter DTSTART = new DateTimeFieldAdapter(Tasks.DTSTART, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the original date of a task. - */ - DateTimeFieldAdapter ORIGINAL_INSTANCE_TIME = new DateTimeFieldAdapter(Tasks.ORIGINAL_INSTANCE_TIME, Tasks.TZ, - Tasks.ORIGINAL_INSTANCE_ALLDAY); - - /** - * Adapter for the raw start date timestamp of a task. - */ - LongFieldAdapter DTSTART_RAW = new LongFieldAdapter(Tasks.DTSTART); - - /** - * Adapter for the due date of a task. - */ - DateTimeFieldAdapter DUE = new DateTimeFieldAdapter(Tasks.DUE, Tasks.TZ, Tasks.IS_ALLDAY); - - /** - * Adapter for the raw due date timestamp of a task. - */ - LongFieldAdapter DUE_RAW = new LongFieldAdapter(Tasks.DUE); - - /** - * Adapter for the start date of a task. - */ - DurationFieldAdapter DURATION = new DurationFieldAdapter(Tasks.DURATION); - - /** - * Adapter for the dirty flag of a task. - */ - BooleanFieldAdapter _DIRTY = new BooleanFieldAdapter(Tasks._DIRTY); - - /** - * Adapter for the deleted flag of a task. - */ - BooleanFieldAdapter _DELETED = new BooleanFieldAdapter(Tasks._DELETED); - - /** - * Adapter for the completed date of a task. - */ - DateTimeFieldAdapter COMPLETED = new DateTimeFieldAdapter(Tasks.COMPLETED, null, null); - - /** - * Adapter for the created date of a task. - */ - DateTimeFieldAdapter CREATED = new DateTimeFieldAdapter(Tasks.CREATED, null, null); - - /** - * Adapter for the last modified date of a task. - */ - DateTimeFieldAdapter LAST_MODIFIED = new DateTimeFieldAdapter(Tasks.LAST_MODIFIED, null, null); - - /** - * Adapter for the URL of a task. - */ - UrlFieldAdapter URL = new UrlFieldAdapter(TaskContract.Tasks.URL); - - /** - * Adapter for the UID of a task. - */ - StringFieldAdapter _UID = new StringFieldAdapter(TaskContract.Tasks._UID); - - /** - * Adapter for the raw time zone of a task. - */ - StringFieldAdapter TIMEZONE_RAW = new StringFieldAdapter(TaskContract.Tasks.TZ); - - /** - * Adapter for the Color of the task. - */ - IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskContract.Tasks.LIST_COLOR); - - /** - * Adapter for the access level of the task list. - */ - IntegerFieldAdapter LIST_ACCESS_LEVEL = new IntegerFieldAdapter(TaskContract.Tasks.LIST_ACCESS_LEVEL); - - /** - * Adapter for the visibility setting of the task list. - */ - BooleanFieldAdapter LIST_VISIBLE = new BooleanFieldAdapter(TaskContract.Tasks.VISIBLE); - - /** - * Adpater for the ID of the task. - */ - IntegerFieldAdapter TASK_ID = new IntegerFieldAdapter(TaskContract.Tasks._ID); - - /** - * Adapter for the IS_CLOSED flag of a task. - */ - BooleanFieldAdapter IS_CLOSED = new BooleanFieldAdapter(TaskContract.Tasks.IS_CLOSED); - - /** - * Adapter for the IS_NEW flag of a task. - */ - BooleanFieldAdapter IS_NEW = new BooleanFieldAdapter(TaskContract.Tasks.IS_NEW); - - /** - * Adapter for the PINNED flag of a task. - */ - BooleanFieldAdapter PINNED = new BooleanFieldAdapter(TaskContract.Tasks.PINNED); - - /** - * Adapter for the HAS_ALARMS flag of a task. - */ - BooleanFieldAdapter HAS_ALARMS = new BooleanFieldAdapter(TaskContract.Tasks.HAS_ALARMS); - - /** - * Adapter for the HAS_PROPERTIES flag of a task. - */ - BooleanFieldAdapter HAS_PROPERTIES = new BooleanFieldAdapter(TaskContract.Tasks.HAS_PROPERTIES); - - /** - * Adapter for the RRULE of a task. - */ - RRuleFieldAdapter RRULE = new RRuleFieldAdapter(TaskContract.Tasks.RRULE); - - /** - * Adapter for the RDATE of a task. - */ - DateTimeIterableFieldAdapter RDATE = new DateTimeIterableFieldAdapter(TaskContract.Tasks.RDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the EXDATE of a task. - */ - DateTimeIterableFieldAdapter EXDATE = new DateTimeIterableFieldAdapter(TaskContract.Tasks.EXDATE, - TaskContract.Tasks.TZ); - - /** - * Adapter for the SYNC1 field of a task. - */ - BinaryFieldAdapter SYNC1 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC1); - - /** - * Adapter for the SYNC2 field of a task. - */ - BinaryFieldAdapter SYNC2 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC2); - - /** - * Adapter for the SYNC3 field of a task. - */ - BinaryFieldAdapter SYNC3 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC3); - - /** - * Adapter for the SYNC4 field of a task. - */ - BinaryFieldAdapter SYNC4 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC4); - - /** - * Adapter for the SYNC5 field of a task. - */ - BinaryFieldAdapter SYNC5 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC5); - - /** - * Adapter for the SYNC6 field of a task. - */ - BinaryFieldAdapter SYNC6 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC6); - - /** - * Adapter for the SYNC7 field of a task. - */ - BinaryFieldAdapter SYNC7 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC7); - - /** - * Adapter for the SYNC8 field of a task. - */ - BinaryFieldAdapter SYNC8 = new BinaryFieldAdapter(TaskContract.Tasks.SYNC8); - - /** - * Adapter for the SYNC_VERSION field of a task. - */ - BinaryFieldAdapter SYNC_VERSION = new BinaryFieldAdapter(TaskContract.Tasks.SYNC_VERSION); - - /** - * Adapter for the SYNC_ID field of a task. - */ - StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskContract.Tasks._SYNC_ID); - - /** - * Adapter for the due date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_DUE = new DateTimeFieldAdapter(Instances.INSTANCE_DUE, Tasks.TZ, - Tasks.IS_ALLDAY); - - /** - * Adapter for the start date of a task instance. - */ - DateTimeFieldAdapter INSTANCE_START = new DateTimeFieldAdapter(Instances.INSTANCE_START, Tasks.TZ, - Tasks.IS_ALLDAY); - - /** - * Returns whether the adapted task is recurring. - * - * @return true if the task is recurring, false 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 true if the recurrence set has changed, false 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(); -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java deleted file mode 100644 index 0d6c24d..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class BinaryFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java deleted file mode 100644 index ef7e948..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java +++ /dev/null @@ -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}. - *

- * Implementation detail: - *

- * The values are loaded and stored as 0 (for false) and 1 (for true). - * - * @param - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class BooleanFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java deleted file mode 100644 index 5c9159b..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java +++ /dev/null @@ -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}. - *

- * {@link DateTime} values are stored as three separate values: - *

    - *
  • a timestamp in milliseconds since the epoch
  • - *
  • a time zone
  • - *
  • an allday flag
  • - *
- *

- * This adapter combines those three fields to a {@link DateTime} value. If the time zone field is null the time zone is always set to UTC. - * - * @param - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class DateTimeFieldAdapter extends SimpleFieldAdapter -{ - 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 null 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 null 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); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java deleted file mode 100644 index 3f1ebbc..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapter.java +++ /dev/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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class DateTimeIterableFieldAdapter extends SimpleFieldAdapter, 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 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 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 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 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 - { - - public DateTimeList(TimeZone timeZone, String dateTimeList) - { - super(new Mapped<>( - datetime -> !datetime.isFloating() && timeZone != null ? datetime.shiftTimeZone(timeZone) : datetime, - new Mapped( - charSequence -> DateTime.parse(timeZone, charSequence.toString()), - new Split(dateTimeList, ',')))); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java deleted file mode 100644 index 5a5f8eb..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class DurationFieldAdapter extends SimpleFieldAdapter -{ - - 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java deleted file mode 100644 index e9fe287..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java +++ /dev/null @@ -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 - * The type of the value this adapter stores. - * @param - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public interface FieldAdapter -{ - - /** - * Check if a value is present and non-null 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 null) 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-null 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-null 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); - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java deleted file mode 100644 index 28b8a01..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class FloatFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java deleted file mode 100644 index 933c5e8..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class IntegerFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java deleted file mode 100644 index 517ca23..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class LongFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java deleted file mode 100644 index 201b075..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class RRuleFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java deleted file mode 100644 index 2752ba9..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java +++ /dev/null @@ -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 - * The Type of the field this adapter handles. - * @param - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public abstract class SimpleFieldAdapter implements FieldAdapter -{ - - /** - * 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)); - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java deleted file mode 100644 index 4c5311a..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class StringFieldAdapter extends SimpleFieldAdapter -{ - - /** - * 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); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java b/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java deleted file mode 100644 index 53496b8..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java +++ /dev/null @@ -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 - * The type of the entity the field belongs to. - * - * @author Marten Gajda - */ -public final class UrlFieldAdapter extends SimpleFieldAdapter -{ - - 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); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java deleted file mode 100644 index 8ae6323..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/EntityProcessor.java +++ /dev/null @@ -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 insert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - - T update(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - - void delete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java deleted file mode 100644 index 87f7379..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/Logging.java +++ /dev/null @@ -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> implements EntityProcessor -{ - public static final String TAG = "Logging EntityProcessor"; - private final EntityProcessor mDelegate; - - - public Logging(EntityProcessor 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()); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java deleted file mode 100644 index d86f026..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/NoOpProcessor.java +++ /dev/null @@ -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> implements EntityProcessor -{ - @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 - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java deleted file mode 100644 index 8c98fd0..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Detaching.java +++ /dev/null @@ -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 -{ - - private final EntityProcessor mDelegate; - private final EntityProcessor mTaskDelegate; - - - public Detaching(EntityProcessor delegate, EntityProcessor 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 - *

- * - 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). - *

- */ - @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. - *

- * 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. - *

- * - 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 - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java deleted file mode 100644 index f02ed1d..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/TaskValueDelegate.java +++ /dev/null @@ -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 -{ - private final static Iterable> 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 mDelegate; - - - public TaskValueDelegate(EntityProcessor 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(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 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, TaskAdapter> addfieldAdapter, FieldAdapter, 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); - } - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java deleted file mode 100644 index 238e650..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/instances/Validating.java +++ /dev/null @@ -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 -{ - private final static Iterable> 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> RECURRENCE_FIELD_ADAPTERS = new Seq<>( - TaskAdapter.RRULE, - TaskAdapter.RDATE, - TaskAdapter.EXDATE); - - private static final Iterable> 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 mDelegate; - - - public Validating(EntityProcessor 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 instanceId = new NullSafe<>(entityAdapter.taskAdapter().valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)); - Optional 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."); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java deleted file mode 100644 index 0e33369..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/ListCommitProcessor.java +++ /dev/null @@ -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 -{ - - @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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java deleted file mode 100644 index 8b27215..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/lists/Validating.java +++ /dev/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 -{ - private final EntityProcessor mDelegate; - - - public Validating(EntityProcessor 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 - * true 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."); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java deleted file mode 100644 index 65bbe9a..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/AutoCompleting.java +++ /dev/null @@ -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. - *

- * 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 -{ - private final EntityProcessor 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 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); - } - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java deleted file mode 100644 index 1891ff3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Instantiating.java +++ /dev/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 -{ - /** - * 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. - *

- * TODO: get rid of it - */ - private final static BooleanFieldAdapter UPDATE_REQUESTED = new BooleanFieldAdapter( - "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 mDelegate; - - - public Instantiating(EntityProcessor 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. - *

- * 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 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, Optional>> 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> 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java deleted file mode 100644 index ea9d9cb..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Moving.java +++ /dev/null @@ -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. - *

- * 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 -{ - private final EntityProcessor mDelegate; - - - public Moving(EntityProcessor 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; - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java deleted file mode 100644 index 15e82b4..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Originating.java +++ /dev/null @@ -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 -{ - private final EntityProcessor mDelegate; - - - public Originating(EntityProcessor 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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java deleted file mode 100644 index d641779..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Relating.java +++ /dev/null @@ -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. - *

- * 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. - *

- * It also updates {@link TaskContract.Property.Relation#RELATED_UID} when a tasks - * is synced the first time and a UID has been set. - *

- * - * @author Marten Gajda - */ -public final class Relating implements EntityProcessor -{ - private final EntityProcessor mDelegate; - - - public Relating(EntityProcessor 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()) }); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java deleted file mode 100644 index a08f9b7..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Reparenting.java +++ /dev/null @@ -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 -{ - private final EntityProcessor mDelegate; - - - public Reparenting(EntityProcessor 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); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java deleted file mode 100644 index 5b62b8e..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Searchable.java +++ /dev/null @@ -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 -{ - private final EntityProcessor mDelegate; - - - public Searchable(EntityProcessor 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)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java deleted file mode 100644 index 677dd61..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/TaskCommitProcessor.java +++ /dev/null @@ -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 -{ - @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); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java deleted file mode 100644 index a28a97e..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/Validating.java +++ /dev/null @@ -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 -{ - private static final String[] TASKLIST_ID_PROJECTION = { TaskContract.TaskLists._ID }; - private static final String TASKLISTS_ID_SELECTION = TaskContract.TaskLists._ID + "="; - - private final EntityProcessor mDelegate; - - - public Validating(EntityProcessor 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 - * true 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"); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java deleted file mode 100644 index c9e606c..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Dated.java +++ /dev/null @@ -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 -{ - - public Dated(Optional dateTime, String timeStampColumn, String sortingColumn, Single 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; - })); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java deleted file mode 100644 index fafc5ca..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Distant.java +++ /dev/null @@ -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 -{ - - public Distant(int distance, Single delegate) - { - super(() -> - { - ContentValues values = delegate.value(); - values.put(TaskContract.Instances.DISTANCE_FROM_CURRENT, distance); - return values; - }); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java deleted file mode 100644 index d39005f..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDated.java +++ /dev/null @@ -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 -{ - public DueDated(Optional due, Single delegate) - { - super(new Dated(due, TaskContract.Instances.INSTANCE_DUE, TaskContract.Instances.INSTANCE_DUE_SORTING, delegate)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java deleted file mode 100644 index 0552a87..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Enduring.java +++ /dev/null @@ -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 -{ - private final Single mDelegate; - - - public Enduring(Single 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( - 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java deleted file mode 100644 index 793ffa1..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/Overridden.java +++ /dev/null @@ -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 -{ - private final Optional mOriginalTime; - private final Single mDelegate; - - - public Overridden(DateTime originalTime, ContentValues delegate) - { - this(new Present<>(originalTime), () -> delegate); - } - - - public Overridden(Optional originalTime, Single 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java deleted file mode 100644 index 5ecb320..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDated.java +++ /dev/null @@ -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 -{ - public StartDated(Optional start, Single delegate) - { - super(new Dated(start, TaskContract.Instances.INSTANCE_START, TaskContract.Instances.INSTANCE_START_SORTING, delegate)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java deleted file mode 100644 index 65be8e9..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelated.java +++ /dev/null @@ -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 -{ - private final long mTaskId; - private final Single mDelegate; - - - public TaskRelated(TaskAdapter taskAdapter, Single delegate) - { - this(taskAdapter.id(), delegate); - } - - - public TaskRelated(long taskId, Single delegate) - { - mTaskId = taskId; - mDelegate = delegate; - } - - - @Override - public ContentValues value() - { - ContentValues values = mDelegate.value(); - values.put(TaskContract.Instances.TASK_ID, mTaskId); - return values; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java b/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java deleted file mode 100644 index b46d3cc..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceData.java +++ /dev/null @@ -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 -{ - @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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java deleted file mode 100644 index 3f37b2e..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/ContainsValues.java +++ /dev/null @@ -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 -{ - 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java deleted file mode 100644 index 5ee653d..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/InstanceValuesIterable.java +++ /dev/null @@ -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> -{ - private final long mId; - private final TaskAdapter mTaskAdapter; - - - public InstanceValuesIterable(long id, TaskAdapter taskAdapter) - { - mId = id; - mTaskAdapter = taskAdapter; - } - - - @Override - public Iterator> iterator() - { - Optional start = new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DTSTART)); - // effective due is either the actual due, start + duration or absent - Optional effectiveDue = new FirstPresent<>( - new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DUE)), - new Zipped<>(start, new NullSafe<>(mTaskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)); - - Single 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 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); - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java deleted file mode 100644 index 43833a1..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/Limited.java +++ /dev/null @@ -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. - *

- * TODO: move to jems - * - * @author Marten Gajda - * @deprecated - */ -@Deprecated -public final class Limited implements Iterable -{ - private final int mCount; - private final Iterable mDelegate; - - - public Limited(int count, Iterable delegate) - { - mCount = count; - mDelegate = delegate; - } - - - @Override - public Iterator iterator() - { - return new LimitedIterator<>(mCount, mDelegate.iterator()); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java deleted file mode 100644 index 88bba90..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/LimitedIterator.java +++ /dev/null @@ -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 extends AbstractBaseIterator -{ - private int mCount; - private final Iterator mDelegate; - - - public LimitedIterator(int count, Iterator 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(); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java deleted file mode 100644 index b8984b6..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/OverrideValuesFunction.java +++ /dev/null @@ -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> -{ - - @Override - public Single value(TaskAdapter taskAdapter) - { - Optional start = new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DTSTART)); - // effective due is either the actual due, start + duration or absent - Optional effectiveDue = new FirstPresent<>( - new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DUE)), - new Zipped<>(start, new NullSafe<>(taskAdapter.valueOf(TaskAdapter.DURATION)), DateTime::addDuration)); - - Single 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()); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java deleted file mode 100644 index a30f3fc..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/Profiled.java +++ /dev/null @@ -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 run(Single 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 run(Fragile 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)); - } - } - -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java deleted file mode 100644 index 731e424..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/Range.java +++ /dev/null @@ -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. - *

- * TODO: implement in jems - * - * @author Marten Gajda - */ -@Deprecated -public final class Range implements Iterable -{ - 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 iterator() - { - return new LimitedIterator<>(mEnd - mStart, new IntSequenceGenerator(mStart)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java deleted file mode 100644 index e5b5fab..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/ResourceArray.java +++ /dev/null @@ -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 -{ - private final Context mContext; - private final int mResource; - - - public ResourceArray(Context context, int resource) - { - mContext = context; - mResource = resource; - } - - - @Override - public Iterator iterator() - { - return new Seq<>(mContext.getResources().getStringArray(mResource)); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java deleted file mode 100644 index e49ac38..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/RowIterator.java +++ /dev/null @@ -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 -{ - 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java deleted file mode 100644 index d96ac73..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/TableColumns.java +++ /dev/null @@ -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> -{ - private final String mTableName; - - - public TableColumns(String tableName) - { - mTableName = tableName; - } - - - @Override - public Iterable value(SQLiteDatabase db) - { - try (Cursor cursor = db.rawQuery(String.format("PRAGMA table_info(%s)", DatabaseUtils.sqlEscapeString(mTableName)), null)) - { - int nameIdx = cursor.getColumnIndexOrThrow("name"); - - List result = new LinkedList<>(); - while (cursor.moveToNext()) - { - result.add(cursor.getString(nameIdx)); - } - - return result; - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java deleted file mode 100644 index b3620b3..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterable.java +++ /dev/null @@ -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 -{ - private final TaskAdapter mTaskAdapter; - - - public TaskInstanceIterable(TaskAdapter taskAdapter) - { - mTaskAdapter = taskAdapter; - } - - - @Override - public Iterator iterator() - { - DateTime dtstart = new Backed(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); - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java deleted file mode 100644 index 5b74cfb..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/TaskInstanceIterator.java +++ /dev/null @@ -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}. - *

- * TODO: this should go to lib-recur - * - * @author Marten Gajda - */ -public final class TaskInstanceIterator extends AbstractBaseIterator -{ - 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java deleted file mode 100644 index 84f0949..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/Timestamps.java +++ /dev/null @@ -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 -{ - private final Iterable mDateTimes; - - - public Timestamps(Iterable 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; - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/With.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/With.java deleted file mode 100644 index 7ae6b44..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/With.java +++ /dev/null @@ -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. - *

- * TODO move to jems if this works out well - * - * @author Marten Gajda - */ -@Deprecated -public final class With implements Procedure> -{ - private final Optional mValue; - - - public With(T value) - { - this(() -> value); - } - - - public With(Single value) - { - this(new SinglePresent<>(value)); - } - - - public With(Optional value) - { - mValue = value; - } - - - @Override - public void process(Procedure delegate) - { - if (mValue.isPresent()) - { - delegate.process(mValue.value()); - } - } -} diff --git a/provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java b/provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java deleted file mode 100644 index 80df3f7..0000000 --- a/provider/src/main/java/org/dmfs/provider/tasks/utils/Zipped.java +++ /dev/null @@ -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}. - *

- * TODO: maybe a more appropriate name? - *

- * TODO: move to jems - * - * @author Marten Gajda - */ -@Deprecated -public final class Zipped extends DelegatingSingle -{ - public Zipped(Optional optionalValue, Single delegate, BiFunction function) - { - super(new Backed(new Mapped<>(from -> function.value(from, delegate.value()), optionalValue), delegate)); - } -} diff --git a/provider/src/main/java/org/dmfs/tasks/contract/TaskContract.java b/provider/src/main/java/org/dmfs/tasks/contract/TaskContract.java deleted file mode 100644 index 3ce329d..0000000 --- a/provider/src/main/java/org/dmfs/tasks/contract/TaskContract.java +++ /dev/null @@ -1,1728 +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.tasks.contract; - -import android.content.ContentResolver; -import android.content.Intent; -import android.net.Uri; -import android.provider.BaseColumns; -import android.provider.SyncStateContract; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; - - -/** - * Task contract. This class defines the interface to the task provider. - *

- * TODO: Add missing javadoc. - *

- *

- * TODO: Specify extended properties - *

- *

- * TODO: Add CONTENT_URI for the attachment store. - *

- *

- * TODO: Also, we could use some refactoring... - *

- * - * @author Marten Gajda - * @author Tobias Reinsch - */ -public final class TaskContract -{ - - private static Map sUriFactories = new HashMap(4); - - /** - * URI parameter to signal that the caller is a sync adapter. - */ - public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter"; - - /** - * URI parameter to signal the request of the extended properties of a task. - */ - public static final String LOAD_PROPERTIES = "load_properties"; - - /** - * URI parameter to submit the account name of the account we operate on. - */ - public static final String ACCOUNT_NAME = "account_name"; - - /** - * URI parameter to submit the account type of the account we operate on. - */ - public static final String ACCOUNT_TYPE = "account_type"; - - /** - * Account name for local, unsynced task lists. - */ - public static final String LOCAL_ACCOUNT_NAME = "Local"; - - /** - * Account type for local, unsynced task lists. - */ - public static final String LOCAL_ACCOUNT_TYPE = "org.dmfs.account.LOCAL"; - - /** - * Broadcast action that's sent when the task database has been initialized, either because the app was launched for the first time or because the app was - * launched after the user cleared the app data. - *

- * The intent data represents the authority of the provider, the MIME type will be {@link #MIMETYPE_AUTHORITY}. - */ - public static final String ACTION_DATABASE_INITIALIZED = "org.dmfs.tasks.DATABASE_INITIALIZED"; - - /** - * A MIME type of an authority. Authorities itself don't seem to have a MIME type in Android, so we just use our own. - */ - public static final String MIMETYPE_AUTHORITY = ContentResolver.CURSOR_DIR_BASE_TYPE + "/vnd.org.dmfs.authority.mimetype"; - - /** - * The action of the broadcast that's send when a task becomes due. The intent data will be a {@link Uri} of the task that became due. - */ - public static final String ACTION_BROADCAST_TASK_DUE = "org.dmfs.android.tasks.TASK_DUE"; - - /** - * The action of the broadcast that's send when a task starts. The intent data will be a {@link Uri} of the task that has started. - */ - public static final String ACTION_BROADCAST_TASK_STARTING = "org.dmfs.android.tasks.TASK_START"; - - /** - * A Long extra that contains a timestamp of the event that's triggered. So this is either the timestamp of the start or due date of the task. - */ - public final static String EXTRA_TASK_TIMESTAMP = "org.dmfs.provider.tasks.extra.TIMESTAMP"; - - /** - * A Boolean extra to indicate that the event that was triggered is an all-day date. - */ - public final static String EXTRA_TASK_ALLDAY = "org.dmfs.provider.tasks.extra.ALLDAY"; - - /** - * A String extra containing the timezone id of the task. - */ - public final static String EXTRA_TASK_TIMEZONE = "org.dmfs.provider.tasks.extra.TIMEZONE"; - - /** - * A String extra containing the title of the task. - */ - public final static String EXTRA_TASK_TITLE = "org.dmfs.provider.tasks.extra.TITLE"; - - /** - * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of {@link Uri}s that have been modified. This always - * goes along with an {@link #EXTRA_OPERATIONS} which contains a code for the operation executed on a Uri at the same index. - */ - public final static String EXTRA_OPERATIONS_URIS = "org.dmfs.tasks.OPERATIONS_URIS"; - - /** - * The name of the {@link Intent#ACTION_PROVIDER_CHANGED} extra that contains the {@link ArrayList} of provider operation codes. The following codes are - * used: - *

    - *
  • 0 - for inserts
  • - *
  • 1 - for updates
  • - *
  • 2 - for deletes
  • - *
- */ - public final static String EXTRA_OPERATIONS = "org.dmfs.tasks.OPERATIONS"; - - - /** - * Private constructor to prevent instantiation. - */ - private TaskContract() - { - } - - - /** - * A table provided for sync adapters to use for storing private sync state data. - *

- * Only sync adapters are allowed to access this table and they may access their own rows only. - *

- * Note that only one row per account will be stored. Updating or inserting a sync state for a specific account will override any previous sync state for - * this account. - */ - public static class SyncState implements SyncStateContract.Columns, BaseColumns - { - public final static String CONTENT_URI_PATH = "syncstate"; - - - /** - * Get the sync state content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - /** - * Get the base content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(); - } - - - /** - * A set of columns for synchronization purposes. These columns exist in {@link Tasks} and in {@link TaskLists} but have different meanings. Only sync - * adapters are allowed to change these values. - * - * @author Marten Gajda - */ - public interface CommonSyncColumns - { - - /** - * A unique Sync ID as set by the sync adapter. - *

- * Value: String - *

- */ - String _SYNC_ID = "_sync_id"; - - /** - * Sync version as set by the sync adapter. - *

- * Value: String - *

- */ - String SYNC_VERSION = "sync_version"; - - /** - * Indicates that a task or a task list has been changed. - *

- * Value: Integer - *

- */ - String _DIRTY = "_dirty"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC1 = "sync1"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC2 = "sync2"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC3 = "sync3"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC4 = "sync4"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC5 = "sync5"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC6 = "sync6"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC7 = "sync7"; - - /** - * A general purpose column for the sync adapter. - *

- * Value: String - *

- */ - String SYNC8 = "sync8"; - - } - - - /** - * Additional sync columns for task lists. - * - * @author Marten Gajda - */ - public interface TaskListSyncColumns - { - - /** - * The name of the account this list belongs to. This field is write-once. - *

- * Value: String - *

- */ - String ACCOUNT_NAME = "account_name"; - - /** - * The type of the account this list belongs to. This field is write-once. - *

- * Value: String - *

- */ - String ACCOUNT_TYPE = "account_type"; - } - - - /** - * Additional sync columns for tasks. - * - * @author Marten Gajda - */ - public interface TaskSyncColumns - { - /** - * The UID of a task. This is field can be changed by a sync adapter only. - *

- * Value: String - *

- */ - String _UID = "_uid"; - - /** - * Deleted flag of a task. This is set to 1 by the content provider when a task app deletes a task. The sync adapter has to remove the task - * again to finish the removal. This value is read-only. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String _DELETED = "_deleted"; - } - - - /** - * Data columns of task lists. - * - * @author Marten Gajda - */ - public interface TaskListColumns - { - - /** - * List ID. - *

- * Value: Long - *

- *

- * read-only - *

- */ - String _ID = "_id"; - - /** - * The name of the task list. - *

- * Value: String - *

- */ - String LIST_NAME = "list_name"; - - /** - * The color of this list as integer (0xaarrggbb). Only the sync adapter can change this. - *

- * Value: Integer - *

- */ - String LIST_COLOR = "list_color"; - - /** - * The access level a user has on this list. This value is not used yet, sync adapters should set it to 0. - *

- * Value: Integer - *

- */ - String ACCESS_LEVEL = "list_access_level"; - - /** - * Indicates that a task list is set to be visible. - *

- * Value: Integer (0 or 1) - *

- */ - String VISIBLE = "visible"; - - /** - * Indicates that a task list is set to be synced. - *

- * Value: Integer (0 or 1) - *

- */ - String SYNC_ENABLED = "sync_enabled"; - - /** - * The email address of the list owner. - *

- * Value: String - *

- */ - String OWNER = "list_owner"; - - } - - - /** - * The task list table holds one entry for each task list. - * - * @author Marten Gajda - */ - public static final class TaskLists implements TaskListColumns, TaskListSyncColumns, CommonSyncColumns - { - public static final String CONTENT_URI_PATH = "tasklists"; - - /** - * The default sort order. - */ - public static final String DEFAULT_SORT_ORDER = ACCOUNT_NAME + ", " + LIST_NAME; - - /** - * An array of columns only a sync adapter is allowed to change. - */ - public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { - ACCESS_LEVEL, _DIRTY, OWNER, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, - _SYNC_ID, SYNC_VERSION, }; - - - /** - * Get the task list content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - /** - * Task data columns. Defines all the values a task can have at most once. - * - * @author Marten Gajda - */ - public interface TaskColumns extends BaseColumns - { - - /** - * The row id of a task. This value is read-only - *

- * Value: Integer - *

- */ - String _ID = "_id"; - - /** - * The local version number of this task. The only guarantee about the value is, it's incremented whenever the task changes (this includes any - * changes applied by sync adapters). - *

- * Note, there is no guarantee about how much it's incremented other than by at least 1. - *

- * Value: Integer - *

- * read-only - */ - String VERSION = "version"; - - /** - * The id of the list this task belongs to. This value is write-once and must not be null. - *

- * Value: Integer - *

- */ - String LIST_ID = "list_id"; - - /** - * The title of the task. - *

- * Value: String - *

- */ - String TITLE = "title"; - - /** - * The location of the task. - *

- * Value: String - *

- */ - String LOCATION = "location"; - - /** - * A geographic location related to the task. The should be a string in the format "longitude,latitude". - *

- * Value: String - *

- */ - String GEO = "geo"; - - /** - * The description of a task. - *

- * Value: String - *

- */ - String DESCRIPTION = "description"; - - /** - * The URL iCalendar field for this task. Must be a valid URI if not null- - *

- * Value: String - *

- */ - String URL = "url"; - - /** - * The email address of the organizer if any, {@code null} otherwise. - *

- * Value: String - *

- */ - String ORGANIZER = "organizer"; - - /** - * The priority of a task. This is an Integer between zero and 9. Zero means there is no priority set. 1 is the highest priority and 9 the lowest. - *

- * Value: Integer - *

- */ - String PRIORITY = "priority"; - - /** - * The default value of {@link #PRIORITY}. - */ - int PRIORITY_DEFAULT = 0; - - /** - * The classification of a task. This value must be either null or one of {@link #CLASSIFICATION_PUBLIC}, {@link #CLASSIFICATION_PRIVATE}, - * {@link #CLASSIFICATION_CONFIDENTIAL}. - *

- * Value: Integer - *

- */ - String CLASSIFICATION = "class"; - - /** - * Classification value for public tasks. - */ - int CLASSIFICATION_PUBLIC = 0; - - /** - * Classification value for private tasks. - */ - int CLASSIFICATION_PRIVATE = 1; - - /** - * Classification value for confidential tasks. - */ - int CLASSIFICATION_CONFIDENTIAL = 2; - - /** - * Default value of {@link #CLASSIFICATION}. - */ - Integer CLASSIFICATION_DEFAULT = null; - - /** - * Date of completion of this task in milliseconds since the epoch or {@code null} if this task has not been completed yet. - *

- * Value: Long - *

- */ - String COMPLETED = "completed"; - - /** - * Indicates that the date of completion is an all-day date. - *

- * Value: Integer - *

- */ - String COMPLETED_IS_ALLDAY = "completed_is_allday"; - - /** - * A number between 0 and 100 that indicates the progress of the task or null. - *

- * Value: Integer (0-100) - *

- */ - String PERCENT_COMPLETE = "percent_complete"; - - /** - * The status of this task. One of {@link #STATUS_NEEDS_ACTION},{@link #STATUS_IN_PROCESS}, {@link #STATUS_COMPLETED}, {@link #STATUS_CANCELLED}. - *

- * Value: Integer - *

- */ - String STATUS = "status"; - - /** - * A specific status indicating that nothing has been done yet. - */ - int STATUS_NEEDS_ACTION = 0; - - /** - * A specific status indicating that some work has been done. - */ - int STATUS_IN_PROCESS = 1; - - /** - * A specific status indicating that the task is completed. - */ - int STATUS_COMPLETED = 2; - - /** - * A specific status indicating that the task has been cancelled. - */ - int STATUS_CANCELLED = 3; - - /** - * The default status is "needs action". - */ - int STATUS_DEFAULT = STATUS_NEEDS_ACTION; - - /** - * A flag that indicates a task is new (i.e. not work has been done yet). This flag is read-only. Its value is 1 when - * {@link #STATUS} equals {@link #STATUS_NEEDS_ACTION} and 0 otherwise. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String IS_NEW = "is_new"; - - /** - * A flag that indicates a task is closed (no more work has to be done). This flag is read-only. Its value is 1 when - * {@link #STATUS} equals {@link #STATUS_COMPLETED} or {@link #STATUS_CANCELLED} and 0 otherwise. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String IS_CLOSED = "is_closed"; - - /** - * An individual color for this task in the format 0xaarrggbb or {@code null} to use {@link TaskListColumns#LIST_COLOR} instead. - *

- * Value: Integer - *

- */ - String TASK_COLOR = "task_color"; - - /** - * When this task starts in milliseconds since the epoch. - *

- * Value: Long - *

- */ - String DTSTART = "dtstart"; - - /** - * Boolean: flag that indicates that this is an all-day task. - */ - String IS_ALLDAY = "is_allday"; - - /** - * When this task has been created in milliseconds since the epoch. - *

- * Value: Long - *

- */ - String CREATED = "created"; - - /** - * When this task had been modified the last time in milliseconds since the epoch. - *

- * Value: Long - *

- */ - String LAST_MODIFIED = "last_modified"; - - /** - * String: An Olson Id of the time zone of this task. If this value is null, it's automatically replaced by the local time zone. - */ - String TZ = "tz"; - - /** - * When this task is due in milliseconds since the epoch. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task - * has no due date). - *

- * Value: Long - *

- */ - String DUE = "due"; - - /** - * The duration of this task. Only one of {@link #DUE} or {@link #DURATION} must be supplied (or none of both if the task has no due date). Setting a - * {@link #DURATION} is not allowed when {@link #DTSTART} is null. The Value must be a duration string as in RFC 5545 Section 3.3.6. - *

- * Value: String - *

- */ - String DURATION = "duration"; - - /** - * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 - * and RFC 5545 Section 3.3.5) that contains dates of instances of e recurring task. - * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. - *

- * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - String RDATE = "rdate"; - - /** - * A comma separated list of time Strings in RFC 5545 format (see RFC 5545 Section 3.3.4 - * and RFC 5545 Section 3.3.5) that contains dates of exceptions of a recurring task. - * All-day tasks must use the DATE format specified in section 3.3.4 of RFC 5545. - *

- * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - String EXDATE = "exdate"; - - /** - * A recurrence rule as specified in RFC 5545 Section 3.3.10. - *

- * This value must be {@code null} for exception instances. - *

- * Value: String - *

- */ - String RRULE = "rrule"; - - /** - * The _sync_id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or - * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. - *

- * Value: String - *

- */ - String ORIGINAL_INSTANCE_SYNC_ID = "original_instance_sync_id"; - - /** - * The row id of the original event if this is an exception, null otherwise. Only one of {@link #ORIGINAL_INSTANCE_SYNC_ID} or - * {@link #ORIGINAL_INSTANCE_ID} must be set if this task is an exception. The other one will be updated by the content provider. - *

- * Value: Long - *

- */ - String ORIGINAL_INSTANCE_ID = "original_instance_id"; - - /** - * The time in milliseconds since the Epoch of the original instance that is overridden by this instance or null if this task is not a - * recurring instance. - *

- * Value: Long - *

- */ - String ORIGINAL_INSTANCE_TIME = "original_instance_time"; - - /** - * A flag indicating that the original instance was an all-day task. - *

- * Value: Integer - *

- */ - String ORIGINAL_INSTANCE_ALLDAY = "original_instance_allday"; - - /** - * The row id of the parent task. null if the task has no parent task. - *

- * Note, when writing this value the task {@link Property.Relation} properties are updated accordingly. Any parent or child relations which - * make this a child of another task are deleted and a new {@link Property.Relation#RELTYPE_PARENT} relation pointing to the new parent is created. - * Be aware that Siblings will be split, i.e. they are not moved to the new parent. Currently this might cause siblings to become orphans if they - * don't have a parent-child relationship. This behavior may change in future version. - *

- * - *

- * Value: Long - *

- */ - String PARENT_ID = "parent_id"; - - /** - * The sorting of this task under it's parent task. - *

- * Value: String - *

- */ - String SORTING = "sorting"; - - /** - * Indicates how many alarms a task has. 0 means the task has no alarms. This field is read only as it's set automatically. - *

- * Value: Integer - *

- * Read-only - */ - String HAS_ALARMS = "has_alarms"; - - /** - * Indicates that this task has extended properties like attachments, alarms or relations. This field is read only as it's set automatically. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String HAS_PROPERTIES = "has_properties"; - - /** - * Indicates that this task has been pinned to the notification area. This flag is moved to the exception when an exception for the first instance of a - * recurring task is created. That means, if you edit a pinned recurring task, the pinned flag is moved to the exception and cleared from the master - * task. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String PINNED = "pinned"; - } - - - /** - * Columns that are valid in a search query. - * - * @author Marten Gajda - */ - public interface TaskSearchColumns - { - /** - * The score of a task in a search result. It's an indicator for the relevance of the task. Value is in (0, 1.0] where 0 would be "no relevance" at all - * (though the result doesn't contain such tasks). - *

- * Value: Float - *

- */ - String SCORE = "score"; - } - - - /** - * The task table stores the data of all tasks. - * - * @author Marten Gajda - */ - public static final class Tasks implements TaskColumns, CommonSyncColumns, TaskSyncColumns, TaskSearchColumns - { - /** - * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; - - /** - * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; - - /** - * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_NAME = TaskLists.LIST_NAME; - /** - * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. To change the color of an individual task use {@code TASK_COLOR} instead. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_COLOR = TaskLists.LIST_COLOR; - - /** - * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_OWNER = TaskLists.OWNER; - - /** - * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; - - /** - * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String VISIBLE = "visible"; - - public static final String CONTENT_URI_PATH = "tasks"; - - public static final String SEARCH_URI_PATH = "tasks_search"; - - public static final String SEARCH_QUERY_PARAMETER = "q"; - - public static final String DEFAULT_SORT_ORDER = DUE; - - public static final String[] SYNC_ADAPTER_COLUMNS = new String[] { - _DIRTY, SYNC1, SYNC2, SYNC3, SYNC4, SYNC5, SYNC6, SYNC7, SYNC8, _SYNC_ID, - SYNC_VERSION, }; - - - /** - * Get the tasks content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - - public static Uri getSearchUri(String authority, String query) - { - Uri.Builder builder = getUriFactory(authority).getUri(SEARCH_URI_PATH).buildUpon(); - builder.appendQueryParameter(SEARCH_QUERY_PARAMETER, Uri.encode(query)); - return builder.build(); - } - } - - - /** - * Columns of a task instance. - * - * @author Yannic Ahrens - * @author Marten Gajda - */ - public interface InstanceColumns - { - /** - * _ID of task this instance belongs to. - *

- * Value: Long - *

- */ - String TASK_ID = "task_id"; - - /** - * The start date of an instance in milliseconds since the epoch or null if the instance has no start date. At present this is read only. - *

- * Value: Long - *

- */ - String INSTANCE_START = "instance_start"; - - /** - * The due date of an instance in milliseconds since the epoch or null if the instance has no due date. At present this is read only. - *

- * Value: Long - *

- */ - String INSTANCE_DUE = "instance_due"; - - /** - * This column should be used in an order clause to sort instances by start date. The only guarantee about the values in this column is the sort order. - * Don't make any other assumptions about the value. - *

- * Value: Long - *

- *

- * read-only - *

- */ - String INSTANCE_START_SORTING = "instance_start_sorting"; - - /** - * This column should be used in an order clause to sort instances by due date. The only guarantee about the values in this column is the sort order. - * Don't make any other assumptions about the value. - *

- * Value: Long - *

- *

- * read-only - *

- */ - String INSTANCE_DUE_SORTING = "instance_due_sorting"; - - /** - * The duration of an instance in milliseconds or null if the instance has only one of start or due date or none of both. At present this - * is read only. - *

- * Value: Long - *

- */ - String INSTANCE_DURATION = "instance_duration"; - - /** - * The start of the original instance as specified in the master task. For non-recurring task instances this is {@code null}. - *

- * For recurring tasks, these are the timestamps which have been derived from the recurrence rule or dates, except those specified as exdates. - */ - String INSTANCE_ORIGINAL_TIME = "instance_original_time"; - - /** - * The distance of the instance from the current one. For closed instances this is always {@code -1}, for the current instance this is {@code 0}. For - * the instance after the current one this is {@code 1}, for the instance after that one it's {@code 2}, etc.. - *

- * Value: Integer - *

- * read-only - */ - String DISTANCE_FROM_CURRENT = "distance_from_current"; - } - - - /** - * A table containing one entry per task instance. This table is writable in order to allow modification of single instances of a task. Write operations to - * this table will be converted into operations on overrides and forwarded to the task table. - *

- * Note: The {@link #DTSTART}, {@link #DUE} values of instances of recurring tasks represent the actual instance values, i.e. they are different for each - * instance ({@link #DURATION} is always {@code null}). - *

- * Also, none of the instances are recurring themselves, so {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} are always {@code null}. - *

- * TODO: Insert all instances of recurring tasks. - *

- * The following operations are supported: - *

- *

Insert

- *

- * Note, the data of an insert must not contain the fields {@link #RRULE}, {@link #RDATE} or {@link #EXDATE}. If the new instance belongs to an existing - * task the data must contain the fields {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME}. Also note, this table supports writing {@link - * #DURATION} (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} - * {@link #DUE} date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. - *

- * If there already is an instance (with or without override) for the given {@link #ORIGINAL_INSTANCE_ID} and {@link #ORIGINAL_INSTANCE_TIME} an exception - * is thrown. - *

- *
ORIGINAL_INSTANCE_ID valueResult
absent or emptyA new non-recurring task is created with the given - * values.
a valid {@link Tasks} row {@code _ID}An {@link #RDATE} for the given {@link #ORIGINAL_INSTANCE_TIME} time is added to - * the given master task, any {@link #EXDATE} for this time is removed. The task is inserted as an override to the given master. No fields are inherited - * though. {@link #ORIGINAL_INSTANCE_ALLDAY} will be set to {@link #IS_ALLDAY} of the master. - *

- * Note, if the given master is non-recurring, this operation will turn it into a recurring task.

invalid {@link Tasks} row {@code - * _ID}An exception is thrown.
- *

- *

Update

- *

- * Note, the data of an update must not contain any fields related to recurrence ({@link #RRULE}, {@link #RDATE}, {@link #EXDATE}, {@link - * #ORIGINAL_INSTANCE_ID}, {@link #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY}). Also note, this table supports writing {@link #DURATION} - * (if the instance has a {@link #DTSTART}), but reading it back will always return a {@code null} {@link #DURATION} and a non-{@code null} {@link #DUE} - * date. Reading the task in the tasks table will, however, return the original {@link #DURATION}. - *

- * - *
Target task typeResult
Recurring master taskA new override is created with the given data.

Note, - * any fields which are not provided are inherited from the master, except for {@link #DTSTART} and {@link #DUE} which will be inherited from the instance - * and {@link #DURATION}, {@link #RRULE}, {@link #RDATE} and {@link #EXDATE} which are set to {@code null}. {@link #ORIGINAL_INSTANCE_ID}, {@link - * #ORIGINAL_INSTANCE_TIME} and {@link #ORIGINAL_INSTANCE_ALLDAY} will be set accordingly.

Single instance taskThe task is - * updated with the given values.
Recurrence override with existing masterThe task is updated with the given values.
Recurrence override without existing masterThe task is updated with the given values.
- *

- *

Delete

- *

- * - * - *
Target task typeResult
Recurring master taskAn {@link #EXDATE} for this instance is added, any {@link - * #RDATE} for this instance is removed. The instance row is removed.

TODO: mark the task deleted if the remaining recurrence set is empty

Single instance taskThe {@link Tasks#_DELETED} flag of the task is set.
Recurrence override with existing - * masterThe {@link Tasks#_DELETED} flag of the override is set, an {@link #EXDATE} for this instance is added to the master, any {@link #RDATE} - * for this instance is removed from the master. TODO: mark the master deleted if the remaining recurrence set of the master is empty
Recurrence override without existing masterThe {@link Tasks#_DELETED} flag of the task is set.
- * - * @author Yannic Ahrens - * @author Marten Gajda - */ - public static final class Instances implements TaskColumns, InstanceColumns - { - - /** - * The name of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_NAME = TaskLists.ACCOUNT_NAME; - - /** - * The type of the account the task belongs to. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String ACCOUNT_TYPE = TaskLists.ACCOUNT_TYPE; - - /** - * The name of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_NAME = TaskLists.LIST_NAME; - /** - * The color of the list this task belongs to as integer (0xaarrggbb). This is auto-derived from the list the task belongs to. Do not write this value - * here. To change the color of an individual task use {@code TASK_COLOR} instead. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_COLOR = TaskLists.LIST_COLOR; - - /** - * The owner of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: String - *

- *

- * read-only - *

- */ - public static final String LIST_OWNER = TaskLists.OWNER; - - /** - * The access level of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String LIST_ACCESS_LEVEL = TaskLists.ACCESS_LEVEL; - - /** - * The visibility of the list this task belongs. This is auto-derived from the list the task belongs to. Do not write this value here. - *

- * Value: Integer - *

- *

- * read-only - *

- */ - public static final String VISIBLE = "visible"; - - /** - * Flag indicating that ths is an instance of a recurring task. - *

- * Value: Integer - *

- * read-only - */ - public static final String IS_RECURRING = "is_recurring"; - - public static final String CONTENT_URI_PATH = "instances"; - - public static final String DEFAULT_SORT_ORDER = INSTANCE_DUE_SORTING; - - - /** - * Get the instances content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - /** - * Available values in Categories. - *

- * Categories are per account. It's up to the front-end to ensure consistency of category colors across accounts. - * - * @author Marten Gajda - */ - public interface CategoriesColumns - { - - String _ID = "_id"; - - String ACCOUNT_NAME = "account_name"; - - String ACCOUNT_TYPE = "account_type"; - - String NAME = "name"; - - String COLOR = "color"; - } - - - public static final class Categories implements CategoriesColumns - { - - public static final String CONTENT_URI_PATH = "categories"; - - public static final String DEFAULT_SORT_ORDER = NAME; - - - /** - * Get the categories content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - public interface AlarmsColumns - { - String ALARM_ID = "alarm_id"; - - String LAST_TRIGGER = "last_trigger"; - - String NEXT_TRIGGER = "next_trigger"; - } - - - public static final class Alarms implements AlarmsColumns - { - - public static final String CONTENT_URI_PATH = "alarms"; - - - /** - * Get the alarms content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - public interface PropertySyncColumns - { - String SYNC1 = "prop_sync1"; - - String SYNC2 = "prop_sync2"; - - String SYNC3 = "prop_sync3"; - - String SYNC4 = "prop_sync4"; - - String SYNC5 = "prop_sync5"; - - String SYNC6 = "prop_sync6"; - - String SYNC7 = "prop_sync7"; - - String SYNC8 = "prop_sync8"; - } - - - public interface PropertyColumns - { - - String PROPERTY_ID = "property_id"; - - String TASK_ID = "task_id"; - - String MIMETYPE = "mimetype"; - - String VERSION = "prop_version"; - - String DATA0 = "data0"; - - String DATA1 = "data1"; - - String DATA2 = "data2"; - - String DATA3 = "data3"; - - String DATA4 = "data4"; - - String DATA5 = "data5"; - - String DATA6 = "data6"; - - String DATA7 = "data7"; - - String DATA8 = "data8"; - - String DATA9 = "data9"; - - String DATA10 = "data10"; - - String DATA11 = "data11"; - - String DATA12 = "data12"; - - String DATA13 = "data13"; - - String DATA14 = "data14"; - - String DATA15 = "data15"; - } - - - public static final class Properties implements PropertySyncColumns, PropertyColumns - { - - public static final String CONTENT_URI_PATH = "properties"; - - public static final String DEFAULT_SORT_ORDER = DATA0; - - - /** - * Get the properties content {@link Uri} using the given authority. - * - * @param authority - * The authority. - * - * @return A {@link Uri}. - */ - public static Uri getContentUri(String authority) - { - return getUriFactory(authority).getUri(CONTENT_URI_PATH); - } - - } - - - public interface Property - { - /** - * Attached documents. - *

- * Note: Attachments are write-once. To change an attachment you'll have to remove and re-add it. - *

- * - * @author Marten Gajda - */ - interface Attachment extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attachment"; - - /** - * URL of the attachment. This is the link that points to the attached resource. - *

- * Value: String - *

- */ - String URL = DATA1; - - /** - * The display name of the attachment, if any. - *

- * Value: String - *

- */ - String DISPLAY_NAME = DATA2; - - /** - * Content-type of the attachment. - *

- * Value: String - *

- */ - String FORMAT = DATA3; - - /** - * File size of the attachment or -1 if unknown. - *

- * Value: Long - *

- */ - String SIZE = DATA4; - - /** - * A content {@link Uri} that can be used to retrieve the attachment. Sync adapters can set this field if they know how to download the attachment - * without going through the browser. - *

- * Value: String - *

- */ - String CONTENT_URI = DATA5; - - } - - - interface Attendee extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/attendee"; - - /** - * Name of the contact, if known. - *

- * Value: String - *

- */ - String NAME = DATA0; - - /** - * Email address of the contact. - *

- * Value: String - *

- */ - String EMAIL = DATA1; - - String ROLE = DATA2; - - String STATUS = DATA3; - - String RSVP = DATA4; - } - - - /** - * Categories are immutable. For creation is either the category id or name necessary - */ - interface Category extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/category"; - - /** - * Row id of the category. - *

- * Value: Long - *

- */ - String CATEGORY_ID = DATA0; - - /** - * The name of the category - *

- * Value: String - *

- */ - String CATEGORY_NAME = DATA1; - - /** - * The decimal coded color of the category - *

- * Value: Integer - *

- *

- * read-only - *

- */ - String CATEGORY_COLOR = DATA2; - } - - - interface Comment extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/comment"; - - /** - * Comment text. - *

- * Value: String - *

- */ - String COMMENT = DATA0; - - /** - * Language code of the comment as defined in RFC5646 or null. - *

- * Value: String - *

- */ - String LANGUAGE = DATA1; - } - - - interface Contact extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/contact"; - - String NAME = DATA0; - - String LANGUAGE = DATA1; - } - - - /** - * Relations of a task. - *

- * When writing a relation, exactly one of {@link #RELATED_ID} or {@link #RELATED_UID} must be present. The missing value and {@link - * #RELATED_CONTENT_URI} will be populated automatically if possible. - *

- * {@link Tasks#PARENT_ID} is updated automatically if possible. - */ - interface Relation extends PropertyColumns - { - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/relation"; - - /** - * The row id of the related task. May be -1 if the property doesn't refer to a task in this database or if it doesn't refer to a task - * at all. - *

- * Value: long - *

- */ - String RELATED_ID = DATA1; - - /** - * The relation type. This must be one of the {@code RELTYPE_*} values. - *

- * Value: int - *

- */ - String RELATED_TYPE = DATA2; - - /** - * The UID of the related object. - *

- * Value: String - *

- */ - String RELATED_UID = DATA3; - - /** - * The content Uri of a related object in another Android content provider, if found. - *

- * Value: String (URI) - *

- *

- * This field is read-only. - *

- */ - String RELATED_CONTENT_URI = DATA5; - - /** - * The related object is the parent of the object owning this relation. - */ - int RELTYPE_PARENT = 0; - - /** - * The related object is the child of the object owning this relation. - */ - int RELTYPE_CHILD = 1; - - /** - * The related object is a sibling of the object owning this relation. - */ - int RELTYPE_SIBLING = 2; - - } - - - interface Alarm extends PropertyColumns - { - - int ALARM_TYPE_NOTHING = 0; - - int ALARM_TYPE_MESSAGE = 1; - - int ALARM_TYPE_EMAIL = 2; - - int ALARM_TYPE_SMS = 3; - - int ALARM_TYPE_SOUND = 4; - - int ALARM_REFERENCE_DUE_DATE = 1; - - int ALARM_REFERENCE_START_DATE = 2; - - /** - * The mime-type of this property. - */ - String CONTENT_ITEM_TYPE = ContentResolver.CURSOR_ITEM_BASE_TYPE + "/alarm"; - - /** - * Number of minutes from the reference date when the alarm goes off. If the value is < 0 the alarm will go off after the reference date. - *

- * Value: Integer - *

- */ - String MINUTES_BEFORE = DATA0; - - /** - * The reference date for the alarm. Either {@link #ALARM_REFERENCE_DUE_DATE} or {@link #ALARM_REFERENCE_START_DATE}. - *

- * Value: Integer - *

- */ - String REFERENCE = DATA1; - - /** - * A message that appears with the alarm. - *

- * Value: String - *

- */ - String MESSAGE = DATA2; - - /** - * The type of the alarm. Use the provided alarm types {@link #ALARM_TYPE_MESSAGE}, {@link #ALARM_TYPE_SOUND}, {@link #ALARM_TYPE_NOTHING}, - * {@link #ALARM_TYPE_EMAIL} and {@link #ALARM_TYPE_SMS}. - *

- * Value: Integer - *

- */ - String ALARM_TYPE = DATA3; - } - - } - - - private static synchronized UriFactory getUriFactory(String authority) - { - UriFactory uriFactory = sUriFactories.get(authority); - if (uriFactory == null) - { - uriFactory = new UriFactory(authority); - uriFactory.addUri(SyncState.CONTENT_URI_PATH); - uriFactory.addUri(TaskLists.CONTENT_URI_PATH); - uriFactory.addUri(Tasks.CONTENT_URI_PATH); - uriFactory.addUri(Tasks.SEARCH_URI_PATH); - uriFactory.addUri(Instances.CONTENT_URI_PATH); - uriFactory.addUri(Categories.CONTENT_URI_PATH); - uriFactory.addUri(Alarms.CONTENT_URI_PATH); - uriFactory.addUri(Properties.CONTENT_URI_PATH); - sUriFactories.put(authority, uriFactory); - - } - return uriFactory; - } - -} diff --git a/provider/src/main/java/org/dmfs/tasks/contract/UriFactory.java b/provider/src/main/java/org/dmfs/tasks/contract/UriFactory.java deleted file mode 100644 index 0092aca..0000000 --- a/provider/src/main/java/org/dmfs/tasks/contract/UriFactory.java +++ /dev/null @@ -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.tasks.contract; - -import android.net.Uri; - -import java.util.HashMap; -import java.util.Map; - - -/** - * TODO - */ -public final class UriFactory -{ - private final String mAuthority; - private final Map mUriMap = new HashMap(16); - - - UriFactory(String authority) - { - mAuthority = authority; - mUriMap.put(null, Uri.parse("content://" + authority)); - } - - - void addUri(String path) - { - mUriMap.put(path, Uri.parse("content://" + mAuthority + "/" + path)); - } - - - Uri getUri() - { - return mUriMap.get(null); - } - - - Uri getUri(String path) - { - return mUriMap.get(path); - } -} diff --git a/provider/src/main/res/drawable/ic_24_agendula_tasks.xml b/provider/src/main/res/drawable/ic_24_agendula_tasks.xml deleted file mode 100644 index a5420b4..0000000 --- a/provider/src/main/res/drawable/ic_24_agendula_tasks.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/provider/src/main/res/values-cs/strings.xml b/provider/src/main/res/values-cs/strings.xml deleted file mode 100644 index 9b043e6..0000000 --- a/provider/src/main/res/values-cs/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - přečíst úkoly - Číst úkoly a seznamy úkolů - zapsat úkoly - Vytvořit, změnit a smazat úkoly a seznamy úkolů - Vytvořit, změnit a smazat úkoly a seznamy úkolů - - diff --git a/provider/src/main/res/values-de/strings.xml b/provider/src/main/res/values-de/strings.xml deleted file mode 100644 index 2f16cc7..0000000 --- a/provider/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Aufgaben lesen - Aufgaben lesen - Aufgaben schreiben - Aufgaben und Aufgabenlisten erstellen, bearbeiten und löschen - Aufgaben lesen und verwalten - - diff --git a/provider/src/main/res/values-es/strings.xml b/provider/src/main/res/values-es/strings.xml deleted file mode 100644 index c2808fb..0000000 --- a/provider/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - leer tareas - Leer tareas y listas de tareas - escribir tareas - Crear, modificar y borrar tareas y listas de tareas - Crear, modificar y borrar tareas y listas de tareas - - diff --git a/provider/src/main/res/values-fr/strings.xml b/provider/src/main/res/values-fr/strings.xml deleted file mode 100644 index 165e07d..0000000 --- a/provider/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Lire tâches - Autoriser une application à lire les tâches de la listes de tâches - Écrire tâches - Autoriser une application à écrire des tâches dans la listes de tâches - Autoriser une application à écrire des tâches dans la listes de tâches - - diff --git a/provider/src/main/res/values-hu/strings.xml b/provider/src/main/res/values-hu/strings.xml deleted file mode 100644 index 5f73108..0000000 --- a/provider/src/main/res/values-hu/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - feladatok olvasása - Feladatok és feladatlisták olvasása - feladatok írása - Feladatok és feladatlisták létrehozása, módosítása és törlése - Feladatok és feladatlisták létrehozása, módosítása és törlése - - diff --git a/provider/src/main/res/values-it/strings.xml b/provider/src/main/res/values-it/strings.xml deleted file mode 100644 index c321b6f..0000000 --- a/provider/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - lettura attività - lettura attività ed elenchi di attività - scrittura attività - creazione, modifica e eliminazione di attività ed elenchi di attività - accesso e modifica delle attività - - diff --git a/provider/src/main/res/values-ja/strings.xml b/provider/src/main/res/values-ja/strings.xml deleted file mode 100644 index 5a6f8b7..0000000 --- a/provider/src/main/res/values-ja/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - タスクを読む - タスクとタスクリストを読み込みます。 - タスクを書く - タスクとタスクリストを作成、変更、削除します。 - タスクとタスクリストを作成、変更、削除します。 - - diff --git a/provider/src/main/res/values-nl/strings.xml b/provider/src/main/res/values-nl/strings.xml deleted file mode 100644 index cd41d16..0000000 --- a/provider/src/main/res/values-nl/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - leestaken - Staat een app toe om taken in je takenlijst te lezen - schrijftaken - Staat een app toe om taken in je takenlijst aan te maken - Staat een app tpe om taken in je takenlijst aan te maken - - diff --git a/provider/src/main/res/values-pl/strings.xml b/provider/src/main/res/values-pl/strings.xml deleted file mode 100644 index 41f57c1..0000000 --- a/provider/src/main/res/values-pl/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - Czytaj zadania - Pozwala aplikacji na czytanie zadań z Twojej listy zadań - Zapisuj zadania - Pozwala aplikacji na zapisywanie zadań na Twojej liście zadań - Pozwala aplikacji na zapisywanie zadań na Twojej liście zadań - - diff --git a/provider/src/main/res/values-pt-rBR/strings.xml b/provider/src/main/res/values-pt-rBR/strings.xml deleted file mode 100644 index c93660c..0000000 --- a/provider/src/main/res/values-pt-rBR/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - ler tarefas - Permite que um aplicativo leia as tarefas na sua lista de tarefas - escrever tarefas - Permite que um aplicativo escreva tarefas na sua lista de tarefas - Permite que um aplicativo escreva tarefas na sua lista de tarefas - - \ No newline at end of file diff --git a/provider/src/main/res/values-pt-rPT/strings.xml b/provider/src/main/res/values-pt-rPT/strings.xml deleted file mode 100644 index 73ea9e8..0000000 --- a/provider/src/main/res/values-pt-rPT/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - ler tarefas - Ler tarefas e listas de tarefas - escrever tarefas - Criar, modificar e eliminar tarefas e listas de tarefas - Criar, modificar e eliminar tarefas e listas de tarefas - - diff --git a/provider/src/main/res/values-ru/strings.xml b/provider/src/main/res/values-ru/strings.xml deleted file mode 100644 index 4b8d77f..0000000 --- a/provider/src/main/res/values-ru/strings.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - чтение задач - Разрешить приложению читать задачи из Ваших списков задач - запись задач - Разрешить приложению сохранять задачи в Ваших списках задач - Разрешить приложению сохранять задачи в Ваших списках задач - - diff --git a/provider/src/main/res/values-sr/strings.xml b/provider/src/main/res/values-sr/strings.xml deleted file mode 100644 index 3562f79..0000000 --- a/provider/src/main/res/values-sr/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - читање задатака - Дозвољава апликацији да чита задатке са ваше листе задатака - упис задатака - Дозвољава апликацији да уноси задатке у вашу листу задатака - Дозвољава апликацији да уноси задатке у вашу листу задатака - - diff --git a/provider/src/main/res/values-uk/strings.xml b/provider/src/main/res/values-uk/strings.xml deleted file mode 100644 index 1cdb330..0000000 --- a/provider/src/main/res/values-uk/strings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - читання завдань - Дозволити додатку читати ваші завдання з ваших переліків завдань - запис завдань - Дозволити додатку зберігати завдання до ваших переліків завдань - Дозволити додатку зберігати завдання до ваших переліків завдань - - diff --git a/provider/src/main/res/values/agendula_defaults.xml b/provider/src/main/res/values/agendula_defaults.xml deleted file mode 100644 index 816a10b..0000000 --- a/provider/src/main/res/values/agendula_defaults.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - de.jeanlucmakiola.agendula.tasks - - diff --git a/provider/src/main/res/values/agendula_provider_changed_receivers.xml b/provider/src/main/res/values/agendula_provider_changed_receivers.xml deleted file mode 100644 index a4ac2ab..0000000 --- a/provider/src/main/res/values/agendula_provider_changed_receivers.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/provider/src/main/res/values/strings.xml b/provider/src/main/res/values/strings.xml deleted file mode 100644 index a65c33b..0000000 --- a/provider/src/main/res/values/strings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - Agendula tasks - - - - read tasks - read tasks and task lists - write tasks - create, modify and delete tasks and task lists - access and manage tasks - - diff --git a/provider/src/test/java/org/dmfs/provider/tasks/ProviderAccountCleanupTest.java b/provider/src/test/java/org/dmfs/provider/tasks/ProviderAccountCleanupTest.java deleted file mode 100644 index 04c332b..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/ProviderAccountCleanupTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright 2026 Jean-Luc Makiola - * - * 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.content.ContentResolver; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; - -import org.dmfs.tasks.contract.TaskContract; -import org.dmfs.tasks.contract.TaskContract.TaskLists; -import org.dmfs.tasks.contract.TaskContract.Tasks; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.Robolectric; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.android.controller.ContentProviderController; -import org.robolectric.RuntimeEnvironment; - -import de.jeanlucmakiola.agendula.provider.R; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeFalse; - - -/** - * Agendula's own test, not upstream's. - *

- * Local-only mode is the default Agendula ships, and it rests on a claim worth pinning down: that the provider works with no account on the device at - * all. That is open question 3 in {@code docs/STORAGE-AND-SYNC.md}, and it is not obvious — we dropped {@code GET_ACCOUNTS} from a provider whose list - * cleanup was written assuming it, so the failure this guards against is the provider quietly deleting the user's task lists. See change 1 in - * {@code provider/PROVENANCE.md}. - *

- * Robolectric, so this is not a substitute for running it on a device — but it does hold the invariant against future edits to the cleanup path. - * - * @author Jean-Luc Makiola - */ -@RunWith(RobolectricTestRunner.class) -public class ProviderAccountCleanupTest -{ - private ContentProviderController mController; - private ContentResolver mResolver; - private String mAuthority; - - - @Before - public void setUp() - { - // Robolectric ships no aarch64 SQLite in either backend: the native runtime refuses outright and the LEGACY sqlite4java shadow throws - // "Architecture 'aarch64' is not supported". Every other test in this module is architecture-independent; this is the only one that opens a - // database. Skipping beats failing on an ARM64 dev machine — but note that means these assertions are only actually exercised on x86_64, which - // CI is. If this ever starts skipping in CI, the invariant has stopped being checked at all. - assumeFalse( - "Robolectric has no SQLite backend for aarch64 — this class only runs on x86_64", - System.getProperty("os.arch", "").contains("aarch64")); - - Context context = RuntimeEnvironment.getApplication(); - Utils.clearOwnAccountTypesCache(); - mAuthority = context.getString(R.string.agendula_tasks_authority); - mController = Robolectric.buildContentProvider(TaskProvider.class).create(mAuthority); - mResolver = context.getContentResolver(); - } - - - @After - public void tearDown() - { - Utils.clearOwnAccountTypesCache(); - // Null when setUp bailed out on the architecture assumption above. - if (mController != null) - { - mController.shutdown(); - } - } - - - /** - * The authority is ours, not dmfs's. Guards against a resource merge or a careless resync quietly handing our provider back to {@code org.dmfs.tasks}, - * which would make Agendula and OpenTasks mutually uninstallable. - */ - @Test - public void authorityIsOurs() - { - assertEquals("de.jeanlucmakiola.agendula.tasks", mAuthority); - } - - - /** - * The whole of Local mode: create a local list and a task in it with no account anywhere on the device, and read both back. - */ - @Test - public void localListAndTaskSurviveWithNoAccount() - { - Uri listUri = insertLocalList("Groceries"); - assertNotNull(listUri); - - long listId = Long.parseLong(listUri.getLastPathSegment()); - ContentValues task = new ContentValues(); - task.put(Tasks.LIST_ID, listId); - task.put(Tasks.TITLE, "Oat milk"); - Uri taskUri = mResolver.insert(Tasks.getContentUri(mAuthority), task); - assertNotNull(taskUri); - - assertEquals(1, countRows(TaskLists.getContentUri(mAuthority))); - assertEquals(1, countRows(Tasks.getContentUri(mAuthority))); - } - - - /** - * The bug this exists for. An account-update sweep reporting zero accounts — exactly what we see without {@code GET_ACCOUNTS} — must not take a synced - * list with it. Upstream would delete this list. - */ - @Test - public void cleanUpDoesNotPruneListsOfAccountTypesWeDoNotAuthenticate() - { - insertSyncedList("Shared shopping", "someone@example.org", "bitfire.at.davdroid"); - assertEquals(1, countRows(TaskLists.getContentUri(mAuthority))); - - // No authenticator in this package, so nothing is prunable and no account is visible. - Utils.cleanUpLists( - RuntimeEnvironment.getApplication(), - mController.get().getDatabaseHelper(RuntimeEnvironment.getApplication()).getWritableDatabase(), - new Account[0], - mAuthority); - - assertEquals( - "a list whose account type we cannot enumerate must never be pruned", - 1, - countRows(TaskLists.getContentUri(mAuthority))); - } - - - /** - * Local lists are exempt from cleanup regardless — upstream's rule, restated here because Local mode depends on it and it is easy to lose in a resync. - */ - @Test - public void cleanUpNeverPrunesLocalLists() - { - insertLocalList("Groceries"); - - Utils.cleanUpLists( - RuntimeEnvironment.getApplication(), - mController.get().getDatabaseHelper(RuntimeEnvironment.getApplication()).getWritableDatabase(), - new Account[0], - mAuthority); - - assertEquals(1, countRows(TaskLists.getContentUri(mAuthority))); - } - - - /** - * With no authenticator of our own, the prunable set is empty — which is what makes the two tests above hold by construction rather than by luck. - */ - @Test - public void weAuthenticateNoAccountTypesYet() - { - assertTrue(Utils.ownAccountTypes(RuntimeEnvironment.getApplication()).isEmpty()); - } - - - private Uri insertLocalList(String name) - { - return insertSyncedList(name, TaskContract.LOCAL_ACCOUNT_NAME, TaskContract.LOCAL_ACCOUNT_TYPE); - } - - - private Uri insertSyncedList(String name, String accountName, String accountType) - { - ContentValues values = new ContentValues(); - values.put(TaskLists.LIST_NAME, name); - values.put(TaskLists.VISIBLE, 1); - values.put(TaskLists.SYNC_ENABLED, 1); - return mResolver.insert(asSyncAdapter(TaskLists.getContentUri(mAuthority), accountName, accountType), values); - } - - - private static Uri asSyncAdapter(Uri uri, String accountName, String accountType) - { - return uri.buildUpon() - .appendQueryParameter(TaskContract.CALLER_IS_SYNCADAPTER, "true") - .appendQueryParameter(TaskContract.ACCOUNT_NAME, accountName) - .appendQueryParameter(TaskContract.ACCOUNT_TYPE, accountType) - .build(); - } - - - private int countRows(Uri uri) - { - try (Cursor cursor = mResolver.query(uri, null, null, null, null)) - { - assertNotNull(cursor); - return cursor.getCount(); - } - } -} diff --git a/provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java b/provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java deleted file mode 100644 index 29ff809..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/model/adapters/DateTimeIterableFieldAdapterTest.java +++ /dev/null @@ -1,223 +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.model.adapters; - -import android.content.ContentValues; - -import org.dmfs.iterables.EmptyIterable; -import org.dmfs.iterables.elementary.Seq; -import org.dmfs.provider.tasks.model.TaskAdapter; -import org.dmfs.rfc5545.DateTime; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DateTimeIterableFieldAdapterTest -{ - @Test - public void testFieldName() - { - assertThat(new DateTimeIterableFieldAdapter<>("x", "y").fieldName(), is("x")); - } - - - @Test - public void testGetFromCVAllDay1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109"))); - } - - - @Test - public void testGetFromCVAllDay2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109,20180110"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109"), DateTime.parse("20180110"))); - } - - - @Test - public void testGetFromCVFloating1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000"); - values.putNull("y"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109T140000"))); - } - - - @Test - public void testGetFromCVFloating2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000,20180110T140000"); - values.putNull("y"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("20180109T140000"), DateTime.parse("20180110T140000"))); - } - - - @Test - public void testGetFromCVAbsolute1() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000Z"); - values.put("y", "Europe/Berlin"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("Europe/Berlin", "20180109T150000"))); - } - - - @Test - public void testGetFromCVAbsolute2() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - values.put("x", "20180109T140000Z,20180110T140000Z"); - values.put("y", "Europe/Berlin"); - assertThat(adapter.getFrom(values), iteratesTo(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"))); - } - - - @Test - public void testSetInNull() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, null); - assertThat(values.getAsString("x"), nullValue()); - } - - - @Test - public void testSetInEmpty() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, EmptyIterable.instance()); - assertThat(values.getAsString("x"), nullValue()); - } - - - @Test - public void testSetInSingleAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"))); - assertThat(values.getAsString("x"), is("20180109")); - } - - - @Test - public void testSetInSingleFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000")); - } - - - @Test - public void testSetInSingleAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z")); - } - - - @Test - public void testSetInDoubleAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"), DateTime.parse("20180110"))); - assertThat(values.getAsString("x"), is("20180109,20180110")); - } - - - @Test - public void testSetInDoubleFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"), DateTime.parse("20180110T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000")); - } - - - @Test - public void testSetInDoubleAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z")); - } - - - @Test - public void testSetInMultiAllDay() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109"), DateTime.parse("20180110"), DateTime.parse("20180111"))); - assertThat(values.getAsString("x"), is("20180109,20180110,20180111")); - } - - - @Test - public void testSetInMultiFloating() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("20180109T150000"), DateTime.parse("20180110T150000"), DateTime.parse("20180111T150000"))); - assertThat(values.getAsString("x"), is("20180109T150000,20180110T150000,20180111T150000")); - } - - - @Test - public void testSetInMultiAbsolute() - { - ContentValues values = new ContentValues(); - FieldAdapter, ?> adapter = new DateTimeIterableFieldAdapter("x", "y"); - adapter.setIn(values, new Seq<>(DateTime.parse("Europe/Berlin", "20180109T150000"), DateTime.parse("Europe/Berlin", "20180110T150000"), - DateTime.parse("Europe/Berlin", "20180111T150000"))); - assertThat(values.getAsString("x"), is("20180109T140000Z,20180110T140000Z,20180111T140000Z")); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java deleted file mode 100644 index 4d60f5b..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DatedTest.java +++ /dev/null @@ -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.elementary.Present; -import org.dmfs.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.rfc5545.DateTime; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DatedTest -{ - - @Test - public void testAbsent() - { - ContentValues instanceData = new Dated(absent(), "ts", "sorting", ContentValues::new).value(); - // this shouldn't really add any values and go by the "defaults" - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testPresent() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new Dated(new Present<>(start), "ts", "sorting", ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong("ts", start.getTimestamp())); - assertThat(instanceData, new ContentValuesWithLong("sorting", start.getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java deleted file mode 100644 index c8be962..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DistantTest.java +++ /dev/null @@ -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.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DistantTest -{ - - @Test - public void test() - { - ContentValues instanceData = new Distant(100, ContentValues::new).value(); - assertThat(instanceData.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(100)); - assertThat(instanceData.size(), is(1)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java deleted file mode 100644 index 25840e9..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/DueDatedTest.java +++ /dev/null @@ -1,84 +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.elementary.Present; -import org.dmfs.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.rfc5545.DateTime; -import org.dmfs.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.TimeZone; - -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class DueDatedTest -{ - - @Test - public void testNone() - { - ContentValues instanceData = new DueDated(absent(), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, nullValue(Long.class))); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, nullValue(Long.class))); - // this doesn't actually add anything, the ContentValues are expected to contain null values. - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testStartEurope() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testStartAmerica() - { - DateTime start = DateTime.parse("America/New_York", "20171208T125500"); - - ContentValues instanceData = new DueDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DUE_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java deleted file mode 100644 index 4fbd8f5..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/EnduringTest.java +++ /dev/null @@ -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.processors.tasks.instancedata; - -import android.content.ContentValues; - -import org.dmfs.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class EnduringTest -{ - @Test - public void testNoValue() - { - assertThat(new Enduring(ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(ContentValues::new).value().size(), is(1)); - } - - - @Test - public void testStartValue() - { - ContentValues values = new ContentValues(1); - values.put(TaskContract.Instances.INSTANCE_START, 10); - assertThat(new Enduring(() -> new ContentValues(values)), - hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); - } - - - @Test - public void testDueValue() - { - ContentValues values = new ContentValues(1); - values.put(TaskContract.Instances.INSTANCE_DUE, 10); - assertThat(new Enduring(() -> new ContentValues(values)), - hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, nullValue(Long.class)))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(2)); - } - - - @Test - public void testStartDueValue() - { - ContentValues values = new ContentValues(2); - values.put(TaskContract.Instances.INSTANCE_START, 1); - values.put(TaskContract.Instances.INSTANCE_DUE, 10); - assertThat(new Enduring(() -> new ContentValues(values)), hasValue(new ContentValuesWithLong(TaskContract.Instances.INSTANCE_DURATION, 9))); - assertThat(new Enduring(() -> new ContentValues(values)).value().size(), is(3)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java deleted file mode 100644 index e56d8a4..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/OverriddenTest.java +++ /dev/null @@ -1,110 +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.elementary.Present; -import org.dmfs.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.rfc5545.DateTime; -import org.dmfs.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.optional.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class OverriddenTest -{ - @Test - public void testAbsent() - { - ContentValues instanceData = new Overridden(absent(), ContentValues::new).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testAbsentWithStart() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testAbsentWithDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testAbsentWithStartAndDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(absent(), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, nullValue(Long.class))); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testPresent() - { - - ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), ContentValues::new).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); - assertThat(instanceData.size(), is(1)); - } - - - @Test - public void testPresentWithStartAndDue() - { - ContentValues values = new ContentValues(); - values.put(TaskContract.Instances.INSTANCE_START, 10); - values.put(TaskContract.Instances.INSTANCE_DUE, 20); - - ContentValues instanceData = new Overridden(new Present<>(new DateTime(40)), () -> new ContentValues(values)).value(); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_ORIGINAL_TIME, 40)); - assertThat(instanceData.size(), is(3)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java deleted file mode 100644 index 85676d1..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/StartDatedTest.java +++ /dev/null @@ -1,84 +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.elementary.Present; -import org.dmfs.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.rfc5545.DateTime; -import org.dmfs.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import java.util.TimeZone; - -import static org.dmfs.optional.Absent.absent; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class StartDatedTest -{ - - @Test - public void testNone() - { - ContentValues instanceData = new StartDated(absent(), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, nullValue(Long.class))); - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, nullValue(Long.class))); - // this doesn't actually add anything, the ContentValues are expected to contain null values. - assertThat(instanceData.size(), is(0)); - } - - - @Test - public void testStartEurope() - { - DateTime start = DateTime.parse("Europe/Berlin", "20171208T125500"); - - ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } - - - @Test - public void testStartAmerica() - { - DateTime start = DateTime.parse("America/New_York", "20171208T125500"); - - ContentValues instanceData = new StartDated(new Present<>(start), ContentValues::new).value(); - - assertThat(instanceData, new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START, start.getTimestamp())); - assertThat(instanceData, - new ContentValuesWithLong(TaskContract.Instances.INSTANCE_START_SORTING, start.shiftTimeZone(TimeZone.getDefault()).getInstance())); - assertThat(instanceData.size(), is(2)); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java deleted file mode 100644 index 760ca34..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/TaskRelatedTest.java +++ /dev/null @@ -1,44 +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.provider.tasks.utils.ContentValuesWithLong; -import org.dmfs.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class TaskRelatedTest -{ - @Test - public void testValue() - { - assertThat(new TaskRelated(123, ContentValues::new), hasValue(new ContentValuesWithLong(TaskContract.Instances.TASK_ID, 123))); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java b/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java deleted file mode 100644 index f78f8bf..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/processors/tasks/instancedata/VanillaInstanceDataTest.java +++ /dev/null @@ -1,53 +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.tasks.contract.TaskContract; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class VanillaInstanceDataTest -{ - @Test - public void testValue() - { - ContentValues values = new VanillaInstanceData().value(); - assertThat(values.get(TaskContract.Instances.INSTANCE_START), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_START_SORTING), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DUE), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DUE_SORTING), nullValue()); - assertThat(values.get(TaskContract.Instances.INSTANCE_DURATION), nullValue()); - assertThat(values.get(TaskContract.Instances.DISTANCE_FROM_CURRENT), is(0)); - assertThat(values.get(TaskContract.Instances.INSTANCE_ORIGINAL_TIME), nullValue()); - assertThat(values.size(), is(7)); - } - -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java b/provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java deleted file mode 100644 index 750f477..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/utils/ContainsValuesTest.java +++ /dev/null @@ -1,94 +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.MatrixCursor; - -import org.dmfs.iterables.elementary.Seq; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.hamcrest.matchers.predicate.PredicateMatcher.satisfiedBy; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class ContainsValuesTest -{ - @Test - public void test() - { - ContentValues values = new ContentValues(); - values.put("a", 123); - values.put("b", "stringValue"); - values.put("c", new byte[] { 3, 2, 1 }); - values.putNull("d"); - - MatrixCursor cursor = new MatrixCursor(new String[] { "c", "b", "a", "d", "f" }); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", "123", null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2 }, "stringValue", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValueX", 123, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 1234, null, "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue", 123, "123", "xyz")); - cursor.addRow(new Seq<>(321, "stringValueX", "1234", "123", "xyz")); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1, 0 }, "stringValueX", 1234, "123", "xyz")); - - cursor.moveToFirst(); - assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(satisfiedBy(cursor))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - cursor.moveToNext(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - } - - - @Test - public void testMissingColumns() - { - ContentValues values = new ContentValues(); - values.put("a", 123); - values.put("b", "stringValue"); - values.put("c", new byte[] { 3, 2, 1 }); - values.putNull("d"); - - MatrixCursor cursor = new MatrixCursor(new String[] { "c", "b" }); - cursor.addRow(new Seq<>(new byte[] { 3, 2, 1 }, "stringValue")); - - cursor.moveToFirst(); - assertThat(new ContainsValues(values), is(not(satisfiedBy(cursor)))); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java b/provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java deleted file mode 100644 index 6dafc00..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/utils/ContentValuesWithLong.java +++ /dev/null @@ -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.utils; - -import android.content.ContentValues; - -import org.hamcrest.FeatureMatcher; -import org.hamcrest.Matcher; - -import static org.hamcrest.Matchers.is; - - -/** - * A {@link Matcher} to test if {@link ContentValues} contain a specific Long value. - *

- * TODO: can we convert that into a more generic {@link ContentValues} matcher? It might be useful in other places. - *

- * TODO: also consider moving this to "Test-Bolts" - */ -public final class ContentValuesWithLong extends FeatureMatcher -{ - private final String mKey; - - - public ContentValuesWithLong(String valueKey, long value) - { - this(valueKey, is(value)); - } - - - public ContentValuesWithLong(String valueKey, Matcher matcher) - { - super(matcher, "Long value " + valueKey, "Long value " + valueKey); - mKey = valueKey; - } - - - @Override - protected Long featureValueOf(ContentValues actual) - { - return actual.getAsLong(mKey); - } -} diff --git a/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java b/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java deleted file mode 100644 index 81eff9c..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIterableTest.java +++ /dev/null @@ -1,188 +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.iterables.elementary.Seq; -import org.dmfs.provider.tasks.model.ContentValuesTaskAdapter; -import org.dmfs.provider.tasks.model.TaskAdapter; -import org.dmfs.rfc5545.DateTime; -import org.dmfs.rfc5545.recur.RecurrenceRule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - -import static org.dmfs.jems.hamcrest.matchers.IterableMatcher.iteratesTo; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class TaskInstanceIterableTest -{ - @Test - public void testAbsolute() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } - - - @Test - public void testAllDay() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("20170606"), - DateTime.parse("20170608"), - DateTime.parse("20170610"), - DateTime.parse("20170612"), - DateTime.parse("20170614"), - DateTime.parse("20170616"), - DateTime.parse("20170618"), - DateTime.parse("20170620"), - DateTime.parse("20170622"), - DateTime.parse("20170624") - )); - } - - - @Test - public void testFloating() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("20170606T121314"), - DateTime.parse("20170608T121314"), - DateTime.parse("20170610T121314"), - DateTime.parse("20170612T121314"), - DateTime.parse("20170614T121314"), - DateTime.parse("20170616T121314"), - DateTime.parse("20170618T121314"), - DateTime.parse("20170620T121314"), - DateTime.parse("20170622T121314"), - DateTime.parse("20170624T121314") - )); - } - - - @Test - public void testRDate() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RDATE, new Seq<>( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } - - - @Test - public void testRDateAndRRule() throws Exception - { - TaskAdapter taskAdapter = new ContentValuesTaskAdapter(new ContentValues()); - taskAdapter.set(TaskAdapter.DTSTART, DateTime.parse("Europe/Berlin", "20170606T121314")); - taskAdapter.set(TaskAdapter.RRULE, new RecurrenceRule("FREQ=DAILY;INTERVAL=2;COUNT=10")); - taskAdapter.set(TaskAdapter.RDATE, new Seq<>( - DateTime.parse("Europe/Berlin", "20170606T121313"), - DateTime.parse("Europe/Berlin", "20170608T121313"), - DateTime.parse("Europe/Berlin", "20170610T121313"), - DateTime.parse("Europe/Berlin", "20170612T121313"), - DateTime.parse("Europe/Berlin", "20170614T121313"), - DateTime.parse("Europe/Berlin", "20170616T121313"), - DateTime.parse("Europe/Berlin", "20170618T121313"), - DateTime.parse("Europe/Berlin", "20170620T121313"), - DateTime.parse("Europe/Berlin", "20170622T121313"), - DateTime.parse("Europe/Berlin", "20170624T121313") - )); - - assertThat(new TaskInstanceIterable(taskAdapter), - iteratesTo( - DateTime.parse("Europe/Berlin", "20170606T121313"), - DateTime.parse("Europe/Berlin", "20170606T121314"), - DateTime.parse("Europe/Berlin", "20170608T121313"), - DateTime.parse("Europe/Berlin", "20170608T121314"), - DateTime.parse("Europe/Berlin", "20170610T121313"), - DateTime.parse("Europe/Berlin", "20170610T121314"), - DateTime.parse("Europe/Berlin", "20170612T121313"), - DateTime.parse("Europe/Berlin", "20170612T121314"), - DateTime.parse("Europe/Berlin", "20170614T121313"), - DateTime.parse("Europe/Berlin", "20170614T121314"), - DateTime.parse("Europe/Berlin", "20170616T121313"), - DateTime.parse("Europe/Berlin", "20170616T121314"), - DateTime.parse("Europe/Berlin", "20170618T121313"), - DateTime.parse("Europe/Berlin", "20170618T121314"), - DateTime.parse("Europe/Berlin", "20170620T121313"), - DateTime.parse("Europe/Berlin", "20170620T121314"), - DateTime.parse("Europe/Berlin", "20170622T121313"), - DateTime.parse("Europe/Berlin", "20170622T121314"), - DateTime.parse("Europe/Berlin", "20170624T121313"), - DateTime.parse("Europe/Berlin", "20170624T121314") - )); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java b/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java deleted file mode 100644 index 552f43d..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/utils/TaskInstanceIteratorTest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2021 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.rfc5545.DateTime; -import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; -import org.dmfs.rfc5545.recur.RecurrenceRule; -import org.dmfs.rfc5545.recurrenceset.RecurrenceRuleAdapter; -import org.dmfs.rfc5545.recurrenceset.RecurrenceSet; -import org.junit.Test; - -import java.util.TimeZone; - -import static org.dmfs.jems.hamcrest.matchers.iterator.IteratorMatcher.iteratorOf; -import static org.junit.Assert.assertThat; - - -/** - * @author Marten Gajda - */ -public class TaskInstanceIteratorTest -{ - private final static String TIMEZONE = "Europe/Berlin"; - - - @Test - public void testAbsolute() throws InvalidRecurrenceRuleException - { - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse(TIMEZONE, "20210201T120000"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(TimeZone.getTimeZone(TIMEZONE), start.getTimestamp()), TIMEZONE), - iteratorOf( - DateTime.parse(TIMEZONE, "20210201T120000"), - DateTime.parse(TIMEZONE, "20210202T120000"), - DateTime.parse(TIMEZONE, "20210203T120000") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse(TIMEZONE, "20210201T120000"), - DateTime.parse(TIMEZONE, "20210202T120000"), - DateTime.parse(TIMEZONE, "20210203T120000") - ) - ); - } - - - @Test - public void testFloating() throws InvalidRecurrenceRuleException - { - - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse("20210201T120000"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null), - iteratorOf( - DateTime.parse("20210201T120000"), - DateTime.parse("20210202T120000"), - DateTime.parse("20210203T120000") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse("20210201T120000"), - DateTime.parse("20210202T120000"), - DateTime.parse("20210203T120000") - ) - ); - } - - - @Test - public void testAllDay() throws InvalidRecurrenceRuleException - { - - RecurrenceSet recurrenceSet = new RecurrenceSet(); - recurrenceSet.addInstances(new RecurrenceRuleAdapter(new RecurrenceRule("FREQ=DAILY;COUNT=3"))); - DateTime start = DateTime.parse("20210201"); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet.iterator(null, start.getTimestamp()), null), - iteratorOf( - DateTime.parse("20210201"), - DateTime.parse("20210202"), - DateTime.parse("20210203") - ) - ); - - assertThat( - () -> new TaskInstanceIterator(start, recurrenceSet), - iteratorOf( - DateTime.parse("20210201"), - DateTime.parse("20210202"), - DateTime.parse("20210203") - ) - ); - } -} \ No newline at end of file diff --git a/provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java b/provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java deleted file mode 100644 index 843b94a..0000000 --- a/provider/src/test/java/org/dmfs/provider/tasks/utils/ZippedTest.java +++ /dev/null @@ -1,60 +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.elementary.Present; -import org.dmfs.jems.single.elementary.ValueSingle; -import org.junit.Test; - -import static org.dmfs.jems.hamcrest.matchers.SingleMatcher.hasValue; -import static org.dmfs.jems.mockito.doubles.TestDoubles.dummy; -import static org.dmfs.jems.mockito.doubles.TestDoubles.failingMock; -import static org.dmfs.jems.optional.elementary.Absent.absent; -import static org.hamcrest.Matchers.sameInstance; -import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.doReturn; - - -/** - * @author Marten Gajda - */ -public class ZippedTest -{ - @Test - public void testPresent() - { - Object dummyPresentValue = new Object(); - Object dummySingleValue = new Object(); - Object dummyResult = new Object(); - BiFunction mockFunction = failingMock(BiFunction.class); - doReturn(dummyResult).when(mockFunction).value(dummyPresentValue, dummySingleValue); - assertThat(new Zipped<>(new Present<>(dummyPresentValue), new ValueSingle<>(dummySingleValue), mockFunction), hasValue(sameInstance(dummyResult))); - } - - - @Test - public void testAbsent() - { - Object dummyObject = new Object(); - // AGENDULA CHANGE: the diamond was `new Zipped<>(...)`. `absent()` pins nothing, so with no - // target type javac infers Zipped for the argument and Matcher> for the - // matcher, and the two don't meet. Java 8 let it through; we compile at 17. Naming the type - // is the whole fix — the assertion is upstream's. - assertThat(new Zipped(absent(), new ValueSingle<>(dummyObject), dummy(BiFunction.class)), hasValue(sameInstance(dummyObject))); - } -} \ No newline at end of file diff --git a/provider/src/test/resources/robolectric.properties b/provider/src/test/resources/robolectric.properties deleted file mode 100644 index d5e7bba..0000000 --- a/provider/src/test/resources/robolectric.properties +++ /dev/null @@ -1,26 +0,0 @@ -# Robolectric normally takes its SDK level from the module's targetSdk, but a -# library module has none to read — only :app sets one — so without this every -# @RunWith(RobolectricTestRunner.class) class fails at DefaultSdkPicker before a -# single assertion runs. -# -# 34 rather than :app's targetSdk 36: these tests exercise the provider's pure -# data plumbing (ContentValues, MatrixCursor, instance-data processors), none of -# which changed across those levels, and every level listed here costs a separate -# android-all jar download on a cold test run. -sdk=34 - -# Robolectric installs the Conscrypt security provider during environment setup -# whether a test needs TLS or not, and conscrypt-openjdk-uber ships no -# linux-aarch_64 native, so on an ARM64 machine every Robolectric test dies in -# setUpApplicationState with UnsatisfiedLinkError before reaching an assertion. -# Nothing here opens a socket — these are ContentValues and cursor tests — so the -# provider is pure overhead. Turning it off also keeps the suite arch-portable -# rather than passing on x86_64 CI and failing on an ARM laptop. -conscryptMode=OFF - -# Note for anyone running the suite on ARM64: Robolectric has no aarch64 SQLite -# in *either* backend — the native runtime refuses outright and the LEGACY -# sqlite4java shadow throws "Architecture 'aarch64' is not supported". So the one -# test class that needs a database, ProviderAccountCleanupTest, skips itself -# there rather than failing; see the assumption at the top of that class. Every -# other test in this module is architecture-independent and runs everywhere. diff --git a/settings.gradle.kts b/settings.gradle.kts index 3f9d68d..f522deb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,9 +26,4 @@ dependencyResolutionManagement { rootProject.name = "Agendula" include(":app") -// Agendula's own task store — the dmfs task provider vendored under our -// authority. In-tree rather than a submodule or a Maven artifact: F-Droid -// requires from-source, and the upstream AAR hardcodes dmfs's permission names -// in its manifest where they cannot be renamed. See provider/PROVENANCE.md. -include(":provider") includeBuild("floret-kit")