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 Setnull
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 Settrue
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 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 ArrayListtrue
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 SetisNew
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 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 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 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 _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-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 null
.
70 | */
71 | public true
if the field has been overridden, false
otherwise.
80 | */
81 | public 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 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 0
(for false
) and 1
(for true
).
30 | *
31 | * @author Marten Gajda null
the time zone is always set to UTC.
39 | *
40 | * @author Marten Gajda 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 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
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 | *
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 | *
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 Gajdatrue
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