Grant consistency of size of folders with every update operation
[pub/Android/ownCloud.git] / src / com / owncloud / android / providers / FileContentProvider.java
1 /* ownCloud Android client application
2 * Copyright (C) 2011 Bartek Przybylski
3 * Copyright (C) 2012-2013 ownCloud Inc.
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License version 2,
7 * as published by the Free Software Foundation.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *
17 */
18
19 package com.owncloud.android.providers;
20
21 import java.util.ArrayList;
22 import java.util.HashMap;
23
24 import com.owncloud.android.Log_OC;
25 import com.owncloud.android.datamodel.FileDataStorageManager;
26 import com.owncloud.android.db.ProviderMeta;
27 import com.owncloud.android.db.ProviderMeta.ProviderTableMeta;
28
29
30 import android.content.ContentProvider;
31 import android.content.ContentProviderOperation;
32 import android.content.ContentProviderResult;
33 import android.content.ContentUris;
34 import android.content.ContentValues;
35 import android.content.Context;
36 import android.content.OperationApplicationException;
37 import android.content.UriMatcher;
38 import android.database.Cursor;
39 import android.database.SQLException;
40 import android.database.sqlite.SQLiteDatabase;
41 import android.database.sqlite.SQLiteOpenHelper;
42 import android.database.sqlite.SQLiteQueryBuilder;
43 import android.net.Uri;
44 import android.text.TextUtils;
45
46 /**
47 * The ContentProvider for the ownCloud App.
48 *
49 * @author Bartek Przybylski
50 * @author David A. Velasco
51 *
52 */
53 public class FileContentProvider extends ContentProvider {
54
55 private DataBaseHelper mDbHelper;
56
57 private static HashMap<String, String> mProjectionMap;
58 static {
59 mProjectionMap = new HashMap<String, String>();
60 mProjectionMap.put(ProviderTableMeta._ID, ProviderTableMeta._ID);
61 mProjectionMap.put(ProviderTableMeta.FILE_PARENT,
62 ProviderTableMeta.FILE_PARENT);
63 mProjectionMap.put(ProviderTableMeta.FILE_PATH,
64 ProviderTableMeta.FILE_PATH);
65 mProjectionMap.put(ProviderTableMeta.FILE_NAME,
66 ProviderTableMeta.FILE_NAME);
67 mProjectionMap.put(ProviderTableMeta.FILE_CREATION,
68 ProviderTableMeta.FILE_CREATION);
69 mProjectionMap.put(ProviderTableMeta.FILE_MODIFIED,
70 ProviderTableMeta.FILE_MODIFIED);
71 mProjectionMap.put(ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA,
72 ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA);
73 mProjectionMap.put(ProviderTableMeta.FILE_CONTENT_LENGTH,
74 ProviderTableMeta.FILE_CONTENT_LENGTH);
75 mProjectionMap.put(ProviderTableMeta.FILE_CONTENT_TYPE,
76 ProviderTableMeta.FILE_CONTENT_TYPE);
77 mProjectionMap.put(ProviderTableMeta.FILE_STORAGE_PATH,
78 ProviderTableMeta.FILE_STORAGE_PATH);
79 mProjectionMap.put(ProviderTableMeta.FILE_LAST_SYNC_DATE,
80 ProviderTableMeta.FILE_LAST_SYNC_DATE);
81 mProjectionMap.put(ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA,
82 ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA);
83 mProjectionMap.put(ProviderTableMeta.FILE_KEEP_IN_SYNC,
84 ProviderTableMeta.FILE_KEEP_IN_SYNC);
85 mProjectionMap.put(ProviderTableMeta.FILE_ACCOUNT_OWNER,
86 ProviderTableMeta.FILE_ACCOUNT_OWNER);
87 mProjectionMap.put(ProviderTableMeta.FILE_ETAG,
88 ProviderTableMeta.FILE_ETAG);
89 }
90
91 private static final int SINGLE_FILE = 1;
92 private static final int DIRECTORY = 2;
93 private static final int ROOT_DIRECTORY = 3;
94 private static final UriMatcher mUriMatcher;
95
96 public static final String METHOD_UPDATE_FOLDER_SIZE = "updateFolderSize";
97
98 static {
99 mUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
100 mUriMatcher.addURI(ProviderMeta.AUTHORITY_FILES, null, ROOT_DIRECTORY);
101 mUriMatcher.addURI(ProviderMeta.AUTHORITY_FILES, "file/", SINGLE_FILE);
102 mUriMatcher.addURI(ProviderMeta.AUTHORITY_FILES, "file/#", SINGLE_FILE);
103 mUriMatcher.addURI(ProviderMeta.AUTHORITY_FILES, "dir/", DIRECTORY);
104 mUriMatcher.addURI(ProviderMeta.AUTHORITY_FILES, "dir/#", DIRECTORY);
105 }
106
107 @Override
108 public int delete(Uri uri, String where, String[] whereArgs) {
109 //Log_OC.d(TAG, "Deleting " + uri + " at provider " + this);
110 int count = 0;
111 SQLiteDatabase db = mDbHelper.getWritableDatabase();
112 db.beginTransaction();
113 try {
114 count = delete(db, uri, where, whereArgs);
115 db.setTransactionSuccessful();
116 } finally {
117 db.endTransaction();
118 }
119 getContext().getContentResolver().notifyChange(uri, null);
120 return count;
121 }
122
123
124 private int delete(SQLiteDatabase db, Uri uri, String where, String[] whereArgs) {
125 int count = 0;
126 switch (mUriMatcher.match(uri)) {
127 case SINGLE_FILE:
128 /*Cursor c = query(db, uri, null, where, whereArgs, null);
129 String remotePath = "(unexisting)";
130 if (c != null && c.moveToFirst()) {
131 remotePath = c.getString(c.getColumnIndex(ProviderTableMeta.FILE_PATH));
132 }
133 Log_OC.d(TAG, "Removing FILE " + remotePath);
134 */
135 count = db.delete(ProviderTableMeta.DB_NAME,
136 ProviderTableMeta._ID
137 + "="
138 + uri.getPathSegments().get(1)
139 + (!TextUtils.isEmpty(where) ? " AND (" + where
140 + ")" : ""), whereArgs);
141 /* just for log
142 if (c!=null) {
143 c.close();
144 }
145 */
146 break;
147 case DIRECTORY:
148 // deletion of folder is recursive
149 /*
150 Uri folderUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, Long.parseLong(uri.getPathSegments().get(1)));
151 Cursor folder = query(db, folderUri, null, null, null, null);
152 String folderName = "(unknown)";
153 if (folder != null && folder.moveToFirst()) {
154 folderName = folder.getString(folder.getColumnIndex(ProviderTableMeta.FILE_PATH));
155 }
156 */
157 Cursor children = query(uri, null, null, null, null);
158 if (children != null && children.moveToFirst()) {
159 long childId;
160 boolean isDir;
161 //String remotePath;
162 while (!children.isAfterLast()) {
163 childId = children.getLong(children.getColumnIndex(ProviderTableMeta._ID));
164 isDir = "DIR".equals(children.getString(children.getColumnIndex(ProviderTableMeta.FILE_CONTENT_TYPE)));
165 //remotePath = children.getString(children.getColumnIndex(ProviderTableMeta.FILE_PATH));
166 if (isDir) {
167 count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_DIR, childId), null, null);
168 } else {
169 count += delete(db, ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, childId), null, null);
170 }
171 children.moveToNext();
172 }
173 children.close();
174 } /*else {
175 Log_OC.d(TAG, "No child to remove in DIRECTORY " + folderName);
176 }
177 Log_OC.d(TAG, "Removing DIRECTORY " + folderName + " (or maybe not) ");
178 */
179 count += db.delete(ProviderTableMeta.DB_NAME,
180 ProviderTableMeta._ID
181 + "="
182 + uri.getPathSegments().get(1)
183 + (!TextUtils.isEmpty(where) ? " AND (" + where
184 + ")" : ""), whereArgs);
185 /* Just for log
186 if (folder != null) {
187 folder.close();
188 }*/
189 break;
190 case ROOT_DIRECTORY:
191 //Log_OC.d(TAG, "Removing ROOT!");
192 count = db.delete(ProviderTableMeta.DB_NAME, where, whereArgs);
193 break;
194 default:
195 //Log_OC.e(TAG, "Unknown uri " + uri);
196 throw new IllegalArgumentException("Unknown uri: " + uri.toString());
197 }
198 return count;
199 }
200
201
202 @Override
203 public String getType(Uri uri) {
204 switch (mUriMatcher.match(uri)) {
205 case ROOT_DIRECTORY:
206 return ProviderTableMeta.CONTENT_TYPE;
207 case SINGLE_FILE:
208 return ProviderTableMeta.CONTENT_TYPE_ITEM;
209 default:
210 throw new IllegalArgumentException("Unknown Uri id."
211 + uri.toString());
212 }
213 }
214
215 @Override
216 public Uri insert(Uri uri, ContentValues values) {
217 //Log_OC.d(TAG, "Inserting " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this);
218 Uri newUri = null;
219 SQLiteDatabase db = mDbHelper.getWritableDatabase();
220 db.beginTransaction();
221 try {
222 newUri = insert(db, uri, values);
223 db.setTransactionSuccessful();
224 } finally {
225 db.endTransaction();
226 }
227 getContext().getContentResolver().notifyChange(newUri, null);
228 return newUri;
229 }
230
231 private Uri insert(SQLiteDatabase db, Uri uri, ContentValues values) {
232 if (mUriMatcher.match(uri) != SINGLE_FILE &&
233 mUriMatcher.match(uri) != ROOT_DIRECTORY) {
234 //Log_OC.e(TAG, "Inserting invalid URI: " + uri);
235 throw new IllegalArgumentException("Unknown uri id: " + uri);
236 }
237
238 long rowId = db.insert(ProviderTableMeta.DB_NAME, null, values);
239 if (rowId > 0) {
240 Uri insertedFileUri = ContentUris.withAppendedId(ProviderTableMeta.CONTENT_URI_FILE, rowId);
241 //Log_OC.d(TAG, "Inserted " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this);
242 return insertedFileUri;
243 } else {
244 //Log_OC.d(TAG, "Error while inserting " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this);
245 throw new SQLException("ERROR " + uri);
246 }
247 }
248
249
250 @Override
251 public boolean onCreate() {
252 mDbHelper = new DataBaseHelper(getContext());
253 return true;
254 }
255
256
257 @Override
258 public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
259 Cursor result = null;
260 SQLiteDatabase db = mDbHelper.getReadableDatabase();
261 db.beginTransaction();
262 try {
263 result = query(db, uri, projection, selection, selectionArgs, sortOrder);
264 db.setTransactionSuccessful();
265 } finally {
266 db.endTransaction();
267 }
268 return result;
269 }
270
271 private Cursor query(SQLiteDatabase db, Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
272 SQLiteQueryBuilder sqlQuery = new SQLiteQueryBuilder();
273
274 sqlQuery.setTables(ProviderTableMeta.DB_NAME);
275 sqlQuery.setProjectionMap(mProjectionMap);
276
277 switch (mUriMatcher.match(uri)) {
278 case ROOT_DIRECTORY:
279 break;
280 case DIRECTORY:
281 String folderId = uri.getPathSegments().get(1);
282 sqlQuery.appendWhere(ProviderTableMeta.FILE_PARENT + "="
283 + folderId);
284 break;
285 case SINGLE_FILE:
286 if (uri.getPathSegments().size() > 1) {
287 sqlQuery.appendWhere(ProviderTableMeta._ID + "="
288 + uri.getPathSegments().get(1));
289 }
290 break;
291 default:
292 throw new IllegalArgumentException("Unknown uri id: " + uri);
293 }
294
295 String order;
296 if (TextUtils.isEmpty(sortOrder)) {
297 order = ProviderTableMeta.DEFAULT_SORT_ORDER;
298 } else {
299 order = sortOrder;
300 }
301
302 // DB case_sensitive
303 db.execSQL("PRAGMA case_sensitive_like = true");
304 Cursor c = sqlQuery.query(db, projection, selection, selectionArgs, null, null, order);
305 c.setNotificationUri(getContext().getContentResolver(), uri);
306 return c;
307 }
308
309 @Override
310 public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
311
312 //Log_OC.d(TAG, "Updating " + values.getAsString(ProviderTableMeta.FILE_PATH) + " at provider " + this);
313 int count = 0;
314 SQLiteDatabase db = mDbHelper.getWritableDatabase();
315 db.beginTransaction();
316 try {
317 count = update(db, uri, values, selection, selectionArgs);
318 db.setTransactionSuccessful();
319 } finally {
320 db.endTransaction();
321 }
322 getContext().getContentResolver().notifyChange(uri, null);
323 return count;
324 }
325
326
327 private int update(SQLiteDatabase db, Uri uri, ContentValues values, String selection, String[] selectionArgs) {
328 switch (mUriMatcher.match(uri)) {
329 case DIRECTORY:
330 return updateFolderSize(db, selectionArgs[0]);
331 default:
332 return db.update(ProviderTableMeta.DB_NAME, values, selection, selectionArgs);
333 }
334 }
335
336
337 private int updateFolderSize(SQLiteDatabase db, String folderId) {
338 int count = 0;
339 String [] whereArgs = new String[] { folderId };
340
341 // read current size saved for the folder
342 long folderSize = 0;
343 long folderParentId = -1;
344 Uri selectFolderUri = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_FILE, folderId);
345 String[] folderProjection = new String[] { ProviderTableMeta.FILE_CONTENT_LENGTH, ProviderTableMeta.FILE_PARENT};
346 String folderWhere = ProviderTableMeta._ID + "=?";
347 Cursor folderCursor = query(db, selectFolderUri, folderProjection, folderWhere, whereArgs, null);
348 if (folderCursor != null && folderCursor.moveToFirst()) {
349 folderSize = folderCursor.getLong(folderCursor.getColumnIndex(ProviderTableMeta.FILE_CONTENT_LENGTH));;
350 folderParentId = folderCursor.getLong(folderCursor.getColumnIndex(ProviderTableMeta.FILE_PARENT));;
351 }
352 folderCursor.close();
353
354 // read and sum sizes of children
355 long childrenSize = 0;
356 Uri selectChildrenUri = Uri.withAppendedPath(ProviderTableMeta.CONTENT_URI_DIR, folderId);
357 String[] childrenProjection = new String[] { ProviderTableMeta.FILE_CONTENT_LENGTH, ProviderTableMeta.FILE_PARENT};
358 String childrenWhere = ProviderTableMeta.FILE_PARENT + "=?";
359 Cursor childrenCursor = query(db, selectChildrenUri, childrenProjection, childrenWhere, whereArgs, null);
360 if (childrenCursor != null && childrenCursor.moveToFirst()) {
361 while (!childrenCursor.isAfterLast()) {
362 childrenSize += childrenCursor.getLong(childrenCursor.getColumnIndex(ProviderTableMeta.FILE_CONTENT_LENGTH));
363 childrenCursor.moveToNext();
364 }
365 }
366 childrenCursor.close();
367
368 // update if needed
369 if (folderSize != childrenSize) {
370 Log_OC.d("FileContentProvider", "Updating " + folderSize + " to " + childrenSize);
371 ContentValues cv = new ContentValues();
372 cv.put(ProviderTableMeta.FILE_CONTENT_LENGTH, childrenSize);
373 count = db.update(ProviderTableMeta.DB_NAME, cv, folderWhere, whereArgs);
374
375 // propagate update until root
376 if (folderParentId > FileDataStorageManager.ROOT_PARENT_ID) {
377 Log_OC.d("FileContentProvider", "Propagating update to " + folderParentId);
378 updateFolderSize(db, String.valueOf(folderParentId));
379 } else {
380 Log_OC.d("FileContentProvider", "NOT propagating to " + folderParentId);
381 }
382 } else {
383 Log_OC.d("FileContentProvider", "NOT updating, sizes are " + folderSize + " and " + childrenSize);
384 }
385 return count;
386 }
387
388
389 @Override
390 public ContentProviderResult[] applyBatch (ArrayList<ContentProviderOperation> operations) throws OperationApplicationException {
391 Log_OC.d("FileContentProvider", "applying batch in provider " + this + " (temporary: " + isTemporary() + ")" );
392 ContentProviderResult[] results = new ContentProviderResult[operations.size()];
393 int i=0;
394
395 SQLiteDatabase db = mDbHelper.getWritableDatabase();
396 db.beginTransaction(); // it's supposed that transactions can be nested
397 try {
398 for (ContentProviderOperation operation : operations) {
399 results[i] = operation.apply(this, results, i);
400 i++;
401 }
402 db.setTransactionSuccessful();
403 } finally {
404 db.endTransaction();
405 }
406 Log_OC.d("FileContentProvider", "applied batch in provider " + this);
407 return results;
408 }
409
410
411 class DataBaseHelper extends SQLiteOpenHelper {
412
413 public DataBaseHelper(Context context) {
414 super(context, ProviderMeta.DB_NAME, null, ProviderMeta.DB_VERSION);
415
416 }
417
418 @Override
419 public void onCreate(SQLiteDatabase db) {
420 // files table
421 Log_OC.i("SQL", "Entering in onCreate");
422 db.execSQL("CREATE TABLE " + ProviderTableMeta.DB_NAME + "("
423 + ProviderTableMeta._ID + " INTEGER PRIMARY KEY, "
424 + ProviderTableMeta.FILE_NAME + " TEXT, "
425 + ProviderTableMeta.FILE_PATH + " TEXT, "
426 + ProviderTableMeta.FILE_PARENT + " INTEGER, "
427 + ProviderTableMeta.FILE_CREATION + " INTEGER, "
428 + ProviderTableMeta.FILE_MODIFIED + " INTEGER, "
429 + ProviderTableMeta.FILE_CONTENT_TYPE + " TEXT, "
430 + ProviderTableMeta.FILE_CONTENT_LENGTH + " INTEGER, "
431 + ProviderTableMeta.FILE_STORAGE_PATH + " TEXT, "
432 + ProviderTableMeta.FILE_ACCOUNT_OWNER + " TEXT, "
433 + ProviderTableMeta.FILE_LAST_SYNC_DATE + " INTEGER, "
434 + ProviderTableMeta.FILE_KEEP_IN_SYNC + " INTEGER, "
435 + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " INTEGER, "
436 + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " INTEGER, "
437 + ProviderTableMeta.FILE_ETAG + " TEXT );"
438 );
439 }
440
441 @Override
442 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
443 Log_OC.i("SQL", "Entering in onUpgrade");
444 boolean upgraded = false;
445 if (oldVersion == 1 && newVersion >= 2) {
446 Log_OC.i("SQL", "Entering in the #1 ADD in onUpgrade");
447 db.execSQL("ALTER TABLE " + ProviderTableMeta.DB_NAME +
448 " ADD COLUMN " + ProviderTableMeta.FILE_KEEP_IN_SYNC + " INTEGER " +
449 " DEFAULT 0");
450 upgraded = true;
451 }
452 if (oldVersion < 3 && newVersion >= 3) {
453 Log_OC.i("SQL", "Entering in the #2 ADD in onUpgrade");
454 db.beginTransaction();
455 try {
456 db.execSQL("ALTER TABLE " + ProviderTableMeta.DB_NAME +
457 " ADD COLUMN " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " INTEGER " +
458 " DEFAULT 0");
459
460 // assume there are not local changes pending to upload
461 db.execSQL("UPDATE " + ProviderTableMeta.DB_NAME +
462 " SET " + ProviderTableMeta.FILE_LAST_SYNC_DATE_FOR_DATA + " = " + System.currentTimeMillis() +
463 " WHERE " + ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL");
464
465 upgraded = true;
466 db.setTransactionSuccessful();
467 } finally {
468 db.endTransaction();
469 }
470 }
471 if (oldVersion < 4 && newVersion >= 4) {
472 Log_OC.i("SQL", "Entering in the #3 ADD in onUpgrade");
473 db.beginTransaction();
474 try {
475 db .execSQL("ALTER TABLE " + ProviderTableMeta.DB_NAME +
476 " ADD COLUMN " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " INTEGER " +
477 " DEFAULT 0");
478
479 db.execSQL("UPDATE " + ProviderTableMeta.DB_NAME +
480 " SET " + ProviderTableMeta.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA + " = " + ProviderTableMeta.FILE_MODIFIED +
481 " WHERE " + ProviderTableMeta.FILE_STORAGE_PATH + " IS NOT NULL");
482
483 upgraded = true;
484 db.setTransactionSuccessful();
485 } finally {
486 db.endTransaction();
487 }
488 }
489 if (!upgraded)
490 Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion);
491
492 if (oldVersion < 5 && newVersion >= 5) {
493 Log_OC.i("SQL", "Entering in the #4 ADD in onUpgrade");
494 db.beginTransaction();
495 try {
496 db .execSQL("ALTER TABLE " + ProviderTableMeta.DB_NAME +
497 " ADD COLUMN " + ProviderTableMeta.FILE_ETAG + " TEXT " +
498 " DEFAULT NULL");
499
500 upgraded = true;
501 db.setTransactionSuccessful();
502 } finally {
503 db.endTransaction();
504 }
505 }
506 if (!upgraded)
507 Log_OC.i("SQL", "OUT of the ADD in onUpgrade; oldVersion == " + oldVersion + ", newVersion == " + newVersion);
508 }
509 }
510
511 }