├── .classpath ├── .gitignore ├── .project ├── AndroidManifest.xml ├── CHANGELOG.txt ├── LICENSE ├── NOTICE ├── README.md ├── build.xml ├── pom.xml ├── proguard-project.txt ├── project.properties └── src └── org └── dmfs ├── ngrams └── NGramGenerator.java └── provider └── tasks ├── ContentOperation.java ├── FTSDatabaseHelper.java ├── ProviderOperation.java ├── ProviderOperationsLog.java ├── SQLiteContentProvider.java ├── TaskContract.java ├── TaskDatabaseHelper.java ├── TaskProvider.java ├── TaskProviderBroadcastReceiver.java ├── UriFactory.java ├── Utils.java ├── handler ├── AlarmHandler.java ├── CategoryHandler.java ├── DefaultPropertyHandler.java ├── PropertyHandler.java ├── PropertyHandlerFactory.java └── RelationHandler.java ├── model ├── AbstractListAdapter.java ├── AbstractTaskAdapter.java ├── ContentValuesListAdapter.java ├── ContentValuesTaskAdapter.java ├── CursorContentValuesListAdapter.java ├── CursorContentValuesTaskAdapter.java ├── EntityAdapter.java ├── ListAdapter.java ├── TaskAdapter.java └── adapters │ ├── BinaryFieldAdapter.java │ ├── BooleanFieldAdapter.java │ ├── DateTimeArrayFieldAdapter.java │ ├── DateTimeFieldAdapter.java │ ├── DurationFieldAdapter.java │ ├── FieldAdapter.java │ ├── FloatFieldAdapter.java │ ├── IntegerFieldAdapter.java │ ├── LongFieldAdapter.java │ ├── RRuleFieldAdapter.java │ ├── SimpleFieldAdapter.java │ ├── StringFieldAdapter.java │ └── UrlFieldAdapter.java └── processors ├── AbstractEntityProcessor.java ├── EntityProcessor.java ├── lists ├── ListExecutionProcessor.java └── ListValidatorProcessor.java └── tasks ├── AutoUpdateProcessor.java ├── ChangeListProcessor.java ├── FtsProcessor.java ├── RelationProcessor.java ├── TaskExecutionProcessor.java ├── TaskInstancesProcessor.java ├── TaskValidatorProcessor.java └── TestProcessor.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary files 2 | *~ 3 | 4 | # local settings 5 | .settings/ 6 | 7 | # binaries 8 | bin/ 9 | 10 | # exported files 11 | jars/ 12 | 13 | #imported libs 14 | libs/ 15 | 16 | #class files 17 | *.class 18 | 19 | # built application files 20 | *.apk 21 | *.ap_ 22 | 23 | # # files for the dex VM 24 | *.dex 25 | 26 | # # Java class files 27 | *.class 28 | 29 | # # Local configuration file (sdk path, etc) 30 | local.properties 31 | /.settings 32 | 33 | # generated files 34 | gen/ 35 | # 36 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | task-provider 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | 2 | OpenTasks-Provider 1.1.8 3 | ======================== 4 | 5 | TaskContract changes 6 | ------------------------ 7 | * add constants for start and due notification broadcast actions and extras 8 | Note: the broadcast extra values for due and start broadcasts have changed! 9 | * add constants for ACTION_PROVIDER_CHANGED broadcasts extras 10 | 11 | Internal changes 12 | ------------------------ 13 | * add list of changes to ACTION_PROVIDER_CHANGED broadcast 14 | * remove static method to get TaskDatabaseHelper 15 | * refactor notification & broadcast handling 16 | * replace all broadcast receivers by a single receiver -> update OpenTasks AndroidManifest.xml 17 | * perform all actions within the provider context 18 | * refactor database operation handling 19 | * introduce processor chains to handle task and task list operations 20 | * add model for tasks and task lists to provide type safe field access 21 | * add some indexes to speed up certain operations -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Open source task provider for Android 2 | Copyright 2012-2015 Marten Gajda 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Deprecated Repository 3 | 4 | Note: OpenTasks Provider is now a module inside the OpenTasks repository. This separate repoistory is no longer maintained. 5 | 6 | # opentasks-provider 7 | 8 | __An open source task provider for Android__ 9 | 10 | This is a task provider for Android. It supports multiple accounts and multiple lists per account. It aims to fully support RFC 5545 VTODO as well as other task models. 11 | 12 | ## Requirements 13 | 14 | * Android SDK Level 8 or higher. 15 | * [lib-recur](https://github.com/dmfs/lib-recur) 16 | * [rfc5545-datetime](https://github.com/dmfs/rfc5545-datetime) 17 | 18 | ## Usage 19 | 20 | This is a library project to be bundled with a task app. To use it include the following into your AndroidManifest 21 | 22 | ```xml 23 | 24 | ... 25 | 26 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | ``` 96 | 97 | ## TODO: 98 | 99 | * add support for recurrence 100 | * add support for extended attributes like alarms, attendees and categories 101 | 102 | ## License 103 | 104 | Copyright (c) Marten Gajda 2015, licensed under Apache2. 105 | 106 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | org.dmfs 6 | opentasks-provider 7 | 1.1.8 8 | jar 9 | OpenTasks Provider 10 | An open source task provider for Android. 11 | https://github.com/dmfs/opentasks-provider 12 | 13 | 14 | The Apache License, Version 2.0 15 | http://www.apache.org/licenses/LICENSE-2.0.txt 16 | 17 | 18 | 19 | 20 | Marten Gajda 21 | marten@dmfs.org 22 | dmfs 23 | http://dmfs.org 24 | 25 | 26 | 27 | scm:git:git@github.com:dmfs/opentasks-provider.git 28 | 29 | scm:git:git@github.com:dmfs/opentasks-provider.git 30 | 31 | git@github.com:dmfs/opentasks-provider.git 32 | 33 | 34 | 35 | 36 | junit 37 | junit 38 | 4.0 39 | jar 40 | test 41 | true 42 | 43 | 44 | 45 | 46 | org.dmfs 47 | rfc5545-datetime 48 | 0.2.4 49 | 50 | 51 | 52 | org.dmfs 53 | lib-recur 54 | 0.9.3 55 | 56 | 57 | 58 | com.google.android 59 | android 60 | 5.1.1 61 | provided 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-23 15 | android.library=true 16 | -------------------------------------------------------------------------------- /src/org/dmfs/ngrams/NGramGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.ngrams; 19 | 20 | import java.util.HashSet; 21 | import java.util.Locale; 22 | import java.util.Set; 23 | import java.util.regex.Pattern; 24 | 25 | 26 | /** 27 | * Generator for N-grams from a given String. 28 | * 29 | * @author Marten Gajda 30 | */ 31 | public final class NGramGenerator 32 | { 33 | /** 34 | * A {@link Pattern} that matches anything that doesn't belong to a word or number. 35 | */ 36 | private final static Pattern SEPARATOR_PATTERN = Pattern.compile("[^\\p{L}\\p{M}\\d]+"); 37 | 38 | /** 39 | * A {@link Pattern} that matches anything that doesn't belong to a word. 40 | */ 41 | private final static Pattern SEPARATOR_PATTERN_NO_NUMBERS = Pattern.compile("[^\\p{L}\\p{M}]+"); 42 | 43 | private final int mN; 44 | private final int mMinWordLen; 45 | private boolean mAllLowercase = true; 46 | private boolean mReturnNumbers = true; 47 | private boolean mAddSpaceInFront = false; 48 | private Locale mLocale = Locale.getDefault(); 49 | 50 | private char[] mTempArray; 51 | 52 | 53 | public NGramGenerator(int n) 54 | { 55 | this(n, 1); 56 | } 57 | 58 | 59 | public NGramGenerator(int n, int minWordLen) 60 | { 61 | mN = n; 62 | mMinWordLen = minWordLen; 63 | mTempArray = new char[n]; 64 | mTempArray[0] = ' '; 65 | } 66 | 67 | 68 | /** 69 | * Set whether to convert all words to lower-case first. 70 | * 71 | * @param lowercase 72 | * true to convert the test to lower case first. 73 | * @return This instance. 74 | */ 75 | public NGramGenerator setAllLowercase(boolean lowercase) 76 | { 77 | mAllLowercase = lowercase; 78 | return this; 79 | } 80 | 81 | 82 | /** 83 | * Set whether to index the beginning of a word with a space in front. This slightly raises the weight of word beginnings when searching. 84 | * 85 | * @param addSpace 86 | * true to add a space in front of each word, false otherwise. 87 | * @return This instance. 88 | */ 89 | public NGramGenerator setAddSpaceInFront(boolean addSpace) 90 | { 91 | mAddSpaceInFront = addSpace; 92 | return this; 93 | } 94 | 95 | 96 | /** 97 | * 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 98 | * false. 99 | * 100 | * @param locale 101 | * The {@link Locale} to user for the conversion to lower case. 102 | * @return This instance. 103 | */ 104 | public NGramGenerator setLocale(Locale locale) 105 | { 106 | mLocale = locale; 107 | return this; 108 | } 109 | 110 | 111 | /** 112 | * Get all N-grams contained in the given String. 113 | * 114 | * @param data 115 | * The String to analyze. 116 | * @return A {@link Set} containing all N-grams. 117 | */ 118 | public Set getNgrams(String data) 119 | { 120 | Set result = new HashSet(128); 121 | 122 | return getNgrams(result, data); 123 | } 124 | 125 | 126 | /** 127 | * Get all N-grams contained in the given String. 128 | * 129 | * @param set 130 | * The set to add all the N-grams to, or null to create a new set. 131 | * @param data 132 | * The String to analyze. 133 | * 134 | * @return The {@link Set} containing the N-grams. 135 | */ 136 | public Set getNgrams(Set set, String data) 137 | { 138 | if (mAllLowercase) 139 | { 140 | data = data.toLowerCase(mLocale); 141 | } 142 | 143 | String[] words = mReturnNumbers ? SEPARATOR_PATTERN.split(data) : SEPARATOR_PATTERN_NO_NUMBERS.split(data); 144 | 145 | if (set == null) 146 | { 147 | set = new HashSet(128); 148 | } 149 | 150 | for (String word : words) 151 | { 152 | getNgrams(word, set); 153 | } 154 | 155 | return set; 156 | } 157 | 158 | 159 | public void getNgrams(String word, Set ngrams) 160 | { 161 | final int len = word.length(); 162 | final int minWordLen = mMinWordLen; 163 | 164 | if (len < minWordLen) 165 | { 166 | return; 167 | } 168 | 169 | final int n = mN; 170 | final int last = Math.max(1, len - n + 1); 171 | 172 | for (int i = 0; i < last; ++i) 173 | { 174 | ngrams.add(word.substring(i, Math.min(i + n, len))); 175 | } 176 | 177 | if (mAddSpaceInFront) 178 | { 179 | /* 180 | * Add another String with a space and the first n-1 characters of the word. 181 | * 182 | * We could just call 183 | * 184 | * ngrams.add(" " + word.substring(0, Math.min(len, n - 1)); 185 | * 186 | * But it's probably way more efficient like this: 187 | */ 188 | char[] tempArray = mTempArray; 189 | 190 | int count = Math.min(len, n - 1); 191 | for (int i = 0; i < count; ++i) 192 | { 193 | tempArray[i + 1] = word.charAt(i); 194 | } 195 | ngrams.add(new String(tempArray)); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/ProviderOperation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks; 19 | 20 | import java.util.List; 21 | 22 | import org.dmfs.provider.tasks.model.EntityAdapter; 23 | import org.dmfs.provider.tasks.processors.EntityProcessor; 24 | 25 | import android.database.sqlite.SQLiteDatabase; 26 | import android.util.Log; 27 | 28 | 29 | /** 30 | * Provides handlers for INSERT, UPDATE and DELETE operations for {@link EntityAdapter}s. 31 | * 32 | * @author Marten Gajda 33 | */ 34 | public enum ProviderOperation 35 | { 36 | 37 | /** 38 | * Handles insert operations. 39 | */ 40 | INSERT { 41 | @Override 42 | > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 43 | { 44 | processor.beforeInsert(db, entityAdapter, isSyncAdapter); 45 | } 46 | 47 | 48 | @Override 49 | > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 50 | { 51 | processor.afterInsert(db, entityAdapter, isSyncAdapter); 52 | } 53 | }, 54 | 55 | /** 56 | * Handles update operations. 57 | */ 58 | UPDATE { 59 | @Override 60 | > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 61 | { 62 | processor.beforeUpdate(db, entityAdapter, isSyncAdapter); 63 | } 64 | 65 | 66 | @Override 67 | > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 68 | { 69 | processor.afterUpdate(db, entityAdapter, isSyncAdapter); 70 | } 71 | }, 72 | 73 | /** 74 | * Handles delete operations. 75 | */ 76 | DELETE { 77 | @Override 78 | > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 79 | { 80 | processor.beforeDelete(db, entityAdapter, isSyncAdapter); 81 | } 82 | 83 | 84 | @Override 85 | > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter) 86 | { 87 | processor.afterDelete(db, entityAdapter, isSyncAdapter); 88 | } 89 | }; 90 | 91 | private final static String TAG = "OpenTasks.Operation"; 92 | 93 | 94 | abstract > void executeBeforeProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); 95 | 96 | 97 | abstract > void executeAfterProcessor(SQLiteDatabase db, EntityProcessor processor, T entityAdapter, boolean isSyncAdapter); 98 | 99 | 100 | /** 101 | * Executes this operation by running the respective methods of the given {@link EntityProcessor}s. 102 | * 103 | * @param db 104 | * An {@link SQLiteDatabase}. 105 | * @param processors 106 | * The {@link EntityProcessor} chain. 107 | * @param entityAdapter 108 | * The {@link EntityAdapter} to operate on. 109 | * @param isSyncAdapter 110 | * true if this operation is triggered by a sync adapter, false otherwise. 111 | * @param log 112 | * An {@link ProviderOperationsLog} to log this operation. 113 | * @param authority 114 | * The authority of this provider. 115 | */ 116 | public > void execute(SQLiteDatabase db, List> processors, T entityAdapter, boolean isSyncAdapter, 117 | ProviderOperationsLog log, String authority) 118 | { 119 | long start = System.currentTimeMillis(); 120 | 121 | for (EntityProcessor processor : processors) 122 | { 123 | executeBeforeProcessor(db, processor, entityAdapter, isSyncAdapter); 124 | } 125 | 126 | for (EntityProcessor processor : processors) 127 | { 128 | executeAfterProcessor(db, processor, entityAdapter, isSyncAdapter); 129 | } 130 | 131 | if (this != UPDATE || entityAdapter.hasUpdates()) // don't log empty operations 132 | { 133 | log.log(this, entityAdapter.uri(authority)); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/ProviderOperationsLog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks; 19 | 20 | import java.util.ArrayList; 21 | 22 | import android.net.Uri; 23 | import android.os.Bundle; 24 | 25 | 26 | /** 27 | * A log to track all content provider operations. 28 | * 29 | * @author Marten Gajda 30 | */ 31 | public class ProviderOperationsLog 32 | { 33 | private ArrayList mUris = new ArrayList(16); 34 | 35 | private ArrayList mOperations = new ArrayList(16); 36 | 37 | 38 | /** 39 | * Add an operation on the given {@link Uri} to the log. 40 | * 41 | * @param operation 42 | * The {@link ProviderOperation} that was executed. 43 | * @param uri 44 | * The {@link Uri} that the operation was executed on. 45 | */ 46 | public void log(ProviderOperation operation, Uri uri) 47 | { 48 | synchronized (this) 49 | { 50 | mUris.add(uri); 51 | mOperations.add(operation.ordinal()); 52 | } 53 | } 54 | 55 | 56 | /** 57 | * Adds the operations log to the given {@link Bundle}, creating one if the given bundle is null. 58 | * 59 | * @param bundle 60 | * A {@link Bundle} or null. 61 | * @param clearLog 62 | * true to clear the log afterwards, false to keep it. 63 | * @return The {@link Bundle} that was passed or created. 64 | */ 65 | public Bundle toBundle(Bundle bundle, boolean clearLog) 66 | { 67 | if (bundle == null) 68 | { 69 | bundle = new Bundle(2); 70 | } 71 | 72 | synchronized (this) 73 | { 74 | bundle.putParcelableArrayList(TaskContract.EXTRA_OPERATIONS_URIS, mUris); 75 | bundle.putIntegerArrayList(TaskContract.EXTRA_OPERATIONS, mOperations); 76 | if (clearLog) 77 | { 78 | // we can't just clear the ArrayLists, because the Bundle keeps a reference to them 79 | mUris = new ArrayList(16); 80 | mOperations = new ArrayList(16); 81 | } 82 | } 83 | return bundle; 84 | } 85 | 86 | 87 | /** 88 | * Returns a new {@link Bundle} containing the log. 89 | * 90 | * @param clearLog 91 | * true to clear the log afterwards, false to keep it. 92 | * @return The {@link Bundle} that was created. 93 | */ 94 | public Bundle toBundle(boolean clearLog) 95 | { 96 | return toBundle(null, clearLog); 97 | } 98 | 99 | 100 | /** 101 | * Returns whether any operations have been logged or not. 102 | * 103 | * @return true if this log is empty, false if it contains any logs of operations. 104 | */ 105 | public boolean isEmpty() 106 | { 107 | return mUris.size() == 0; 108 | } 109 | } -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/SQLiteContentProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2009 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License 15 | */ 16 | 17 | package org.dmfs.provider.tasks; 18 | 19 | import java.util.ArrayList; 20 | import java.util.HashSet; 21 | import java.util.Set; 22 | 23 | import android.content.ContentProvider; 24 | import android.content.ContentProviderOperation; 25 | import android.content.ContentProviderResult; 26 | import android.content.ContentResolver; 27 | import android.content.ContentValues; 28 | import android.content.Context; 29 | import android.content.OperationApplicationException; 30 | import android.database.sqlite.SQLiteDatabase; 31 | import android.database.sqlite.SQLiteOpenHelper; 32 | import android.net.Uri; 33 | 34 | 35 | /** 36 | * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage. 37 | */ 38 | /* 39 | * Changed by marten@dmfs.org: 40 | * 41 | * 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 42 | * also pass the database to the *InTransaction methods. 43 | * 44 | * update visibility of class and methods 45 | */ 46 | abstract class SQLiteContentProvider extends ContentProvider 47 | { 48 | 49 | @SuppressWarnings("unused") 50 | private static final String TAG = "SQLiteContentProvider"; 51 | 52 | private SQLiteOpenHelper mOpenHelper; 53 | private Set mChangedUris; 54 | 55 | private final ThreadLocal mApplyingBatch = new ThreadLocal(); 56 | private static final int SLEEP_AFTER_YIELD_DELAY = 4000; 57 | 58 | /** 59 | * Maximum number of operations allowed in a batch between yield points. 60 | */ 61 | private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500; 62 | 63 | 64 | @Override 65 | public boolean onCreate() 66 | { 67 | Context context = getContext(); 68 | mOpenHelper = getDatabaseHelper(context); 69 | mChangedUris = new HashSet(); 70 | return true; 71 | } 72 | 73 | 74 | /** 75 | * Returns a {@link SQLiteOpenHelper} that can open the database. 76 | */ 77 | protected abstract SQLiteOpenHelper getDatabaseHelper(Context context); 78 | 79 | 80 | /** 81 | * The equivalent of the {@link #insert} method, but invoked within a transaction. 82 | */ 83 | public abstract Uri insertInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, boolean callerIsSyncAdapter); 84 | 85 | 86 | /** 87 | * The equivalent of the {@link #update} method, but invoked within a transaction. 88 | */ 89 | public abstract int updateInTransaction(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs, 90 | boolean callerIsSyncAdapter); 91 | 92 | 93 | /** 94 | * The equivalent of the {@link #delete} method, but invoked within a transaction. 95 | */ 96 | public abstract int deleteInTransaction(SQLiteDatabase db, Uri uri, String selection, String[] selectionArgs, boolean callerIsSyncAdapter); 97 | 98 | 99 | /** 100 | * Call this to add a URI to the list of URIs to be notified when the transaction is committed. 101 | */ 102 | protected void postNotifyUri(Uri uri) 103 | { 104 | synchronized (mChangedUris) 105 | { 106 | mChangedUris.add(uri); 107 | } 108 | } 109 | 110 | 111 | public boolean isCallerSyncAdapter(Uri uri) 112 | { 113 | return false; 114 | } 115 | 116 | 117 | public SQLiteOpenHelper getDatabaseHelper() 118 | { 119 | return mOpenHelper; 120 | } 121 | 122 | 123 | private boolean applyingBatch() 124 | { 125 | return mApplyingBatch.get() != null && mApplyingBatch.get(); 126 | } 127 | 128 | 129 | @Override 130 | public Uri insert(Uri uri, ContentValues values) 131 | { 132 | Uri result = null; 133 | boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); 134 | boolean applyingBatch = applyingBatch(); 135 | SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 136 | if (!applyingBatch) 137 | { 138 | db.beginTransaction(); 139 | try 140 | { 141 | result = insertInTransaction(db, uri, values, callerIsSyncAdapter); 142 | db.setTransactionSuccessful(); 143 | } 144 | finally 145 | { 146 | db.endTransaction(); 147 | } 148 | 149 | onEndTransaction(callerIsSyncAdapter); 150 | } 151 | else 152 | { 153 | result = insertInTransaction(db, uri, values, callerIsSyncAdapter); 154 | } 155 | return result; 156 | } 157 | 158 | 159 | @Override 160 | public int bulkInsert(Uri uri, ContentValues[] values) 161 | { 162 | int numValues = values.length; 163 | boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); 164 | SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 165 | db.beginTransaction(); 166 | try 167 | { 168 | for (int i = 0; i < numValues; i++) 169 | { 170 | insertInTransaction(db, uri, values[i], callerIsSyncAdapter); 171 | db.yieldIfContendedSafely(); 172 | } 173 | db.setTransactionSuccessful(); 174 | } 175 | finally 176 | { 177 | db.endTransaction(); 178 | } 179 | 180 | onEndTransaction(callerIsSyncAdapter); 181 | return numValues; 182 | } 183 | 184 | 185 | @Override 186 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) 187 | { 188 | int count = 0; 189 | boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); 190 | boolean applyingBatch = applyingBatch(); 191 | SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 192 | if (!applyingBatch) 193 | { 194 | db.beginTransaction(); 195 | try 196 | { 197 | count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); 198 | db.setTransactionSuccessful(); 199 | } 200 | finally 201 | { 202 | db.endTransaction(); 203 | } 204 | 205 | onEndTransaction(callerIsSyncAdapter); 206 | } 207 | else 208 | { 209 | count = updateInTransaction(db, uri, values, selection, selectionArgs, callerIsSyncAdapter); 210 | } 211 | 212 | return count; 213 | } 214 | 215 | 216 | @Override 217 | public int delete(Uri uri, String selection, String[] selectionArgs) 218 | { 219 | int count = 0; 220 | boolean callerIsSyncAdapter = isCallerSyncAdapter(uri); 221 | boolean applyingBatch = applyingBatch(); 222 | SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 223 | if (!applyingBatch) 224 | { 225 | db.beginTransaction(); 226 | try 227 | { 228 | count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); 229 | db.setTransactionSuccessful(); 230 | } 231 | finally 232 | { 233 | db.endTransaction(); 234 | } 235 | 236 | onEndTransaction(callerIsSyncAdapter); 237 | } 238 | else 239 | { 240 | count = deleteInTransaction(db, uri, selection, selectionArgs, callerIsSyncAdapter); 241 | } 242 | return count; 243 | } 244 | 245 | 246 | @Override 247 | public ContentProviderResult[] applyBatch(ArrayList operations) throws OperationApplicationException 248 | { 249 | int ypCount = 0; 250 | int opCount = 0; 251 | boolean callerIsSyncAdapter = false; 252 | SQLiteDatabase db = mOpenHelper.getWritableDatabase(); 253 | db.beginTransaction(); 254 | try 255 | { 256 | mApplyingBatch.set(true); 257 | final int numOperations = operations.size(); 258 | final ContentProviderResult[] results = new ContentProviderResult[numOperations]; 259 | for (int i = 0; i < numOperations; i++) 260 | { 261 | if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) 262 | { 263 | throw new OperationApplicationException("Too many content provider operations between yield points. " 264 | + "The maximum number of operations per yield point is " + MAX_OPERATIONS_PER_YIELD_POINT, ypCount); 265 | } 266 | final ContentProviderOperation operation = operations.get(i); 267 | if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) 268 | { 269 | callerIsSyncAdapter = true; 270 | } 271 | if (i > 0 && operation.isYieldAllowed()) 272 | { 273 | opCount = 0; 274 | if (db.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) 275 | { 276 | ypCount++; 277 | } 278 | } 279 | results[i] = operation.apply(this, results, i); 280 | } 281 | db.setTransactionSuccessful(); 282 | return results; 283 | } 284 | finally 285 | { 286 | mApplyingBatch.set(false); 287 | db.endTransaction(); 288 | onEndTransaction(callerIsSyncAdapter); 289 | } 290 | } 291 | 292 | 293 | protected void onEndTransaction(boolean callerIsSyncAdapter) 294 | { 295 | Set changed; 296 | synchronized (mChangedUris) 297 | { 298 | changed = new HashSet(mChangedUris); 299 | mChangedUris.clear(); 300 | } 301 | ContentResolver resolver = getContext().getContentResolver(); 302 | for (Uri uri : changed) 303 | { 304 | boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri); 305 | resolver.notifyChange(uri, null, syncToNetwork); 306 | } 307 | } 308 | 309 | 310 | protected boolean syncToNetwork(Uri uri) 311 | { 312 | return false; 313 | } 314 | 315 | } 316 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/TaskProviderBroadcastReceiver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks; 19 | 20 | import java.util.TimeZone; 21 | 22 | import org.dmfs.rfc5545.DateTime; 23 | 24 | import android.annotation.SuppressLint; 25 | import android.app.AlarmManager; 26 | import android.app.PendingIntent; 27 | import android.content.BroadcastReceiver; 28 | import android.content.Context; 29 | import android.content.Intent; 30 | import android.os.Build; 31 | 32 | 33 | /** 34 | * A receiver for all task provider related broadcasts. This receiver merely forwards all incoming broadcasts to the provider, so they can be handled 35 | * asynchronously in the provider context. 36 | * 37 | * @author Marten Gajda 38 | */ 39 | public class TaskProviderBroadcastReceiver extends BroadcastReceiver 40 | { 41 | private final static int REQUEST_CODE_ALARM = 1337; 42 | 43 | private final static String ACTION_NOTIFICATION_ALARM = "org.dmfs.tasks.provider.NOTIFICATION_ALARM"; 44 | 45 | 46 | /** 47 | * Registers a system alarm to update notifications at a specific time. 48 | * 49 | * @param context 50 | * A Context. 51 | * @param updateTime 52 | * When to fire the alarm. 53 | */ 54 | @SuppressLint("NewApi") 55 | static void planNotificationUpdate(Context context, DateTime updateTime) 56 | { 57 | AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 58 | Intent alarmIntent = new Intent(context, TaskProviderBroadcastReceiver.class); 59 | alarmIntent.setAction(ACTION_NOTIFICATION_ALARM); 60 | 61 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, REQUEST_CODE_ALARM, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); 62 | 63 | // cancel any previous alarm 64 | am.cancel(pendingIntent); 65 | 66 | if (updateTime.isFloating()) 67 | { 68 | // convert floating times to absolute times 69 | updateTime = new DateTime(TimeZone.getDefault(), updateTime.getYear(), updateTime.getMonth(), updateTime.getDayOfMonth(), updateTime.getHours(), 70 | updateTime.getMinutes(), updateTime.getSeconds()); 71 | } 72 | 73 | // AlarmManager API changed in v19 (KitKat) and the "set" method is not called at the exact time anymore 74 | if (Build.VERSION.SDK_INT > 18) 75 | { 76 | am.setExact(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); 77 | } 78 | else 79 | { 80 | am.set(AlarmManager.RTC_WAKEUP, updateTime.getTimestamp(), pendingIntent); 81 | } 82 | } 83 | 84 | 85 | @Override 86 | public void onReceive(Context context, Intent intent) 87 | { 88 | String action = intent.getAction(); 89 | switch (action) 90 | { 91 | case Intent.ACTION_TIMEZONE_CHANGED: 92 | { 93 | // the local timezone has been changed, notify the provider to take the necessary steps. 94 | // don't trigger the notifications update yet, because the timezone update will run asynhronously and we need to wait till that's finished 95 | ContentOperation.UPDATE_TIMEZONE.fire(context, null); 96 | } 97 | case ACTION_NOTIFICATION_ALARM: 98 | { 99 | // it's time for the next notification 100 | ContentOperation.POST_NOTIFICATIONS.fire(context, null); 101 | } 102 | default: 103 | { 104 | // at this time all other actions trigger an update of the notification alarm 105 | ContentOperation.UPDATE_NOTIFICATION_ALARM.fire(context, null); 106 | } 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/UriFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks; 19 | 20 | import java.util.HashMap; 21 | import java.util.Map; 22 | 23 | import android.net.Uri; 24 | 25 | 26 | public class UriFactory 27 | { 28 | public final String authority; 29 | 30 | private final Map mUriMap = new HashMap(16); 31 | 32 | 33 | UriFactory(String authority) 34 | { 35 | this.authority = authority; 36 | mUriMap.put((String) null, Uri.parse("content://" + authority)); 37 | } 38 | 39 | 40 | void addUri(String path) 41 | { 42 | mUriMap.put(path, Uri.parse("content://" + authority + "/" + path)); 43 | } 44 | 45 | 46 | public Uri getUri() 47 | { 48 | return mUriMap.get(null); 49 | } 50 | 51 | 52 | public Uri getUri(String path) 53 | { 54 | return mUriMap.get(path); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks; 19 | 20 | import java.util.ArrayList; 21 | import java.util.Arrays; 22 | import java.util.List; 23 | 24 | import org.dmfs.provider.tasks.TaskContract.Instances; 25 | import org.dmfs.provider.tasks.TaskContract.SyncState; 26 | import org.dmfs.provider.tasks.TaskContract.TaskListColumns; 27 | import org.dmfs.provider.tasks.TaskContract.TaskListSyncColumns; 28 | import org.dmfs.provider.tasks.TaskContract.TaskLists; 29 | import org.dmfs.provider.tasks.TaskContract.Tasks; 30 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 31 | 32 | import android.accounts.Account; 33 | import android.content.ContentResolver; 34 | import android.content.Context; 35 | import android.content.Intent; 36 | import android.database.Cursor; 37 | import android.database.sqlite.SQLiteDatabase; 38 | 39 | 40 | /** 41 | * The Class Utils. 42 | * 43 | * @author Tobias Reinsch 44 | * @author Marten Gajda 45 | */ 46 | public class Utils 47 | { 48 | public static void sendActionProviderChangedBroadCast(Context context, String authority) 49 | { 50 | // TODO: Using the TaskContract content uri results in a "Unknown URI content" error message. Using the Tasks content uri instead will break the 51 | // broadcast receiver. We have to find away around this 52 | Intent providerChangedIntent = new Intent(Intent.ACTION_PROVIDER_CHANGED, TaskContract.getContentUri(authority)); 53 | context.sendBroadcast(providerChangedIntent); 54 | } 55 | 56 | 57 | public static void cleanUpLists(Context context, SQLiteDatabase db, Account[] accounts, String authority) 58 | { 59 | // make a list of the accounts array 60 | List accountList = Arrays.asList(accounts); 61 | 62 | db.beginTransaction(); 63 | 64 | try 65 | { 66 | Cursor c = db.query(Tables.LISTS, new String[] { TaskListColumns._ID, TaskListSyncColumns.ACCOUNT_NAME, TaskListSyncColumns.ACCOUNT_TYPE }, null, 67 | null, null, null, null); 68 | 69 | // build a list of all task list ids that no longer have an account 70 | List obsoleteLists = new ArrayList(); 71 | try 72 | { 73 | while (c.moveToNext()) 74 | { 75 | String accountType = c.getString(2); 76 | // mark list for removal if it is non-local and the account 77 | // is not in accountList 78 | if (!TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) 79 | { 80 | Account account = new Account(c.getString(1), accountType); 81 | if (!accountList.contains(account)) 82 | { 83 | obsoleteLists.add(c.getLong(0)); 84 | 85 | // remove syncstate for this account right away 86 | db.delete(Tables.SYNCSTATE, SyncState.ACCOUNT_NAME + "=? and " + SyncState.ACCOUNT_TYPE + "=?", new String[] { account.name, 87 | account.type }); 88 | } 89 | } 90 | } 91 | } 92 | finally 93 | { 94 | c.close(); 95 | } 96 | 97 | if (obsoleteLists.size() == 0) 98 | { 99 | // nothing to do here 100 | return; 101 | } 102 | 103 | // remove all accounts in the list 104 | for (Long id : obsoleteLists) 105 | { 106 | if (id != null) 107 | { 108 | db.delete(Tables.LISTS, TaskListColumns._ID + "=" + id, null); 109 | } 110 | } 111 | db.setTransactionSuccessful(); 112 | } 113 | finally 114 | { 115 | db.endTransaction(); 116 | } 117 | // notify all observers 118 | 119 | ContentResolver cr = context.getContentResolver(); 120 | cr.notifyChange(TaskLists.getContentUri(authority), null); 121 | cr.notifyChange(Tasks.getContentUri(authority), null); 122 | cr.notifyChange(Instances.getContentUri(authority), null); 123 | 124 | Utils.sendActionProviderChangedBroadCast(context, authority); 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/AlarmHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.Property; 21 | 22 | import android.content.ContentValues; 23 | import android.database.Cursor; 24 | import android.database.sqlite.SQLiteDatabase; 25 | 26 | 27 | /** 28 | * This class is used to handle alarm property values during database transactions. 29 | * 30 | * @author Tobias Reinsch 31 | * 32 | */ 33 | public class AlarmHandler extends PropertyHandler 34 | { 35 | 36 | // private static final String[] ALARM_ID_PROJECTION = { Alarms.ALARM_ID }; 37 | // private static final String ALARM_SELECTION = Alarms.ALARM_ID + " =?"; 38 | 39 | /** 40 | * Validates the content of the alarm prior to insert and update transactions. 41 | * 42 | * @param db 43 | * The {@link SQLiteDatabase}. 44 | * @param taskId 45 | * The id of the task this property belongs to. 46 | * @param propertyId 47 | * The id of the property if isNew is false. If isNew is true this value is ignored. 48 | * @param isNew 49 | * Indicates that the content is new and not an update. 50 | * @param values 51 | * The {@link ContentValues} to validate. 52 | * @param isSyncAdapter 53 | * Indicates that the transaction was triggered from a SyncAdapter. 54 | * 55 | * @return The valid {@link ContentValues}. 56 | * 57 | * @throws IllegalArgumentException 58 | * if the {@link ContentValues} are invalid. 59 | */ 60 | @Override 61 | public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) 62 | { 63 | // row id can not be changed or set manually 64 | if (values.containsKey(Property.Alarm.PROPERTY_ID)) 65 | { 66 | throw new IllegalArgumentException("_ID can not be set manually"); 67 | } 68 | 69 | if (!values.containsKey(Property.Alarm.MINUTES_BEFORE)) 70 | { 71 | throw new IllegalArgumentException("alarm property requires a time offset"); 72 | } 73 | 74 | if (!values.containsKey(Property.Alarm.REFERENCE) || values.getAsInteger(Property.Alarm.REFERENCE) < 0) 75 | { 76 | throw new IllegalArgumentException("alarm property requires a valid reference date "); 77 | } 78 | 79 | if (!values.containsKey(Property.Alarm.ALARM_TYPE)) 80 | { 81 | throw new IllegalArgumentException("alarm property requires an alarm type"); 82 | } 83 | 84 | return values; 85 | } 86 | 87 | 88 | /** 89 | * Inserts the alarm into the database. 90 | * 91 | * @param db 92 | * The {@link SQLiteDatabase}. 93 | * @param taskId 94 | * The id of the task the new property belongs to. 95 | * @param values 96 | * The {@link ContentValues} to insert. 97 | * @param isSyncAdapter 98 | * Indicates that the transaction was triggered from a SyncAdapter. 99 | * 100 | * @return The row id of the new alarm as long 101 | */ 102 | @Override 103 | public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) 104 | { 105 | values = validateValues(db, taskId, -1, true, values, isSyncAdapter); 106 | return super.insert(db, taskId, values, isSyncAdapter); 107 | } 108 | 109 | 110 | /** 111 | * Updates the alarm in the database. 112 | * 113 | * @param db 114 | * The {@link SQLiteDatabase}. 115 | * @param taskId 116 | * The id of the task this property belongs to. 117 | * @param propertyId 118 | * The id of the property. 119 | * @param values 120 | * The {@link ContentValues} to update. 121 | * @param oldValues 122 | * A {@link Cursor} pointing to the old values in the database. 123 | * @param isSyncAdapter 124 | * Indicates that the transaction was triggered from a SyncAdapter. 125 | * 126 | * @return The number of rows affected. 127 | */ 128 | @Override 129 | public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) 130 | { 131 | values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); 132 | return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/CategoryHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.Categories; 21 | import org.dmfs.provider.tasks.TaskContract.Properties; 22 | import org.dmfs.provider.tasks.TaskContract.Property.Category; 23 | import org.dmfs.provider.tasks.TaskContract.Tasks; 24 | import org.dmfs.provider.tasks.TaskDatabaseHelper.CategoriesMapping; 25 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 26 | 27 | import android.content.ContentValues; 28 | import android.database.Cursor; 29 | import android.database.sqlite.SQLiteDatabase; 30 | 31 | 32 | /** 33 | * This class is used to handle category property values during database transactions. 34 | * 35 | * @author Tobias Reinsch 36 | * 37 | */ 38 | public class CategoryHandler extends PropertyHandler 39 | { 40 | 41 | private static final String[] CATEGORY_ID_PROJECTION = { Categories._ID, Categories.NAME, Categories.COLOR }; 42 | 43 | private static final String CATEGORY_ID_SELECTION = Categories._ID + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; 44 | private static final String CATEGORY_NAME_SELECTION = Categories.NAME + "=? and " + Categories.ACCOUNT_NAME + "=? and " + Categories.ACCOUNT_TYPE + "=?"; 45 | 46 | public static final String IS_NEW_CATEGORY = "is_new_category"; 47 | 48 | 49 | /** 50 | * Validates the content of the category prior to insert and update transactions. 51 | * 52 | * @param db 53 | * The {@link SQLiteDatabase}. 54 | * @param taskId 55 | * The id of the task this property belongs to. 56 | * @param propertyId 57 | * The id of the property if isNew is false. If isNew is true this value is ignored. 58 | * @param isNew 59 | * Indicates that the content is new and not an update. 60 | * @param values 61 | * The {@link ContentValues} to validate. 62 | * @param isSyncAdapter 63 | * Indicates that the transaction was triggered from a SyncAdapter. 64 | * 65 | * @return The valid {@link ContentValues}. 66 | * 67 | * @throws IllegalArgumentException 68 | * if the {@link ContentValues} are invalid. 69 | */ 70 | @Override 71 | public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) 72 | { 73 | // the category requires a name or an id 74 | if (!values.containsKey(Category.CATEGORY_ID) && !values.containsKey(Category.CATEGORY_NAME)) 75 | { 76 | throw new IllegalArgumentException("Neiter an id nor a category name was supplied for the category property."); 77 | } 78 | 79 | // get the matching task & account for the property 80 | if (!values.containsKey(Properties.TASK_ID)) 81 | { 82 | throw new IllegalArgumentException("No task id was supplied for the category property"); 83 | } 84 | String[] queryArgs = { values.getAsString(Properties.TASK_ID) }; 85 | String[] queryProjection = { Tasks.ACCOUNT_NAME, Tasks.ACCOUNT_TYPE }; 86 | String querySelection = Tasks._ID + "=?"; 87 | Cursor taskCursor = db.query(Tables.TASKS_VIEW, queryProjection, querySelection, queryArgs, null, null, null); 88 | 89 | String accountName = null; 90 | String accountType = null; 91 | try 92 | { 93 | if (taskCursor.moveToNext()) 94 | { 95 | accountName = taskCursor.getString(0); 96 | accountType = taskCursor.getString(1); 97 | 98 | values.put(Categories.ACCOUNT_NAME, accountName); 99 | values.put(Categories.ACCOUNT_TYPE, accountType); 100 | } 101 | } 102 | finally 103 | { 104 | if (taskCursor != null) 105 | { 106 | taskCursor.close(); 107 | } 108 | } 109 | 110 | if (accountName != null && accountType != null) 111 | { 112 | // search for matching categories 113 | String[] categoryArgs; 114 | Cursor cursor; 115 | 116 | if (values.containsKey(Categories._ID)) 117 | { 118 | // serach by ID 119 | categoryArgs = new String[] { values.getAsString(Category.CATEGORY_ID), accountName, accountType }; 120 | cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_ID_SELECTION, categoryArgs, null, null, null); 121 | } 122 | else 123 | { 124 | // search by name 125 | categoryArgs = new String[] { values.getAsString(Category.CATEGORY_NAME), accountName, accountType }; 126 | cursor = db.query(Tables.CATEGORIES, CATEGORY_ID_PROJECTION, CATEGORY_NAME_SELECTION, categoryArgs, null, null, null); 127 | } 128 | try 129 | { 130 | if (cursor != null && cursor.getCount() == 1) 131 | { 132 | cursor.moveToNext(); 133 | Long categoryID = cursor.getLong(0); 134 | String categoryName = cursor.getString(1); 135 | int color = cursor.getInt(2); 136 | 137 | values.put(Category.CATEGORY_ID, categoryID); 138 | values.put(Category.CATEGORY_NAME, categoryName); 139 | values.put(Category.CATEGORY_COLOR, color); 140 | values.put(IS_NEW_CATEGORY, false); 141 | } 142 | else 143 | { 144 | values.put(IS_NEW_CATEGORY, true); 145 | } 146 | } 147 | finally 148 | { 149 | if (cursor != null) 150 | { 151 | cursor.close(); 152 | } 153 | } 154 | 155 | } 156 | 157 | return values; 158 | } 159 | 160 | 161 | /** 162 | * Inserts the category into the database. 163 | * 164 | * @param db 165 | * The {@link SQLiteDatabase}. 166 | * @param taskId 167 | * The id of the task the new property belongs to. 168 | * @param values 169 | * The {@link ContentValues} to insert. 170 | * @param isSyncAdapter 171 | * Indicates that the transaction was triggered from a SyncAdapter. 172 | * 173 | * @return The row id of the new category as long 174 | */ 175 | @Override 176 | public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) 177 | { 178 | values = validateValues(db, taskId, -1, true, values, isSyncAdapter); 179 | values = getOrInsertCategory(db, values); 180 | 181 | // insert property row and create relation 182 | long id = super.insert(db, taskId, values, isSyncAdapter); 183 | insertRelation(db, taskId, values.getAsLong(Category.CATEGORY_ID), id); 184 | 185 | // update FTS entry with category name 186 | updateFTSEntry(db, taskId, id, values.getAsString(Category.CATEGORY_NAME)); 187 | return id; 188 | } 189 | 190 | 191 | /** 192 | * Updates the category in the database. 193 | * 194 | * @param db 195 | * The {@link SQLiteDatabase}. 196 | * @param taskId 197 | * The id of the task this property belongs to. 198 | * @param propertyId 199 | * The id of the property. 200 | * @param values 201 | * The {@link ContentValues} to update. 202 | * @param oldValues 203 | * A {@link Cursor} pointing to the old values in the database. 204 | * @param isSyncAdapter 205 | * Indicates that the transaction was triggered from a SyncAdapter. 206 | * 207 | * @return The number of rows affected. 208 | */ 209 | @Override 210 | public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) 211 | { 212 | values = validateValues(db, taskId, propertyId, false, values, isSyncAdapter); 213 | values = getOrInsertCategory(db, values); 214 | 215 | if (values.containsKey(Category.CATEGORY_NAME)) 216 | { 217 | // update FTS entry with new category name 218 | updateFTSEntry(db, taskId, propertyId, values.getAsString(Category.CATEGORY_NAME)); 219 | } 220 | 221 | return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); 222 | } 223 | 224 | 225 | /** 226 | * Check if a category with matching {@link ContentValues} exists and returns the existing category or creates a new category in the database. 227 | * 228 | * @param db 229 | * The {@link SQLiteDatabase}. 230 | * @param values 231 | * The {@link ContentValues} of the category. 232 | * @return The {@link ContentValues} of the existing or new category. 233 | */ 234 | private ContentValues getOrInsertCategory(SQLiteDatabase db, ContentValues values) 235 | { 236 | if (values.getAsBoolean(IS_NEW_CATEGORY)) 237 | { 238 | // insert new category in category table 239 | ContentValues newCategoryValues = new ContentValues(4); 240 | newCategoryValues.put(Categories.ACCOUNT_NAME, values.getAsString(Categories.ACCOUNT_NAME)); 241 | newCategoryValues.put(Categories.ACCOUNT_TYPE, values.getAsString(Categories.ACCOUNT_TYPE)); 242 | newCategoryValues.put(Categories.NAME, values.getAsString(Category.CATEGORY_NAME)); 243 | newCategoryValues.put(Categories.COLOR, values.getAsInteger(Category.CATEGORY_COLOR)); 244 | 245 | long categoryID = db.insert(Tables.CATEGORIES, "", newCategoryValues); 246 | values.put(Category.CATEGORY_ID, categoryID); 247 | } 248 | 249 | // remove redundant values 250 | values.remove(IS_NEW_CATEGORY); 251 | values.remove(Categories.ACCOUNT_NAME); 252 | values.remove(Categories.ACCOUNT_TYPE); 253 | 254 | return values; 255 | } 256 | 257 | 258 | /** 259 | * Inserts a relation entry in the database to link task and category. 260 | * 261 | * @param db 262 | * The {@link SQLiteDatabase}. 263 | * @param taskId 264 | * The row id of the task. 265 | * @param categoryId 266 | * The row id of the category. 267 | * @return The row id of the inserted relation. 268 | */ 269 | private long insertRelation(SQLiteDatabase db, long taskId, long categoryId, long propertyId) 270 | { 271 | ContentValues relationValues = new ContentValues(3); 272 | relationValues.put(CategoriesMapping.TASK_ID, taskId); 273 | relationValues.put(CategoriesMapping.CATEGORY_ID, categoryId); 274 | relationValues.put(CategoriesMapping.PROPERTY_ID, propertyId); 275 | return db.insert(Tables.CATEGORIES_MAPPING, "", relationValues); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/DefaultPropertyHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import android.content.ContentValues; 21 | import android.database.sqlite.SQLiteDatabase; 22 | 23 | 24 | /** 25 | * This class is used to handle properties with unknown / unsupported mime-types. 26 | * 27 | * @author Tobias Reinsch 28 | * 29 | */ 30 | public class DefaultPropertyHandler extends PropertyHandler 31 | { 32 | 33 | /** 34 | * Validates the content of the alarm prior to insert and update transactions. 35 | * 36 | * @param db 37 | * The {@link SQLiteDatabase}. 38 | * @param isNew 39 | * Indicates that the content is new and not an update. 40 | * @param values 41 | * The {@link ContentValues} to validate. 42 | * @param isSyncAdapter 43 | * Indicates that the transaction was triggered from a SyncAdapter. 44 | * 45 | * @return The valid {@link ContentValues}. 46 | * 47 | * @throws IllegalArgumentException 48 | * if the {@link ContentValues} are invalid. 49 | */ 50 | @Override 51 | public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) 52 | { 53 | return values; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/PropertyHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import org.dmfs.provider.tasks.FTSDatabaseHelper; 21 | import org.dmfs.provider.tasks.TaskContract.Properties; 22 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 23 | 24 | import android.content.ContentValues; 25 | import android.database.Cursor; 26 | import android.database.sqlite.SQLiteDatabase; 27 | 28 | 29 | /** 30 | * Abstract class that is used as template for specific property handlers. 31 | * 32 | * @author Tobias Reinsch 33 | * 34 | */ 35 | public abstract class PropertyHandler 36 | { 37 | 38 | /** 39 | * Validates the content of the property prior to insert and update transactions. 40 | * 41 | * @param db 42 | * The {@link SQLiteDatabase}. 43 | * @param taskId 44 | * The id of the task this property belongs to. 45 | * @param propertyId 46 | * The id of the property if isNew is false. If isNew is true this value is ignored. 47 | * @param isNew 48 | * Indicates that the content is new and not an update. 49 | * @param values 50 | * The {@link ContentValues} to validate. 51 | * @param isSyncAdapter 52 | * Indicates that the transaction was triggered from a SyncAdapter. 53 | * 54 | * @return The valid {@link ContentValues}. 55 | * 56 | * @throws IllegalArgumentException 57 | * if the {@link ContentValues} are invalid. 58 | */ 59 | public abstract ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter); 60 | 61 | 62 | /** 63 | * Inserts the property {@link ContentValues} into the database. 64 | * 65 | * @param db 66 | * The {@link SQLiteDatabase}. 67 | * @param taskId 68 | * The id of the task the new property belongs to. 69 | * @param values 70 | * The {@link ContentValues} to insert. 71 | * @param isSyncAdapter 72 | * Indicates that the transaction was triggered from a SyncAdapter. 73 | * 74 | * @return The row id of the new property as long 75 | */ 76 | public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) 77 | { 78 | return db.insert(Tables.PROPERTIES, "", values); 79 | } 80 | 81 | 82 | /** 83 | * Updates the property {@link ContentValues} in the database. 84 | * 85 | * @param db 86 | * The {@link SQLiteDatabase}. 87 | * @param taskId 88 | * The id of the task this property belongs to. 89 | * @param propertyId 90 | * The id of the property. 91 | * @param values 92 | * The {@link ContentValues} to update. 93 | * @param oldValues 94 | * A {@link Cursor} pointing to the old values in the database. 95 | * @param isSyncAdapter 96 | * Indicates that the transaction was triggered from a SyncAdapter. 97 | * 98 | * @return The number of rows affected. 99 | */ 100 | public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) 101 | { 102 | return db.update(Tables.PROPERTIES, values, Properties.PROPERTY_ID + "=" + propertyId, null); 103 | } 104 | 105 | 106 | /** 107 | * Deletes the property in the database. 108 | * 109 | * @param db 110 | * The belonging database. 111 | * @param taskId 112 | * The id of the task this property belongs to. 113 | * @param propertyId 114 | * The id of the property. 115 | * @param oldValues 116 | * A {@link Cursor} pointing to the old values in the database. 117 | * @param isSyncAdapter 118 | * Indicates that the transaction was triggered from a SyncAdapter. 119 | * @return 120 | */ 121 | public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) 122 | { 123 | return db.delete(Tables.PROPERTIES, Properties.PROPERTY_ID + "=" + propertyId, null); 124 | 125 | } 126 | 127 | 128 | /** 129 | * Method hook to insert FTS entries on database migration. 130 | * 131 | * @param db 132 | * The {@link SQLiteDatabase}. 133 | * @param taskId 134 | * the row id of the task this property belongs to 135 | * @param propertyId 136 | * the id of the property 137 | * @param text 138 | * the searchable text of the property. If the property has multiple text snippets to search in, concat them separated by a space. 139 | */ 140 | protected void updateFTSEntry(SQLiteDatabase db, long taskId, long propertyId, String text) 141 | { 142 | FTSDatabaseHelper.updatePropertyFTSEntry(db, taskId, propertyId, text); 143 | 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/PropertyHandlerFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.Property.Alarm; 21 | import org.dmfs.provider.tasks.TaskContract.Property.Category; 22 | import org.dmfs.provider.tasks.TaskContract.Property.Relation; 23 | 24 | 25 | /** 26 | * A factory that creates the matching {@link PropertyHandler} for the given mimetype. 27 | * 28 | * @author Tobias Reinsch 29 | * 30 | */ 31 | public class PropertyHandlerFactory 32 | { 33 | private final static PropertyHandler CATEGORY_HANDLER = new CategoryHandler(); 34 | private final static PropertyHandler ALARM_HANDLER = new AlarmHandler(); 35 | private final static PropertyHandler RELATION_HANDLER = new RelationHandler(); 36 | private final static PropertyHandler DEFAULT_PROPERTY_HANDLER = new DefaultPropertyHandler(); 37 | 38 | 39 | /** 40 | * Creates a specific {@link PropertyHandler}. 41 | * 42 | * @param mimeType 43 | * The mimetype of the property. 44 | * @return The matching {@link PropertyHandler} for the given mimetype or null 45 | */ 46 | public static PropertyHandler get(String mimeType) 47 | { 48 | if (Category.CONTENT_ITEM_TYPE.equals(mimeType)) 49 | { 50 | return CATEGORY_HANDLER; 51 | } 52 | if (Alarm.CONTENT_ITEM_TYPE.equals(mimeType)) 53 | { 54 | return ALARM_HANDLER; 55 | } 56 | if (Relation.CONTENT_ITEM_TYPE.equals(mimeType)) 57 | { 58 | return RELATION_HANDLER; 59 | } 60 | return DEFAULT_PROPERTY_HANDLER; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/handler/RelationHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.handler; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.Property.Relation; 21 | import org.dmfs.provider.tasks.TaskContract.Property.Relation.RelType; 22 | import org.dmfs.provider.tasks.TaskContract.Tasks; 23 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 24 | 25 | import android.content.ContentValues; 26 | import android.database.Cursor; 27 | import android.database.sqlite.SQLiteDatabase; 28 | 29 | 30 | /** 31 | * Handles any inserts, updates and deletes on the relations table. 32 | * 33 | * @author Marten Gajda 34 | */ 35 | public class RelationHandler extends PropertyHandler 36 | { 37 | 38 | @Override 39 | public ContentValues validateValues(SQLiteDatabase db, long taskId, long propertyId, boolean isNew, ContentValues values, boolean isSyncAdapter) 40 | { 41 | if (values.containsKey(Relation.RELATED_CONTENT_URI)) 42 | { 43 | throw new IllegalArgumentException("setting of RELATED_CONTENT_URI not allowed"); 44 | } 45 | 46 | Long id = values.getAsLong(Relation.RELATED_ID); 47 | String uid = values.getAsString(Relation.RELATED_UID); 48 | String uri = values.getAsString(Relation.RELATED_URI); 49 | 50 | if (id == null && uri == null && uid != null) 51 | { 52 | values.putNull(Relation.RELATED_ID); 53 | values.putNull(Relation.RELATED_URI); 54 | } 55 | else if (id == null && uid == null && uri != null) 56 | { 57 | values.putNull(Relation.RELATED_ID); 58 | values.putNull(Relation.RELATED_UID); 59 | } 60 | else if (id != null && uid == null && uri == null) 61 | { 62 | values.putNull(Relation.RELATED_URI); 63 | values.putNull(Relation.RELATED_UID); 64 | } 65 | else 66 | { 67 | throw new IllegalArgumentException("exactly one of RELATED_ID, RELATED_UID and RELATED_URI must be non-null"); 68 | } 69 | 70 | return values; 71 | } 72 | 73 | 74 | @Override 75 | public long insert(SQLiteDatabase db, long taskId, ContentValues values, boolean isSyncAdapter) 76 | { 77 | validateValues(db, taskId, -1, true, values, isSyncAdapter); 78 | resolveFields(db, values); 79 | updateParentId(db, taskId, values, null); 80 | return super.insert(db, taskId, values, isSyncAdapter); 81 | } 82 | 83 | 84 | @Override 85 | public int update(SQLiteDatabase db, long taskId, long propertyId, ContentValues values, Cursor oldValues, boolean isSyncAdapter) 86 | { 87 | validateValues(db, taskId, propertyId, false, values, isSyncAdapter); 88 | resolveFields(db, values); 89 | updateParentId(db, taskId, values, oldValues); 90 | return super.update(db, taskId, propertyId, values, oldValues, isSyncAdapter); 91 | } 92 | 93 | 94 | @Override 95 | public int delete(SQLiteDatabase db, long taskId, long propertyId, Cursor oldValues, boolean isSyncAdapter) 96 | { 97 | clearParentId(db, taskId, oldValues); 98 | return super.delete(db, taskId, propertyId, oldValues, isSyncAdapter); 99 | } 100 | 101 | 102 | /** 103 | * Resolve _id or _uid, depending of which value is given. We can't resolve anything if only {@link Relation#RELATED_URI} is 104 | * given. The given values are update in-place. 105 | *

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

