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