DocumentsStorageProvider.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /*
  2. * Nextcloud Android client application
  3. *
  4. * @author Bartosz Przybylski
  5. * Copyright (C) 2016 Bartosz Przybylski <bart.p.pl@gmail.com>
  6. *
  7. * This program is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License version 2,
  9. * as published by the Free Software Foundation.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. *
  19. */
  20. package com.owncloud.android.providers;
  21. import android.accounts.Account;
  22. import android.accounts.AuthenticatorException;
  23. import android.accounts.OperationCanceledException;
  24. import android.annotation.SuppressLint;
  25. import android.annotation.TargetApi;
  26. import android.content.ContentResolver;
  27. import android.content.Context;
  28. import android.content.Intent;
  29. import android.content.SharedPreferences;
  30. import android.content.res.AssetFileDescriptor;
  31. import android.database.Cursor;
  32. import android.graphics.Point;
  33. import android.os.Build;
  34. import android.os.CancellationSignal;
  35. import android.os.Handler;
  36. import android.os.Looper;
  37. import android.os.ParcelFileDescriptor;
  38. import android.preference.PreferenceManager;
  39. import android.provider.DocumentsProvider;
  40. import android.util.Log;
  41. import android.widget.Toast;
  42. import com.evernote.android.job.JobRequest;
  43. import com.evernote.android.job.util.Device;
  44. import com.owncloud.android.MainApp;
  45. import com.owncloud.android.R;
  46. import com.owncloud.android.authentication.AccountUtils;
  47. import com.owncloud.android.datamodel.FileDataStorageManager;
  48. import com.owncloud.android.datamodel.OCFile;
  49. import com.owncloud.android.files.services.FileDownloader;
  50. import com.owncloud.android.files.services.FileUploader;
  51. import com.owncloud.android.lib.common.OwnCloudAccount;
  52. import com.owncloud.android.lib.common.OwnCloudClient;
  53. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  54. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  55. import com.owncloud.android.lib.common.utils.Log_OC;
  56. import com.owncloud.android.operations.CreateFolderOperation;
  57. import com.owncloud.android.operations.RefreshFolderOperation;
  58. import com.owncloud.android.operations.RemoveFileOperation;
  59. import com.owncloud.android.operations.RenameFileOperation;
  60. import com.owncloud.android.operations.SynchronizeFileOperation;
  61. import com.owncloud.android.operations.UploadFileOperation;
  62. import com.owncloud.android.ui.activity.ConflictsResolveActivity;
  63. import com.owncloud.android.ui.activity.Preferences;
  64. import com.owncloud.android.utils.FileStorageUtils;
  65. import org.nextcloud.providers.cursors.FileCursor;
  66. import org.nextcloud.providers.cursors.RootCursor;
  67. import java.io.File;
  68. import java.io.FileNotFoundException;
  69. import java.io.IOException;
  70. import java.util.ArrayList;
  71. import java.util.HashMap;
  72. import java.util.List;
  73. import java.util.Map;
  74. @TargetApi(Build.VERSION_CODES.KITKAT)
  75. public class DocumentsStorageProvider extends DocumentsProvider {
  76. private static final String TAG = "DocumentsStorageProvider";
  77. private FileDataStorageManager currentStorageManager;
  78. private Map<Long, FileDataStorageManager> rootIdToStorageManager;
  79. private OwnCloudClient client;
  80. @Override
  81. public Cursor queryRoots(String[] projection) throws FileNotFoundException {
  82. SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext());
  83. if (Preferences.LOCK_PASSCODE.equals(appPrefs.getString(Preferences.PREFERENCE_LOCK, "")) ||
  84. Preferences.LOCK_DEVICE_CREDENTIALS.equals(appPrefs.getString(Preferences.PREFERENCE_LOCK, ""))) {
  85. return new FileCursor();
  86. }
  87. initiateStorageMap();
  88. final RootCursor result = new RootCursor(projection);
  89. for (Account account : AccountUtils.getAccounts(getContext())) {
  90. result.addRoot(account, getContext());
  91. }
  92. return result;
  93. }
  94. @Override
  95. public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
  96. final long docId = Long.parseLong(documentId);
  97. updateCurrentStorageManagerIfNeeded(docId);
  98. final FileCursor result = new FileCursor(projection);
  99. if (currentStorageManager == null) {
  100. for (Map.Entry<Long, FileDataStorageManager> entry : rootIdToStorageManager.entrySet()) {
  101. if (entry.getValue().getFileById(docId) != null) {
  102. currentStorageManager = entry.getValue();
  103. break;
  104. }
  105. }
  106. }
  107. if (currentStorageManager == null) {
  108. throw new FileNotFoundException("File with id " + documentId + " not found");
  109. }
  110. OCFile file = currentStorageManager.getFileById(docId);
  111. if (file != null) {
  112. result.addFile(file);
  113. }
  114. return result;
  115. }
  116. @SuppressLint("LongLogTag")
  117. @Override
  118. public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
  119. throws FileNotFoundException {
  120. final long folderId = Long.parseLong(parentDocumentId);
  121. updateCurrentStorageManagerIfNeeded(folderId);
  122. final OCFile browsedDir = currentStorageManager.getFileById(folderId);
  123. Account account = currentStorageManager.getAccount();
  124. if (Device.getNetworkType(getContext()).equals(JobRequest.NetworkType.UNMETERED)) {
  125. RemoteOperationResult result = new RefreshFolderOperation(browsedDir, System.currentTimeMillis(), false,
  126. false, true, currentStorageManager, account,
  127. getContext()).execute(client);
  128. if (!result.isSuccess()) {
  129. throw new FileNotFoundException("Failed to update document " + parentDocumentId);
  130. }
  131. }
  132. final FileCursor resultCursor = new FileCursor(projection);
  133. for (OCFile file : currentStorageManager.getFolderContent(browsedDir, false)) {
  134. resultCursor.addFile(file);
  135. }
  136. return resultCursor;
  137. }
  138. @SuppressLint("LongLogTag")
  139. @Override
  140. public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal)
  141. throws FileNotFoundException {
  142. final long docId = Long.parseLong(documentId);
  143. updateCurrentStorageManagerIfNeeded(docId);
  144. OCFile ocFile = currentStorageManager.getFileById(docId);
  145. if (ocFile == null) {
  146. throw new FileNotFoundException("File not found: " + documentId);
  147. }
  148. Account account = currentStorageManager.getAccount();
  149. Context context = getContext();
  150. if (context == null) {
  151. throw new FileNotFoundException("Context may not be null!");
  152. }
  153. if (!ocFile.isDown()) {
  154. Intent i = new Intent(getContext(), FileDownloader.class);
  155. i.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
  156. i.putExtra(FileDownloader.EXTRA_FILE, ocFile);
  157. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
  158. context.startForegroundService(i);
  159. } else {
  160. context.startService(i);
  161. }
  162. do {
  163. if (!waitOrGetCancelled(cancellationSignal)) {
  164. throw new FileNotFoundException("File with id " + documentId + " not found!");
  165. }
  166. ocFile = currentStorageManager.getFileById(docId);
  167. if (ocFile == null) {
  168. throw new FileNotFoundException("File with id " + documentId + " not found!");
  169. }
  170. } while (!ocFile.isDown());
  171. } else {
  172. OCFile finalFile = ocFile;
  173. Thread syncThread = new Thread(() -> {
  174. try {
  175. FileDataStorageManager storageManager =
  176. new FileDataStorageManager(account, context.getContentResolver());
  177. SynchronizeFileOperation sfo =
  178. new SynchronizeFileOperation(finalFile, null, account, true, context);
  179. RemoteOperationResult result = sfo.execute(storageManager, context);
  180. if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) {
  181. // ISSUE 5: if the user is not running the app (this is a service!),
  182. // this can be very intrusive; a notification should be preferred
  183. Intent i = new Intent(context, ConflictsResolveActivity.class);
  184. i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
  185. i.putExtra(ConflictsResolveActivity.EXTRA_FILE, finalFile);
  186. i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, account);
  187. context.startActivity(i);
  188. } else {
  189. FileStorageUtils.checkIfFileFinishedSaving(finalFile);
  190. if (!result.isSuccess()) {
  191. showToast();
  192. }
  193. }
  194. } catch (Exception exception) {
  195. showToast();
  196. }
  197. });
  198. syncThread.start();
  199. try {
  200. syncThread.join();
  201. } catch (InterruptedException e) {
  202. Log.e(TAG, "Failed to wait for thread to finish");
  203. }
  204. }
  205. File file = new File(ocFile.getStoragePath());
  206. int accessMode = ParcelFileDescriptor.parseMode(mode);
  207. boolean isWrite = (mode.indexOf('w') != -1);
  208. final OCFile oldFile = ocFile;
  209. final OCFile newFile = ocFile;
  210. if (isWrite) {
  211. try {
  212. Handler handler = new Handler(context.getMainLooper());
  213. return ParcelFileDescriptor.open(file, accessMode, handler, l -> {
  214. RemoteOperationResult result = new SynchronizeFileOperation(newFile, oldFile, account, true,
  215. context)
  216. .execute(client, currentStorageManager);
  217. boolean success = result.isSuccess();
  218. if (!success) {
  219. Log_OC.e(TAG, "Failed to update document with id " + documentId);
  220. }
  221. });
  222. } catch (IOException e) {
  223. throw new FileNotFoundException("Failed to open/edit document with id " + documentId);
  224. }
  225. } else {
  226. return ParcelFileDescriptor.open(file, accessMode);
  227. }
  228. }
  229. private void showToast() {
  230. Handler handler = new Handler(Looper.getMainLooper());
  231. handler.post(() -> Toast.makeText(MainApp.getAppContext(),
  232. R.string.file_not_synced,
  233. Toast.LENGTH_SHORT).show());
  234. }
  235. @Override
  236. public boolean onCreate() {
  237. return true;
  238. }
  239. @Override
  240. public AssetFileDescriptor openDocumentThumbnail(String documentId,
  241. Point sizeHint,
  242. CancellationSignal signal)
  243. throws FileNotFoundException {
  244. long docId = Long.parseLong(documentId);
  245. updateCurrentStorageManagerIfNeeded(docId);
  246. OCFile file = currentStorageManager.getFileById(docId);
  247. if (file == null) {
  248. throw new FileNotFoundException("File with id " + documentId + " not found!");
  249. }
  250. File realFile = new File(file.getStoragePath());
  251. return new AssetFileDescriptor(
  252. ParcelFileDescriptor.open(realFile, ParcelFileDescriptor.MODE_READ_ONLY),
  253. 0,
  254. AssetFileDescriptor.UNKNOWN_LENGTH);
  255. }
  256. @Override
  257. public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
  258. long docId = Long.parseLong(documentId);
  259. updateCurrentStorageManagerIfNeeded(docId);
  260. OCFile file = currentStorageManager.getFileById(docId);
  261. if (file == null) {
  262. throw new FileNotFoundException("File " + documentId + " not found!");
  263. }
  264. RemoteOperationResult result = new RenameFileOperation(file.getRemotePath(), displayName)
  265. .execute(client, currentStorageManager);
  266. if (!result.isSuccess()) {
  267. throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
  268. result.getException());
  269. }
  270. return null;
  271. }
  272. @Override
  273. public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
  274. updateCurrentStorageManagerIfNeeded(rootId);
  275. OCFile root = currentStorageManager.getFileByPath("/");
  276. FileCursor result = new FileCursor(projection);
  277. for (OCFile f : findFiles(root, query)) {
  278. result.addFile(f);
  279. }
  280. return result;
  281. }
  282. @Override
  283. public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
  284. long docId = Long.parseLong(documentId);
  285. updateCurrentStorageManagerIfNeeded(docId);
  286. OCFile parent = currentStorageManager.getFileById(docId);
  287. if (parent == null) {
  288. throw new FileNotFoundException("Parent file not found");
  289. }
  290. if ("vnd.android.document/directory".equalsIgnoreCase(mimeType)) {
  291. return createFolder(parent, displayName, documentId);
  292. } else {
  293. return createFile(parent, displayName, documentId);
  294. }
  295. }
  296. private String createFolder(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
  297. CreateFolderOperation createFolderOperation = new CreateFolderOperation(parent.getRemotePath() + displayName
  298. + "/", true);
  299. RemoteOperationResult result = createFolderOperation.execute(client, currentStorageManager);
  300. if (!result.isSuccess()) {
  301. throw new FileNotFoundException("Failed to create document with name " +
  302. displayName + " and documentId " + documentId);
  303. }
  304. String newDirPath = parent.getRemotePath() + displayName + "/";
  305. OCFile newFolder = currentStorageManager.getFileByPath(newDirPath);
  306. return String.valueOf(newFolder.getFileId());
  307. }
  308. private String createFile(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
  309. Context context = getContext();
  310. if (context == null) {
  311. throw new FileNotFoundException("Context may not be null!");
  312. }
  313. Account account = currentStorageManager.getAccount();
  314. // create dummy file
  315. File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
  316. File emptyFile = new File(tempDir, displayName);
  317. try {
  318. if (!emptyFile.createNewFile()) {
  319. throw new FileNotFoundException("File could not be created");
  320. }
  321. } catch (IOException e) {
  322. throw new FileNotFoundException("File could not be created");
  323. }
  324. FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
  325. requester.uploadNewFile(getContext(), account, new String[]{emptyFile.getAbsolutePath()},
  326. new String[]{parent.getRemotePath() + displayName}, null,
  327. FileUploader.LOCAL_BEHAVIOUR_MOVE, true, UploadFileOperation.CREATED_BY_USER, false,
  328. false);
  329. try {
  330. Thread.sleep(2000);
  331. } catch (InterruptedException e) {
  332. Log_OC.e(TAG, "Thread interruption error");
  333. }
  334. RemoteOperationResult updateParent = new RefreshFolderOperation(parent, System.currentTimeMillis(),
  335. false, false, true, currentStorageManager,
  336. account, getContext()).execute(client);
  337. if (!updateParent.isSuccess()) {
  338. throw new FileNotFoundException("Failed to create document with documentId " + documentId);
  339. }
  340. String newFilePath = parent.getRemotePath() + displayName;
  341. OCFile newFile = currentStorageManager.getFileByPath(newFilePath);
  342. return String.valueOf(newFile.getFileId());
  343. }
  344. @Override
  345. public void deleteDocument(String documentId) throws FileNotFoundException {
  346. long docId = Long.parseLong(documentId);
  347. updateCurrentStorageManagerIfNeeded(docId);
  348. OCFile file = currentStorageManager.getFileById(docId);
  349. if (file == null) {
  350. throw new FileNotFoundException("File " + documentId + " not found!");
  351. }
  352. Account account = currentStorageManager.getAccount();
  353. RemoveFileOperation removeFileOperation = new RemoveFileOperation(file.getRemotePath(), false, account, true,
  354. getContext());
  355. RemoteOperationResult result = removeFileOperation.execute(client, currentStorageManager);
  356. if (!result.isSuccess()) {
  357. throw new FileNotFoundException("Failed to delete document with documentId " + documentId);
  358. }
  359. }
  360. @SuppressLint("LongLogTag")
  361. private void updateCurrentStorageManagerIfNeeded(long docId) {
  362. if (rootIdToStorageManager == null) {
  363. try {
  364. queryRoots(FileCursor.DEFAULT_DOCUMENT_PROJECTION);
  365. } catch (FileNotFoundException e) {
  366. Log.e(TAG, "Failed to query roots");
  367. }
  368. }
  369. if (currentStorageManager == null ||
  370. rootIdToStorageManager.containsKey(docId) && currentStorageManager != rootIdToStorageManager.get(docId)) {
  371. currentStorageManager = rootIdToStorageManager.get(docId);
  372. }
  373. try {
  374. Account account = currentStorageManager.getAccount();
  375. OwnCloudAccount ocAccount = new OwnCloudAccount(account, MainApp.getAppContext());
  376. client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
  377. } catch (OperationCanceledException | IOException | AuthenticatorException |
  378. com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
  379. Log_OC.e(TAG, "Failed to set client", e);
  380. }
  381. }
  382. private void updateCurrentStorageManagerIfNeeded(String rootId) {
  383. for (FileDataStorageManager data : rootIdToStorageManager.values()) {
  384. if (data.getAccount().name.equals(rootId)) {
  385. currentStorageManager = data;
  386. }
  387. }
  388. }
  389. @SuppressLint("UseSparseArrays")
  390. private void initiateStorageMap() throws FileNotFoundException {
  391. rootIdToStorageManager = new HashMap<>();
  392. Context context = getContext();
  393. if (context == null) {
  394. throw new FileNotFoundException("Context may not be null!");
  395. }
  396. ContentResolver contentResolver = context.getContentResolver();
  397. for (Account account : AccountUtils.getAccounts(getContext())) {
  398. final FileDataStorageManager storageManager = new FileDataStorageManager(account, contentResolver);
  399. final OCFile rootDir = storageManager.getFileByPath("/");
  400. rootIdToStorageManager.put(rootDir.getFileId(), storageManager);
  401. }
  402. }
  403. private boolean waitOrGetCancelled(CancellationSignal cancellationSignal) {
  404. try {
  405. Thread.sleep(1000);
  406. } catch (InterruptedException e) {
  407. return false;
  408. }
  409. return !(cancellationSignal != null && cancellationSignal.isCanceled());
  410. }
  411. List<OCFile> findFiles(OCFile root, String query) {
  412. List<OCFile> result = new ArrayList<>();
  413. for (OCFile f : currentStorageManager.getFolderContent(root, false)) {
  414. if (f.isFolder()) {
  415. result.addAll(findFiles(f, query));
  416. } else if (f.getFileName().contains(query)) {
  417. result.add(f);
  418. }
  419. }
  420. return result;
  421. }
  422. }