DocumentsStorageProvider.java 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Bartosz Przybylski
  5. * @author Chris Narkiewicz
  6. * Copyright (C) 2016 Bartosz Przybylski <bart.p.pl@gmail.com>
  7. * Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License version 2,
  11. * as published by the Free Software Foundation.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. *
  21. */
  22. package com.owncloud.android.providers;
  23. import android.accounts.Account;
  24. import android.accounts.AuthenticatorException;
  25. import android.accounts.OperationCanceledException;
  26. import android.annotation.SuppressLint;
  27. import android.content.ContentResolver;
  28. import android.content.Context;
  29. import android.content.res.AssetFileDescriptor;
  30. import android.database.Cursor;
  31. import android.graphics.Point;
  32. import android.net.Uri;
  33. import android.os.AsyncTask;
  34. import android.os.Bundle;
  35. import android.os.CancellationSignal;
  36. import android.os.Handler;
  37. import android.os.ParcelFileDescriptor;
  38. import android.provider.DocumentsContract;
  39. import android.provider.DocumentsProvider;
  40. import android.util.Log;
  41. import android.util.SparseArray;
  42. import com.nextcloud.client.account.UserAccountManager;
  43. import com.nextcloud.client.account.UserAccountManagerImpl;
  44. import com.nextcloud.client.files.downloader.DownloadTask;
  45. import com.nextcloud.client.preferences.AppPreferences;
  46. import com.nextcloud.client.preferences.AppPreferencesImpl;
  47. import com.owncloud.android.MainApp;
  48. import com.owncloud.android.R;
  49. import com.owncloud.android.datamodel.FileDataStorageManager;
  50. import com.owncloud.android.datamodel.OCFile;
  51. import com.owncloud.android.datamodel.ThumbnailsCacheManager;
  52. import com.owncloud.android.files.services.FileDownloader;
  53. import com.owncloud.android.files.services.FileUploader;
  54. import com.owncloud.android.files.services.NameCollisionPolicy;
  55. import com.owncloud.android.lib.common.OwnCloudAccount;
  56. import com.owncloud.android.lib.common.OwnCloudClient;
  57. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  58. import com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException;
  59. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  60. import com.owncloud.android.lib.common.utils.Log_OC;
  61. import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation;
  62. import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
  63. import com.owncloud.android.operations.CopyFileOperation;
  64. import com.owncloud.android.operations.CreateFolderOperation;
  65. import com.owncloud.android.operations.DownloadFileOperation;
  66. import com.owncloud.android.operations.MoveFileOperation;
  67. import com.owncloud.android.operations.RefreshFolderOperation;
  68. import com.owncloud.android.operations.RemoveFileOperation;
  69. import com.owncloud.android.operations.RenameFileOperation;
  70. import com.owncloud.android.ui.activity.SettingsActivity;
  71. import com.owncloud.android.utils.FileStorageUtils;
  72. import com.owncloud.android.utils.MimeTypeUtil;
  73. import org.nextcloud.providers.cursors.FileCursor;
  74. import org.nextcloud.providers.cursors.RootCursor;
  75. import java.io.File;
  76. import java.io.FileNotFoundException;
  77. import java.io.IOException;
  78. import java.util.ArrayList;
  79. import java.util.List;
  80. import java.util.Objects;
  81. import java.util.concurrent.Executor;
  82. import java.util.concurrent.Executors;
  83. import java.util.concurrent.TimeUnit;
  84. import androidx.annotation.NonNull;
  85. import androidx.annotation.VisibleForTesting;
  86. import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
  87. import static android.os.ParcelFileDescriptor.MODE_WRITE_ONLY;
  88. import static com.owncloud.android.datamodel.OCFile.PATH_SEPARATOR;
  89. import static com.owncloud.android.datamodel.OCFile.ROOT_PATH;
  90. import static com.owncloud.android.files.services.FileUploader.LOCAL_BEHAVIOUR_DELETE;
  91. public class DocumentsStorageProvider extends DocumentsProvider {
  92. private static final String TAG = DocumentsStorageProvider.class.getSimpleName();
  93. private static final long CACHE_EXPIRATION = TimeUnit.MILLISECONDS.convert(1, TimeUnit.MINUTES);
  94. UserAccountManager accountManager;
  95. @VisibleForTesting
  96. static final String DOCUMENTID_SEPARATOR = "/";
  97. private static final int DOCUMENTID_PARTS = 2;
  98. private final SparseArray<FileDataStorageManager> rootIdToStorageManager = new SparseArray<>();
  99. private final Executor executor = Executors.newCachedThreadPool();
  100. @Override
  101. public Cursor queryRoots(String[] projection) {
  102. // always recreate storage manager collection, as it will change after account creation/removal
  103. // and we need to serve document(tree)s with persist permissions
  104. initiateStorageMap();
  105. Context context = MainApp.getAppContext();
  106. AppPreferences preferences = AppPreferencesImpl.fromContext(context);
  107. if (SettingsActivity.LOCK_PASSCODE.equals(preferences.getLockPreference()) ||
  108. SettingsActivity.LOCK_DEVICE_CREDENTIALS.equals(preferences.getLockPreference())) {
  109. return new FileCursor();
  110. }
  111. final RootCursor result = new RootCursor(projection);
  112. for(int i = 0; i < rootIdToStorageManager.size(); i++) {
  113. result.addRoot(new Document(rootIdToStorageManager.valueAt(i), ROOT_PATH), getContext());
  114. }
  115. return result;
  116. }
  117. public static void notifyRootsChanged(Context context) {
  118. String authority = context.getString(R.string.document_provider_authority);
  119. Uri rootsUri = DocumentsContract.buildRootsUri(authority);
  120. context.getContentResolver().notifyChange(rootsUri, null);
  121. }
  122. @Override
  123. public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
  124. Log.d(TAG, "queryDocument(), id=" + documentId);
  125. Document document = toDocument(documentId);
  126. final FileCursor result = new FileCursor(projection);
  127. result.addFile(document);
  128. return result;
  129. }
  130. @SuppressLint("LongLogTag")
  131. @Override
  132. public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
  133. throws FileNotFoundException {
  134. Log.d(TAG, "queryChildDocuments(), id=" + parentDocumentId);
  135. Context context = getNonNullContext();
  136. Document parentFolder = toDocument(parentDocumentId);
  137. FileDataStorageManager storageManager = parentFolder.getStorageManager();
  138. final FileCursor resultCursor = new FileCursor(projection);
  139. for (OCFile file : storageManager.getFolderContent(parentFolder.getFile(), false)) {
  140. resultCursor.addFile(new Document(storageManager, file));
  141. }
  142. boolean isLoading = false;
  143. if (parentFolder.isExpired()) {
  144. final ReloadFolderDocumentTask task = new ReloadFolderDocumentTask(parentFolder, result ->
  145. context.getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false));
  146. task.executeOnExecutor(executor);
  147. resultCursor.setLoadingTask(task);
  148. isLoading = true;
  149. }
  150. final Bundle extra = new Bundle();
  151. extra.putBoolean(DocumentsContract.EXTRA_LOADING, isLoading);
  152. resultCursor.setExtras(extra);
  153. resultCursor.setNotificationUri(context.getContentResolver(), toNotifyUri(parentFolder));
  154. return resultCursor;
  155. }
  156. @SuppressLint("LongLogTag")
  157. @Override
  158. public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal)
  159. throws FileNotFoundException {
  160. Log.d(TAG, "openDocument(), id=" + documentId);
  161. Document document = toDocument(documentId);
  162. Context context = getNonNullContext();
  163. OCFile ocFile = document.getFile();
  164. Account account = document.getAccount();
  165. int accessMode = ParcelFileDescriptor.parseMode(mode);
  166. boolean writeOnly = (accessMode & MODE_WRITE_ONLY) != 0;
  167. boolean wasNotYetStored = ocFile.getStoragePath() == null;
  168. boolean needsDownload = (!writeOnly || wasNotYetStored) && (!ocFile.isDown() || hasServerChange(document));
  169. if (needsDownload) {
  170. if (ocFile.getLocalModificationTimestamp() > ocFile.getLastSyncDateForData()) {
  171. // TODO show a conflict notification with a pending intent that shows a ConflictResolveDialog
  172. Log_OC.w(TAG, "Conflict found!");
  173. } else {
  174. DownloadFileOperation downloadFileOperation = new DownloadFileOperation(account, ocFile, context);
  175. RemoteOperationResult result = downloadFileOperation.execute(document.getClient());
  176. if (!result.isSuccess()) {
  177. Log_OC.e(TAG, result.toString());
  178. throw new FileNotFoundException("Error downloading file: " + ocFile.getFileName());
  179. }
  180. saveDownloadedFile(document.getStorageManager(), downloadFileOperation, ocFile);
  181. }
  182. }
  183. File file = new File(ocFile.getStoragePath());
  184. if (accessMode != MODE_READ_ONLY) {
  185. // The calling thread is not guaranteed to have a Looper, so we can't block it with the OnCloseListener.
  186. // Thus, we are unable to do a synchronous upload and have to start an asynchronous one.
  187. Handler handler = new Handler(context.getMainLooper());
  188. try {
  189. return ParcelFileDescriptor.open(file, accessMode, handler, error -> {
  190. if (error == null) { // no error
  191. // As we can't upload the file synchronously, let's at least update its metadata here already.
  192. ocFile.setFileLength(file.length());
  193. ocFile.setModificationTimestamp(System.currentTimeMillis());
  194. document.getStorageManager().saveFile(ocFile);
  195. // TODO disable upload notifications as DocumentsProvider users already show them
  196. // upload file with FileUploader service (off main thread)
  197. FileUploader.uploadUpdateFile(
  198. context,
  199. account,
  200. ocFile,
  201. LOCAL_BEHAVIOUR_DELETE,
  202. NameCollisionPolicy.OVERWRITE,
  203. false);
  204. } else { // error, no upload needed
  205. Log_OC.e(TAG, "File was closed with an error: " + ocFile.getFileName(), error);
  206. }
  207. });
  208. } catch (IOException e) {
  209. throw new FileNotFoundException("Failed to open document for writing " + ocFile.getFileName());
  210. }
  211. } else {
  212. return ParcelFileDescriptor.open(file, accessMode);
  213. }
  214. }
  215. private boolean hasServerChange(Document document) throws FileNotFoundException {
  216. Context context = getNonNullContext();
  217. OCFile ocFile = document.getFile();
  218. RemoteOperationResult result = new CheckEtagRemoteOperation(ocFile.getRemotePath(), ocFile.getEtag())
  219. .execute(document.getAccount(), context);
  220. switch (result.getCode()) {
  221. case ETAG_CHANGED:
  222. return true;
  223. case ETAG_UNCHANGED:
  224. return false;
  225. case FILE_NOT_FOUND:
  226. default:
  227. Log_OC.e(TAG, result.toString());
  228. throw new FileNotFoundException("Error synchronizing file: " + ocFile.getFileName());
  229. }
  230. }
  231. /**
  232. * Updates the OC File after a successful download.
  233. *
  234. * TODO unify with code from {@link FileDownloader} and {@link DownloadTask}.
  235. */
  236. private void saveDownloadedFile(FileDataStorageManager storageManager, DownloadFileOperation dfo, OCFile file) {
  237. long syncDate = System.currentTimeMillis();
  238. file.setLastSyncDateForProperties(syncDate);
  239. file.setLastSyncDateForData(syncDate);
  240. file.setUpdateThumbnailNeeded(true);
  241. file.setModificationTimestamp(dfo.getModificationTimestamp());
  242. file.setModificationTimestampAtLastSyncForData(dfo.getModificationTimestamp());
  243. file.setEtag(dfo.getEtag());
  244. file.setMimeType(dfo.getMimeType());
  245. String savePath = dfo.getSavePath();
  246. file.setStoragePath(savePath);
  247. file.setFileLength(new File(savePath).length());
  248. file.setRemoteId(dfo.getFile().getRemoteId());
  249. storageManager.saveFile(file);
  250. if (MimeTypeUtil.isMedia(dfo.getMimeType())) {
  251. FileDataStorageManager.triggerMediaScan(file.getStoragePath(), file);
  252. }
  253. storageManager.saveConflict(file, null);
  254. }
  255. @Override
  256. public boolean onCreate() {
  257. accountManager = UserAccountManagerImpl.fromContext(getContext());
  258. // initiate storage manager collection, because we need to serve document(tree)s
  259. // with persist permissions
  260. initiateStorageMap();
  261. return true;
  262. }
  263. @Override
  264. public AssetFileDescriptor openDocumentThumbnail(String documentId,
  265. Point sizeHint,
  266. CancellationSignal signal)
  267. throws FileNotFoundException {
  268. Log.d(TAG, "openDocumentThumbnail(), id=" + documentId);
  269. Document document = toDocument(documentId);
  270. OCFile file = document.getFile();
  271. boolean exists = ThumbnailsCacheManager.containsBitmap(ThumbnailsCacheManager.PREFIX_THUMBNAIL
  272. + file.getRemoteId());
  273. if (!exists) {
  274. ThumbnailsCacheManager.generateThumbnailFromOCFile(file, document.getAccount(), getContext());
  275. }
  276. return new AssetFileDescriptor(DiskLruImageCacheFileProvider.getParcelFileDescriptorForOCFile(file),
  277. 0,
  278. file.getFileLength());
  279. }
  280. @Override
  281. public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
  282. Log.d(TAG, "renameDocument(), id=" + documentId);
  283. Document document = toDocument(documentId);
  284. RemoteOperationResult result = new RenameFileOperation(document.getRemotePath(), displayName)
  285. .execute(document.getClient(), document.getStorageManager());
  286. if (!result.isSuccess()) {
  287. Log_OC.e(TAG, result.toString());
  288. throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
  289. result.getException());
  290. }
  291. Context context = getNonNullContext();
  292. context.getContentResolver().notifyChange(toNotifyUri(document.getParent()), null, false);
  293. return null;
  294. }
  295. @Override
  296. public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
  297. Log.d(TAG, "copyDocument(), id=" + sourceDocumentId);
  298. Document document = toDocument(sourceDocumentId);
  299. FileDataStorageManager storageManager = document.getStorageManager();
  300. Document targetFolder = toDocument(targetParentDocumentId);
  301. RemoteOperationResult result = new CopyFileOperation(document.getRemotePath(), targetFolder.getRemotePath())
  302. .execute(document.getClient(), storageManager);
  303. if (!result.isSuccess()) {
  304. Log_OC.e(TAG, result.toString());
  305. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  306. + " to " + targetParentDocumentId);
  307. }
  308. Context context = getNonNullContext();
  309. Account account = document.getAccount();
  310. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
  311. false, false, true, storageManager,
  312. account, context)
  313. .execute(targetFolder.getClient());
  314. if (!updateParent.isSuccess()) {
  315. Log_OC.e(TAG, updateParent.toString());
  316. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  317. + " to " + targetParentDocumentId);
  318. }
  319. String newPath = targetFolder.getRemotePath() + document.getFile().getFileName();
  320. if (document.getFile().isFolder()) {
  321. newPath = newPath + PATH_SEPARATOR;
  322. }
  323. Document newFile = new Document(storageManager, newPath);
  324. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  325. return newFile.getDocumentId();
  326. }
  327. @Override
  328. public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)
  329. throws FileNotFoundException {
  330. Log.d(TAG, "moveDocument(), id=" + sourceDocumentId);
  331. Document document = toDocument(sourceDocumentId);
  332. Document targetFolder = toDocument(targetParentDocumentId);
  333. RemoteOperationResult result = new MoveFileOperation(document.getRemotePath(), targetFolder.getRemotePath())
  334. .execute(document.getClient(), document.getStorageManager());
  335. if (!result.isSuccess()) {
  336. Log_OC.e(TAG, result.toString());
  337. throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
  338. + " to " + targetParentDocumentId);
  339. }
  340. Document sourceFolder = toDocument(sourceParentDocumentId);
  341. Context context = getNonNullContext();
  342. context.getContentResolver().notifyChange(toNotifyUri(sourceFolder), null, false);
  343. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  344. return sourceDocumentId;
  345. }
  346. @Override
  347. public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
  348. Log.d(TAG, "querySearchDocuments(), rootId=" + rootId);
  349. FileCursor result = new FileCursor(projection);
  350. FileDataStorageManager storageManager = getStorageManager(rootId);
  351. if (storageManager == null) {
  352. return result;
  353. }
  354. for (Document d : findFiles(new Document(storageManager, ROOT_PATH), query)) {
  355. result.addFile(d);
  356. }
  357. return result;
  358. }
  359. @Override
  360. public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
  361. Log.d(TAG, "createDocument(), id=" + documentId);
  362. Document folderDocument = toDocument(documentId);
  363. if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {
  364. return createFolder(folderDocument, displayName);
  365. } else {
  366. return createFile(folderDocument, displayName, mimeType);
  367. }
  368. }
  369. private String createFolder(Document targetFolder, String displayName) throws FileNotFoundException {
  370. Context context = getNonNullContext();
  371. String newDirPath = targetFolder.getRemotePath() + displayName + PATH_SEPARATOR;
  372. FileDataStorageManager storageManager = targetFolder.getStorageManager();
  373. RemoteOperationResult result = new CreateFolderOperation(newDirPath,
  374. accountManager.getUser(),
  375. context)
  376. .execute(targetFolder.getClient(), storageManager);
  377. if (!result.isSuccess()) {
  378. Log_OC.e(TAG, result.toString());
  379. throw new FileNotFoundException("Failed to create document with name " +
  380. displayName + " and documentId " + targetFolder.getDocumentId());
  381. }
  382. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
  383. false, false, true, storageManager,
  384. targetFolder.getAccount(), context)
  385. .execute(targetFolder.getClient());
  386. if (!updateParent.isSuccess()) {
  387. Log_OC.e(TAG, updateParent.toString());
  388. throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
  389. }
  390. Document newFolder = new Document(storageManager, newDirPath);
  391. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  392. return newFolder.getDocumentId();
  393. }
  394. private String createFile(Document targetFolder, String displayName, String mimeType) throws FileNotFoundException {
  395. Account account = targetFolder.getAccount();
  396. // create dummy file
  397. File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
  398. if (!tempDir.exists() && !tempDir.mkdirs()) {
  399. throw new FileNotFoundException("Temp folder could not be created: " + tempDir.getAbsolutePath());
  400. }
  401. File emptyFile = new File(tempDir, displayName);
  402. if (emptyFile.exists() && !emptyFile.delete()) {
  403. throw new FileNotFoundException("Previous file could not be deleted");
  404. }
  405. try {
  406. if (!emptyFile.createNewFile()) {
  407. throw new FileNotFoundException("File could not be created");
  408. }
  409. } catch (IOException e) {
  410. throw getFileNotFoundExceptionWithCause("File could not be created", e);
  411. }
  412. String newFilePath = targetFolder.getRemotePath() + displayName;
  413. // FIXME we need to update the mimeType somewhere else as well
  414. // perform the upload, no need for chunked operation as we have a empty file
  415. OwnCloudClient client = targetFolder.getClient();
  416. RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(),
  417. newFilePath,
  418. mimeType,
  419. "",
  420. String.valueOf(System.currentTimeMillis() / 1000),
  421. false)
  422. .execute(client);
  423. if (!result.isSuccess()) {
  424. Log_OC.e(TAG, result.toString());
  425. throw new FileNotFoundException("Failed to upload document with path " + newFilePath);
  426. }
  427. Context context = getNonNullContext();
  428. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(),
  429. System.currentTimeMillis(),
  430. false,
  431. false,
  432. true,
  433. targetFolder.getStorageManager(),
  434. account,
  435. context)
  436. .execute(client);
  437. if (!updateParent.isSuccess()) {
  438. Log_OC.e(TAG, updateParent.toString());
  439. throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
  440. }
  441. Document newFile = new Document(targetFolder.getStorageManager(), newFilePath);
  442. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  443. return newFile.getDocumentId();
  444. }
  445. @Override
  446. public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
  447. deleteDocument(documentId);
  448. }
  449. @Override
  450. public void deleteDocument(String documentId) throws FileNotFoundException {
  451. Log.d(TAG, "deleteDocument(), id=" + documentId);
  452. Context context = getNonNullContext();
  453. Document document = toDocument(documentId);
  454. // get parent here, because it is not available anymore after the document was deleted
  455. Document parentFolder = document.getParent();
  456. recursiveRevokePermission(document);
  457. OCFile file = document.getStorageManager().getFileByPath(document.getRemotePath());
  458. RemoteOperationResult result = new RemoveFileOperation(file,
  459. false,
  460. document.getAccount(),
  461. true,
  462. context)
  463. .execute(document.getClient(), document.getStorageManager());
  464. if (!result.isSuccess()) {
  465. throw new FileNotFoundException("Failed to delete document with documentId " + documentId);
  466. }
  467. context.getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
  468. }
  469. private void recursiveRevokePermission(Document document) {
  470. FileDataStorageManager storageManager = document.getStorageManager();
  471. OCFile file = document.getFile();
  472. if (file.isFolder()) {
  473. for (OCFile child : storageManager.getFolderContent(file, false)) {
  474. recursiveRevokePermission(new Document(storageManager, child));
  475. }
  476. }
  477. revokeDocumentPermission(document.getDocumentId());
  478. }
  479. @Override
  480. public boolean isChildDocument(String parentDocumentId, String documentId) {
  481. Log.d(TAG, "isChildDocument(), parent=" + parentDocumentId + ", id=" + documentId);
  482. try {
  483. // get file for parent document
  484. Document parentDocument = toDocument(parentDocumentId);
  485. OCFile parentFile = parentDocument.getFile();
  486. if (parentFile == null) {
  487. throw new FileNotFoundException("No parent file with ID " + parentDocumentId);
  488. }
  489. // get file for child candidate document
  490. Document currentDocument = toDocument(documentId);
  491. OCFile childFile = currentDocument.getFile();
  492. if (childFile == null) {
  493. throw new FileNotFoundException("No child file with ID " + documentId);
  494. }
  495. String parentPath = parentFile.getDecryptedRemotePath();
  496. String childPath = childFile.getDecryptedRemotePath();
  497. // The alternative is to go up the folder hierarchy from currentDocument with getParent()
  498. // until we arrive at parentDocument or the storage root.
  499. // However, especially for long paths this is expensive and can take substantial time.
  500. // The solution below uses paths and is faster by a factor of 2-10 depending on the nesting level of child.
  501. // So far, the same document with its unique ID can never be in two places at once.
  502. // If this assumption ever changes, this code would need to be adapted.
  503. return parentDocument.getAccount() == currentDocument.getAccount() && childPath.startsWith(parentPath);
  504. } catch (FileNotFoundException e) {
  505. Log.e(TAG, "failed to check for child document", e);
  506. }
  507. return false;
  508. }
  509. private FileNotFoundException getFileNotFoundExceptionWithCause(String msg, Exception cause) {
  510. FileNotFoundException e = new FileNotFoundException(msg);
  511. e.initCause(cause);
  512. return e;
  513. }
  514. private FileDataStorageManager getStorageManager(String rootId) {
  515. for(int i = 0; i < rootIdToStorageManager.size(); i++) {
  516. FileDataStorageManager storageManager = rootIdToStorageManager.valueAt(i);
  517. if (storageManager.getAccount().name.equals(rootId)) {
  518. return storageManager;
  519. }
  520. }
  521. return null;
  522. }
  523. private void initiateStorageMap() {
  524. rootIdToStorageManager.clear();
  525. ContentResolver contentResolver = getContext().getContentResolver();
  526. for (Account account : accountManager.getAccounts()) {
  527. final FileDataStorageManager storageManager = new FileDataStorageManager(account, contentResolver);
  528. rootIdToStorageManager.put(account.hashCode(), storageManager);
  529. }
  530. }
  531. private List<Document> findFiles(Document root, String query) {
  532. FileDataStorageManager storageManager = root.getStorageManager();
  533. List<Document> result = new ArrayList<>();
  534. for (OCFile f : storageManager.getFolderContent(root.getFile(), false)) {
  535. if (f.isFolder()) {
  536. result.addAll(findFiles(new Document(storageManager, f), query));
  537. } else if (f.getFileName().contains(query)) {
  538. result.add(new Document(storageManager, f));
  539. }
  540. }
  541. return result;
  542. }
  543. private Uri toNotifyUri(Document document) {
  544. return DocumentsContract.buildDocumentUri(
  545. getContext().getString(R.string.document_provider_authority),
  546. document.getDocumentId());
  547. }
  548. private Document toDocument(String documentId) throws FileNotFoundException {
  549. String[] separated = documentId.split(DOCUMENTID_SEPARATOR, DOCUMENTID_PARTS);
  550. if (separated.length != DOCUMENTID_PARTS) {
  551. throw new FileNotFoundException("Invalid documentID " + documentId + "!");
  552. }
  553. FileDataStorageManager storageManager = rootIdToStorageManager.get(Integer.parseInt(separated[0]));
  554. if (storageManager == null) {
  555. throw new FileNotFoundException("No storage manager associated for " + documentId + "!");
  556. }
  557. return new Document(storageManager, Long.parseLong(separated[1]));
  558. }
  559. /**
  560. * Returns a {@link Context} guaranteed to be non-null.
  561. *
  562. * @throws IllegalStateException if called before {@link #onCreate()}.
  563. */
  564. @NonNull
  565. private Context getNonNullContext() {
  566. Context context = getContext();
  567. if (context == null) {
  568. throw new IllegalStateException();
  569. }
  570. return context;
  571. }
  572. public interface OnTaskFinishedCallback {
  573. void onTaskFinished(RemoteOperationResult result);
  574. }
  575. static class ReloadFolderDocumentTask extends AsyncTask<Void, Void, RemoteOperationResult> {
  576. private final Document folder;
  577. private final OnTaskFinishedCallback callback;
  578. ReloadFolderDocumentTask(Document folder, OnTaskFinishedCallback callback) {
  579. this.folder = folder;
  580. this.callback = callback;
  581. }
  582. @Override
  583. public final RemoteOperationResult doInBackground(Void... params) {
  584. Log.d(TAG, "run ReloadFolderDocumentTask(), id=" + folder.getDocumentId());
  585. return new RefreshFolderOperation(folder.getFile(), System.currentTimeMillis(), false,
  586. false, true, folder.getStorageManager(), folder.getAccount(),
  587. MainApp.getAppContext())
  588. .execute(folder.getClient());
  589. }
  590. @Override
  591. public final void onPostExecute(RemoteOperationResult result) {
  592. if (callback != null) {
  593. callback.onTaskFinished(result);
  594. }
  595. }
  596. }
  597. public class Document {
  598. private final FileDataStorageManager storageManager;
  599. private final long fileId;
  600. Document(FileDataStorageManager storageManager, long fileId) {
  601. this.storageManager = storageManager;
  602. this.fileId = fileId;
  603. }
  604. Document(FileDataStorageManager storageManager, OCFile file) {
  605. this.storageManager = storageManager;
  606. this.fileId = file.getFileId();
  607. }
  608. Document(FileDataStorageManager storageManager, String filePath) {
  609. this.storageManager = storageManager;
  610. this.fileId = storageManager.getFileByPath(filePath).getFileId();
  611. }
  612. public String getDocumentId() {
  613. for(int i = 0; i < rootIdToStorageManager.size(); i++) {
  614. if (Objects.equals(storageManager, rootIdToStorageManager.valueAt(i))) {
  615. return rootIdToStorageManager.keyAt(i) + DOCUMENTID_SEPARATOR + fileId;
  616. }
  617. }
  618. return null;
  619. }
  620. FileDataStorageManager getStorageManager() {
  621. return storageManager;
  622. }
  623. public Account getAccount() {
  624. return getStorageManager().getAccount();
  625. }
  626. public OCFile getFile() {
  627. return getStorageManager().getFileById(fileId);
  628. }
  629. public String getRemotePath() {
  630. return getFile().getRemotePath();
  631. }
  632. OwnCloudClient getClient() {
  633. try {
  634. OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), MainApp.getAppContext());
  635. return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
  636. } catch (OperationCanceledException | IOException | AuthenticatorException | AccountNotFoundException e) {
  637. Log_OC.e(TAG, "Failed to set client", e);
  638. }
  639. return null;
  640. }
  641. boolean isExpired() {
  642. return getFile().getLastSyncDateForData() + CACHE_EXPIRATION < System.currentTimeMillis();
  643. }
  644. Document getParent() {
  645. long parentId = getFile().getParentId();
  646. if (parentId <= 0) {
  647. return null;
  648. }
  649. return new Document(getStorageManager(), parentId);
  650. }
  651. }
  652. }