108 | * 109 | * @param db 110 | * The task database. 111 | * @param values 112 | * The {@link ContentValues}. 113 | */ 114 | private void resolveFields(SQLiteDatabase db, ContentValues values) 115 | { 116 | Long id = values.getAsLong(Relation.RELATED_ID); 117 | String uid = values.getAsString(Relation.RELATED_UID); 118 | 119 | if (id != null) 120 | { 121 | values.put(Relation.RELATED_UID, resolveTaskStringField(db, Tasks._ID, id.toString(), Tasks._UID)); 122 | } 123 | else if (uid != null) 124 | { 125 | values.put(Relation.RELATED_ID, resolveTaskLongField(db, Tasks._UID, uid, Tasks._ID)); 126 | } 127 | } 128 | 129 | 130 | private Long resolveTaskLongField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) 131 | { 132 | String result = resolveTaskStringField(db, selectionField, selectionValue, resultField); 133 | if (result != null) 134 | { 135 | return Long.parseLong(result); 136 | } 137 | return null; 138 | } 139 | 140 | 141 | private String resolveTaskStringField(SQLiteDatabase db, String selectionField, String selectionValue, String resultField) 142 | { 143 | Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, new String[] { resultField }, selectionField + "=?", new String[] { selectionValue }, null, null, 144 | null); 145 | if (c != null) 146 | { 147 | try 148 | { 149 | if (c.moveToNext()) 150 | { 151 | return c.getString(0); 152 | } 153 | } 154 | finally 155 | { 156 | c.close(); 157 | } 158 | } 159 | return null; 160 | } 161 | 162 | 163 | /** 164 | * Update {@link Tasks#PARENT_ID} when a parent is assigned to a child. 165 | * 166 | * @param db 167 | * @param taskId 168 | * @param values 169 | * @param oldValues 170 | */ 171 | private void updateParentId(SQLiteDatabase db, long taskId, ContentValues values, Cursor oldValues) 172 | { 173 | int type; 174 | if (values.containsKey(Relation.RELATED_TYPE)) 175 | { 176 | type = values.getAsInteger(Relation.RELATED_TYPE); 177 | } 178 | else 179 | { 180 | type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); 181 | } 182 | 183 | if (type == RelType.PARENT.ordinal()) 184 | { 185 | // this is a link to the parent, we need to update the PARENT_ID of this task, if we can 186 | 187 | if (values.containsKey(Relation.RELATED_ID)) 188 | { 189 | ContentValues taskValues = new ContentValues(1); 190 | taskValues.put(Tasks.PARENT_ID, values.getAsLong(Relation.RELATED_ID)); 191 | db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); 192 | } 193 | // else: the parent task is probably not synced yet, we have to fix this in RelationUpdaterHook 194 | } 195 | else if (type == RelType.CHILD.ordinal()) 196 | { 197 | // this is a link to a child, we need to update the PARENT_ID of the linked task 198 | 199 | if (values.getAsLong(Relation.RELATED_ID) != null) 200 | { 201 | ContentValues taskValues = new ContentValues(1); 202 | taskValues.put(Tasks.PARENT_ID, taskId); 203 | db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + values.getAsLong(Relation.RELATED_ID), null); 204 | } 205 | // else: the child task is probably not synced yet, we have to fix this in RelationUpdaterHook 206 | } 207 | else if (type == RelType.SIBLING.ordinal()) 208 | { 209 | // this is a link to a sibling, we need to copy the PARENT_ID of the linked task to this task 210 | if (values.getAsLong(Relation.RELATED_ID) != null) 211 | { 212 | // get the parent of the other task first 213 | Long otherParent = resolveTaskLongField(db, Tasks._ID, values.getAsString(Relation.RELATED_ID), Tasks.PARENT_ID); 214 | 215 | ContentValues taskValues = new ContentValues(1); 216 | taskValues.put(Tasks.PARENT_ID, otherParent); 217 | db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); 218 | } 219 | // else: the sibling task is probably not synced yet, we have to fix this in RelationUpdaterHook 220 | } 221 | } 222 | 223 | 224 | /** 225 | * Clear {@link Tasks#PARENT_ID} if a link is removed. 226 | * 227 | * @param db 228 | * @param taskId 229 | * @param oldValues 230 | */ 231 | private void clearParentId(SQLiteDatabase db, long taskId, Cursor oldValues) 232 | { 233 | int type = oldValues.getInt(oldValues.getColumnIndex(Relation.RELATED_TYPE)); 234 | 235 | /* 236 | * 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 237 | * parent relationship has been created and the old one is removed afterwards. In that case we can not simply clear the PARENT_ID. 238 | * 239 | * FIXME: For now we ignore that fact. But we should fix it. 240 | */ 241 | 242 | if (type == RelType.PARENT.ordinal()) 243 | { 244 | // this was a link to the parent, we're orphaned now, so clear PARENT_ID of this task 245 | 246 | ContentValues taskValues = new ContentValues(1); 247 | taskValues.putNull(Tasks.PARENT_ID); 248 | db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + taskId, null); 249 | } 250 | else if (type == RelType.CHILD.ordinal()) 251 | { 252 | // this was a link to a child, the child is orphaned now, clear its PARENT_ID 253 | 254 | int relIdCol = oldValues.getColumnIndex(Relation.RELATED_ID); 255 | if (!oldValues.isNull(relIdCol)) 256 | { 257 | ContentValues taskValues = new ContentValues(1); 258 | taskValues.putNull(Tasks.PARENT_ID); 259 | db.update(TaskDatabaseHelper.Tables.TASKS, taskValues, Tasks._ID + "=" + oldValues.getLong(relIdCol), null); 260 | } 261 | } 262 | else if (type == RelType.SIBLING.ordinal()) 263 | { 264 | /* 265 | * 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. 266 | * 267 | * FIXME: properly handle this case 268 | */ 269 | } 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/AbstractListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 22 | 23 | import android.content.ContentUris; 24 | import android.content.ContentValues; 25 | import android.net.Uri; 26 | 27 | 28 | /** 29 | * An abstract implementation of a {@link ListAdapter} to server as the base for more concrete adapters. 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public abstract class AbstractListAdapter implements ListAdapter 34 | { 35 | private final ContentValues mState = new ContentValues(10); 36 | 37 | 38 | @Override 39 | public Uri uri(String authority) 40 | { 41 | return ContentUris.withAppendedId(TaskContract.TaskLists.getContentUri(authority), id()); 42 | } 43 | 44 | 45 | @Override 46 | public T getState(FieldAdapter stateFieldAdater) 47 | { 48 | return stateFieldAdater.getFrom(mState); 49 | } 50 | 51 | 52 | @Override 53 | public void setState(FieldAdapter stateFieldAdater, T value) 54 | { 55 | stateFieldAdater.setIn(mState, value); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/AbstractTaskAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 22 | 23 | import android.content.ContentUris; 24 | import android.content.ContentValues; 25 | import android.net.Uri; 26 | 27 | 28 | /** 29 | * An abstract implementation of a {@link TaskAdapter} to server as the base for more concrete adapters. 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public abstract class AbstractTaskAdapter implements TaskAdapter 34 | { 35 | private final ContentValues mState = new ContentValues(10); 36 | 37 | 38 | @Override 39 | public Uri uri(String authority) 40 | { 41 | return ContentUris.withAppendedId(TaskContract.Tasks.getContentUri(authority), id()); 42 | } 43 | 44 | 45 | @Override 46 | public boolean isRecurring() 47 | { 48 | return valueOf(RRULE) != null || valueOf(RDATE) != null; 49 | } 50 | 51 | 52 | @Override 53 | public boolean recurrenceUpdated() 54 | { 55 | return isUpdated(RRULE) || isUpdated(DTSTART) || isUpdated(DUE) || isUpdated(DURATION) || isUpdated(RDATE) || isUpdated(EXDATE); 56 | } 57 | 58 | 59 | @Override 60 | public T getState(FieldAdapter stateFieldAdater) 61 | { 62 | return stateFieldAdater.getFrom(mState); 63 | } 64 | 65 | 66 | @Override 67 | public void setState(FieldAdapter stateFieldAdater, T value) 68 | { 69 | stateFieldAdater.setIn(mState, value); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/ContentValuesListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 22 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 23 | 24 | import android.content.ContentValues; 25 | import android.database.sqlite.SQLiteDatabase; 26 | 27 | 28 | /** 29 | * @author Marten Gajda 30 | */ 31 | public class ContentValuesListAdapter extends AbstractListAdapter 32 | { 33 | private long mId; 34 | private final ContentValues mValues; 35 | 36 | 37 | public ContentValuesListAdapter(ContentValues values) 38 | { 39 | this(-1L, values); 40 | } 41 | 42 | 43 | public ContentValuesListAdapter(long id, ContentValues values) 44 | { 45 | mId = id; 46 | mValues = values; 47 | } 48 | 49 | 50 | @Override 51 | public long id() 52 | { 53 | return mId; 54 | } 55 | 56 | 57 | @Override 58 | public T valueOf(FieldAdapter fieldAdapter) 59 | { 60 | return fieldAdapter.getFrom(mValues); 61 | } 62 | 63 | 64 | @Override 65 | public T oldValueOf(FieldAdapter fieldAdapter) 66 | { 67 | return null; 68 | } 69 | 70 | 71 | @Override 72 | public boolean isUpdated(FieldAdapter fieldAdapter) 73 | { 74 | return fieldAdapter.isSetIn(mValues); 75 | } 76 | 77 | 78 | @Override 79 | public boolean isWriteable() 80 | { 81 | return true; 82 | } 83 | 84 | 85 | @Override 86 | public boolean hasUpdates() 87 | { 88 | return mValues.size() > 0; 89 | } 90 | 91 | 92 | @Override 93 | public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException 94 | { 95 | fieldAdapter.setIn(mValues, value); 96 | } 97 | 98 | 99 | @Override 100 | public void unset(FieldAdapter fieldAdapter) throws IllegalStateException 101 | { 102 | fieldAdapter.removeFrom(mValues); 103 | } 104 | 105 | 106 | @Override 107 | public int commit(SQLiteDatabase db) 108 | { 109 | if (mValues.size() == 0) 110 | { 111 | return 0; 112 | } 113 | 114 | if (mId < 0) 115 | { 116 | mId = db.insert(TaskDatabaseHelper.Tables.LISTS, null, mValues); 117 | return mId > 0 ? 1 : 0; 118 | } 119 | else 120 | { 121 | return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); 122 | } 123 | } 124 | 125 | 126 | @Override 127 | public ListAdapter duplicate() 128 | { 129 | return new ContentValuesListAdapter(new ContentValues(mValues)); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/ContentValuesTaskAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 22 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 23 | 24 | import android.content.ContentValues; 25 | import android.database.sqlite.SQLiteDatabase; 26 | 27 | 28 | /** 29 | * A {@link TaskAdapter} for tasks that are stored in a {@link ContentValues}. 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public class ContentValuesTaskAdapter extends AbstractTaskAdapter 34 | { 35 | private long mId; 36 | private final ContentValues mValues; 37 | 38 | 39 | public ContentValuesTaskAdapter(ContentValues values) 40 | { 41 | this(-1L, values); 42 | } 43 | 44 | 45 | public ContentValuesTaskAdapter(long id, ContentValues values) 46 | { 47 | mId = id; 48 | mValues = values; 49 | } 50 | 51 | 52 | @Override 53 | public long id() 54 | { 55 | return mId; 56 | } 57 | 58 | 59 | @Override 60 | public T valueOf(FieldAdapter fieldAdapter) 61 | { 62 | return fieldAdapter.getFrom(mValues); 63 | } 64 | 65 | 66 | @Override 67 | public T oldValueOf(FieldAdapter fieldAdapter) 68 | { 69 | return null; 70 | } 71 | 72 | 73 | @Override 74 | public boolean isUpdated(FieldAdapter fieldAdapter) 75 | { 76 | return fieldAdapter.isSetIn(mValues); 77 | } 78 | 79 | 80 | @Override 81 | public boolean isWriteable() 82 | { 83 | return true; 84 | } 85 | 86 | 87 | @Override 88 | public boolean hasUpdates() 89 | { 90 | return mValues.size() > 0; 91 | } 92 | 93 | 94 | @Override 95 | public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException 96 | { 97 | fieldAdapter.setIn(mValues, value); 98 | } 99 | 100 | 101 | @Override 102 | public void unset(FieldAdapter fieldAdapter) throws IllegalStateException 103 | { 104 | fieldAdapter.removeFrom(mValues); 105 | } 106 | 107 | 108 | @Override 109 | public int commit(SQLiteDatabase db) 110 | { 111 | if (mValues.size() == 0) 112 | { 113 | return 0; 114 | } 115 | 116 | if (mId < 0) 117 | { 118 | mId = db.insert(TaskDatabaseHelper.Tables.TASKS, null, mValues); 119 | return mId > 0 ? 1 : 0; 120 | } 121 | else 122 | { 123 | return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); 124 | } 125 | } 126 | 127 | 128 | @Override 129 | public TaskAdapter duplicate() 130 | { 131 | return new ContentValuesTaskAdapter(new ContentValues(mValues)); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/CursorContentValuesListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 22 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 23 | 24 | import android.content.ContentValues; 25 | import android.database.Cursor; 26 | import android.database.sqlite.SQLiteDatabase; 27 | 28 | 29 | /** 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public class CursorContentValuesListAdapter extends AbstractListAdapter 34 | { 35 | private final long mId; 36 | private final Cursor mCursor; 37 | private final ContentValues mValues; 38 | 39 | 40 | public CursorContentValuesListAdapter(long id, Cursor cursor, ContentValues values) 41 | { 42 | mId = id; 43 | mCursor = cursor; 44 | mValues = values; 45 | } 46 | 47 | 48 | @Override 49 | public long id() 50 | { 51 | return mId; 52 | } 53 | 54 | 55 | @Override 56 | public T valueOf(FieldAdapter fieldAdapter) 57 | { 58 | return fieldAdapter.getFrom(mCursor, mValues); 59 | } 60 | 61 | 62 | @Override 63 | public T oldValueOf(FieldAdapter fieldAdapter) 64 | { 65 | return fieldAdapter.getFrom(mCursor); 66 | } 67 | 68 | 69 | @Override 70 | public boolean isUpdated(FieldAdapter fieldAdapter) 71 | { 72 | return mValues != null && fieldAdapter.isSetIn(mValues); 73 | } 74 | 75 | 76 | @Override 77 | public boolean isWriteable() 78 | { 79 | return true; 80 | } 81 | 82 | 83 | @Override 84 | public boolean hasUpdates() 85 | { 86 | return mValues != null && mValues.size() > 0; 87 | } 88 | 89 | 90 | @Override 91 | public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException 92 | { 93 | fieldAdapter.setIn(mValues, value); 94 | } 95 | 96 | 97 | @Override 98 | public void unset(FieldAdapter fieldAdapter) throws IllegalStateException 99 | { 100 | fieldAdapter.removeFrom(mValues); 101 | } 102 | 103 | 104 | @Override 105 | public int commit(SQLiteDatabase db) 106 | { 107 | if (mValues.size() == 0) 108 | { 109 | return 0; 110 | } 111 | 112 | return db.update(TaskDatabaseHelper.Tables.LISTS, mValues, TaskContract.TaskListColumns._ID + "=" + mId, null); 113 | } 114 | 115 | 116 | @Override 117 | public ListAdapter duplicate() 118 | { 119 | ContentValues newValues = new ContentValues(mValues); 120 | 121 | // copy all columns (except _ID) that are not in the values yet 122 | for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) 123 | { 124 | String column = mCursor.getColumnName(i); 125 | if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) 126 | { 127 | newValues.put(column, mCursor.getString(i)); 128 | } 129 | } 130 | 131 | return new ContentValuesListAdapter(newValues); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/CursorContentValuesTaskAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 22 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 23 | 24 | import android.content.ContentValues; 25 | import android.database.Cursor; 26 | import android.database.sqlite.SQLiteDatabase; 27 | 28 | 29 | /** 30 | * A {@link TaskAdapter} that adapts a {@link Cursor} and a {@link ContentValues} instance. All changes are written to the {@link ContentValues} and can be 31 | * stored in the database with {@link #commit(SQLiteDatabase)}. 32 | * 33 | * @author Marten Gajda 34 | */ 35 | public class CursorContentValuesTaskAdapter extends AbstractTaskAdapter 36 | { 37 | private final long mId; 38 | private final Cursor mCursor; 39 | private final ContentValues mValues; 40 | 41 | 42 | public CursorContentValuesTaskAdapter(Cursor cursor, ContentValues values) 43 | { 44 | if (cursor == null && !_ID.existsIn(values)) 45 | { 46 | mId = -1L; 47 | } 48 | else 49 | { 50 | mId = _ID.getFrom(cursor); 51 | } 52 | mCursor = cursor; 53 | mValues = values; 54 | } 55 | 56 | 57 | public CursorContentValuesTaskAdapter(long id, Cursor cursor, ContentValues values) 58 | { 59 | mId = id; 60 | mCursor = cursor; 61 | mValues = values; 62 | } 63 | 64 | 65 | @Override 66 | public long id() 67 | { 68 | return mId; 69 | } 70 | 71 | 72 | @Override 73 | public T valueOf(FieldAdapter fieldAdapter) 74 | { 75 | if (mValues == null) 76 | { 77 | return fieldAdapter.getFrom(mCursor); 78 | } 79 | return fieldAdapter.getFrom(mCursor, mValues); 80 | } 81 | 82 | 83 | @Override 84 | public T oldValueOf(FieldAdapter fieldAdapter) 85 | { 86 | return fieldAdapter.getFrom(mCursor); 87 | } 88 | 89 | 90 | @Override 91 | public boolean isUpdated(FieldAdapter fieldAdapter) 92 | { 93 | return mValues != null && fieldAdapter.isSetIn(mValues); 94 | } 95 | 96 | 97 | @Override 98 | public boolean isWriteable() 99 | { 100 | return mValues != null; 101 | } 102 | 103 | 104 | @Override 105 | public boolean hasUpdates() 106 | { 107 | return mValues != null && mValues.size() > 0; 108 | } 109 | 110 | 111 | @Override 112 | public void set(FieldAdapter fieldAdapter, T value) throws IllegalStateException 113 | { 114 | fieldAdapter.setIn(mValues, value); 115 | } 116 | 117 | 118 | @Override 119 | public void unset(FieldAdapter fieldAdapter) throws IllegalStateException 120 | { 121 | fieldAdapter.removeFrom(mValues); 122 | } 123 | 124 | 125 | @Override 126 | public int commit(SQLiteDatabase db) 127 | { 128 | if (mValues.size() == 0) 129 | { 130 | return 0; 131 | } 132 | 133 | return db.update(TaskDatabaseHelper.Tables.TASKS, mValues, TaskContract.TaskColumns._ID + "=" + mId, null); 134 | } 135 | 136 | 137 | @Override 138 | public TaskAdapter duplicate() 139 | { 140 | ContentValues newValues = new ContentValues(mValues); 141 | 142 | // copy all columns (except _ID) that are not in the values yet 143 | for (int i = 0, count = mCursor.getColumnCount(); i < count; ++i) 144 | { 145 | String column = mCursor.getColumnName(i); 146 | if (!newValues.containsKey(column) && !TaskContract.Tasks._ID.equals(column)) 147 | { 148 | newValues.put(column, mCursor.getString(i)); 149 | } 150 | } 151 | 152 | return new ContentValuesTaskAdapter(newValues); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/EntityAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.model.adapters.FieldAdapter; 21 | 22 | import android.content.ContentValues; 23 | import android.database.Cursor; 24 | import android.database.sqlite.SQLiteDatabase; 25 | import android.net.Uri; 26 | 27 | 28 | /** 29 | * Adapter to read values of a specific entity type from primitive data sets like {@link Cursor}s or {@link ContentValues}s. 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public interface EntityAdapter 34 | { 35 | /** 36 | * Returns the row id of the entity or -1 if the entity has not been stored yet. 37 | * 38 | * @return The entity row id or -1. 39 | */ 40 | public long id(); 41 | 42 | 43 | /** 44 | * Returns the {@link Uri} of the entity using the given authority. 45 | * 46 | * @param authority 47 | * The authority of this provider. 48 | * @return A {@link Uri} or null if this entity has not been stored yet. 49 | */ 50 | public Uri uri(String authority); 51 | 52 | 53 | /** 54 | * Returns the value identified by the given {@link FieldAdapter}. 55 | * 56 | * @param fieldAdapter 57 | * The {@link FieldAdapter} of the value to return. 58 | * @return The value, maybe be null. 59 | */ 60 | public T valueOf(FieldAdapter fieldAdapter); 61 | 62 | 63 | /** 64 | * Returns the old value identified by the given {@link FieldAdapter}. This will be equal to the value returned by {@link #valueOf(FieldAdapter)} unless it 65 | * has been overridden, in which case this returns the former value. 66 | * 67 | * @param fieldAdapter 68 | * The {@link FieldAdapter} of the value to return. 69 | * @return The value, maybe be null. 70 | */ 71 | public T oldValueOf(FieldAdapter fieldAdapter); 72 | 73 | 74 | /** 75 | * Returns whether the given field has been overridden or not. 76 | * 77 | * @param fieldAdapter 78 | * The {@link FieldAdapter} of the field to check. 79 | * @return true if the field has been overridden, false otherwise. 80 | */ 81 | public boolean isUpdated(FieldAdapter fieldAdapter); 82 | 83 | 84 | /** 85 | * Returns whether this adapter supports modifying values. 86 | * 87 | * @return true if the task values can be changed by this adapter, false otherwise. 88 | */ 89 | public boolean isWriteable(); 90 | 91 | 92 | /** 93 | * Returns whether any value has been modified. 94 | * 95 | * @return true if there are modified values, false otherwise. 96 | */ 97 | public boolean hasUpdates(); 98 | 99 | 100 | /** 101 | * Sets a value of the adapted entity. The value is identified by a {@link FieldAdapter}. 102 | * 103 | * @param fieldAdapter 104 | * The {@link FieldAdapter} of the value to set. 105 | * @param value 106 | * The new value. 107 | */ 108 | public void set(FieldAdapter fieldAdapter, T value); 109 | 110 | 111 | /** 112 | * Remove a value from the change set. In effect the respective field will keep it's old value. 113 | * 114 | * @param fieldAdapter 115 | * The {@link FieldAdapter} of the field to un-set. 116 | */ 117 | public void unset(FieldAdapter fieldAdapter); 118 | 119 | 120 | /** 121 | * Commit all changes to the database. 122 | * 123 | * @param db 124 | * A writable database. 125 | * @return The number of entries affected. This may be 0 if no fields have been changed. 126 | */ 127 | public int commit(SQLiteDatabase db); 128 | 129 | 130 | /** 131 | * 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 132 | * {@link EntityAdapter} and will be lost once it gets garbage collected. 133 | * 134 | * @param stateFieldAdater 135 | * The {@link FieldAdapter} of a state field. 136 | * @return The value of the state field. 137 | */ 138 | public T getState(FieldAdapter stateFieldAdater); 139 | 140 | 141 | /** 142 | * 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. 143 | * 144 | * @param stateFieldAdater 145 | * The {@link FieldAdapter} of the state field to set. 146 | * @param value 147 | * The new state value. 148 | */ 149 | public void setState(FieldAdapter stateFieldAdater, T value); 150 | 151 | 152 | /*** 153 | * Creates a {@link EntityAdapter} for a new entity initialized with the values of this entity (except for _ID). 154 | * 155 | * @return A new {@link EntityAdapter} having the same values. 156 | */ 157 | public EntityAdapter duplicate(); 158 | } 159 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/ListAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.TaskLists; 21 | import org.dmfs.provider.tasks.model.adapters.IntegerFieldAdapter; 22 | import org.dmfs.provider.tasks.model.adapters.LongFieldAdapter; 23 | import org.dmfs.provider.tasks.model.adapters.StringFieldAdapter; 24 | 25 | import android.content.ContentValues; 26 | import android.database.Cursor; 27 | 28 | 29 | /** 30 | * Adapter to read list values from primitive data sets like {@link Cursor}s or {@link ContentValues}s. 31 | * 32 | * @author Marten Gajda 33 | */ 34 | public interface ListAdapter extends EntityAdapter 35 | { 36 | /** 37 | * Adapter for the row id of a task list. 38 | */ 39 | public final static LongFieldAdapter _ID = new LongFieldAdapter(TaskLists._ID); 40 | 41 | /** 42 | * Adapter for the _sync_id of a list. 43 | */ 44 | public final static StringFieldAdapter SYNC_ID = new StringFieldAdapter(TaskLists._SYNC_ID); 45 | 46 | /** 47 | * Adapter for the sync version of a list. 48 | */ 49 | public final static StringFieldAdapter SYNC_VERSION = new StringFieldAdapter(TaskLists.SYNC_VERSION); 50 | 51 | /** 52 | * Adapter for the account name of a list. 53 | */ 54 | public final static StringFieldAdapter ACCOUNT_NAME = new StringFieldAdapter(TaskLists.ACCOUNT_NAME); 55 | 56 | /** 57 | * Adapter for the account type of a list. 58 | */ 59 | public final static StringFieldAdapter ACCOUNT_TYPE = new StringFieldAdapter(TaskLists.ACCOUNT_TYPE); 60 | 61 | /** 62 | * Adapter for the owner of a list. 63 | */ 64 | public final static StringFieldAdapter OWNER = new StringFieldAdapter(TaskLists.OWNER); 65 | 66 | /** 67 | * Adapter for the name of a list. 68 | */ 69 | public final static StringFieldAdapter LIST_NAME = new StringFieldAdapter(TaskLists.LIST_NAME); 70 | 71 | /** 72 | * Adapter for the color of a list. 73 | */ 74 | public final static IntegerFieldAdapter LIST_COLOR = new IntegerFieldAdapter(TaskLists.LIST_COLOR); 75 | 76 | 77 | /*** 78 | * Creates a {@link ListAdapter} for a new task initialized with the values of this task (except for _ID). 79 | * 80 | * @return A new task having the same values. 81 | */ 82 | @Override 83 | public ListAdapter duplicate(); 84 | } 85 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/BinaryFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a binary value from a {@link Cursor} or {@link ContentValues}. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the entity the field belongs to. 31 | */ 32 | public final class BinaryFieldAdapter extends SimpleFieldAdapter 33 | { 34 | 35 | /** 36 | * The field name this adapter uses to store the values. 37 | */ 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link BinaryFieldAdapter}. 43 | * 44 | * @param fieldName 45 | * The name of the field to use when loading or storing the value. 46 | */ 47 | public BinaryFieldAdapter(String fieldName) 48 | { 49 | if (fieldName == null) 50 | { 51 | throw new IllegalArgumentException("fieldName must not be null"); 52 | } 53 | mFieldName = fieldName; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public byte[] getFrom(ContentValues values) 66 | { 67 | return values.getAsByteArray(mFieldName); 68 | } 69 | 70 | 71 | @Override 72 | public byte[] getFrom(Cursor cursor) 73 | { 74 | int columnIdx = cursor.getColumnIndex(mFieldName); 75 | if (columnIdx < 0) 76 | { 77 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 78 | } 79 | return cursor.isNull(columnIdx) ? null : cursor.getBlob(columnIdx); 80 | } 81 | 82 | 83 | @Override 84 | public void setIn(ContentValues values, byte[] value) 85 | { 86 | if (value != null) 87 | { 88 | values.put(mFieldName, value); 89 | } 90 | else 91 | { 92 | values.putNull(mFieldName); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/BooleanFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a {@link Boolean} value from a {@link Cursor} or {@link ContentValues}. 26 | *

27 | * Implementation detail: 28 | *

29 | * The values are loaded and stored as 0 (for false) and 1 (for true). 30 | * 31 | * @author Marten Gajda 32 | * 33 | * @param 34 | * The type of the entity the field belongs to. 35 | */ 36 | public final class BooleanFieldAdapter extends SimpleFieldAdapter 37 | { 38 | 39 | /** 40 | * The field name this adapter uses to store the values. 41 | */ 42 | private final String mFieldName; 43 | 44 | 45 | /** 46 | * Constructor for a new {@link BooleanFieldAdapter}. 47 | * 48 | * @param fieldName 49 | * The name of the field to use when loading or storing the value. 50 | */ 51 | public BooleanFieldAdapter(String fieldName) 52 | { 53 | if (fieldName == null) 54 | { 55 | throw new IllegalArgumentException("fieldName must not be null"); 56 | } 57 | mFieldName = fieldName; 58 | } 59 | 60 | 61 | @Override 62 | String fieldName() 63 | { 64 | return mFieldName; 65 | } 66 | 67 | 68 | @Override 69 | public Boolean getFrom(ContentValues values) 70 | { 71 | Integer value = values.getAsInteger(mFieldName); 72 | 73 | return value != null && value > 0; 74 | } 75 | 76 | 77 | @Override 78 | public Boolean getFrom(Cursor cursor) 79 | { 80 | int columnIdx = cursor.getColumnIndex(mFieldName); 81 | if (columnIdx < 0) 82 | { 83 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 84 | } 85 | return !cursor.isNull(columnIdx) && cursor.getInt(columnIdx) > 0; 86 | } 87 | 88 | 89 | @Override 90 | public void setIn(ContentValues values, Boolean value) 91 | { 92 | values.put(mFieldName, value ? 1 : 0); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/DateTimeArrayFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import java.io.IOException; 21 | import java.util.TimeZone; 22 | import java.util.regex.Pattern; 23 | 24 | import org.dmfs.rfc5545.DateTime; 25 | 26 | import android.content.ContentValues; 27 | import android.database.Cursor; 28 | 29 | 30 | /** 31 | * Knows how to load and store arrays of {@link DateTime} values from a {@link Cursor} or {@link ContentValues}. 32 | * 33 | * @author Marten Gajda 34 | * 35 | * @param 36 | * The type of the entity the field belongs to. 37 | */ 38 | public final class DateTimeArrayFieldAdapter extends SimpleFieldAdapter 39 | { 40 | private final static Pattern SEPARATOR_PATTERN = Pattern.compile(","); 41 | 42 | private final String mDateTimeListFieldName; 43 | private final String mTimeZoneFieldName; 44 | 45 | 46 | /** 47 | * Constructor for a new {@link DateTimeArrayFieldAdapter}. 48 | * 49 | * @param datetimeListFieldName 50 | * The name of the field that holds the {@link DateTime} list. 51 | * @param timezoneFieldName 52 | * The name of the field that holds the time zone name. 53 | */ 54 | public DateTimeArrayFieldAdapter(String datetimeListFieldName, String timezoneFieldName) 55 | { 56 | if (datetimeListFieldName == null) 57 | { 58 | throw new IllegalArgumentException("datetimeListFieldName must not be null"); 59 | } 60 | mDateTimeListFieldName = datetimeListFieldName; 61 | mTimeZoneFieldName = timezoneFieldName; 62 | } 63 | 64 | 65 | @Override 66 | String fieldName() 67 | { 68 | return mDateTimeListFieldName; 69 | } 70 | 71 | 72 | @Override 73 | public DateTime[] getFrom(ContentValues values) 74 | { 75 | String datetimeList = values.getAsString(mDateTimeListFieldName); 76 | if (datetimeList == null) 77 | { 78 | // no list, return null 79 | return null; 80 | } 81 | 82 | // create a new TimeZone for the given time zone string 83 | String timezoneString = mTimeZoneFieldName == null ? null : values.getAsString(mTimeZoneFieldName); 84 | TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); 85 | 86 | String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); 87 | 88 | DateTime[] result = new DateTime[datetimes.length]; 89 | for (int i = 0, count = datetimes.length; i < count; ++i) 90 | { 91 | DateTime value = DateTime.parse(timeZone, datetimes[i]); 92 | 93 | if (!value.isAllDay() && value.isFloating()) 94 | { 95 | throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); 96 | } 97 | 98 | result[i] = value; 99 | if (i > 0 && result[0].isAllDay() != value.isAllDay()) 100 | { 101 | throw new IllegalArgumentException("DateTime values must all be of the same type."); 102 | } 103 | } 104 | 105 | return result; 106 | } 107 | 108 | 109 | @Override 110 | public DateTime[] getFrom(Cursor cursor) 111 | { 112 | int tdLIdx = cursor.getColumnIndex(mDateTimeListFieldName); 113 | int tzIdx = mTimeZoneFieldName == null ? -1 : cursor.getColumnIndex(mTimeZoneFieldName); 114 | 115 | if (tdLIdx < 0 || (mTimeZoneFieldName != null && tzIdx < 0)) 116 | { 117 | throw new IllegalArgumentException("At least one column is missing in cursor."); 118 | } 119 | 120 | if (cursor.isNull(tdLIdx)) 121 | { 122 | // if the time stamp list is null we return null 123 | return null; 124 | } 125 | 126 | String datetimeList = cursor.getString(tdLIdx); 127 | 128 | // create a new TimeZone for the given time zone string 129 | String timezoneString = mTimeZoneFieldName == null ? null : cursor.getString(tzIdx); 130 | TimeZone timeZone = timezoneString == null ? null : TimeZone.getTimeZone(timezoneString); 131 | 132 | String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); 133 | 134 | DateTime[] result = new DateTime[datetimes.length]; 135 | for (int i = 0, count = datetimes.length; i < count; ++i) 136 | { 137 | DateTime value = DateTime.parse(timeZone, datetimes[i]); 138 | 139 | if (!value.isAllDay() && value.isFloating()) 140 | { 141 | throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); 142 | } 143 | 144 | result[i] = value; 145 | if (i > 0 && result[0].isAllDay() != value.isAllDay()) 146 | { 147 | throw new IllegalArgumentException("DateTime values must all be of the same type."); 148 | } 149 | } 150 | 151 | return result; 152 | } 153 | 154 | 155 | @Override 156 | public DateTime[] getFrom(Cursor cursor, ContentValues values) 157 | { 158 | int tsIdx; 159 | int tzIdx; 160 | String datetimeList; 161 | String timeZoneId = null; 162 | 163 | if (values != null && values.containsKey(mDateTimeListFieldName)) 164 | { 165 | if (values.getAsLong(mDateTimeListFieldName) == null) 166 | { 167 | // the date times are null, so we return null 168 | return null; 169 | } 170 | datetimeList = values.getAsString(mDateTimeListFieldName); 171 | } 172 | else if (cursor != null && (tsIdx = cursor.getColumnIndex(mDateTimeListFieldName)) >= 0) 173 | { 174 | if (cursor.isNull(tsIdx)) 175 | { 176 | // the date times are null, so we return null 177 | return null; 178 | } 179 | datetimeList = cursor.getString(tsIdx); 180 | } 181 | else 182 | { 183 | throw new IllegalArgumentException("Missing date time list column."); 184 | } 185 | 186 | if (mTimeZoneFieldName != null) 187 | { 188 | if (values != null && values.containsKey(mTimeZoneFieldName)) 189 | { 190 | timeZoneId = values.getAsString(mTimeZoneFieldName); 191 | } 192 | else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTimeZoneFieldName)) >= 0) 193 | { 194 | timeZoneId = cursor.getString(tzIdx); 195 | } 196 | else 197 | { 198 | throw new IllegalArgumentException("Missing timezone column."); 199 | } 200 | } 201 | 202 | // create a new TimeZone for the given time zone string 203 | TimeZone timeZone = timeZoneId == null ? null : TimeZone.getTimeZone(timeZoneId); 204 | 205 | String[] datetimes = SEPARATOR_PATTERN.split(datetimeList); 206 | 207 | DateTime[] result = new DateTime[datetimes.length]; 208 | for (int i = 0, count = datetimes.length; i < count; ++i) 209 | { 210 | DateTime value = DateTime.parse(timeZone, datetimes[i]); 211 | 212 | if (!value.isAllDay() && value.isFloating()) 213 | { 214 | throw new IllegalArgumentException("DateTime values must not be floating, unless they are all-day."); 215 | } 216 | 217 | result[i] = value; 218 | if (i > 0 && result[0].isAllDay() != value.isAllDay()) 219 | { 220 | throw new IllegalArgumentException("DateTime values must all be of the same type."); 221 | } 222 | } 223 | 224 | return result; 225 | } 226 | 227 | 228 | @Override 229 | public void setIn(ContentValues values, DateTime[] value) 230 | { 231 | if (value != null && value.length > 0) 232 | { 233 | try 234 | { 235 | // Note: we only store the datetime strings, not the timezone 236 | StringBuilder result = new StringBuilder(value.length * 17 /* this is the maximum length */); 237 | 238 | boolean first = true; 239 | for (DateTime datetime : value) 240 | { 241 | if (first) 242 | { 243 | first = false; 244 | } 245 | else 246 | { 247 | result.append(','); 248 | } 249 | DateTime outvalue = datetime.isFloating() ? datetime : datetime.shiftTimeZone(DateTime.UTC); 250 | outvalue.writeTo(result); 251 | } 252 | values.put(mDateTimeListFieldName, result.toString()); 253 | } 254 | catch (IOException e) 255 | { 256 | throw new RuntimeException("Can not serialize datetime list."); 257 | } 258 | 259 | } 260 | else 261 | { 262 | values.put(mDateTimeListFieldName, (Long) null); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/DateTimeFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import java.util.TimeZone; 21 | 22 | import org.dmfs.rfc5545.DateTime; 23 | 24 | import android.content.ContentValues; 25 | import android.database.Cursor; 26 | 27 | 28 | /** 29 | * Knows how to load and store {@link DateTime} values from a {@link Cursor} or {@link ContentValues}. 30 | * 31 | * {@link DateTime} values are stored as three separate values: 32 | *

    33 | *
  • a timestamp in milliseconds since the epoch
  • 34 | *
  • a time zone
  • 35 | *
  • an allday flag
  • 36 | *
37 | * 38 | * 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. 39 | * 40 | * @author Marten Gajda 41 | * 42 | * @param 43 | * The type of the entity the field belongs to. 44 | */ 45 | public final class DateTimeFieldAdapter extends SimpleFieldAdapter 46 | { 47 | private final String mTimestampField; 48 | private final String mTzField; 49 | private final String mAllDayField; 50 | private final boolean mAllDayDefault; 51 | 52 | 53 | /** 54 | * Constructor for a new {@link DateTimeFieldAdapter}. 55 | * 56 | * @param timestampField 57 | * The name of the field that holds the time stamp in milliseconds. 58 | * @param tzField 59 | * 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. 60 | * @param alldayField 61 | * 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 62 | * non-allday. 63 | */ 64 | public DateTimeFieldAdapter(String timestampField, String tzField, String alldayField) 65 | { 66 | if (timestampField == null) 67 | { 68 | throw new IllegalArgumentException("timestampField must not be null"); 69 | } 70 | mTimestampField = timestampField; 71 | mTzField = tzField; 72 | mAllDayField = alldayField; 73 | mAllDayDefault = false; 74 | } 75 | 76 | 77 | @Override 78 | String fieldName() 79 | { 80 | return mTimestampField; 81 | } 82 | 83 | 84 | @Override 85 | public DateTime getFrom(ContentValues values) 86 | { 87 | Long timestamp = values.getAsLong(mTimestampField); 88 | if (timestamp == null) 89 | { 90 | // if the time stamp is null we return null 91 | return null; 92 | } 93 | // create a new Time for the given time zone, falling back to UTC if none is given 94 | String timezone = mTzField == null ? null : values.getAsString(mTzField); 95 | DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); 96 | 97 | // cache mAlldayField locally 98 | String allDayField = mAllDayField; 99 | 100 | // set the allday flag appropriately 101 | Integer allDayInt = allDayField == null ? null : values.getAsInteger(allDayField); 102 | 103 | if ((allDayInt != null && allDayInt != 0) || (allDayField == null && mAllDayDefault)) 104 | { 105 | value = value.toAllDay(); 106 | } 107 | 108 | return value; 109 | } 110 | 111 | 112 | @Override 113 | public DateTime getFrom(Cursor cursor) 114 | { 115 | int tsIdx = cursor.getColumnIndex(mTimestampField); 116 | int tzIdx = mTzField == null ? -1 : cursor.getColumnIndex(mTzField); 117 | int adIdx = mAllDayField == null ? -1 : cursor.getColumnIndex(mAllDayField); 118 | 119 | if (tsIdx < 0 || (mTzField != null && tzIdx < 0) || (mAllDayField != null && adIdx < 0)) 120 | { 121 | throw new IllegalArgumentException("At least one column is missing in cursor."); 122 | } 123 | 124 | if (cursor.isNull(tsIdx)) 125 | { 126 | // if the time stamp is null we return null 127 | return null; 128 | } 129 | 130 | Long timestamp = cursor.getLong(tsIdx); 131 | 132 | // create a new Time for the given time zone, falling back to UTC if none is given 133 | String timezone = mTzField == null ? null : cursor.getString(tzIdx); 134 | DateTime value = new DateTime(timezone == null ? DateTime.UTC : TimeZone.getTimeZone(timezone), timestamp); 135 | 136 | // set the allday flag appropriately 137 | Integer allDayInt = adIdx < 0 ? null : cursor.getInt(adIdx); 138 | 139 | if ((allDayInt != null && allDayInt != 0) || (mAllDayField == null && mAllDayDefault)) 140 | { 141 | value = value.toAllDay(); 142 | } 143 | return value; 144 | } 145 | 146 | 147 | @Override 148 | public DateTime getFrom(Cursor cursor, ContentValues values) 149 | { 150 | int tsIdx; 151 | int tzIdx; 152 | int adIdx; 153 | long timestamp; 154 | String timeZoneId = null; 155 | Integer allDay = 0; 156 | 157 | if (values != null && values.containsKey(mTimestampField)) 158 | { 159 | if (values.getAsLong(mTimestampField) == null) 160 | { 161 | // if the time stamp is null we return null 162 | return null; 163 | } 164 | timestamp = values.getAsLong(mTimestampField); 165 | } 166 | else if (cursor != null && (tsIdx = cursor.getColumnIndex(mTimestampField)) >= 0) 167 | { 168 | if (cursor.isNull(tsIdx)) 169 | { 170 | // if the time stamp is null we return null 171 | return null; 172 | } 173 | timestamp = cursor.getLong(tsIdx); 174 | } 175 | else 176 | { 177 | throw new IllegalArgumentException("Missing timestamp column."); 178 | } 179 | 180 | if (mTzField != null) 181 | { 182 | if (values != null && values.containsKey(mTzField)) 183 | { 184 | timeZoneId = values.getAsString(mTzField); 185 | } 186 | else if (cursor != null && (tzIdx = cursor.getColumnIndex(mTzField)) >= 0) 187 | { 188 | timeZoneId = cursor.getString(tzIdx); 189 | } 190 | else 191 | { 192 | throw new IllegalArgumentException("Missing timezone column."); 193 | } 194 | } 195 | 196 | if (mAllDayField != null) 197 | { 198 | if (values != null && values.containsKey(mAllDayField)) 199 | { 200 | allDay = values.getAsInteger(mAllDayField); 201 | } 202 | else if (cursor != null && (adIdx = cursor.getColumnIndex(mAllDayField)) >= 0) 203 | { 204 | allDay = cursor.getInt(adIdx); 205 | } 206 | else 207 | { 208 | throw new IllegalArgumentException("Missing timezone column."); 209 | } 210 | } 211 | 212 | // create a new Time for the given time zone, falling back to UTC if none is given 213 | DateTime value = new DateTime(timeZoneId == null ? DateTime.UTC : TimeZone.getTimeZone(timeZoneId), timestamp); 214 | 215 | if (allDay != 0) 216 | { 217 | value = value.toAllDay(); 218 | } 219 | return value; 220 | } 221 | 222 | 223 | @Override 224 | public void setIn(ContentValues values, DateTime value) 225 | { 226 | if (value != null) 227 | { 228 | // just store all three parts separately 229 | values.put(mTimestampField, value.getTimestamp()); 230 | 231 | if (mTzField != null) 232 | { 233 | TimeZone timezone = value.getTimeZone(); 234 | values.put(mTzField, timezone == null ? null : timezone.getID()); 235 | } 236 | if (mAllDayField != null) 237 | { 238 | values.put(mAllDayField, value.isAllDay() ? 1 : 0); 239 | } 240 | } 241 | else 242 | { 243 | // write timestamp only, other fields may still use allday and timezone 244 | values.put(mTimestampField, (Long) null); 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/DurationFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import org.dmfs.rfc5545.Duration; 21 | 22 | import android.content.ContentValues; 23 | import android.database.Cursor; 24 | 25 | 26 | /** 27 | * Knows how to load and store {@link Duration} values from a {@link Cursor} or {@link ContentValues}. 28 | * 29 | * @author Marten Gajda 30 | * 31 | * @param 32 | * The type of the entity the field belongs to. 33 | */ 34 | public final class DurationFieldAdapter extends SimpleFieldAdapter 35 | { 36 | 37 | private final String mFieldName; 38 | 39 | 40 | /** 41 | * Constructor for a new {@link DurationFieldAdapter}. 42 | * 43 | * @param urlField 44 | * The field name that holds the {@link Duration}. 45 | */ 46 | public DurationFieldAdapter(String urlField) 47 | { 48 | if (urlField == null) 49 | { 50 | throw new IllegalArgumentException("urlField must not be null"); 51 | } 52 | mFieldName = urlField; 53 | } 54 | 55 | 56 | @Override 57 | String fieldName() 58 | { 59 | return mFieldName; 60 | } 61 | 62 | 63 | @Override 64 | public Duration getFrom(ContentValues values) 65 | { 66 | String rawValue = values.getAsString(mFieldName); 67 | if (rawValue == null) 68 | { 69 | return null; 70 | } 71 | 72 | return Duration.parse(rawValue); 73 | } 74 | 75 | 76 | @Override 77 | public Duration getFrom(Cursor cursor) 78 | { 79 | int columnIdx = cursor.getColumnIndex(mFieldName); 80 | if (columnIdx < 0) 81 | { 82 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 83 | } 84 | 85 | if (cursor.isNull(columnIdx)) 86 | { 87 | return null; 88 | } 89 | 90 | return Duration.parse(cursor.getString(columnIdx)); 91 | } 92 | 93 | 94 | @Override 95 | public void setIn(ContentValues values, Duration value) 96 | { 97 | if (value != null) 98 | { 99 | values.put(mFieldName, value.toString()); 100 | } 101 | else 102 | { 103 | values.putNull(mFieldName); 104 | } 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/FieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a specific field from or to {@link ContentValues} or from {@link Cursor}s. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the value this adapter stores. 31 | * @param 32 | * The type of the entity the field belongs to. 33 | */ 34 | public interface FieldAdapter 35 | { 36 | 37 | /** 38 | * Check if a value is present and non-null in the given {@link ContentValues}. 39 | * 40 | * @param values 41 | * The {@link ContentValues} to check. 42 | * @return 43 | */ 44 | public boolean existsIn(ContentValues values); 45 | 46 | 47 | /** 48 | * Check if a value is present (may be null) in the given {@link ContentValues}. 49 | * 50 | * @param values 51 | * The {@link ContentValues} to check. 52 | * @return 53 | */ 54 | public boolean isSetIn(ContentValues values); 55 | 56 | 57 | /** 58 | * Get the value from the given {@link ContentValues} 59 | * 60 | * @param values 61 | * The {@link ContentValues} that contain the value to return. 62 | * @return The value. 63 | */ 64 | public FieldType getFrom(ContentValues values); 65 | 66 | 67 | /** 68 | * Check if a value is present and non-null in the given {@link Cursor}. 69 | * 70 | * @param cursor 71 | * The {@link Cursor} that contains the value to check. 72 | * @return 73 | */ 74 | public boolean existsIn(Cursor cursor); 75 | 76 | 77 | /** 78 | * Get the value from the given {@link Cursor} 79 | * 80 | * @param cursor 81 | * The {@link Cursor} that contain the value to return. 82 | * @return The value. 83 | */ 84 | public FieldType getFrom(Cursor cursor); 85 | 86 | 87 | /** 88 | * Check if a value is present and non-null in the given {@link Cursor} or {@link ContentValues}. 89 | * 90 | * @param cursor 91 | * The {@link Cursor} that contains the value to check. 92 | * @param values 93 | * The {@link ContentValues} that contains the value to check. 94 | * @return 95 | */ 96 | public boolean existsIn(Cursor cursor, ContentValues values); 97 | 98 | 99 | /** 100 | * Get the value from the given {@link Cursor} or {@link ContentValues}, with the {@link ContentValues} taking precedence over the cursor values. 101 | * 102 | * @param cursor 103 | * The {@link Cursor} that contains the value to return. 104 | * @param values 105 | * The {@link ContentValues} that contains the value to return. 106 | * @return The value. 107 | */ 108 | public FieldType getFrom(Cursor cursor, ContentValues values); 109 | 110 | 111 | /** 112 | * Set a value in the given {@link ContentValues}. 113 | * 114 | * @param values 115 | * The {@link ContentValues} to store the new value in. 116 | * @param value 117 | * The new value to store. 118 | */ 119 | public void setIn(ContentValues values, FieldType value); 120 | 121 | 122 | /** 123 | * Remove a value from the given {@link ContentValues}. 124 | * 125 | * @param values 126 | * The {@link ContentValues} from which to remove the value. 127 | */ 128 | public void removeFrom(ContentValues values); 129 | 130 | 131 | /** 132 | * Copy the value from a {@link Cursor} to the given {@link ContentValues}. 133 | * 134 | * @param source 135 | * The {@link Cursor} that contains the value to copy. 136 | * @param dest 137 | * The {@link ContentValues} to receive the value. 138 | */ 139 | public void copyValue(Cursor source, ContentValues dest); 140 | 141 | 142 | /** 143 | * Copy the value from {@link ContentValues} to another {@link ContentValues} object. 144 | * 145 | * @param source 146 | * The {@link ContentValues} that contains the value to copy. 147 | * @param dest 148 | * The {@link ContentValues} to receive the value. 149 | */ 150 | public void copyValue(ContentValues source, ContentValues dest); 151 | 152 | } 153 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/FloatFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a {@link Float} value from a {@link Cursor} or {@link ContentValues}. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the entity the field belongs to. 31 | */ 32 | public final class FloatFieldAdapter extends SimpleFieldAdapter 33 | { 34 | 35 | /** 36 | * The field name this adapter uses to store the values. 37 | */ 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link FloatFieldAdapter}. 43 | * 44 | * @param fieldName 45 | * The name of the field to use when loading or storing the value. 46 | */ 47 | public FloatFieldAdapter(String fieldName) 48 | { 49 | if (fieldName == null) 50 | { 51 | throw new IllegalArgumentException("fieldName must not be null"); 52 | } 53 | mFieldName = fieldName; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public Float getFrom(ContentValues values) 66 | { 67 | return values.getAsFloat(mFieldName); 68 | } 69 | 70 | 71 | @Override 72 | public Float getFrom(Cursor cursor) 73 | { 74 | int columnIdx = cursor.getColumnIndex(mFieldName); 75 | if (columnIdx < 0) 76 | { 77 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 78 | } 79 | return cursor.isNull(columnIdx) ? null : cursor.getFloat(columnIdx); 80 | } 81 | 82 | 83 | @Override 84 | public void setIn(ContentValues values, Float value) 85 | { 86 | if (value != null) 87 | { 88 | values.put(mFieldName, value); 89 | } 90 | else 91 | { 92 | values.putNull(mFieldName); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/IntegerFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store an {@link Integer} from a {@link Cursor} or {@link ContentValues}. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the entity the field belongs to. 31 | */ 32 | public final class IntegerFieldAdapter extends SimpleFieldAdapter 33 | { 34 | 35 | /** 36 | * The field name this adapter uses to store the values. 37 | */ 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link IntegerFieldAdapter}. 43 | * 44 | * @param fieldName 45 | * The name of the field to use when loading or storing the value. 46 | */ 47 | public IntegerFieldAdapter(String fieldName) 48 | { 49 | if (fieldName == null) 50 | { 51 | throw new IllegalArgumentException("fieldName must not be null"); 52 | } 53 | mFieldName = fieldName; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public Integer getFrom(ContentValues values) 66 | { 67 | // return the value as Integer 68 | return values.getAsInteger(mFieldName); 69 | } 70 | 71 | 72 | @Override 73 | public Integer getFrom(Cursor cursor) 74 | { 75 | int columnIdx = cursor.getColumnIndex(mFieldName); 76 | if (columnIdx < 0) 77 | { 78 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 79 | } 80 | return cursor.isNull(columnIdx) ? null : cursor.getInt(columnIdx); 81 | } 82 | 83 | 84 | @Override 85 | public void setIn(ContentValues values, Integer value) 86 | { 87 | if (value != null) 88 | { 89 | values.put(mFieldName, value); 90 | } 91 | else 92 | { 93 | values.putNull(mFieldName); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/LongFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a {@link Long} value from a {@link Cursor} or {@link ContentValues}. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the entity the field belongs to. 31 | */ 32 | public final class LongFieldAdapter extends SimpleFieldAdapter 33 | { 34 | 35 | /** 36 | * The field name this adapter uses to store the values. 37 | */ 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link LongFieldAdapter}. 43 | * 44 | * @param fieldName 45 | * The name of the field to use when loading or storing the value. 46 | */ 47 | public LongFieldAdapter(String fieldName) 48 | { 49 | if (fieldName == null) 50 | { 51 | throw new IllegalArgumentException("fieldName must not be null"); 52 | } 53 | mFieldName = fieldName; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public Long getFrom(ContentValues values) 66 | { 67 | return values.getAsLong(mFieldName); 68 | } 69 | 70 | 71 | @Override 72 | public Long getFrom(Cursor cursor) 73 | { 74 | int columnIdx = cursor.getColumnIndex(mFieldName); 75 | if (columnIdx < 0) 76 | { 77 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 78 | } 79 | return cursor.isNull(columnIdx) ? null : cursor.getLong(columnIdx); 80 | } 81 | 82 | 83 | @Override 84 | public void setIn(ContentValues values, Long value) 85 | { 86 | if (value != null) 87 | { 88 | values.put(mFieldName, value); 89 | } 90 | else 91 | { 92 | values.putNull(mFieldName); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/RRuleFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import org.dmfs.rfc5545.recur.InvalidRecurrenceRuleException; 21 | import org.dmfs.rfc5545.recur.RecurrenceRule; 22 | 23 | import android.content.ContentValues; 24 | import android.database.Cursor; 25 | 26 | 27 | /** 28 | * Knows how to load and store a {@link RecurrenceRule} from a {@link Cursor} or {@link ContentValues}. 29 | * 30 | * @author Marten Gajda 31 | * 32 | * @param 33 | * The type of the entity the field belongs to. 34 | */ 35 | public final class RRuleFieldAdapter extends SimpleFieldAdapter 36 | { 37 | 38 | /** 39 | * The field name this adapter uses to store the values. 40 | */ 41 | private final String mFieldName; 42 | 43 | 44 | /** 45 | * Constructor for a new {@link RRuleFieldAdapter}. 46 | * 47 | * @param fieldName 48 | * The name of the field to use when loading or storing the value. 49 | */ 50 | public RRuleFieldAdapter(String fieldName) 51 | { 52 | if (fieldName == null) 53 | { 54 | throw new IllegalArgumentException("fieldName must not be null"); 55 | } 56 | mFieldName = fieldName; 57 | } 58 | 59 | 60 | @Override 61 | String fieldName() 62 | { 63 | return mFieldName; 64 | } 65 | 66 | 67 | @Override 68 | public RecurrenceRule getFrom(ContentValues values) 69 | { 70 | String rrule = values.getAsString(mFieldName); 71 | if (rrule == null) 72 | { 73 | return null; 74 | } 75 | try 76 | { 77 | return new RecurrenceRule(rrule); 78 | } 79 | catch (InvalidRecurrenceRuleException e) 80 | { 81 | throw new IllegalArgumentException("can not parse RRULE '" + rrule + "'", e); 82 | } 83 | } 84 | 85 | 86 | @Override 87 | public RecurrenceRule getFrom(Cursor cursor) 88 | { 89 | int columnIdx = cursor.getColumnIndex(mFieldName); 90 | if (columnIdx < 0) 91 | { 92 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 93 | } 94 | if (cursor.isNull(columnIdx)) 95 | { 96 | return null; 97 | } 98 | 99 | try 100 | { 101 | return new RecurrenceRule(cursor.getString(columnIdx)); 102 | } 103 | catch (InvalidRecurrenceRuleException e) 104 | { 105 | throw new IllegalArgumentException("can not parse RRULE '" + cursor.getString(columnIdx) + "'", e); 106 | } 107 | } 108 | 109 | 110 | @Override 111 | public void setIn(ContentValues values, RecurrenceRule value) 112 | { 113 | if (value != null) 114 | { 115 | values.put(mFieldName, value.toString()); 116 | } 117 | else 118 | { 119 | values.putNull(mFieldName); 120 | } 121 | } 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/SimpleFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * An abstract {@link FieldAdapter} that implements a couple of methods as used by most simple FieldAdapters. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The Type of the field this adapter handles. 31 | * 32 | * @param 33 | * The type of the entity the field belongs to. 34 | */ 35 | public abstract class SimpleFieldAdapter implements FieldAdapter 36 | { 37 | 38 | /** 39 | * Returns the sole field name of this adapter. 40 | * 41 | * @return 42 | */ 43 | abstract String fieldName(); 44 | 45 | 46 | @Override 47 | public boolean existsIn(ContentValues values) 48 | { 49 | return values.get(fieldName()) != null; 50 | } 51 | 52 | 53 | @Override 54 | public boolean isSetIn(ContentValues values) 55 | { 56 | return values.containsKey(fieldName()); 57 | } 58 | 59 | 60 | @Override 61 | public boolean existsIn(Cursor cursor) 62 | { 63 | int columnIdx = cursor.getColumnIndex(fieldName()); 64 | if (columnIdx < 0) 65 | { 66 | throw new IllegalArgumentException("The column '" + fieldName() + "' is missing in cursor."); 67 | } 68 | 69 | return !cursor.isNull(columnIdx); 70 | } 71 | 72 | 73 | @Override 74 | public FieldType getFrom(Cursor cursor, ContentValues values) 75 | { 76 | return values.containsKey(fieldName()) ? getFrom(values) : getFrom(cursor); 77 | } 78 | 79 | 80 | @Override 81 | public boolean existsIn(Cursor cursor, ContentValues values) 82 | { 83 | return existsIn(values) || existsIn(cursor); 84 | } 85 | 86 | 87 | @Override 88 | public void removeFrom(ContentValues values) 89 | { 90 | values.remove(fieldName()); 91 | } 92 | 93 | 94 | @Override 95 | public void copyValue(Cursor cursor, ContentValues values) 96 | { 97 | setIn(values, getFrom(cursor)); 98 | } 99 | 100 | 101 | @Override 102 | public void copyValue(ContentValues oldValues, ContentValues newValues) 103 | { 104 | setIn(newValues, getFrom(oldValues)); 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/StringFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import android.content.ContentValues; 21 | import android.database.Cursor; 22 | 23 | 24 | /** 25 | * Knows how to load and store a {@link String} value from a {@link Cursor} or {@link ContentValues}. 26 | * 27 | * @author Marten Gajda 28 | * 29 | * @param 30 | * The type of the entity the field belongs to. 31 | */ 32 | public final class StringFieldAdapter extends SimpleFieldAdapter 33 | { 34 | 35 | /** 36 | * The field name this adapter uses to store the values. 37 | */ 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link StringFieldAdapter}. 43 | * 44 | * @param fieldName 45 | * The name of the field to use when loading or storing the value. 46 | */ 47 | public StringFieldAdapter(String fieldName) 48 | { 49 | if (fieldName == null) 50 | { 51 | throw new IllegalArgumentException("fieldName must not be null"); 52 | } 53 | mFieldName = fieldName; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public String getFrom(ContentValues values) 66 | { 67 | // return the value as String 68 | return values.getAsString(mFieldName); 69 | } 70 | 71 | 72 | @Override 73 | public String getFrom(Cursor cursor) 74 | { 75 | int columnIdx = cursor.getColumnIndex(mFieldName); 76 | if (columnIdx < 0) 77 | { 78 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 79 | } 80 | return cursor.getString(columnIdx); 81 | } 82 | 83 | 84 | @Override 85 | public void setIn(ContentValues values, String value) 86 | { 87 | if (value != null) 88 | { 89 | values.put(mFieldName, value); 90 | } 91 | else 92 | { 93 | values.putNull(mFieldName); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/model/adapters/UrlFieldAdapter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.model.adapters; 19 | 20 | import java.net.URI; 21 | import java.net.URL; 22 | 23 | import android.content.ContentValues; 24 | import android.database.Cursor; 25 | 26 | 27 | /** 28 | * Knows how to load and store {@link URL} values from a {@link Cursor} or {@link ContentValues}. 29 | * 30 | * @author Marten Gajda 31 | * 32 | * @param 33 | * The type of the entity the field belongs to. 34 | */ 35 | public final class UrlFieldAdapter extends SimpleFieldAdapter 36 | { 37 | 38 | private final String mFieldName; 39 | 40 | 41 | /** 42 | * Constructor for a new {@link UrlFieldAdapter}. 43 | * 44 | * @param urlField 45 | * The field name that holds the URL. 46 | */ 47 | public UrlFieldAdapter(String urlField) 48 | { 49 | if (urlField == null) 50 | { 51 | throw new IllegalArgumentException("urlField must not be null"); 52 | } 53 | mFieldName = urlField; 54 | } 55 | 56 | 57 | @Override 58 | String fieldName() 59 | { 60 | return mFieldName; 61 | } 62 | 63 | 64 | @Override 65 | public URI getFrom(ContentValues values) 66 | { 67 | return values.get(mFieldName) == null ? null : URI.create(values.getAsString(mFieldName)); 68 | } 69 | 70 | 71 | @Override 72 | public URI getFrom(Cursor cursor) 73 | { 74 | int columnIdx = cursor.getColumnIndex(mFieldName); 75 | if (columnIdx < 0) 76 | { 77 | throw new IllegalArgumentException("The column '" + mFieldName + "' is missing in cursor."); 78 | } 79 | 80 | return cursor.isNull(columnIdx) ? null : URI.create(cursor.getString(columnIdx)); 81 | } 82 | 83 | 84 | @Override 85 | public void setIn(ContentValues values, URI value) 86 | { 87 | if (value != null) 88 | { 89 | values.put(mFieldName, value.toASCIIString()); 90 | } 91 | else 92 | { 93 | values.putNull(mFieldName); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/AbstractEntityProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors; 19 | 20 | import org.dmfs.provider.tasks.model.EntityAdapter; 21 | 22 | import android.database.sqlite.SQLiteDatabase; 23 | 24 | 25 | /** 26 | * A default implementation of {@link EntityProcessor} that does nothing. It can be used as the basis of concrete {@link EntityProcessor}s without having to 27 | * override all the methods. 28 | * 29 | * @author Marten Gajda 30 | */ 31 | public abstract class AbstractEntityProcessor> implements EntityProcessor 32 | { 33 | @Override 34 | public void beforeInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) 35 | { 36 | // the default implementation doesn't do anything 37 | } 38 | 39 | 40 | @Override 41 | public void afterInsert(SQLiteDatabase db, T list, boolean isSyncAdapter) 42 | { 43 | // the default implementation doesn't do anything 44 | } 45 | 46 | 47 | @Override 48 | public void beforeUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) 49 | { 50 | // the default implementation doesn't do anything 51 | } 52 | 53 | 54 | @Override 55 | public void afterUpdate(SQLiteDatabase db, T list, boolean isSyncAdapter) 56 | { 57 | // the default implementation doesn't do anything 58 | } 59 | 60 | 61 | @Override 62 | public void beforeDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) 63 | { 64 | // the default implementation doesn't do anything 65 | } 66 | 67 | 68 | @Override 69 | public void afterDelete(SQLiteDatabase db, T list, boolean isSyncAdapter) 70 | { 71 | // the default implementation doesn't do anything 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/EntityProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors; 19 | 20 | import org.dmfs.provider.tasks.model.EntityAdapter; 21 | 22 | import android.database.sqlite.SQLiteDatabase; 23 | 24 | 25 | /** 26 | * EntityProcessors are called before and after any operation on an entity. They can be used to perform additional operations for each entity. 27 | * 28 | * @param 29 | * The type of the entity adapter. 30 | * @author Marten Gajda 31 | */ 32 | public interface EntityProcessor> 33 | { 34 | /** 35 | * Called before an entity is inserted. 36 | * 37 | * @param db 38 | * A writable database. 39 | * @param entityAdapter 40 | * The {@link EntityAdapter} that's about to be inserted. You can modify the entity at this stage. {@link EntityAdapter#id()} will return an 41 | * invalid value. 42 | * @param isSyncAdapter 43 | */ 44 | public void beforeInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 45 | 46 | 47 | /** 48 | * Called after an entity has been inserted. 49 | * 50 | * @param db 51 | * A writable database. 52 | * @param entityAdapter 53 | * The {@link EntityAdapter} that's has been inserted. Modifying the entity has no effect. 54 | * @param isSyncAdapter 55 | */ 56 | public void afterInsert(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 57 | 58 | 59 | /** 60 | * Called before an entity is updated. 61 | * 62 | * @param db 63 | * A writable database. 64 | * @param entityAdapter 65 | * The {@link EntityAdapter} that's about to be updated. You can modify the entity at this stage. 66 | * @param isSyncAdapter 67 | */ 68 | public void beforeUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 69 | 70 | 71 | /** 72 | * Called after an entity has been updated. 73 | * 74 | * @param db 75 | * A writable database. 76 | * @param entityAdapter 77 | * The {@link EntityAdapter} that's has been updated. Modifying the entity has no effect. 78 | * @param isSyncAdapter 79 | */ 80 | public void afterUpdate(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 81 | 82 | 83 | /** 84 | * Called before an entity is deleted. 85 | *

86 | * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. 87 | * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only 88 | * once with isSyncAdapter == true. 89 | *

90 | *

91 | * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). 92 | *

93 | * 94 | * @param db 95 | * A writable database. 96 | * @param entityAdapter 97 | * The {@link EntityAdapter} that's about to be deleted. Modifying the entity has no effect. 98 | * @param isSyncAdapter 99 | */ 100 | public void beforeDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 101 | 102 | 103 | /** 104 | * Called after an entity is deleted. 105 | *

106 | * Note that may be called twice for each entity. Once when the entity is marked deleted by the UI and once when it's actually removed by the sync adapter. 107 | * Both cases can be distinguished by the isSyncAdapter parameter. If an entity is removed because it was deleted on the server, this will be called only 108 | * once with isSyncAdapter == true. 109 | *

110 | *

111 | * Also note that no processor is called when an entity is removed automatically by a database trigger (e.g. when an entire task list is removed). 112 | *

113 | * 114 | * @param db 115 | * A writable database. 116 | * @param entityAdapter 117 | * The {@link EntityAdapter} that was deleted. The value of {@link EntityAdapter#id()} contains the id of the deleted entity. Modifying the 118 | * entity has no effect. 119 | * @param isSyncAdapter 120 | */ 121 | public void afterDelete(SQLiteDatabase db, T entityAdapter, boolean isSyncAdapter); 122 | } 123 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/lists/ListExecutionProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.lists; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 22 | import org.dmfs.provider.tasks.model.ListAdapter; 23 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 24 | 25 | import android.database.sqlite.SQLiteDatabase; 26 | 27 | 28 | /** 29 | * A processor that performs the actual operations on task lists. 30 | * 31 | * @author Marten Gajda 32 | */ 33 | public class ListExecutionProcessor extends AbstractEntityProcessor 34 | { 35 | 36 | @Override 37 | public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 38 | { 39 | list.commit(db); 40 | } 41 | 42 | 43 | @Override 44 | public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 45 | { 46 | list.commit(db); 47 | } 48 | 49 | 50 | @Override 51 | public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 52 | { 53 | db.delete(Tables.LISTS, TaskContract.TaskLists._ID + "=" + list.id(), null); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/lists/ListValidatorProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.lists; 19 | 20 | import org.dmfs.provider.tasks.model.ListAdapter; 21 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 22 | 23 | import android.database.sqlite.SQLiteDatabase; 24 | import android.text.TextUtils; 25 | 26 | 27 | /** 28 | * A processor to validate the values of a task list. 29 | * 30 | * @author Marten Gajda 31 | */ 32 | public class ListValidatorProcessor extends AbstractEntityProcessor 33 | { 34 | @Override 35 | public void beforeInsert(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 36 | { 37 | if (!isSyncAdapter) 38 | { 39 | throw new UnsupportedOperationException("Caller must be a sync adapter to create task lists"); 40 | } 41 | 42 | if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_NAME))) 43 | { 44 | throw new IllegalArgumentException("ACCOUNT_NAME is required on INSERT"); 45 | } 46 | 47 | if (TextUtils.isEmpty(list.valueOf(ListAdapter.ACCOUNT_TYPE))) 48 | { 49 | throw new IllegalArgumentException("ACCOUNT_TYPE is required on INSERT"); 50 | } 51 | 52 | verifyCommon(list, isSyncAdapter); 53 | } 54 | 55 | 56 | @Override 57 | public void beforeUpdate(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 58 | { 59 | if (list.isUpdated(ListAdapter.ACCOUNT_NAME)) 60 | { 61 | throw new IllegalArgumentException("ACCOUNT_NAME is write-once"); 62 | } 63 | 64 | if (list.isUpdated(ListAdapter.ACCOUNT_TYPE)) 65 | { 66 | throw new IllegalArgumentException("ACCOUNT_TYPE is write-once"); 67 | } 68 | 69 | verifyCommon(list, isSyncAdapter); 70 | } 71 | 72 | 73 | @Override 74 | public void beforeDelete(SQLiteDatabase db, ListAdapter list, boolean isSyncAdapter) 75 | { 76 | if (!isSyncAdapter) 77 | { 78 | throw new UnsupportedOperationException("Caller must be a sync adapter to delete task lists"); 79 | } 80 | } 81 | 82 | 83 | /** 84 | * Performs tests that are common to insert an update operations. 85 | * 86 | * @param list 87 | * The {@link ListAdapter} to verify. 88 | * @param isSyncAdapter 89 | * true if the caller is a sync adapter, false otherwise. 90 | */ 91 | private void verifyCommon(ListAdapter list, boolean isSyncAdapter) 92 | { 93 | // row id can not be changed or set manually 94 | if (list.isUpdated(ListAdapter._ID)) 95 | { 96 | throw new IllegalArgumentException("_ID can not be set manually"); 97 | } 98 | 99 | if (isSyncAdapter) 100 | { 101 | // sync adapters may do all the stuff below 102 | return; 103 | } 104 | 105 | if (list.isUpdated(ListAdapter.LIST_COLOR)) 106 | { 107 | throw new IllegalArgumentException("Only sync adapters can change the LIST_COLOR."); 108 | } 109 | if (list.isUpdated(ListAdapter.LIST_NAME)) 110 | { 111 | throw new IllegalArgumentException("Only sync adapters can change the LIST_NAME."); 112 | } 113 | if (list.isUpdated(ListAdapter.SYNC_ID)) 114 | { 115 | throw new IllegalArgumentException("Only sync adapters can change the _SYNC_ID."); 116 | } 117 | if (list.isUpdated(ListAdapter.SYNC_VERSION)) 118 | { 119 | throw new IllegalArgumentException("Only sync adapters can change SYNC_VERSION."); 120 | } 121 | if (list.isUpdated(ListAdapter.OWNER)) 122 | { 123 | throw new IllegalArgumentException("Only sync adapters can change the list OWNER."); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/AutoUpdateProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskContract.Tasks; 22 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 23 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 24 | import org.dmfs.provider.tasks.model.TaskAdapter; 25 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 26 | import org.dmfs.rfc5545.DateTime; 27 | 28 | import android.content.ContentValues; 29 | import android.database.Cursor; 30 | import android.database.sqlite.SQLiteDatabase; 31 | 32 | 33 | /** 34 | * A processor to adjust some task values automatically. 35 | *

36 | * Other then recurrence exceptions no relations are handled by this code. Relation specific changes go to {@link RelationProcessor}. 37 | * 38 | * @author Marten Gajda 39 | */ 40 | public class AutoUpdateProcessor extends AbstractEntityProcessor 41 | { 42 | 43 | private static final String[] TASK_ID_PROJECTION = { Tasks._ID }; 44 | private static final String[] TASK_SYNC_ID_PROJECTION = { Tasks._SYNC_ID }; 45 | 46 | private static final String SYNC_ID_SELECTION = Tasks._SYNC_ID + "=?"; 47 | private static final String TASK_ID_SELECTION = Tasks._ID + "=?"; 48 | 49 | 50 | @Override 51 | public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 52 | { 53 | updateFields(db, task, isSyncAdapter); 54 | 55 | if (!isSyncAdapter) 56 | { 57 | // set created date for tasks created on the device 58 | task.set(TaskAdapter.CREATED, new DateTime(System.currentTimeMillis())); 59 | } 60 | } 61 | 62 | 63 | @Override 64 | public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 65 | { 66 | if (isSyncAdapter && task.isRecurring()) 67 | { 68 | // task is recurring, update ORIGINAL_INSTANCE_ID of all exceptions that may already exists 69 | ContentValues values = new ContentValues(1); 70 | TaskAdapter.ORIGINAL_INSTANCE_ID.setIn(values, task.id()); 71 | db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_SYNC_ID + "=? and " 72 | + TaskContract.Tasks.ORIGINAL_INSTANCE_ID + " is null", new String[] { task.valueOf(TaskAdapter.SYNC_ID) }); 73 | } 74 | } 75 | 76 | 77 | @Override 78 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 79 | { 80 | updateFields(db, task, isSyncAdapter); 81 | } 82 | 83 | 84 | @Override 85 | public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 86 | { 87 | if (isSyncAdapter && task.isRecurring() && task.isUpdated(TaskAdapter.SYNC_ID)) 88 | { 89 | // task is recurring, update ORIGINAL_INSTANCE_SYNC_ID of all exceptions that may already exists 90 | ContentValues values = new ContentValues(1); 91 | TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID.setIn(values, task.valueOf(TaskAdapter.SYNC_ID)); 92 | db.update(TaskDatabaseHelper.Tables.TASKS, values, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + task.id(), null); 93 | } 94 | } 95 | 96 | 97 | private void updateFields(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 98 | { 99 | if (!isSyncAdapter) 100 | { 101 | task.set(TaskAdapter._DIRTY, true); 102 | task.set(TaskAdapter.LAST_MODIFIED, new DateTime(System.currentTimeMillis())); 103 | 104 | // set proper STATUS if task has been completed 105 | if (task.valueOf(TaskAdapter.COMPLETED) != null && !task.isUpdated(TaskAdapter.STATUS)) 106 | { 107 | task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); 108 | } 109 | } 110 | 111 | if (task.isUpdated(TaskAdapter.PRIORITY)) 112 | { 113 | Integer priority = task.valueOf(TaskAdapter.PRIORITY); 114 | if (priority != null && priority == 0) 115 | { 116 | // replace priority 0 by null, it's the default and we need that for proper sorting 117 | task.set(TaskAdapter.PRIORITY, null); 118 | } 119 | } 120 | 121 | // Find corresponding ORIGINAL_INSTANCE_ID 122 | if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID)) 123 | { 124 | String[] syncId = { task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) }; 125 | Cursor cursor = db.query(Tables.TASKS, TASK_ID_PROJECTION, SYNC_ID_SELECTION, syncId, null, null, null); 126 | try 127 | { 128 | if (cursor.moveToNext()) 129 | { 130 | Long originalId = cursor.getLong(0); 131 | task.set(TaskAdapter.ORIGINAL_INSTANCE_ID, originalId); 132 | } 133 | } 134 | finally 135 | { 136 | if (cursor != null) 137 | { 138 | cursor.close(); 139 | } 140 | } 141 | } 142 | else if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) // Find corresponding ORIGINAL_INSTANCE_SYNC_ID 143 | { 144 | String[] id = { Long.toString(task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID)) }; 145 | Cursor cursor = db.query(Tables.TASKS, TASK_SYNC_ID_PROJECTION, TASK_ID_SELECTION, id, null, null, null); 146 | try 147 | { 148 | if (cursor.moveToNext()) 149 | { 150 | String originalSyncId = cursor.getString(0); 151 | task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, originalSyncId); 152 | } 153 | } 154 | finally 155 | { 156 | if (cursor != null) 157 | { 158 | cursor.close(); 159 | } 160 | } 161 | } 162 | 163 | // check that PERCENT_COMPLETE is an Integer between 0 and 100 if supplied also update status and completed accordingly 164 | if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) 165 | { 166 | Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); 167 | 168 | if (!isSyncAdapter && percent != null && percent == 100) 169 | { 170 | if (!task.isUpdated(TaskAdapter.STATUS)) 171 | { 172 | task.set(TaskAdapter.STATUS, Tasks.STATUS_COMPLETED); 173 | } 174 | 175 | if (!task.isUpdated(TaskAdapter.COMPLETED)) 176 | { 177 | task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); 178 | } 179 | } 180 | else if (!isSyncAdapter && percent != null) 181 | { 182 | if (!task.isUpdated(TaskAdapter.COMPLETED)) 183 | { 184 | task.set(TaskAdapter.COMPLETED, null); 185 | } 186 | } 187 | } 188 | 189 | // validate STATUS and set IS_NEW and IS_CLOSED accordingly 190 | if (task.isUpdated(TaskAdapter.STATUS) || task.id() < 0 /* this is true when the task is new */) 191 | { 192 | Integer status = task.valueOf(TaskAdapter.STATUS); 193 | if (status == null) 194 | { 195 | status = Tasks.STATUS_DEFAULT; 196 | task.set(TaskAdapter.STATUS, status); 197 | } 198 | 199 | task.set(TaskAdapter.IS_NEW, status == null || status == Tasks.STATUS_NEEDS_ACTION); 200 | task.set(TaskAdapter.IS_CLOSED, status != null && (status == Tasks.STATUS_COMPLETED || status == Tasks.STATUS_CANCELLED)); 201 | 202 | /* 203 | * 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 204 | * adapter. 205 | */ 206 | if (status == Tasks.STATUS_COMPLETED && !isSyncAdapter) 207 | { 208 | task.set(TaskAdapter.PERCENT_COMPLETE, 100); 209 | if (!task.isUpdated(TaskAdapter.COMPLETED)) 210 | { 211 | task.set(TaskAdapter.COMPLETED, new DateTime(System.currentTimeMillis())); 212 | } 213 | } 214 | else if (!isSyncAdapter) 215 | { 216 | task.set(TaskAdapter.COMPLETED, null); 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/ChangeListProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 22 | import org.dmfs.provider.tasks.model.CursorContentValuesTaskAdapter; 23 | import org.dmfs.provider.tasks.model.TaskAdapter; 24 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 25 | 26 | import android.content.ContentValues; 27 | import android.database.Cursor; 28 | import android.database.sqlite.SQLiteDatabase; 29 | 30 | 31 | /** 32 | * 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 33 | * operation. 34 | *

35 | * 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 36 | * to support moving exception sets of tasks without known master instance. 37 | * 38 | * @author Marten Gajda 39 | */ 40 | public class ChangeListProcessor extends AbstractEntityProcessor 41 | { 42 | 43 | @Override 44 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 45 | { 46 | if (isSyncAdapter) 47 | { 48 | // sync-adapters have to implement the move logic themselves 49 | return; 50 | } 51 | 52 | if (!task.isUpdated(TaskAdapter.LIST_ID)) 53 | { 54 | // list has not been changed 55 | return; 56 | } 57 | 58 | long oldList = task.oldValueOf(TaskAdapter.LIST_ID); 59 | long newList = task.valueOf(TaskAdapter.LIST_ID); 60 | 61 | if (oldList == newList) 62 | { 63 | // list has not been changed 64 | return; 65 | } 66 | 67 | Long newMasterId; 68 | Long deletedMasterId = null; 69 | 70 | if (task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null) 71 | { 72 | // this is an exception, move the master first 73 | newMasterId = task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID); 74 | if (newMasterId != null) 75 | { 76 | // find the master task 77 | Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks._ID + "=" + newMasterId, null, null, null, null); 78 | try 79 | { 80 | if (c.moveToFirst()) 81 | { 82 | // move the master task 83 | deletedMasterId = moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, null, true); 84 | } 85 | 86 | } 87 | finally 88 | { 89 | c.close(); 90 | } 91 | } 92 | 93 | // now move this exception, make sure we link the deleted exception to the deleted master 94 | moveTask(db, task, oldList, newList, deletedMasterId, false); 95 | } 96 | else 97 | { 98 | newMasterId = task.id(); 99 | // move the task to the new list 100 | deletedMasterId = moveTask(db, task, oldList, newList, null, false); 101 | } 102 | 103 | if (task.isRecurring() || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_ID) != null) 104 | { 105 | // This task is recurring and may have exceptions or it's an exception itself. Move all (other) exceptions to the new list. 106 | Cursor c = db.query(TaskDatabaseHelper.Tables.TASKS, null, TaskContract.Tasks.ORIGINAL_INSTANCE_ID + "=" + newMasterId + " and " 107 | + TaskContract.Tasks._ID + "!=" + task.id(), null, null, null, null); 108 | try 109 | { 110 | while (c.moveToNext()) 111 | { 112 | moveTask(db, new CursorContentValuesTaskAdapter(c, new ContentValues(16)), oldList, newList, deletedMasterId, true); 113 | } 114 | } 115 | finally 116 | { 117 | c.close(); 118 | } 119 | } 120 | 121 | } 122 | 123 | 124 | private Long moveTask(SQLiteDatabase db, TaskAdapter task, long oldList, long newList, Long deletedOriginalId, boolean commitTask) 125 | { 126 | /* 127 | * 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 128 | * different account or even account-type), so we emulate a copy & delete operation. 129 | * 130 | * 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 131 | * 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 132 | * handle that correctly. 133 | */ 134 | 135 | Long result = null; 136 | 137 | // 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) 138 | if (task.valueOf(TaskAdapter.SYNC_ID) != null || task.valueOf(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) != null 139 | || task.valueOf(TaskAdapter.SYNC_VERSION) != null) 140 | { 141 | TaskAdapter deletedTask = task.duplicate(); 142 | deletedTask.set(TaskAdapter.LIST_ID, oldList); 143 | deletedTask.set(TaskAdapter.ORIGINAL_INSTANCE_ID, deletedOriginalId); 144 | deletedTask.set(TaskAdapter._DELETED, true); 145 | 146 | // make sure we unset any values that do not exist in the tasks table 147 | deletedTask.unset(TaskAdapter.LIST_COLOR); 148 | deletedTask.unset(TaskAdapter.LIST_NAME); 149 | deletedTask.unset(TaskAdapter.ACCOUNT_NAME); 150 | deletedTask.unset(TaskAdapter.ACCOUNT_TYPE); 151 | deletedTask.unset(TaskAdapter.LIST_OWNER); 152 | deletedTask.unset(TaskAdapter.LIST_ACCESS_LEVEL); 153 | deletedTask.unset(TaskAdapter.LIST_VISIBLE); 154 | 155 | // create the deleted task 156 | deletedTask.commit(db); 157 | 158 | result = deletedTask.id(); 159 | } 160 | 161 | // clear all sync fields to convert the existing task to a new task 162 | task.set(TaskAdapter.LIST_ID, newList); 163 | task.set(TaskAdapter._DIRTY, true); 164 | task.set(TaskAdapter.SYNC1, null); 165 | task.set(TaskAdapter.SYNC2, null); 166 | task.set(TaskAdapter.SYNC3, null); 167 | task.set(TaskAdapter.SYNC4, null); 168 | task.set(TaskAdapter.SYNC5, null); 169 | task.set(TaskAdapter.SYNC6, null); 170 | task.set(TaskAdapter.SYNC7, null); 171 | task.set(TaskAdapter.SYNC8, null); 172 | task.set(TaskAdapter.SYNC_ID, null); 173 | task.set(TaskAdapter.SYNC_VERSION, null); 174 | task.set(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID, null); 175 | if (commitTask) 176 | { 177 | task.commit(db); 178 | } 179 | 180 | return result; 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/FtsProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.FTSDatabaseHelper; 21 | import org.dmfs.provider.tasks.model.TaskAdapter; 22 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 23 | 24 | import android.database.sqlite.SQLiteDatabase; 25 | 26 | 27 | /** 28 | * A {@link TaskProcessor} to update the fast text search table when inserting or updating a task. 29 | * 30 | * @author Marten Gajda 31 | */ 32 | public class FtsProcessor extends AbstractEntityProcessor 33 | { 34 | 35 | @Override 36 | public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 37 | { 38 | FTSDatabaseHelper.updateTaskFTSEntries(db, task); 39 | } 40 | 41 | 42 | @Override 43 | public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 44 | { 45 | FTSDatabaseHelper.updateTaskFTSEntries(db, task); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/RelationProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.Property.Relation; 21 | import org.dmfs.provider.tasks.TaskContract.Tasks; 22 | import org.dmfs.provider.tasks.TaskDatabaseHelper; 23 | import org.dmfs.provider.tasks.model.TaskAdapter; 24 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 25 | 26 | import android.content.ContentValues; 27 | import android.database.sqlite.SQLiteDatabase; 28 | 29 | 30 | /** 31 | * A processor that updates relations for new tasks. 32 | *

33 | * 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 34 | * {@link Relation#RELATED_ID} value. This processor updates the {@link Relation#RELATED_ID} when a task is inserted. 35 | *

36 | *

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

39 | * TODO: update {@link Tasks#PARENT_ID} of related tasks. 40 | * 41 | * @author Marten Gajda 42 | */ 43 | public class RelationProcessor extends AbstractEntityProcessor 44 | { 45 | 46 | @Override 47 | public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 48 | { 49 | // A new task has been inserted by the sync adapter. Update all relations that point to this task. 50 | 51 | if (!isSyncAdapter) 52 | { 53 | // the task was created on the device, so it doesn't have a UID 54 | return; 55 | } 56 | 57 | String uid = task.valueOf(TaskAdapter._UID); 58 | 59 | if (uid != null) 60 | { 61 | ContentValues v = new ContentValues(1); 62 | v.put(Relation.RELATED_ID, task.id()); 63 | 64 | db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_UID + "=?", new String[] { 65 | Relation.CONTENT_ITEM_TYPE, uid }); 66 | } 67 | } 68 | 69 | 70 | @Override 71 | public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 72 | { 73 | // A task has been updated and may have received a UID by the sync adapter. Update all by-id references to this task. 74 | 75 | if (!isSyncAdapter) 76 | { 77 | // only sync adapters may assign a UID 78 | return; 79 | } 80 | 81 | String uid = task.valueOf(TaskAdapter._UID); 82 | 83 | if (uid != null) 84 | { 85 | ContentValues v = new ContentValues(1); 86 | v.put(Relation.RELATED_UID, uid); 87 | 88 | db.update(TaskDatabaseHelper.Tables.PROPERTIES, v, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { 89 | Relation.CONTENT_ITEM_TYPE, Long.toString(task.id()) }); 90 | } 91 | } 92 | 93 | 94 | @Override 95 | public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 96 | { 97 | if (!isSyncAdapter) 98 | { 99 | // remove once the deletion is final, which is when the sync adapter removes it 100 | return; 101 | } 102 | 103 | db.delete(TaskDatabaseHelper.Tables.PROPERTIES, Relation.MIMETYPE + "= ? AND " + Relation.RELATED_ID + "=?", new String[] { Relation.CONTENT_ITEM_TYPE, 104 | Long.toString(task.id()) }); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/TaskExecutionProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.TaskContract; 21 | import org.dmfs.provider.tasks.TaskContract.TaskColumns; 22 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 23 | import org.dmfs.provider.tasks.model.TaskAdapter; 24 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 25 | 26 | import android.database.sqlite.SQLiteDatabase; 27 | 28 | 29 | /** 30 | * A processor that perfomrs the actual operations on tasks. 31 | * 32 | * @author Marten Gajda 33 | */ 34 | public class TaskExecutionProcessor extends AbstractEntityProcessor 35 | { 36 | 37 | @Override 38 | public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 39 | { 40 | task.commit(db); 41 | } 42 | 43 | 44 | @Override 45 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 46 | { 47 | task.commit(db); 48 | } 49 | 50 | 51 | @Override 52 | public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 53 | { 54 | String accountType = task.valueOf(TaskAdapter.ACCOUNT_TYPE); 55 | 56 | if (isSyncAdapter || TaskContract.LOCAL_ACCOUNT_TYPE.equals(accountType)) 57 | { 58 | // this is a local task or it' removed by a sync adapter, in either case we delete it right away 59 | db.delete(Tables.TASKS, TaskColumns._ID + "=" + task.id(), null); 60 | } 61 | else 62 | { 63 | // just set the deleted flag otherwise 64 | task.set(TaskAdapter._DELETED, true); 65 | task.commit(db); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/TaskInstancesProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import java.sql.RowId; 21 | import java.util.TimeZone; 22 | 23 | import org.dmfs.provider.tasks.TaskContract; 24 | import org.dmfs.provider.tasks.TaskContract.Instances; 25 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 26 | import org.dmfs.provider.tasks.model.TaskAdapter; 27 | import org.dmfs.provider.tasks.model.adapters.BooleanFieldAdapter; 28 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 29 | import org.dmfs.rfc5545.DateTime; 30 | import org.dmfs.rfc5545.Duration; 31 | 32 | import android.content.ContentValues; 33 | import android.database.sqlite.SQLiteDatabase; 34 | import android.net.Uri; 35 | 36 | 37 | /** 38 | * A processor that creates or updates any instance values for a task. 39 | *

40 | * TODO: At present this does not support recurrence. 41 | * 42 | * @author Marten Gajda 43 | */ 44 | public class TaskInstancesProcessor extends AbstractEntityProcessor 45 | { 46 | 47 | /** 48 | * 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 49 | * force an update of the sorting values when the local timezone has been changed. 50 | */ 51 | private final static BooleanFieldAdapter UPDATE_REQUESTED = new BooleanFieldAdapter( 52 | "org.dmfs.tasks.TaskInstanceProcessor.UPDATE_REQUESTED"); 53 | 54 | 55 | /** 56 | * Add a pseudo column to the given {@link ContentValues} to request an instances update, even if no time value has changed. 57 | * 58 | * @param values 59 | * The {@link ContentValues} to add the pseudo column to. 60 | */ 61 | public static void addUpdateRequest(ContentValues values) 62 | { 63 | UPDATE_REQUESTED.setIn(values, true); 64 | } 65 | 66 | 67 | @Override 68 | public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 69 | { 70 | createInstances(db, task); 71 | } 72 | 73 | 74 | @Override 75 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 76 | { 77 | // move the UPDATE requested value to the state 78 | if (task.isUpdated(UPDATE_REQUESTED)) 79 | { 80 | task.setState(UPDATE_REQUESTED, task.valueOf(UPDATE_REQUESTED)); 81 | task.unset(UPDATE_REQUESTED); 82 | } 83 | } 84 | 85 | 86 | @Override 87 | public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 88 | { 89 | if (!task.isUpdated(TaskAdapter.DTSTART) && !task.isUpdated(TaskAdapter.DUE) && !task.isUpdated(TaskAdapter.DURATION) 90 | && !task.getState(UPDATE_REQUESTED)) 91 | { 92 | // date values didn't change and update not requested 93 | return; 94 | } 95 | updateInstances(db, task); 96 | } 97 | 98 | 99 | /** 100 | * Create new {@link ContentValues} for insertion into the instances table. 101 | * 102 | * @param task 103 | * The {@link TaskAdapter} of the task that's about to be inserted. 104 | * @return {@link ContentValues} of the instance of this task. 105 | */ 106 | private ContentValues generateInstanceValues(TaskAdapter task) 107 | { 108 | ContentValues instanceValues = new ContentValues(); 109 | 110 | // get the relevant values from values 111 | DateTime dtstart = task.valueOf(TaskAdapter.DTSTART); 112 | DateTime due = task.valueOf(TaskAdapter.DUE); 113 | Duration duration = task.valueOf(TaskAdapter.DURATION); 114 | 115 | TimeZone localTz = TimeZone.getDefault(); 116 | 117 | if (dtstart != null) 118 | { 119 | // copy dtstart as is 120 | instanceValues.put(Instances.INSTANCE_START, dtstart.getTimestamp()); 121 | instanceValues.put(Instances.INSTANCE_START_SORTING, dtstart.isAllDay() ? dtstart.getInstance() : dtstart.shiftTimeZone(localTz).getInstance()); 122 | } 123 | else 124 | { 125 | instanceValues.putNull(Instances.INSTANCE_START); 126 | instanceValues.putNull(Instances.INSTANCE_START_SORTING); 127 | } 128 | 129 | if (due != null) 130 | { 131 | // copy due and calculate the actual duration, if any 132 | instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); 133 | instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); 134 | if (dtstart != null) 135 | { 136 | instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); 137 | } 138 | else 139 | { 140 | instanceValues.putNull(Instances.INSTANCE_DURATION); 141 | } 142 | } 143 | else if (duration != null) 144 | { 145 | if (dtstart != null) 146 | { 147 | // calculate the actual due value from dtstart and the duration string 148 | due = dtstart.addDuration(duration); 149 | instanceValues.put(Instances.INSTANCE_DUE, due.getTimestamp()); 150 | instanceValues.put(Instances.INSTANCE_DUE_SORTING, due.isAllDay() ? due.getInstance() : due.shiftTimeZone(localTz).getInstance()); 151 | instanceValues.put(Instances.INSTANCE_DURATION, due.getTimestamp() - dtstart.getTimestamp()); 152 | } 153 | else 154 | { 155 | // this case should be filtered by TaskValidatorProcessor, since setting a DURATION without DTSTART is invalid 156 | instanceValues.putNull(Instances.INSTANCE_DURATION); 157 | instanceValues.putNull(Instances.INSTANCE_DUE); 158 | instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); 159 | } 160 | } 161 | else 162 | { 163 | instanceValues.putNull(Instances.INSTANCE_DURATION); 164 | instanceValues.putNull(Instances.INSTANCE_DUE); 165 | instanceValues.putNull(Instances.INSTANCE_DUE_SORTING); 166 | } 167 | return instanceValues; 168 | } 169 | 170 | 171 | /** 172 | * Creates new instances for the given task {@link ContentValues}. 173 | *

174 | * TODO: expand recurrence 175 | *

176 | * 177 | * @param uri 178 | * The {@link Uri} used when inserting the task. 179 | * @param values 180 | * The {@link ContentValues} of the task. 181 | * @param rowId 182 | * The new {@link RowId} of the task. 183 | */ 184 | private void createInstances(SQLiteDatabase db, TaskAdapter task) 185 | { 186 | ContentValues instanceValues = generateInstanceValues(task); 187 | 188 | // set rowID of current Task 189 | instanceValues.put(Instances.TASK_ID, task.id()); 190 | 191 | db.insert(Tables.INSTANCES, null, instanceValues); 192 | } 193 | 194 | 195 | private void updateInstances(SQLiteDatabase db, TaskAdapter task) 196 | { 197 | ContentValues instanceValues = generateInstanceValues(task); 198 | 199 | db.update(Tables.INSTANCES, instanceValues, TaskContract.Instances.TASK_ID + " = " + task.id(), null); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/TaskValidatorProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.TaskContract.TaskLists; 21 | import org.dmfs.provider.tasks.TaskContract.Tasks; 22 | import org.dmfs.provider.tasks.TaskDatabaseHelper.Tables; 23 | import org.dmfs.provider.tasks.model.TaskAdapter; 24 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 25 | import org.dmfs.rfc5545.Duration; 26 | 27 | import android.database.Cursor; 28 | import android.database.sqlite.SQLiteDatabase; 29 | 30 | 31 | /** 32 | * A processor that validates the values of a task. 33 | * 34 | * @author Marten Gajda 35 | */ 36 | public class TaskValidatorProcessor extends AbstractEntityProcessor 37 | { 38 | 39 | private static final String[] TASKLIST_ID_PROJECTION = { TaskLists._ID }; 40 | private static final String TASKLISTS_ID_SELECTION = TaskLists._ID + "="; 41 | 42 | 43 | @Override 44 | public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 45 | { 46 | verifyCommon(task, isSyncAdapter); 47 | 48 | // LIST_ID must be present and refer to an existing TaskList row id 49 | Long listId = task.valueOf(TaskAdapter.LIST_ID); 50 | if (listId == null) 51 | { 52 | throw new IllegalArgumentException("LIST_ID is required on INSERT"); 53 | } 54 | 55 | // TODO: get rid of this query and use a cache instead 56 | // TODO: ensure that the list is writable unless the caller is a sync adapter 57 | Cursor cursor = db.query(Tables.LISTS, TASKLIST_ID_PROJECTION, TASKLISTS_ID_SELECTION + listId, null, null, null, null); 58 | try 59 | { 60 | if (cursor == null || cursor.getCount() != 1) 61 | { 62 | throw new IllegalArgumentException("LIST_ID must refer to an existing TaskList"); 63 | } 64 | } 65 | finally 66 | { 67 | if (cursor != null) 68 | { 69 | cursor.close(); 70 | } 71 | } 72 | 73 | } 74 | 75 | 76 | @Override 77 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 78 | { 79 | verifyCommon(task, isSyncAdapter); 80 | 81 | // only sync adapters can modify original sync id and original instance id of an existing task 82 | if (!isSyncAdapter && (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID) || task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID))) 83 | { 84 | throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID can be modified by sync adapters only"); 85 | } 86 | } 87 | 88 | 89 | /** 90 | * Performs tests that are common to insert an update operations. 91 | * 92 | * @param task 93 | * The {@link TaskAdapter} to verify. 94 | * @param isSyncAdapter 95 | * true if the caller is a sync adapter, false otherwise. 96 | */ 97 | private void verifyCommon(TaskAdapter task, boolean isSyncAdapter) 98 | { 99 | // row id can not be changed or set manually 100 | if (task.isUpdated(TaskAdapter._ID)) 101 | { 102 | throw new IllegalArgumentException("_ID can not be set manually"); 103 | } 104 | 105 | // account name can not be set on a tasks 106 | if (task.isUpdated(TaskAdapter.ACCOUNT_NAME)) 107 | { 108 | throw new IllegalArgumentException("ACCOUNT_NAME can not be set on a tasks"); 109 | } 110 | 111 | // account type can not be set on a tasks 112 | if (task.isUpdated(TaskAdapter.ACCOUNT_TYPE)) 113 | { 114 | throw new IllegalArgumentException("ACCOUNT_TYPE can not be set on a tasks"); 115 | } 116 | 117 | // list color is read only for tasks 118 | if (task.isUpdated(TaskAdapter.LIST_COLOR)) 119 | { 120 | throw new IllegalArgumentException("LIST_COLOR can not be set on a tasks"); 121 | } 122 | 123 | // no one can undelete a task! 124 | if (task.isUpdated(TaskAdapter._DELETED)) 125 | { 126 | throw new IllegalArgumentException("modification of _DELETE is not allowed"); 127 | } 128 | 129 | // only sync adapters are allowed to change the UID 130 | if (!isSyncAdapter && task.isUpdated(TaskAdapter._UID)) 131 | { 132 | throw new IllegalArgumentException("modification of _UID is not allowed"); 133 | } 134 | 135 | // only sync adapters are allowed to remove the dirty flag 136 | if (!isSyncAdapter && task.isUpdated(TaskAdapter._DIRTY)) 137 | { 138 | throw new IllegalArgumentException("modification of _DIRTY is not allowed"); 139 | } 140 | 141 | // only sync adapters are allowed to set creation time 142 | if (!isSyncAdapter && task.isUpdated(TaskAdapter.CREATED)) 143 | { 144 | throw new IllegalArgumentException("modification of CREATED is not allowed"); 145 | } 146 | 147 | // IS_NEW is set automatically 148 | if (task.isUpdated(TaskAdapter.IS_NEW)) 149 | { 150 | throw new IllegalArgumentException("modification of IS_NEW is not allowed"); 151 | } 152 | 153 | // IS_CLOSED is set automatically 154 | if (task.isUpdated(TaskAdapter.IS_CLOSED)) 155 | { 156 | throw new IllegalArgumentException("modification of IS_CLOSED is not allowed"); 157 | } 158 | 159 | // HAS_PROPERTIES is set automatically 160 | if (task.isUpdated(TaskAdapter.HAS_PROPERTIES)) 161 | { 162 | throw new IllegalArgumentException("modification of HAS_PROPERTIES is not allowed"); 163 | } 164 | 165 | // HAS_ALARMS is set automatically 166 | if (task.isUpdated(TaskAdapter.HAS_ALARMS)) 167 | { 168 | throw new IllegalArgumentException("modification of HAS_ALARMS is not allowed"); 169 | } 170 | 171 | // only sync adapters are allowed to set modification time 172 | if (!isSyncAdapter && task.isUpdated(TaskAdapter.LAST_MODIFIED)) 173 | { 174 | throw new IllegalArgumentException("modification of MODIFICATION_TIME is not allowed"); 175 | } 176 | 177 | if (task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_SYNC_ID) && task.isUpdated(TaskAdapter.ORIGINAL_INSTANCE_ID)) 178 | { 179 | throw new IllegalArgumentException("ORIGINAL_INSTANCE_SYNC_ID and ORIGINAL_INSTANCE_ID must not be specified at the same time"); 180 | } 181 | 182 | // check that CLASSIFICATION is an Integer between 0 and 2 if given 183 | if (task.isUpdated(TaskAdapter.CLASSIFICATION)) 184 | { 185 | Integer classification = task.valueOf(TaskAdapter.CLASSIFICATION); 186 | if (classification != null && (classification < 0 || classification > 2)) 187 | { 188 | throw new IllegalArgumentException("CLASSIFICATION must be an integer between 0 and 2"); 189 | } 190 | } 191 | 192 | // check that PRIORITY is an Integer between 0 and 9 if given 193 | if (task.isUpdated(TaskAdapter.PRIORITY)) 194 | { 195 | Integer priority = task.valueOf(TaskAdapter.PRIORITY); 196 | if (priority != null && (priority < 0 || priority > 9)) 197 | { 198 | throw new IllegalArgumentException("PRIORITY must be an integer between 0 and 9"); 199 | } 200 | } 201 | 202 | // check that PERCENT_COMPLETE is an Integer between 0 and 100 203 | if (task.isUpdated(TaskAdapter.PERCENT_COMPLETE)) 204 | { 205 | Integer percent = task.valueOf(TaskAdapter.PERCENT_COMPLETE); 206 | if (percent != null && (percent < 0 || percent > 100)) 207 | { 208 | throw new IllegalArgumentException("PERCENT_COMPLETE must be null or an integer between 0 and 100"); 209 | } 210 | } 211 | 212 | // validate STATUS 213 | if (task.isUpdated(TaskAdapter.STATUS)) 214 | { 215 | Integer status = task.valueOf(TaskAdapter.STATUS); 216 | if (status != null && (status < Tasks.STATUS_NEEDS_ACTION || status > Tasks.STATUS_CANCELLED)) 217 | { 218 | throw new IllegalArgumentException("invalid STATUS: " + status); 219 | } 220 | } 221 | 222 | // ensure that DUE and DURATION are set properly if DTSTART is given 223 | Long dtStart = task.valueOf(TaskAdapter.DTSTART_RAW); 224 | Long due = task.valueOf(TaskAdapter.DUE_RAW); 225 | Duration duration = task.valueOf(TaskAdapter.DURATION); 226 | 227 | if (dtStart != null) 228 | { 229 | if (due != null && duration != null) 230 | { 231 | throw new IllegalArgumentException("Only one of DUE or DURATION must be supplied."); 232 | } 233 | else if (due != null) 234 | { 235 | if (due < dtStart) 236 | { 237 | throw new IllegalArgumentException("DUE must not be < DTSTART"); 238 | } 239 | } 240 | else if (duration != null) 241 | { 242 | if (duration.getSign() == -1) 243 | { 244 | throw new IllegalArgumentException("DURATION must not be negative"); 245 | } 246 | } 247 | } 248 | else if (duration != null) 249 | { 250 | throw new IllegalArgumentException("DURATION must not be supplied without DTSTART"); 251 | } 252 | 253 | // if one of DTSTART or DUE is given, TZ must not be null unless it's an all-day task 254 | if ((dtStart != null || due != null) && !task.valueOf(TaskAdapter.IS_ALLDAY) && task.valueOf(TaskAdapter.TIMEZONE_RAW) == null) 255 | { 256 | throw new IllegalArgumentException("TIMEZONE must be supplied if one of DTSTART or DUE is not null and not all-day"); 257 | } 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/org/dmfs/provider/tasks/processors/tasks/TestProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2015 Marten Gajda 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | * 16 | */ 17 | 18 | package org.dmfs.provider.tasks.processors.tasks; 19 | 20 | import org.dmfs.provider.tasks.model.TaskAdapter; 21 | import org.dmfs.provider.tasks.processors.AbstractEntityProcessor; 22 | 23 | import android.database.sqlite.SQLiteDatabase; 24 | import android.util.Log; 25 | 26 | 27 | /** 28 | * A simple debugging processor. It just logs every operation. 29 | * 30 | * @author Marten Gajda 31 | */ 32 | public class TestProcessor extends AbstractEntityProcessor 33 | { 34 | 35 | @Override 36 | public void beforeInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 37 | { 38 | Log.d("TestProcessor", "before insert processor called"); 39 | } 40 | 41 | 42 | @Override 43 | public void afterInsert(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 44 | { 45 | Log.d("TestProcessor", "after insert processor called for " + task.id()); 46 | } 47 | 48 | 49 | @Override 50 | public void beforeUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 51 | { 52 | Log.d("TestProcessor", "before update processor called for " + task.id()); 53 | } 54 | 55 | 56 | @Override 57 | public void afterUpdate(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 58 | { 59 | Log.d("TestProcessor", "after update processor called for " + task.id()); 60 | } 61 | 62 | 63 | @Override 64 | public void beforeDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 65 | { 66 | Log.d("TestProcessor", "before delete processor called for " + task.id()); 67 | } 68 | 69 | 70 | @Override 71 | public void afterDelete(SQLiteDatabase db, TaskAdapter task, boolean isSyncAdapter) 72 | { 73 | Log.i("TestProcessor", "after delete processor called for " + task.id()); 74 | } 75 | } 76 | --------------------------------------------------------------------------------