DocumentsStorageProvider.java 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815
  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(),
  285. displayName,
  286. document.getStorageManager())
  287. .execute(document.getClient());
  288. if (!result.isSuccess()) {
  289. Log_OC.e(TAG, result.toString());
  290. throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
  291. result.getException());
  292. }
  293. Context context = getNonNullContext();
  294. context.getContentResolver().notifyChange(toNotifyUri(document.getParent()), null, false);
  295. return null;
  296. }
  297. @Override
  298. public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
  299. Log.d(TAG, "copyDocument(), id=" + sourceDocumentId);
  300. Document document = toDocument(sourceDocumentId);
  301. FileDataStorageManager storageManager = document.getStorageManager();
  302. Document targetFolder = toDocument(targetParentDocumentId);
  303. RemoteOperationResult result = new CopyFileOperation(document.getRemotePath(),
  304. targetFolder.getRemotePath(),
  305. document.getStorageManager())
  306. .execute(document.getClient());
  307. if (!result.isSuccess()) {
  308. Log_OC.e(TAG, result.toString());
  309. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  310. + " to " + targetParentDocumentId);
  311. }
  312. Context context = getNonNullContext();
  313. Account account = document.getAccount();
  314. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
  315. false, false, true, storageManager,
  316. account, context)
  317. .execute(targetFolder.getClient());
  318. if (!updateParent.isSuccess()) {
  319. Log_OC.e(TAG, updateParent.toString());
  320. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  321. + " to " + targetParentDocumentId);
  322. }
  323. String newPath = targetFolder.getRemotePath() + document.getFile().getFileName();
  324. if (document.getFile().isFolder()) {
  325. newPath = newPath + PATH_SEPARATOR;
  326. }
  327. Document newFile = new Document(storageManager, newPath);
  328. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  329. return newFile.getDocumentId();
  330. }
  331. @Override
  332. public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)
  333. throws FileNotFoundException {
  334. Log.d(TAG, "moveDocument(), id=" + sourceDocumentId);
  335. Document document = toDocument(sourceDocumentId);
  336. Document targetFolder = toDocument(targetParentDocumentId);
  337. RemoteOperationResult result = new MoveFileOperation(document.getRemotePath(),
  338. targetFolder.getRemotePath(),
  339. document.getStorageManager())
  340. .execute(document.getClient());
  341. if (!result.isSuccess()) {
  342. Log_OC.e(TAG, result.toString());
  343. throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
  344. + " to " + targetParentDocumentId);
  345. }
  346. Document sourceFolder = toDocument(sourceParentDocumentId);
  347. Context context = getNonNullContext();
  348. context.getContentResolver().notifyChange(toNotifyUri(sourceFolder), null, false);
  349. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  350. return sourceDocumentId;
  351. }
  352. @Override
  353. public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
  354. Log.d(TAG, "querySearchDocuments(), rootId=" + rootId);
  355. FileCursor result = new FileCursor(projection);
  356. FileDataStorageManager storageManager = getStorageManager(rootId);
  357. if (storageManager == null) {
  358. return result;
  359. }
  360. for (Document d : findFiles(new Document(storageManager, ROOT_PATH), query)) {
  361. result.addFile(d);
  362. }
  363. return result;
  364. }
  365. @Override
  366. public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
  367. Log.d(TAG, "createDocument(), id=" + documentId);
  368. Document folderDocument = toDocument(documentId);
  369. if (DocumentsContract.Document.MIME_TYPE_DIR.equalsIgnoreCase(mimeType)) {
  370. return createFolder(folderDocument, displayName);
  371. } else {
  372. return createFile(folderDocument, displayName, mimeType);
  373. }
  374. }
  375. private String createFolder(Document targetFolder, String displayName) throws FileNotFoundException {
  376. Context context = getNonNullContext();
  377. String newDirPath = targetFolder.getRemotePath() + displayName + PATH_SEPARATOR;
  378. FileDataStorageManager storageManager = targetFolder.getStorageManager();
  379. RemoteOperationResult result = new CreateFolderOperation(newDirPath,
  380. accountManager.getUser(),
  381. context,
  382. storageManager)
  383. .execute(targetFolder.getClient());
  384. if (!result.isSuccess()) {
  385. Log_OC.e(TAG, result.toString());
  386. throw new FileNotFoundException("Failed to create document with name " +
  387. displayName + " and documentId " + targetFolder.getDocumentId());
  388. }
  389. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(), System.currentTimeMillis(),
  390. false, false, true, storageManager,
  391. targetFolder.getAccount(), context)
  392. .execute(targetFolder.getClient());
  393. if (!updateParent.isSuccess()) {
  394. Log_OC.e(TAG, updateParent.toString());
  395. throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
  396. }
  397. Document newFolder = new Document(storageManager, newDirPath);
  398. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  399. return newFolder.getDocumentId();
  400. }
  401. private String createFile(Document targetFolder, String displayName, String mimeType) throws FileNotFoundException {
  402. Account account = targetFolder.getAccount();
  403. // create dummy file
  404. File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
  405. if (!tempDir.exists() && !tempDir.mkdirs()) {
  406. throw new FileNotFoundException("Temp folder could not be created: " + tempDir.getAbsolutePath());
  407. }
  408. File emptyFile = new File(tempDir, displayName);
  409. if (emptyFile.exists() && !emptyFile.delete()) {
  410. throw new FileNotFoundException("Previous file could not be deleted");
  411. }
  412. try {
  413. if (!emptyFile.createNewFile()) {
  414. throw new FileNotFoundException("File could not be created");
  415. }
  416. } catch (IOException e) {
  417. throw getFileNotFoundExceptionWithCause("File could not be created", e);
  418. }
  419. String newFilePath = targetFolder.getRemotePath() + displayName;
  420. // FIXME we need to update the mimeType somewhere else as well
  421. // perform the upload, no need for chunked operation as we have a empty file
  422. OwnCloudClient client = targetFolder.getClient();
  423. RemoteOperationResult result = new UploadFileRemoteOperation(emptyFile.getAbsolutePath(),
  424. newFilePath,
  425. mimeType,
  426. "",
  427. String.valueOf(System.currentTimeMillis() / 1000),
  428. false)
  429. .execute(client);
  430. if (!result.isSuccess()) {
  431. Log_OC.e(TAG, result.toString());
  432. throw new FileNotFoundException("Failed to upload document with path " + newFilePath);
  433. }
  434. Context context = getNonNullContext();
  435. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder.getFile(),
  436. System.currentTimeMillis(),
  437. false,
  438. false,
  439. true,
  440. targetFolder.getStorageManager(),
  441. account,
  442. context)
  443. .execute(client);
  444. if (!updateParent.isSuccess()) {
  445. Log_OC.e(TAG, updateParent.toString());
  446. throw new FileNotFoundException("Failed to create document with documentId " + targetFolder.getDocumentId());
  447. }
  448. Document newFile = new Document(targetFolder.getStorageManager(), newFilePath);
  449. context.getContentResolver().notifyChange(toNotifyUri(targetFolder), null, false);
  450. return newFile.getDocumentId();
  451. }
  452. @Override
  453. public void removeDocument(String documentId, String parentDocumentId) throws FileNotFoundException {
  454. deleteDocument(documentId);
  455. }
  456. @Override
  457. public void deleteDocument(String documentId) throws FileNotFoundException {
  458. Log.d(TAG, "deleteDocument(), id=" + documentId);
  459. Context context = getNonNullContext();
  460. Document document = toDocument(documentId);
  461. // get parent here, because it is not available anymore after the document was deleted
  462. Document parentFolder = document.getParent();
  463. recursiveRevokePermission(document);
  464. OCFile file = document.getStorageManager().getFileByPath(document.getRemotePath());
  465. RemoteOperationResult result = new RemoveFileOperation(file,
  466. false,
  467. document.getAccount(),
  468. true,
  469. context,
  470. document.getStorageManager())
  471. .execute(document.getClient());
  472. if (!result.isSuccess()) {
  473. throw new FileNotFoundException("Failed to delete document with documentId " + documentId);
  474. }
  475. context.getContentResolver().notifyChange(toNotifyUri(parentFolder), null, false);
  476. }
  477. private void recursiveRevokePermission(Document document) {
  478. FileDataStorageManager storageManager = document.getStorageManager();
  479. OCFile file = document.getFile();
  480. if (file.isFolder()) {
  481. for (OCFile child : storageManager.getFolderContent(file, false)) {
  482. recursiveRevokePermission(new Document(storageManager, child));
  483. }
  484. }
  485. revokeDocumentPermission(document.getDocumentId());
  486. }
  487. @Override
  488. public boolean isChildDocument(String parentDocumentId, String documentId) {
  489. Log.d(TAG, "isChildDocument(), parent=" + parentDocumentId + ", id=" + documentId);
  490. try {
  491. // get file for parent document
  492. Document parentDocument = toDocument(parentDocumentId);
  493. OCFile parentFile = parentDocument.getFile();
  494. if (parentFile == null) {
  495. throw new FileNotFoundException("No parent file with ID " + parentDocumentId);
  496. }
  497. // get file for child candidate document
  498. Document currentDocument = toDocument(documentId);
  499. OCFile childFile = currentDocument.getFile();
  500. if (childFile == null) {
  501. throw new FileNotFoundException("No child file with ID " + documentId);
  502. }
  503. String parentPath = parentFile.getDecryptedRemotePath();
  504. String childPath = childFile.getDecryptedRemotePath();
  505. // The alternative is to go up the folder hierarchy from currentDocument with getParent()
  506. // until we arrive at parentDocument or the storage root.
  507. // However, especially for long paths this is expensive and can take substantial time.
  508. // The solution below uses paths and is faster by a factor of 2-10 depending on the nesting level of child.
  509. // So far, the same document with its unique ID can never be in two places at once.
  510. // If this assumption ever changes, this code would need to be adapted.
  511. return parentDocument.getAccount() == currentDocument.getAccount() && childPath.startsWith(parentPath);
  512. } catch (FileNotFoundException e) {
  513. Log.e(TAG, "failed to check for child document", e);
  514. }
  515. return false;
  516. }
  517. private FileNotFoundException getFileNotFoundExceptionWithCause(String msg, Exception cause) {
  518. FileNotFoundException e = new FileNotFoundException(msg);
  519. e.initCause(cause);
  520. return e;
  521. }
  522. private FileDataStorageManager getStorageManager(String rootId) {
  523. for(int i = 0; i < rootIdToStorageManager.size(); i++) {
  524. FileDataStorageManager storageManager = rootIdToStorageManager.valueAt(i);
  525. if (storageManager.getAccount().name.equals(rootId)) {
  526. return storageManager;
  527. }
  528. }
  529. return null;
  530. }
  531. private void initiateStorageMap() {
  532. rootIdToStorageManager.clear();
  533. ContentResolver contentResolver = getContext().getContentResolver();
  534. for (Account account : accountManager.getAccounts()) {
  535. final FileDataStorageManager storageManager = new FileDataStorageManager(account, contentResolver);
  536. rootIdToStorageManager.put(account.hashCode(), storageManager);
  537. }
  538. }
  539. private List<Document> findFiles(Document root, String query) {
  540. FileDataStorageManager storageManager = root.getStorageManager();
  541. List<Document> result = new ArrayList<>();
  542. for (OCFile f : storageManager.getFolderContent(root.getFile(), false)) {
  543. if (f.isFolder()) {
  544. result.addAll(findFiles(new Document(storageManager, f), query));
  545. } else if (f.getFileName().contains(query)) {
  546. result.add(new Document(storageManager, f));
  547. }
  548. }
  549. return result;
  550. }
  551. private Uri toNotifyUri(Document document) {
  552. return DocumentsContract.buildDocumentUri(
  553. getContext().getString(R.string.document_provider_authority),
  554. document.getDocumentId());
  555. }
  556. private Document toDocument(String documentId) throws FileNotFoundException {
  557. String[] separated = documentId.split(DOCUMENTID_SEPARATOR, DOCUMENTID_PARTS);
  558. if (separated.length != DOCUMENTID_PARTS) {
  559. throw new FileNotFoundException("Invalid documentID " + documentId + "!");
  560. }
  561. FileDataStorageManager storageManager = rootIdToStorageManager.get(Integer.parseInt(separated[0]));
  562. if (storageManager == null) {
  563. throw new FileNotFoundException("No storage manager associated for " + documentId + "!");
  564. }
  565. return new Document(storageManager, Long.parseLong(separated[1]));
  566. }
  567. /**
  568. * Returns a {@link Context} guaranteed to be non-null.
  569. *
  570. * @throws IllegalStateException if called before {@link #onCreate()}.
  571. */
  572. @NonNull
  573. private Context getNonNullContext() {
  574. Context context = getContext();
  575. if (context == null) {
  576. throw new IllegalStateException();
  577. }
  578. return context;
  579. }
  580. public interface OnTaskFinishedCallback {
  581. void onTaskFinished(RemoteOperationResult result);
  582. }
  583. static class ReloadFolderDocumentTask extends AsyncTask<Void, Void, RemoteOperationResult> {
  584. private final Document folder;
  585. private final OnTaskFinishedCallback callback;
  586. ReloadFolderDocumentTask(Document folder, OnTaskFinishedCallback callback) {
  587. this.folder = folder;
  588. this.callback = callback;
  589. }
  590. @Override
  591. public final RemoteOperationResult doInBackground(Void... params) {
  592. Log.d(TAG, "run ReloadFolderDocumentTask(), id=" + folder.getDocumentId());
  593. return new RefreshFolderOperation(folder.getFile(),
  594. System.currentTimeMillis(),
  595. false,
  596. true,
  597. true,
  598. folder.getStorageManager(),
  599. folder.getAccount(),
  600. MainApp.getAppContext())
  601. .execute(folder.getClient());
  602. }
  603. @Override
  604. public final void onPostExecute(RemoteOperationResult result) {
  605. if (callback != null) {
  606. callback.onTaskFinished(result);
  607. }
  608. }
  609. }
  610. public class Document {
  611. private final FileDataStorageManager storageManager;
  612. private final long fileId;
  613. Document(FileDataStorageManager storageManager, long fileId) {
  614. this.storageManager = storageManager;
  615. this.fileId = fileId;
  616. }
  617. Document(FileDataStorageManager storageManager, OCFile file) {
  618. this.storageManager = storageManager;
  619. this.fileId = file.getFileId();
  620. }
  621. Document(FileDataStorageManager storageManager, String filePath) {
  622. this.storageManager = storageManager;
  623. this.fileId = storageManager.getFileByPath(filePath).getFileId();
  624. }
  625. public String getDocumentId() {
  626. for(int i = 0; i < rootIdToStorageManager.size(); i++) {
  627. if (Objects.equals(storageManager, rootIdToStorageManager.valueAt(i))) {
  628. return rootIdToStorageManager.keyAt(i) + DOCUMENTID_SEPARATOR + fileId;
  629. }
  630. }
  631. return null;
  632. }
  633. FileDataStorageManager getStorageManager() {
  634. return storageManager;
  635. }
  636. public Account getAccount() {
  637. return getStorageManager().getAccount();
  638. }
  639. public OCFile getFile() {
  640. return getStorageManager().getFileById(fileId);
  641. }
  642. public String getRemotePath() {
  643. return getFile().getRemotePath();
  644. }
  645. OwnCloudClient getClient() {
  646. try {
  647. OwnCloudAccount ocAccount = new OwnCloudAccount(getAccount(), MainApp.getAppContext());
  648. return OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
  649. } catch (OperationCanceledException | IOException | AuthenticatorException | AccountNotFoundException e) {
  650. Log_OC.e(TAG, "Failed to set client", e);
  651. }
  652. return null;
  653. }
  654. boolean isExpired() {
  655. return getFile().getLastSyncDateForData() + CACHE_EXPIRATION < System.currentTimeMillis();
  656. }
  657. Document getParent() {
  658. long parentId = getFile().getParentId();
  659. if (parentId <= 0) {
  660. return null;
  661. }
  662. return new Document(getStorageManager(), parentId);
  663. }
  664. }
  665. }