DocumentsStorageProvider.java 34 KB

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