DocumentsStorageProvider.java 34 KB

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