1 /* ownCloud Android client application
2 * Copyright (C) 2012 Bartek Przybylski
3 * Copyright (C) 2012-2013 ownCloud Inc.
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.
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.
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/>.
19 package com
.owncloud
.android
.datamodel
;
22 import java
.util
.ArrayList
;
23 import java
.util
.Collections
;
24 import java
.util
.Iterator
;
25 import java
.util
.List
;
26 import java
.util
.Vector
;
28 import com
.owncloud
.android
.DisplayUtils
;
29 import com
.owncloud
.android
.Log_OC
;
30 import com
.owncloud
.android
.db
.ProviderMeta
;
31 import com
.owncloud
.android
.db
.ProviderMeta
.ProviderTableMeta
;
32 import com
.owncloud
.android
.utils
.FileStorageUtils
;
34 import android
.accounts
.Account
;
35 import android
.content
.ContentProviderClient
;
36 import android
.content
.ContentProviderOperation
;
37 import android
.content
.ContentProviderResult
;
38 import android
.content
.ContentResolver
;
39 import android
.content
.ContentValues
;
40 import android
.content
.OperationApplicationException
;
41 import android
.database
.Cursor
;
42 import android
.net
.Uri
;
43 import android
.os
.RemoteException
;
45 public class FileDataStorageManager
implements DataStorageManager
{
47 private ContentResolver mContentResolver
;
48 private ContentProviderClient mContentProvider
;
49 private Account mAccount
;
51 private static String TAG
= "FileDataStorageManager";
53 public FileDataStorageManager(Account account
, ContentResolver cr
) {
54 mContentProvider
= null
;
55 mContentResolver
= cr
;
59 public FileDataStorageManager(Account account
, ContentProviderClient cp
) {
60 mContentProvider
= cp
;
61 mContentResolver
= null
;
66 public OCFile
getFileByPath(String path
) {
67 Cursor c
= getCursorForValue(ProviderTableMeta
.FILE_PATH
, path
);
69 if (c
.moveToFirst()) {
70 file
= createFileInstance(c
);
73 if (file
== null
&& OCFile
.PATH_SEPARATOR
.equals(path
)) {
74 return createRootDir(); // root should always exist
80 private OCFile
createRootDir() {
81 OCFile file
= new OCFile(OCFile
.PATH_SEPARATOR
);
82 file
.setMimetype("DIR");
83 file
.setParentId(DataStorageManager
.ROOT_PARENT_ID
);
89 public OCFile
getFileById(long id
) {
90 Cursor c
= getCursorForValue(ProviderTableMeta
._ID
, String
.valueOf(id
));
92 if (c
.moveToFirst()) {
93 file
= createFileInstance(c
);
99 public OCFile
getFileByLocalPath(String path
) {
100 Cursor c
= getCursorForValue(ProviderTableMeta
.FILE_STORAGE_PATH
, path
);
102 if (c
.moveToFirst()) {
103 file
= createFileInstance(c
);
110 public boolean fileExists(long id
) {
111 return fileExists(ProviderTableMeta
._ID
, String
.valueOf(id
));
115 public boolean fileExists(String path
) {
116 return fileExists(ProviderTableMeta
.FILE_PATH
, path
);
120 public boolean saveFile(OCFile file
) {
121 boolean overriden
= false
;
122 ContentValues cv
= new ContentValues();
123 cv
.put(ProviderTableMeta
.FILE_MODIFIED
, file
.getModificationTimestamp());
124 cv
.put(ProviderTableMeta
.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA
, file
.getModificationTimestampAtLastSyncForData());
125 cv
.put(ProviderTableMeta
.FILE_CREATION
, file
.getCreationTimestamp());
126 cv
.put(ProviderTableMeta
.FILE_CONTENT_LENGTH
, file
.getFileLength());
127 cv
.put(ProviderTableMeta
.FILE_CONTENT_TYPE
, file
.getMimetype());
128 cv
.put(ProviderTableMeta
.FILE_NAME
, file
.getFileName());
129 if (file
.getParentId() != 0)
130 cv
.put(ProviderTableMeta
.FILE_PARENT
, file
.getParentId());
131 cv
.put(ProviderTableMeta
.FILE_PATH
, file
.getRemotePath());
132 if (!file
.isDirectory())
133 cv
.put(ProviderTableMeta
.FILE_STORAGE_PATH
, file
.getStoragePath());
134 cv
.put(ProviderTableMeta
.FILE_ACCOUNT_OWNER
, mAccount
.name
);
135 cv
.put(ProviderTableMeta
.FILE_LAST_SYNC_DATE
, file
.getLastSyncDateForProperties());
136 cv
.put(ProviderTableMeta
.FILE_LAST_SYNC_DATE_FOR_DATA
, file
.getLastSyncDateForData());
137 cv
.put(ProviderTableMeta
.FILE_KEEP_IN_SYNC
, file
.keepInSync() ?
1 : 0);
139 boolean sameRemotePath
= fileExists(file
.getRemotePath());
140 if (sameRemotePath
||
141 fileExists(file
.getFileId()) ) { // for renamed files; no more delete and create
143 if (sameRemotePath
) {
144 OCFile oldFile
= getFileByPath(file
.getRemotePath());
145 file
.setFileId(oldFile
.getFileId());
149 if (getContentResolver() != null
) {
150 getContentResolver().update(ProviderTableMeta
.CONTENT_URI
, cv
,
151 ProviderTableMeta
._ID
+ "=?",
152 new String
[] { String
.valueOf(file
.getFileId()) });
155 getContentProvider().update(ProviderTableMeta
.CONTENT_URI
,
156 cv
, ProviderTableMeta
._ID
+ "=?",
157 new String
[] { String
.valueOf(file
.getFileId()) });
158 } catch (RemoteException e
) {
160 "Fail to insert insert file to database "
165 Uri result_uri
= null
;
166 if (getContentResolver() != null
) {
167 result_uri
= getContentResolver().insert(
168 ProviderTableMeta
.CONTENT_URI_FILE
, cv
);
171 result_uri
= getContentProvider().insert(
172 ProviderTableMeta
.CONTENT_URI_FILE
, cv
);
173 } catch (RemoteException e
) {
175 "Fail to insert insert file to database "
179 if (result_uri
!= null
) {
180 long new_id
= Long
.parseLong(result_uri
.getPathSegments()
182 file
.setFileId(new_id
);
186 if (file
.isDirectory() && file
.needsUpdatingWhileSaving())
187 for (OCFile f
: getDirectoryContent(file
))
190 Log_OC
.d(TAG
, ".........file Time= " + DisplayUtils
.unixTimeToHumanReadable(file
.getModificationTimestamp()));
191 updateSubtreeSize(file
.getParentId());
198 public void saveFiles(List
<OCFile
> files
) {
200 Iterator
<OCFile
> filesIt
= files
.iterator();
201 ArrayList
<ContentProviderOperation
> operations
= new ArrayList
<ContentProviderOperation
>(files
.size());
204 // prepare operations to perform
205 while (filesIt
.hasNext()) {
206 file
= filesIt
.next();
207 ContentValues cv
= new ContentValues();
208 cv
.put(ProviderTableMeta
.FILE_MODIFIED
, file
.getModificationTimestamp());
209 cv
.put(ProviderTableMeta
.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA
, file
.getModificationTimestampAtLastSyncForData());
210 cv
.put(ProviderTableMeta
.FILE_CREATION
, file
.getCreationTimestamp());
211 cv
.put(ProviderTableMeta
.FILE_CONTENT_LENGTH
, file
.getFileLength());
212 cv
.put(ProviderTableMeta
.FILE_CONTENT_TYPE
, file
.getMimetype());
213 cv
.put(ProviderTableMeta
.FILE_NAME
, file
.getFileName());
214 if (file
.getParentId() != 0)
215 cv
.put(ProviderTableMeta
.FILE_PARENT
, file
.getParentId());
216 cv
.put(ProviderTableMeta
.FILE_PATH
, file
.getRemotePath());
217 if (!file
.isDirectory())
218 cv
.put(ProviderTableMeta
.FILE_STORAGE_PATH
, file
.getStoragePath());
219 cv
.put(ProviderTableMeta
.FILE_ACCOUNT_OWNER
, mAccount
.name
);
220 cv
.put(ProviderTableMeta
.FILE_LAST_SYNC_DATE
, file
.getLastSyncDateForProperties());
221 cv
.put(ProviderTableMeta
.FILE_LAST_SYNC_DATE_FOR_DATA
, file
.getLastSyncDateForData());
222 cv
.put(ProviderTableMeta
.FILE_KEEP_IN_SYNC
, file
.keepInSync() ?
1 : 0);
224 if (fileExists(file
.getRemotePath())) {
225 OCFile oldFile
= getFileByPath(file
.getRemotePath());
226 file
.setFileId(oldFile
.getFileId());
227 operations
.add(ContentProviderOperation
.newUpdate(ProviderTableMeta
.CONTENT_URI
).
229 withSelection( ProviderTableMeta
._ID
+ "=?",
230 new String
[] { String
.valueOf(file
.getFileId()) })
233 } else if (fileExists(file
.getFileId())) {
234 OCFile oldFile
= getFileById(file
.getFileId());
235 if (file
.getStoragePath() == null
&& oldFile
.getStoragePath() != null
)
236 file
.setStoragePath(oldFile
.getStoragePath());
237 if (!file
.isDirectory());
238 cv
.put(ProviderTableMeta
.FILE_STORAGE_PATH
, file
.getStoragePath());
240 operations
.add(ContentProviderOperation
.newUpdate(ProviderTableMeta
.CONTENT_URI
).
242 withSelection( ProviderTableMeta
._ID
+ "=?",
243 new String
[] { String
.valueOf(file
.getFileId()) })
247 operations
.add(ContentProviderOperation
.newInsert(ProviderTableMeta
.CONTENT_URI
).withValues(cv
).build());
251 // apply operations in batch
252 ContentProviderResult
[] results
= null
;
254 if (getContentResolver() != null
) {
255 results
= getContentResolver().applyBatch(ProviderMeta
.AUTHORITY_FILES
, operations
);
258 results
= getContentProvider().applyBatch(operations
);
261 } catch (OperationApplicationException e
) {
262 Log_OC
.e(TAG
, "Fail to update/insert list of files to database " + e
.getMessage());
264 } catch (RemoteException e
) {
265 Log_OC
.e(TAG
, "Fail to update/insert list of files to database " + e
.getMessage());
268 // update new id in file objects for insertions
269 if (results
!= null
) {
271 for (int i
=0; i
<results
.length
; i
++) {
272 if (results
[i
].uri
!= null
) {
273 newId
= Long
.parseLong(results
[i
].uri
.getPathSegments().get(1));
274 files
.get(i
).setFileId(newId
);
275 //Log_OC.v(TAG, "Found and added id in insertion for " + files.get(i).getRemotePath());
280 for (OCFile aFile
: files
) {
281 if (aFile
.isDirectory() && aFile
.needsUpdatingWhileSaving())
282 saveFiles(getDirectoryContent(aFile
));
287 public void setAccount(Account account
) {
291 public Account
getAccount() {
295 public void setContentResolver(ContentResolver cr
) {
296 mContentResolver
= cr
;
299 public ContentResolver
getContentResolver() {
300 return mContentResolver
;
303 public void setContentProvider(ContentProviderClient cp
) {
304 mContentProvider
= cp
;
307 public ContentProviderClient
getContentProvider() {
308 return mContentProvider
;
312 public Vector
<OCFile
> getDirectoryContent(OCFile f
) {
313 Vector
<OCFile
> ret
= new Vector
<OCFile
>();
314 if (f
!= null
&& f
.isDirectory() && f
.getFileId() != -1) {
316 Uri req_uri
= Uri
.withAppendedPath(
317 ProviderTableMeta
.CONTENT_URI_DIR
,
318 String
.valueOf(f
.getFileId()));
321 if (getContentProvider() != null
) {
323 c
= getContentProvider().query(req_uri
, null
,
324 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+ "=?",
325 new String
[] { mAccount
.name
}, null
);
326 } catch (RemoteException e
) {
327 Log_OC
.e(TAG
, e
.getMessage());
331 c
= getContentResolver().query(req_uri
, null
,
332 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+ "=?",
333 new String
[] { mAccount
.name
}, null
);
336 if (c
.moveToFirst()) {
338 OCFile child
= createFileInstance(c
);
340 } while (c
.moveToNext());
345 Collections
.sort(ret
);
351 private boolean fileExists(String cmp_key
, String value
) {
353 if (getContentResolver() != null
) {
354 c
= getContentResolver()
355 .query(ProviderTableMeta
.CONTENT_URI
,
358 + ProviderTableMeta
.FILE_ACCOUNT_OWNER
360 new String
[] { value
, mAccount
.name
}, null
);
363 c
= getContentProvider().query(
364 ProviderTableMeta
.CONTENT_URI
,
367 + ProviderTableMeta
.FILE_ACCOUNT_OWNER
+ "=?",
368 new String
[] { value
, mAccount
.name
}, null
);
369 } catch (RemoteException e
) {
371 "Couldn't determine file existance, assuming non existance: "
376 boolean retval
= c
.moveToFirst();
381 private Cursor
getCursorForValue(String key
, String value
) {
383 if (getContentResolver() != null
) {
384 c
= getContentResolver()
385 .query(ProviderTableMeta
.CONTENT_URI
,
388 + ProviderTableMeta
.FILE_ACCOUNT_OWNER
390 new String
[] { value
, mAccount
.name
}, null
);
393 c
= getContentProvider().query(
394 ProviderTableMeta
.CONTENT_URI
,
396 key
+ "=? AND " + ProviderTableMeta
.FILE_ACCOUNT_OWNER
397 + "=?", new String
[] { value
, mAccount
.name
},
399 } catch (RemoteException e
) {
400 Log_OC
.e(TAG
, "Could not get file details: " + e
.getMessage());
407 private OCFile
createFileInstance(Cursor c
) {
410 file
= new OCFile(c
.getString(c
411 .getColumnIndex(ProviderTableMeta
.FILE_PATH
)));
412 file
.setFileId(c
.getLong(c
.getColumnIndex(ProviderTableMeta
._ID
)));
413 file
.setParentId(c
.getLong(c
414 .getColumnIndex(ProviderTableMeta
.FILE_PARENT
)));
415 file
.setMimetype(c
.getString(c
416 .getColumnIndex(ProviderTableMeta
.FILE_CONTENT_TYPE
)));
417 if (!file
.isDirectory()) {
418 file
.setStoragePath(c
.getString(c
419 .getColumnIndex(ProviderTableMeta
.FILE_STORAGE_PATH
)));
420 if (file
.getStoragePath() == null
) {
421 // try to find existing file and bind it with current account; - with the current update of SynchronizeFolderOperation, this won't be necessary anymore after a full synchronization of the account
422 File f
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
));
424 file
.setStoragePath(f
.getAbsolutePath());
425 file
.setLastSyncDateForData(f
.lastModified());
429 file
.setFileLength(c
.getLong(c
430 .getColumnIndex(ProviderTableMeta
.FILE_CONTENT_LENGTH
)));
431 file
.setCreationTimestamp(c
.getLong(c
432 .getColumnIndex(ProviderTableMeta
.FILE_CREATION
)));
433 file
.setModificationTimestamp(c
.getLong(c
434 .getColumnIndex(ProviderTableMeta
.FILE_MODIFIED
)));
435 file
.setModificationTimestampAtLastSyncForData(c
.getLong(c
436 .getColumnIndex(ProviderTableMeta
.FILE_MODIFIED_AT_LAST_SYNC_FOR_DATA
)));
437 file
.setLastSyncDateForProperties(c
.getLong(c
438 .getColumnIndex(ProviderTableMeta
.FILE_LAST_SYNC_DATE
)));
439 file
.setLastSyncDateForData(c
.getLong(c
.
440 getColumnIndex(ProviderTableMeta
.FILE_LAST_SYNC_DATE_FOR_DATA
)));
441 file
.setKeepInSync(c
.getInt(
442 c
.getColumnIndex(ProviderTableMeta
.FILE_KEEP_IN_SYNC
)) == 1 ? true
: false
);
448 public void removeFile(OCFile file
, boolean removeLocalCopy
) {
449 Uri file_uri
= Uri
.withAppendedPath(ProviderTableMeta
.CONTENT_URI_FILE
, ""+file
.getFileId());
450 if (getContentProvider() != null
) {
452 getContentProvider().delete(file_uri
,
453 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+"=?",
454 new String
[]{mAccount
.name
});
455 } catch (RemoteException e
) {
459 getContentResolver().delete(file_uri
,
460 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+"=?",
461 new String
[]{mAccount
.name
});
463 if (file
.isDown() && removeLocalCopy
) {
464 new File(file
.getStoragePath()).delete();
466 if (file
.isDirectory() && removeLocalCopy
) {
467 File f
= new File(FileStorageUtils
.getDefaultSavePathFor(mAccount
.name
, file
));
468 if (f
.exists() && f
.isDirectory() && (f
.list() == null
|| f
.list().length
== 0)) {
473 updateSubtreeSize(file
.getParentId());
477 public void removeDirectory(OCFile dir
, boolean removeDBData
, boolean removeLocalContent
) {
478 // TODO consider possible failures
479 if (dir
!= null
&& dir
.isDirectory() && dir
.getFileId() != -1) {
480 Vector
<OCFile
> children
= getDirectoryContent(dir
);
481 if (children
.size() > 0) {
483 for (int i
=0; i
<children
.size(); i
++) {
484 child
= children
.get(i
);
485 if (child
.isDirectory()) {
486 removeDirectory(child
, removeDBData
, removeLocalContent
);
489 removeFile(child
, removeLocalContent
);
490 } else if (removeLocalContent
) {
491 if (child
.isDown()) {
492 new File(child
.getStoragePath()).delete();
499 removeFile(dir
, true
);
502 updateSubtreeSize(dir
.getParentId());
508 * Updates database for a folder that was moved to a different location.
510 * TODO explore better (faster) implementations
511 * TODO throw exceptions up !
514 public void moveDirectory(OCFile dir
, String newPath
) {
515 // TODO check newPath
517 if (dir
!= null
&& dir
.isDirectory() && dir
.fileExists() && !dir
.getFileName().equals(OCFile
.PATH_SEPARATOR
)) {
518 /// 1. get all the descendants of 'dir' in a single QUERY (including 'dir')
520 if (getContentProvider() != null
) {
522 c
= getContentProvider().query(ProviderTableMeta
.CONTENT_URI
,
524 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+ "=? AND " + ProviderTableMeta
.FILE_PATH
+ " LIKE ?",
525 new String
[] { mAccount
.name
, dir
.getRemotePath() + "%" }, null
);
526 } catch (RemoteException e
) {
527 Log_OC
.e(TAG
, e
.getMessage());
530 c
= getContentResolver().query(ProviderTableMeta
.CONTENT_URI
,
532 ProviderTableMeta
.FILE_ACCOUNT_OWNER
+ "=? AND " + ProviderTableMeta
.FILE_PATH
+ " LIKE ?",
533 new String
[] { mAccount
.name
, dir
.getRemotePath() + "%" }, null
);
536 /// 2. prepare a batch of update operations to change all the descendants
537 ArrayList
<ContentProviderOperation
> operations
= new ArrayList
<ContentProviderOperation
>(c
.getCount());
538 int lengthOfOldPath
= dir
.getRemotePath().length();
539 String defaultSavePath
= FileStorageUtils
.getSavePath(mAccount
.name
);
540 int lengthOfOldStoragePath
= defaultSavePath
.length() + lengthOfOldPath
;
541 if (c
.moveToFirst()) {
543 ContentValues cv
= new ContentValues(); // don't take the constructor out of the loop and clear the object
544 OCFile child
= createFileInstance(c
);
545 cv
.put(ProviderTableMeta
.FILE_PATH
, newPath
+ child
.getRemotePath().substring(lengthOfOldPath
));
546 if (child
.getStoragePath() != null
&& child
.getStoragePath().startsWith(defaultSavePath
)) {
547 cv
.put(ProviderTableMeta
.FILE_STORAGE_PATH
, defaultSavePath
+ newPath
+ child
.getStoragePath().substring(lengthOfOldStoragePath
));
549 operations
.add(ContentProviderOperation
.newUpdate(ProviderTableMeta
.CONTENT_URI
).
551 withSelection( ProviderTableMeta
._ID
+ "=?",
552 new String
[] { String
.valueOf(child
.getFileId()) })
554 } while (c
.moveToNext());
558 /// 3. apply updates in batch
560 if (getContentResolver() != null
) {
561 getContentResolver().applyBatch(ProviderMeta
.AUTHORITY_FILES
, operations
);
564 getContentProvider().applyBatch(operations
);
567 } catch (OperationApplicationException e
) {
568 Log_OC
.e(TAG
, "Fail to update descendants of " + dir
.getFileId() + " in database", e
);
570 } catch (RemoteException e
) {
571 Log_OC
.e(TAG
, "Fail to update desendants of " + dir
.getFileId() + " in database", e
);
578 public Vector
<OCFile
> getDirectoryImages(OCFile directory
) {
579 Vector
<OCFile
> ret
= new Vector
<OCFile
>();
580 if (directory
!= null
) {
581 // TODO better implementation, filtering in the access to database (if possible) instead of here
582 Vector
<OCFile
> tmp
= getDirectoryContent(directory
);
583 OCFile current
= null
;
584 for (int i
=0; i
<tmp
.size(); i
++) {
585 current
= tmp
.get(i
);
586 if (current
.isImage()) {
595 public Vector
<OCFile
> getFilesbyParent(long parentId
) {
597 Vector
<OCFile
> ret
= new Vector
<OCFile
>();
599 Uri req_uri
= Uri
.withAppendedPath(
600 ProviderTableMeta
.CONTENT_URI_DIR
,
601 String
.valueOf(parentId
));
604 if (getContentProvider() != null
) {
606 c
= getContentProvider().query(req_uri
, null
,
607 ProviderTableMeta
.FILE_PARENT
+ "=?" ,
608 new String
[] { String
.valueOf(parentId
)}, null
);
609 } catch (RemoteException e
) {
610 Log_OC
.e(TAG
, e
.getMessage());
614 c
= getContentResolver().query(req_uri
, null
,
615 ProviderTableMeta
.FILE_PARENT
+ "=?" ,
616 new String
[] { String
.valueOf(parentId
)}, null
);
619 if (c
.moveToFirst()) {
621 OCFile child
= createFileInstance(c
);
623 } while (c
.moveToNext());
628 Collections
.sort(ret
);
634 * Calculate and save the folderSize on DB
638 public void saveFolderSize(long id
) {
641 Vector
<OCFile
> files
= getFilesbyParent(id
);
643 Log_OC
.d(TAG
, "Folder " + String
.valueOf(id
) + "--- Number of Files = " + String
.valueOf(files
.size()));
645 for (OCFile f
: files
)
647 folderSize
= folderSize
+ f
.getFileLength();
648 Log_OC
.d(TAG
, "Folder Size = " + String
.valueOf(folderSize
));
651 updatefolderSize(id
, folderSize
);
655 * Update the size value of a folder on DB
658 public int updatefolderSize(long id
, long size
) {
659 ContentValues cv
= new ContentValues();
660 cv
.put(ProviderTableMeta
.FILE_CONTENT_LENGTH
, size
);
661 int result
= getContentResolver().update(ProviderTableMeta
.CONTENT_URI
, cv
, ProviderTableMeta
._ID
+ "=?",
662 new String
[] { String
.valueOf(id
) });
667 * Update the size of a subtree of folder from a file to the root
668 * @param parentId: parent of the file
670 private void updateSubtreeSize(long parentId
) {
674 while (parentId
!= 0) {
676 Log_OC
.d(TAG
, "parent = " + parentId
);
677 // Update the size of the parent
678 saveFolderSize(parentId
);
680 // search the next parent
681 file
= getFileById(parentId
);
682 parentId
= file
.getParentId();