DocumentsStorageProvider.java 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  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.net.Uri;
  34. import android.os.Build;
  35. import android.os.CancellationSignal;
  36. import android.os.Handler;
  37. import android.os.Looper;
  38. import android.os.ParcelFileDescriptor;
  39. import android.preference.PreferenceManager;
  40. import android.provider.DocumentsProvider;
  41. import android.util.Log;
  42. import android.widget.Toast;
  43. import com.evernote.android.job.JobRequest;
  44. import com.evernote.android.job.util.Device;
  45. import com.owncloud.android.MainApp;
  46. import com.owncloud.android.R;
  47. import com.owncloud.android.authentication.AccountUtils;
  48. import com.owncloud.android.datamodel.FileDataStorageManager;
  49. import com.owncloud.android.datamodel.OCFile;
  50. import com.owncloud.android.datamodel.ThumbnailsCacheManager;
  51. import com.owncloud.android.files.services.FileDownloader;
  52. import com.owncloud.android.files.services.FileUploader;
  53. import com.owncloud.android.lib.common.OwnCloudAccount;
  54. import com.owncloud.android.lib.common.OwnCloudClient;
  55. import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
  56. import com.owncloud.android.lib.common.operations.RemoteOperationResult;
  57. import com.owncloud.android.lib.common.utils.Log_OC;
  58. import com.owncloud.android.operations.CopyFileOperation;
  59. import com.owncloud.android.operations.CreateFolderOperation;
  60. import com.owncloud.android.operations.MoveFileOperation;
  61. import com.owncloud.android.operations.RefreshFolderOperation;
  62. import com.owncloud.android.operations.RemoveFileOperation;
  63. import com.owncloud.android.operations.RenameFileOperation;
  64. import com.owncloud.android.operations.SynchronizeFileOperation;
  65. import com.owncloud.android.operations.UploadFileOperation;
  66. import com.owncloud.android.ui.activity.ConflictsResolveActivity;
  67. import com.owncloud.android.ui.activity.SettingsActivity;
  68. import com.owncloud.android.utils.FileStorageUtils;
  69. import com.owncloud.android.utils.UriUtils;
  70. import org.nextcloud.providers.cursors.FileCursor;
  71. import org.nextcloud.providers.cursors.RootCursor;
  72. import java.io.File;
  73. import java.io.FileNotFoundException;
  74. import java.io.IOException;
  75. import java.util.ArrayList;
  76. import java.util.HashMap;
  77. import java.util.List;
  78. import java.util.Map;
  79. @TargetApi(Build.VERSION_CODES.KITKAT)
  80. public class DocumentsStorageProvider extends DocumentsProvider {
  81. private static final String TAG = "DocumentsStorageProvider";
  82. private FileDataStorageManager currentStorageManager;
  83. private Map<Long, FileDataStorageManager> rootIdToStorageManager;
  84. private OwnCloudClient client;
  85. @Override
  86. public Cursor queryRoots(String[] projection) throws FileNotFoundException {
  87. SharedPreferences appPrefs = PreferenceManager.getDefaultSharedPreferences(MainApp.getAppContext());
  88. if (SettingsActivity.LOCK_PASSCODE.equals(appPrefs.getString(SettingsActivity.PREFERENCE_LOCK, "")) ||
  89. SettingsActivity.LOCK_DEVICE_CREDENTIALS.equals(appPrefs.getString(SettingsActivity.PREFERENCE_LOCK, ""))) {
  90. return new FileCursor();
  91. }
  92. initiateStorageMap();
  93. final RootCursor result = new RootCursor(projection);
  94. for (Account account : AccountUtils.getAccounts(getContext())) {
  95. result.addRoot(account, getContext());
  96. }
  97. return result;
  98. }
  99. @Override
  100. public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
  101. final long docId = Long.parseLong(documentId);
  102. updateCurrentStorageManagerIfNeeded(docId);
  103. if (currentStorageManager == null) {
  104. for (Map.Entry<Long, FileDataStorageManager> entry : rootIdToStorageManager.entrySet()) {
  105. if (entry.getValue().getFileById(docId) != null) {
  106. currentStorageManager = entry.getValue();
  107. break;
  108. }
  109. }
  110. }
  111. if (currentStorageManager == null) {
  112. throw new FileNotFoundException("File with id " + documentId + " not found");
  113. }
  114. final FileCursor result = new FileCursor(projection);
  115. OCFile file = currentStorageManager.getFileById(docId);
  116. if (file != null) {
  117. result.addFile(file);
  118. }
  119. return result;
  120. }
  121. @SuppressLint("LongLogTag")
  122. @Override
  123. public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
  124. throws FileNotFoundException {
  125. final long folderId = Long.parseLong(parentDocumentId);
  126. updateCurrentStorageManagerIfNeeded(folderId);
  127. Context context = getContext();
  128. if (context == null) {
  129. throw new FileNotFoundException("Context may not be null");
  130. }
  131. Account account = currentStorageManager.getAccount();
  132. final OCFile browsedDir = currentStorageManager.getFileById(folderId);
  133. if (Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED)) {
  134. RemoteOperationResult result = new RefreshFolderOperation(browsedDir, System.currentTimeMillis(), false,
  135. false, true, currentStorageManager, account,
  136. getContext()).execute(client);
  137. if (!result.isSuccess()) {
  138. throw new FileNotFoundException("Failed to update document " + parentDocumentId);
  139. }
  140. }
  141. final FileCursor resultCursor = new FileCursor(projection);
  142. for (OCFile file : currentStorageManager.getFolderContent(browsedDir, false)) {
  143. resultCursor.addFile(file);
  144. }
  145. return resultCursor;
  146. }
  147. @SuppressLint("LongLogTag")
  148. @Override
  149. public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal cancellationSignal)
  150. throws FileNotFoundException {
  151. final long docId = Long.parseLong(documentId);
  152. updateCurrentStorageManagerIfNeeded(docId);
  153. OCFile ocFile = currentStorageManager.getFileById(docId);
  154. if (ocFile == null) {
  155. throw new FileNotFoundException("File not found: " + documentId);
  156. }
  157. Context context = getContext();
  158. if (context == null) {
  159. throw new FileNotFoundException("Context may not be null!");
  160. }
  161. Account account = currentStorageManager.getAccount();
  162. if (!ocFile.isDown()) {
  163. Intent i = new Intent(getContext(), FileDownloader.class);
  164. i.putExtra(FileDownloader.EXTRA_ACCOUNT, account);
  165. i.putExtra(FileDownloader.EXTRA_FILE, ocFile);
  166. if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
  167. context.startForegroundService(i);
  168. } else {
  169. context.startService(i);
  170. }
  171. do {
  172. if (!waitOrGetCancelled(cancellationSignal)) {
  173. throw new FileNotFoundException("File with id " + documentId + " not found!");
  174. }
  175. ocFile = currentStorageManager.getFileById(docId);
  176. if (ocFile == null) {
  177. throw new FileNotFoundException("File with id " + documentId + " not found!");
  178. }
  179. } while (!ocFile.isDown());
  180. } else {
  181. OCFile finalFile = ocFile;
  182. Thread syncThread = new Thread(() -> {
  183. try {
  184. FileDataStorageManager storageManager =
  185. new FileDataStorageManager(account, context.getContentResolver());
  186. SynchronizeFileOperation sfo =
  187. new SynchronizeFileOperation(finalFile, null, account, true, context);
  188. RemoteOperationResult result = sfo.execute(storageManager, context);
  189. if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) {
  190. // ISSUE 5: if the user is not running the app (this is a service!),
  191. // this can be very intrusive; a notification should be preferred
  192. Intent i = new Intent(context, ConflictsResolveActivity.class);
  193. i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
  194. i.putExtra(ConflictsResolveActivity.EXTRA_FILE, finalFile);
  195. i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, account);
  196. context.startActivity(i);
  197. } else {
  198. FileStorageUtils.checkIfFileFinishedSaving(finalFile);
  199. if (!result.isSuccess()) {
  200. showToast();
  201. }
  202. }
  203. } catch (Exception exception) {
  204. showToast();
  205. }
  206. });
  207. syncThread.start();
  208. try {
  209. syncThread.join();
  210. } catch (InterruptedException e) {
  211. Log.e(TAG, "Failed to wait for thread to finish");
  212. }
  213. }
  214. File file = new File(ocFile.getStoragePath());
  215. int accessMode = ParcelFileDescriptor.parseMode(mode);
  216. boolean isWrite = mode.indexOf('w') != -1;
  217. final OCFile oldFile = ocFile;
  218. final OCFile newFile = ocFile;
  219. if (isWrite) {
  220. try {
  221. Handler handler = new Handler(context.getMainLooper());
  222. return ParcelFileDescriptor.open(file, accessMode, handler, l -> {
  223. RemoteOperationResult result = new SynchronizeFileOperation(newFile, oldFile, account, true,
  224. context)
  225. .execute(client, currentStorageManager);
  226. boolean success = result.isSuccess();
  227. if (!success) {
  228. Log_OC.e(TAG, "Failed to update document with id " + documentId);
  229. }
  230. });
  231. } catch (IOException e) {
  232. throw new FileNotFoundException("Failed to open/edit document with id " + documentId);
  233. }
  234. } else {
  235. return ParcelFileDescriptor.open(file, accessMode);
  236. }
  237. }
  238. private void showToast() {
  239. Handler handler = new Handler(Looper.getMainLooper());
  240. handler.post(() -> Toast.makeText(MainApp.getAppContext(),
  241. R.string.file_not_synced,
  242. Toast.LENGTH_SHORT).show());
  243. }
  244. @Override
  245. public boolean onCreate() {
  246. return true;
  247. }
  248. @Override
  249. public AssetFileDescriptor openDocumentThumbnail(String documentId,
  250. Point sizeHint,
  251. CancellationSignal signal)
  252. throws FileNotFoundException {
  253. long docId = Long.parseLong(documentId);
  254. updateCurrentStorageManagerIfNeeded(docId);
  255. OCFile file = currentStorageManager.getFileById(docId);
  256. if (file == null) {
  257. throw new FileNotFoundException("File with id " + documentId + " not found!");
  258. }
  259. Context context = getContext();
  260. if (context == null) {
  261. throw new FileNotFoundException("Context may not be null!");
  262. }
  263. boolean exists = ThumbnailsCacheManager.containsBitmap(ThumbnailsCacheManager.PREFIX_THUMBNAIL
  264. + file.getRemoteId());
  265. if (!exists) {
  266. ThumbnailsCacheManager.generateThumbnailFromOCFile(file);
  267. }
  268. Uri uri = Uri.parse(UriUtils.URI_CONTENT_SCHEME + context.getResources().getString(
  269. R.string.image_cache_provider_authority) + file.getRemotePath());
  270. return context.getContentResolver().openAssetFileDescriptor(uri, "r");
  271. }
  272. @Override
  273. public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
  274. long docId = Long.parseLong(documentId);
  275. updateCurrentStorageManagerIfNeeded(docId);
  276. OCFile file = currentStorageManager.getFileById(docId);
  277. if (file == null) {
  278. throw new FileNotFoundException("File " + documentId + " not found!");
  279. }
  280. RemoteOperationResult result = new RenameFileOperation(file.getRemotePath(), displayName)
  281. .execute(client, currentStorageManager);
  282. if (!result.isSuccess()) {
  283. throw new FileNotFoundException("Failed to rename document with documentId " + documentId + ": " +
  284. result.getException());
  285. }
  286. return null;
  287. }
  288. @Override
  289. public String copyDocument(String sourceDocumentId, String targetParentDocumentId) throws FileNotFoundException {
  290. long sourceId = Long.parseLong(sourceDocumentId);
  291. updateCurrentStorageManagerIfNeeded(sourceId);
  292. OCFile file = currentStorageManager.getFileById(sourceId);
  293. if (file == null) {
  294. throw new FileNotFoundException("File " + sourceDocumentId + " not found!");
  295. }
  296. long targetId = Long.parseLong(targetParentDocumentId);
  297. OCFile targetFolder = currentStorageManager.getFileById(targetId);
  298. if (targetFolder == null) {
  299. throw new FileNotFoundException("File " + targetParentDocumentId + " not found!");
  300. }
  301. RemoteOperationResult result = new CopyFileOperation(file.getRemotePath(), targetFolder.getRemotePath())
  302. .execute(client, currentStorageManager);
  303. if (!result.isSuccess()) {
  304. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  305. + " to " + targetParentDocumentId);
  306. }
  307. Account account = currentStorageManager.getAccount();
  308. RemoteOperationResult updateParent = new RefreshFolderOperation(targetFolder, System.currentTimeMillis(),
  309. false, false, true, currentStorageManager,
  310. account, getContext()).execute(client);
  311. if (!updateParent.isSuccess()) {
  312. throw new FileNotFoundException("Failed to copy document with documentId " + sourceDocumentId
  313. + " to " + targetParentDocumentId);
  314. }
  315. String newPath = targetFolder.getRemotePath() + file.getFileName();
  316. if (file.isFolder()) {
  317. newPath = newPath + "/";
  318. }
  319. OCFile newFile = currentStorageManager.getFileByPath(newPath);
  320. return String.valueOf(newFile.getFileId());
  321. }
  322. @Override
  323. public String moveDocument(String sourceDocumentId, String sourceParentDocumentId, String targetParentDocumentId)
  324. throws FileNotFoundException {
  325. long sourceId = Long.parseLong(sourceDocumentId);
  326. updateCurrentStorageManagerIfNeeded(sourceId);
  327. OCFile file = currentStorageManager.getFileById(sourceId);
  328. if (file == null) {
  329. throw new FileNotFoundException("File " + sourceDocumentId + " not found!");
  330. }
  331. long targetId = Long.parseLong(targetParentDocumentId);
  332. OCFile targetFolder = currentStorageManager.getFileById(targetId);
  333. if (targetFolder == null) {
  334. throw new FileNotFoundException("File " + targetParentDocumentId + " not found!");
  335. }
  336. RemoteOperationResult result = new MoveFileOperation(file.getRemotePath(), targetFolder.getRemotePath())
  337. .execute(client, currentStorageManager);
  338. if (!result.isSuccess()) {
  339. throw new FileNotFoundException("Failed to move document with documentId " + sourceDocumentId
  340. + " to " + targetParentDocumentId);
  341. }
  342. return String.valueOf(file.getFileId());
  343. }
  344. @Override
  345. public Cursor querySearchDocuments(String rootId, String query, String[] projection) {
  346. updateCurrentStorageManagerIfNeeded(rootId);
  347. OCFile root = currentStorageManager.getFileByPath("/");
  348. FileCursor result = new FileCursor(projection);
  349. for (OCFile f : findFiles(root, query)) {
  350. result.addFile(f);
  351. }
  352. return result;
  353. }
  354. @Override
  355. public String createDocument(String documentId, String mimeType, String displayName) throws FileNotFoundException {
  356. long docId = Long.parseLong(documentId);
  357. updateCurrentStorageManagerIfNeeded(docId);
  358. OCFile parent = currentStorageManager.getFileById(docId);
  359. if (parent == null) {
  360. throw new FileNotFoundException("Parent file not found");
  361. }
  362. if ("vnd.android.document/directory".equalsIgnoreCase(mimeType)) {
  363. return createFolder(parent, displayName, documentId);
  364. } else {
  365. return createFile(parent, displayName, documentId);
  366. }
  367. }
  368. private String createFolder(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
  369. CreateFolderOperation createFolderOperation = new CreateFolderOperation(parent.getRemotePath() + displayName
  370. + "/", true);
  371. RemoteOperationResult result = createFolderOperation.execute(client, currentStorageManager);
  372. if (!result.isSuccess()) {
  373. throw new FileNotFoundException("Failed to create document with name " +
  374. displayName + " and documentId " + documentId);
  375. }
  376. String newDirPath = parent.getRemotePath() + displayName + "/";
  377. OCFile newFolder = currentStorageManager.getFileByPath(newDirPath);
  378. return String.valueOf(newFolder.getFileId());
  379. }
  380. private String createFile(OCFile parent, String displayName, String documentId) throws FileNotFoundException {
  381. Context context = getContext();
  382. if (context == null) {
  383. throw new FileNotFoundException("Context may not be null!");
  384. }
  385. Account account = currentStorageManager.getAccount();
  386. // create dummy file
  387. File tempDir = new File(FileStorageUtils.getTemporalPath(account.name));
  388. File emptyFile = new File(tempDir, displayName);
  389. try {
  390. if (!emptyFile.createNewFile()) {
  391. throw new FileNotFoundException("File could not be created");
  392. }
  393. } catch (IOException e) {
  394. throw new FileNotFoundException("File could not be created");
  395. }
  396. FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
  397. requester.uploadNewFile(getContext(), account, new String[]{emptyFile.getAbsolutePath()},
  398. new String[]{parent.getRemotePath() + displayName}, null,
  399. FileUploader.LOCAL_BEHAVIOUR_MOVE, true, UploadFileOperation.CREATED_BY_USER, false,
  400. false);
  401. try {
  402. Thread.sleep(2000);
  403. } catch (InterruptedException e) {
  404. Log_OC.e(TAG, "Thread interruption error");
  405. }
  406. RemoteOperationResult updateParent = new RefreshFolderOperation(parent, System.currentTimeMillis(),
  407. false, false, true, currentStorageManager,
  408. account, getContext()).execute(client);
  409. if (!updateParent.isSuccess()) {
  410. throw new FileNotFoundException("Failed to create document with documentId " + documentId);
  411. }
  412. String newFilePath = parent.getRemotePath() + displayName;
  413. OCFile newFile = currentStorageManager.getFileByPath(newFilePath);
  414. return String.valueOf(newFile.getFileId());
  415. }
  416. @Override
  417. public void deleteDocument(String documentId) throws FileNotFoundException {
  418. long docId = Long.parseLong(documentId);
  419. updateCurrentStorageManagerIfNeeded(docId);
  420. OCFile file = currentStorageManager.getFileById(docId);
  421. if (file == null) {
  422. throw new FileNotFoundException("File " + documentId + " not found!");
  423. }
  424. Account account = currentStorageManager.getAccount();
  425. RemoveFileOperation removeFileOperation = new RemoveFileOperation(file.getRemotePath(), false, account, true,
  426. getContext());
  427. RemoteOperationResult result = removeFileOperation.execute(client, currentStorageManager);
  428. if (!result.isSuccess()) {
  429. throw new FileNotFoundException("Failed to delete document with documentId " + documentId);
  430. }
  431. }
  432. @SuppressLint("LongLogTag")
  433. private void updateCurrentStorageManagerIfNeeded(long docId) {
  434. if (rootIdToStorageManager == null) {
  435. try {
  436. queryRoots(FileCursor.DEFAULT_DOCUMENT_PROJECTION);
  437. } catch (FileNotFoundException e) {
  438. Log.e(TAG, "Failed to query roots");
  439. }
  440. }
  441. if (currentStorageManager == null ||
  442. rootIdToStorageManager.containsKey(docId) && currentStorageManager != rootIdToStorageManager.get(docId)) {
  443. currentStorageManager = rootIdToStorageManager.get(docId);
  444. }
  445. try {
  446. Account account = currentStorageManager.getAccount();
  447. OwnCloudAccount ocAccount = new OwnCloudAccount(account, MainApp.getAppContext());
  448. client = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, getContext());
  449. } catch (OperationCanceledException | IOException | AuthenticatorException |
  450. com.owncloud.android.lib.common.accounts.AccountUtils.AccountNotFoundException e) {
  451. Log_OC.e(TAG, "Failed to set client", e);
  452. }
  453. }
  454. private void updateCurrentStorageManagerIfNeeded(String rootId) {
  455. for (FileDataStorageManager data : rootIdToStorageManager.values()) {
  456. if (data.getAccount().name.equals(rootId)) {
  457. currentStorageManager = data;
  458. }
  459. }
  460. }
  461. @SuppressLint("UseSparseArrays")
  462. private void initiateStorageMap() throws FileNotFoundException {
  463. rootIdToStorageManager = new HashMap<>();
  464. Context context = getContext();
  465. if (context == null) {
  466. throw new FileNotFoundException("Context may not be null!");
  467. }
  468. ContentResolver contentResolver = context.getContentResolver();
  469. for (Account account : AccountUtils.getAccounts(getContext())) {
  470. final FileDataStorageManager storageManager = new FileDataStorageManager(account, contentResolver);
  471. final OCFile rootDir = storageManager.getFileByPath("/");
  472. rootIdToStorageManager.put(rootDir.getFileId(), storageManager);
  473. }
  474. }
  475. private boolean waitOrGetCancelled(CancellationSignal cancellationSignal) {
  476. try {
  477. Thread.sleep(1000);
  478. } catch (InterruptedException e) {
  479. return false;
  480. }
  481. return !(cancellationSignal != null && cancellationSignal.isCanceled());
  482. }
  483. List<OCFile> findFiles(OCFile root, String query) {
  484. List<OCFile> result = new ArrayList<>();
  485. for (OCFile f : currentStorageManager.getFolderContent(root, false)) {
  486. if (f.isFolder()) {
  487. result.addAll(findFiles(f, query));
  488. } else if (f.getFileName().contains(query)) {
  489. result.add(f);
  490. }
  491. }
  492. return result;
  493. }
  494. }