diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt index 2eabfc2e1499..5023aea57305 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileActionHandler.kt @@ -87,7 +87,7 @@ class FileActionHandler @Inject constructor( R.id.action_unset_favorite -> setFavorite(files, false, host) - else -> return host.helper?.handle(itemId, files, host.view, backgroundJobManager) == true + else -> return host.helper?.handle(itemId, files, backgroundJobManager) == true } return true diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt index 11cb321b3169..ca1991e0039f 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileOperationHelperExtensions.kt @@ -32,11 +32,10 @@ fun FileOperationsHelper.handle(@IdRes itemId: Int, file: OCFile, view: View?): fun FileOperationsHelper.handle( @IdRes itemId: Int, files: Collection, - view: View?, backgroundJobManager: BackgroundJobManager ): Boolean { when (itemId) { - R.id.action_export_file -> exportFiles(files, view?.context, view, backgroundJobManager) + R.id.action_export_file -> exportFiles(files, backgroundJobManager) R.id.action_add_to_album -> addFileToAlbum(files) else -> return false } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java deleted file mode 100755 index 76c146d31e9b..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ /dev/null @@ -1,1264 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2023-2026 TSI-mc - * SPDX-FileCopyrightText: 2020 Chris Narkiewicz - * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky - * SPDX-FileCopyrightText: 2018-2020 Andy Scherzinger - * SPDX-FileCopyrightText: 2016 Atsushi Matsuo - * SPDX-FileCopyrightText: 2015 ownCloud Inc. - * SPDX-FileCopyrightText: 2014 María Asensio Valverde - * SPDX-FileCopyrightText: 2014 David A. Velasco - * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) - */ -package com.owncloud.android.ui.helpers; - -import android.accounts.Account; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Environment; -import android.os.StatFs; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.view.View; -import android.webkit.MimeTypeMap; - -import com.nextcloud.client.account.CurrentAccountProvider; -import com.nextcloud.client.account.User; -import com.nextcloud.client.jobs.BackgroundJobManager; -import com.nextcloud.client.jobs.download.FileDownloadHelper; -import com.nextcloud.client.jobs.upload.FileUploadHelper; -import com.nextcloud.client.jobs.upload.FileUploadWorker; -import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.utils.AppConstants; -import com.nextcloud.utils.EditorUtils; -import com.nextcloud.utils.SnackbarUtil; -import com.nextcloud.utils.extensions.OCFileExtensionsKt; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.datamodel.ArbitraryDataProvider; -import com.owncloud.android.datamodel.ArbitraryDataProviderImpl; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.files.StreamMediaFileOperation; -import com.owncloud.android.lib.common.operations.RemoteOperationResult; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation; -import com.owncloud.android.lib.resources.files.model.FileVersion; -import com.owncloud.android.lib.resources.files.model.ServerFileInterface; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.lib.resources.shares.ShareType; -import com.owncloud.android.lib.resources.status.OCCapability; -import com.owncloud.android.operations.SynchronizeFileOperation; -import com.owncloud.android.services.OperationsService; -import com.owncloud.android.ui.activity.AlbumsPickerActivity; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.ui.activity.ExternalSiteWebView; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.activity.RichDocumentsEditorWebView; -import com.owncloud.android.ui.activity.ShareActivity; -import com.owncloud.android.ui.activity.TextEditorWebView; -import com.owncloud.android.ui.dialog.SendFilesDialog; -import com.owncloud.android.ui.dialog.SendShareDialog; -import com.owncloud.android.ui.events.FavoriteEvent; -import com.owncloud.android.ui.events.FileLockEvent; -import com.owncloud.android.ui.events.SyncEventFinished; -import com.owncloud.android.ui.fragment.OCFileListFragment; -import com.owncloud.android.utils.EncryptionUtils; -import com.owncloud.android.utils.FileStorageUtils; -import com.owncloud.android.utils.UriUtils; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import org.greenrobot.eventbus.EventBus; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Date; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentTransaction; -import kotlin.Unit; - -/** - * Helper implementation for file operations locally and remote. - */ -public class FileOperationsHelper { - - private static final String TAG = FileOperationsHelper.class.getSimpleName(); - private static final Pattern mPatternUrl = Pattern.compile("^URL=(.+)$"); - private static final Pattern mPatternString = Pattern.compile("(.+)"); - private static final String FILE_EXTENSION_URL = "url"; - private static final String FILE_EXTENSION_DESKTOP = "desktop"; - private static final String FILE_EXTENSION_WEBLOC = "webloc"; - public static final int SINGLE_LINK_SIZE = 1; - - private final FileActivity fileActivity; - private final CurrentAccountProvider currentAccount; - private final ConnectivityService connectivityService; - private final EditorUtils editorUtils; - - /// Identifier of operation in progress which result shouldn't be lost - private long mWaitingForOpId = Long.MAX_VALUE; - - public FileOperationsHelper(FileActivity fileActivity, - CurrentAccountProvider currentAccount, - ConnectivityService connectivityService, EditorUtils editorUtils) { - this.fileActivity = fileActivity; - this.currentAccount = currentAccount; - this.connectivityService = connectivityService; - this.editorUtils = editorUtils; - } - - @Nullable - private String getUrlFromFile(String storagePath, Pattern pattern) { - String url = null; - - try (FileInputStream inputStream = new FileInputStream(storagePath); - InputStreamReader fr = new InputStreamReader(inputStream, StandardCharsets.UTF_8); - BufferedReader br = new BufferedReader(fr)) { - String line; - while ((line = br.readLine()) != null) { - Matcher m = pattern.matcher(line); - if (m.find()) { - url = m.group(1); - break; - } - } - } catch (IOException e) { - Log_OC.d(TAG, e.getMessage()); - } - - return url; - } - - @Nullable - private Intent createIntentFromFile(String storagePath) { - String url = null; - int lastIndexOfDot = storagePath.lastIndexOf('.'); - if (lastIndexOfDot >= 0) { - String fileExt = storagePath.substring(lastIndexOfDot + 1); - if (FILE_EXTENSION_URL.equalsIgnoreCase(fileExt) || FILE_EXTENSION_DESKTOP.equalsIgnoreCase(fileExt)) { - // Windows internet shortcut file .url - // Ubuntu internet shortcut file .desktop - url = getUrlFromFile(storagePath, mPatternUrl); - } else if (FILE_EXTENSION_WEBLOC.equalsIgnoreCase(fileExt)) { - // mac internet shortcut file .webloc - url = getUrlFromFile(storagePath, mPatternString); - } - } - if (url == null) { - return null; - } - return new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - } - - - public void startSyncForFileAndIntent(OCFile file, Intent intent) { - new Thread(() -> { - User user = fileActivity.getUser().orElseThrow(RuntimeException::new); - FileDataStorageManager storageManager = new FileDataStorageManager(user, - fileActivity.getContentResolver()); - - // check if file is in conflict (this is known due to latest folder refresh) - if (file.isInConflict()) { - syncFileOrFolder(file, user, storageManager); - EventBus.getDefault().post(new SyncEventFinished(intent)); - - return; - } - - // check if latest sync is >30s ago - OCFile parent = storageManager.getFileById(file.getParentId()); - if (parent != null && - parent.getLastSyncDateForData() + AppConstants.THIRTY_SECONDS_IN_MILLIS > System.currentTimeMillis()) { - EventBus.getDefault().post(new SyncEventFinished(intent)); - - return; - } - - // if offline or walled garden, show old version with warning - if (!connectivityService.getConnectivity().isConnected() || connectivityService.isInternetWalled()) { - fileActivity.runOnUiThread(() -> SnackbarUtil.show(fileActivity, R.string.file_not_synced)); - EventBus.getDefault().post(new SyncEventFinished(intent)); - - return; - } - - // check for changed eTag - CheckEtagRemoteOperation checkEtagOperation = new CheckEtagRemoteOperation(file.getRemotePath(), - file.getEtag()); - RemoteOperationResult result = checkEtagOperation.execute(user, fileActivity); - - // eTag changed, sync file - if (result.getCode() == RemoteOperationResult.ResultCode.ETAG_CHANGED) { - syncFileOrFolder(file, user, storageManager); - } - - EventBus.getDefault().post(new SyncEventFinished(intent)); - }).start(); - } - - private void syncFileOrFolder(OCFile file, User user, FileDataStorageManager storageManager) { - fileActivity.runOnUiThread(() -> fileActivity.showLoadingDialog(fileActivity.getResources() - .getString(R.string.sync_in_progress))); - - SynchronizeFileOperation sfo = new SynchronizeFileOperation(file, - null, - user, - true, - fileActivity, - storageManager, - false); - RemoteOperationResult result = sfo.execute(fileActivity); - - if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { - // ISSUE 5: if the user is not running the app (this is a service!), - // this can be very intrusive; a notification should be preferred - Intent intent = ConflictsResolveActivity.createIntent(file, - user, - -1, - Intent.FLAG_ACTIVITY_NEW_TASK, - fileActivity); - - fileActivity.startActivity(intent); - } else { - if (file.isDown()) { - FileStorageUtils.checkIfFileFinishedSaving(file); - if (!result.isSuccess()) { - fileActivity.runOnUiThread(() -> SnackbarUtil.show(fileActivity, R.string.file_not_synced)); - try { - Thread.sleep(3000); - } catch (InterruptedException e) { - Log_OC.e(TAG, "Failed to sleep for a bit"); - } - } - } - } - fileActivity.dismissLoadingDialog(); - } - - public boolean canOpenFile(OCFile file) { - final Intent openFileWithIntent = createOpenFileIntent(file); - - List launchables = fileActivity.getPackageManager(). - queryIntentActivities(openFileWithIntent, PackageManager.GET_RESOLVED_FILTER); - return !launchables.isEmpty(); - } - - private void openRichDocumentFileWithoutAvailableApps(@NonNull OCFile file) { - Account account = fileActivity.getAccount(); - OCCapability capability = fileActivity.getStorageManager().getCapability(account.name); - final var richDocumentMimeTypeList = capability.getRichDocumentsMimeTypeList(); - - if (richDocumentMimeTypeList != null && - richDocumentMimeTypeList.contains(file.getMimeType()) && - capability.getRichDocumentsDirectEditing().isTrue()) { - openFileAsRichDocument(file, fileActivity); - } else { - SnackbarUtil.show(fileActivity, R.string.file_list_no_app_for_file_type); - } - } - - public void openFile(OCFile file) { - if (file == null) { - Log_OC.e(TAG, "Trying to open a NULL OCFile"); - return; - } - - final Intent openFileWithIntent = createOpenFileIntent(file); - - List availableApps = fileActivity.getPackageManager(). - queryIntentActivities(openFileWithIntent, PackageManager.GET_RESOLVED_FILTER); - - // first always try to use available apps - if (availableApps.isEmpty()) { - openFileWithoutAvailableApps(file); - return; - } - - new Thread(() -> syncAndOpenFile(file, openFileWithIntent)).start(); - } - - private void openFileWithoutAvailableApps(OCFile file) { - Optional optionalUser = fileActivity.getUser(); - - if (optionalUser.isPresent() && editorUtils.isEditorAvailable(optionalUser.get(), file.getMimeType())) { - TextEditorWebView.Companion.startTextEditor(file, fileActivity); - return; - } - - openRichDocumentFileWithoutAvailableApps(file); - } - - private void syncAndOpenFile(OCFile file, Intent openFileWithIntent) { - User user = currentAccount.getUser(); - final var storageManager = new FileDataStorageManager(user, fileActivity.getContentResolver()); - if (isDownloadedAndUpToDate(storageManager.getFileById(file.getFileId()))) { - startOpenFileIntent(openFileWithIntent); - return; - } - - synchronizeFile(file, user, storageManager, () -> startOpenFileIntent(openFileWithIntent)); - } - - public void startSyncForOutdatedFileAndIntent(OCFile file, Intent intent) { - new Thread(() -> { - User user = currentAccount.getUser(); - final var storageManager = new FileDataStorageManager(user, fileActivity.getContentResolver()); - final Runnable postSyncEventFinished = () -> EventBus.getDefault().post(new SyncEventFinished(intent)); - if (!isDownloadedAndOutdated(storageManager.getFileById(file.getFileId()))) { - postSyncEventFinished.run(); - return; - } - - synchronizeFile(file, user, storageManager, postSyncEventFinished); - }).start(); - } - - private void synchronizeFile(OCFile file, User user, FileDataStorageManager storageManager, Runnable onSynced) { - // a fresh object is needed; many things could have occurred to the file - // since it was registered to observe again, assuming that local files - // are linked to a remote file AT MOST, SOMETHING TO BE DONE; - final var sfo = new SynchronizeFileOperation(file,null, user, true, fileActivity, storageManager, false); - final var result = sfo.execute(fileActivity); - - if (result.getCode() == RemoteOperationResult.ResultCode.SYNC_CONFLICT) { - startConflictsResolveActivity(file, user); - return; - } - - if (!result.isSuccess()) { - showFileNotSyncedMessage(); - } - - onSynced.run(); - } - - private boolean isDownloadedAndUpToDate(@Nullable OCFile storedFile) { - return storedFile != null && storedFile.isDown() && !OCFileExtensionsKt.isLocalETagOutdated(storedFile); - } - - private boolean isDownloadedAndOutdated(@Nullable OCFile storedFile) { - return storedFile != null && storedFile.isDown() && OCFileExtensionsKt.isLocalETagOutdated(storedFile); - } - - private void startConflictsResolveActivity(OCFile file, User user) { - // ISSUE 5: if the user is not running the app (this is a service!), - // this can be very intrusive; a notification should be preferred - Intent intent = ConflictsResolveActivity.createIntent(file, - user, - -1, - Intent.FLAG_ACTIVITY_NEW_TASK, - fileActivity); - fileActivity.startActivity(intent); - } - - private void showFileNotSyncedMessage() { - fileActivity.runOnUiThread(() -> SnackbarUtil.show(fileActivity, R.string.file_not_synced)); - - // Sleep to show snackbar message - try { - Thread.sleep(AppConstants.MILLIS_PER_SECOND); - } catch (InterruptedException e) { - Log_OC.e(TAG, "Failed to sleep"); - } - } - - private void startOpenFileIntent(Intent openFileWithIntent) { - fileActivity.runOnUiThread(() -> { - try { - openFileWithIntent.setFlags(openFileWithIntent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); - fileActivity.startActivity(openFileWithIntent); - } catch (ActivityNotFoundException exception) { - SnackbarUtil.show(fileActivity, R.string.file_list_no_app_for_file_type); - } - }); - } - - public void openFileAsRichDocument(OCFile file, Context context) { - Intent collaboraWebViewIntent = new Intent(context, RichDocumentsEditorWebView.class); - collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Collabora"); - collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file); - collaboraWebViewIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); - context.startActivity(collaboraWebViewIntent); - } - - public void openRichWorkspaceWithTextEditor(OCFile file, String url, Context context) { - Intent textEditorIntent = new Intent(context, TextEditorWebView.class); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text"); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_URL, url); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); - context.startActivity(textEditorIntent); - } - - @NonNull - private Intent createOpenFileIntent(OCFile file) { - String storagePath = file.getStoragePath(); - Uri fileUri = file.getExposedFileUri(fileActivity); - - Intent openFileWithIntent = null; - int lastIndexOfDot = storagePath.lastIndexOf('.'); - if (lastIndexOfDot >= 0) { - String fileExt = storagePath.substring(lastIndexOfDot + 1); - String guessedMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExt); - if (guessedMimeType != null) { - openFileWithIntent = new Intent(Intent.ACTION_VIEW); - openFileWithIntent.setDataAndType(fileUri, guessedMimeType); - } - } - - if (openFileWithIntent == null) { - openFileWithIntent = createIntentFromFile(storagePath); - } - - if (openFileWithIntent == null) { - openFileWithIntent = new Intent(Intent.ACTION_VIEW); - openFileWithIntent.setDataAndType(fileUri, file.getMimeType()); - } - - openFileWithIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - - return openFileWithIntent; - } - - public void streamMediaFile(OCFile file) { - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - final User user = currentAccount.getUser(); - new Thread(() -> { - StreamMediaFileOperation sfo = new StreamMediaFileOperation(file.getLocalId()); - final var result = sfo.execute(user, fileActivity); - - fileActivity.dismissLoadingDialog(); - - if (!result.isSuccess()) { - fileActivity.runOnUiThread(() -> SnackbarUtil.show(fileActivity, R.string.stream_not_possible_headline)); - return; - } - - Intent openFileWithIntent = new Intent(Intent.ACTION_VIEW); - Uri uri = Uri.parse((String) result.getData().get(0)); - - openFileWithIntent.setDataAndType(uri, file.getMimeType()); - - fileActivity.startActivity(Intent.createChooser(openFileWithIntent, - fileActivity.getString(R.string.stream))); - }).start(); - } - - /** - * Helper method to share a file via a public link. Starts a request to do it in {@link OperationsService} - * - * @param file The file to share. - * @param password Optional password to protect the public share. - */ - public void shareFileViaPublicShare(OCFile file, String password) { - if (file != null) { - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_SHARE_VIA_LINK); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - if (!TextUtils.isEmpty(password)) { - service.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password); - } - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - } else { - Log_OC.e(TAG, "Trying to share a NULL OCFile"); - // TODO user-level error? - } - } - - public void shareFolderViaSecureFileDrop(@NonNull OCFile file) { - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_SECURE_FILE_DROP); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } - - public void getFileWithLink(@NonNull OCFile file, final ViewThemeUtils viewThemeUtils) { - List shares = fileActivity.getStorageManager().getSharesByPathAndType(file.getRemotePath(), - ShareType.PUBLIC_LINK, - ""); - - if (shares.size() == SINGLE_LINK_SIZE) { - FileActivity.copyAndShareFileLink(fileActivity, file, shares.get(0).getShareLink(), viewThemeUtils); - } else { - if (fileActivity instanceof FileDisplayActivity) { - ((FileDisplayActivity) fileActivity).showDetails(file, 1); - } else { - showShareFile(file); - } - } - - fileActivity.refreshList(); - } - - /** - * Helper method to share a file with a known sharee. Starts a request to do it in {@link OperationsService} - * - * @param file The file to share. - * @param shareeName Name (user name or group name) of the target sharee. - * @param shareType The share type determines the sharee type. - * @param permissions Permissions to grant to sharee on the shared file. - */ - public void shareFileWithSharee(OCFile file, String shareeName, ShareType shareType, int permissions) { - if (file != null) { - // TODO check capability? - fileActivity.showLoadingDialog(fileActivity.getApplicationContext(). - getString(R.string.wait_a_moment)); - - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_SHARE_WITH_SHAREE); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - service.putExtra(OperationsService.EXTRA_SHARE_WITH, shareeName); - service.putExtra(OperationsService.EXTRA_SHARE_TYPE, shareType); - service.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - } else { - Log_OC.e(TAG, "Trying to share a NULL OCFile"); - } - } - - - /** - * Helper method to share a file with a known sharee. Starts a request to do it in {@link OperationsService} - * - * @param file The file to share. - * @param shareeName Name (user name or group name) of the target sharee. - * @param shareType The share type determines the sharee type. - * @param permissions Permissions to grant to sharee on the shared file. - * @param hideFileDownload true/false to hide file download - * @param password Password to set for the public link; null or empty string to clear the current - * password - * @param expirationTimeInMillis Expiration date to set. A negative value clears the current expiration date, - * leaving the link unrestricted. Zero makes no change. - * @param note note message for the receiver. Null or empty for no message - * @param label new label - */ - public void shareFileWithSharee(OCFile file, - String shareeName, - ShareType shareType, - int permissions, - boolean hideFileDownload, - String password, - long expirationTimeInMillis, - String note, - String attributes, - String label, - boolean showLoadingDialog) { - if (file != null) { - // TODO check capability? - if (showLoadingDialog) { - fileActivity.showLoadingDialog(fileActivity.getApplicationContext(). - getString(R.string.wait_a_moment)); - } - - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_SHARE_WITH_SHAREE); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - service.putExtra(OperationsService.EXTRA_SHARE_WITH, shareeName); - service.putExtra(OperationsService.EXTRA_SHARE_TYPE, shareType); - service.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions); - service.putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload); - service.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password); - service.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); - service.putExtra(OperationsService.EXTRA_SHARE_NOTE, (note == null) ? "" : note); - service.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); - service.putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes); - - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - } else { - Log_OC.e(TAG, "Trying to share a NULL OCFile"); - } - } - - /** - * Helper method to revert to a file version. Starts a request to do it in {@link OperationsService} - * - * @param fileVersion The file version to restore - */ - public void restoreFileVersion(FileVersion fileVersion) { - if (fileVersion != null) { - fileActivity.showLoadingDialog(fileActivity.getApplicationContext(). - getString(R.string.wait_a_moment)); - - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_RESTORE_VERSION); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_FILE_VERSION, fileVersion); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } else { - Log_OC.e(TAG, "Trying to restore a NULL FileVersion"); - } - } - - /** - * Helper method to unshare a file publicly shared via link. Starts a request to do it in {@link OperationsService} - * - * @param file The file to unshare. - */ - public void unShareShare(ServerFileInterface file, long shareId) { - Intent intent = new Intent(fileActivity, OperationsService.class); - intent.setAction(OperationsService.ACTION_UNSHARE); - intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - intent.putExtra(OperationsService.EXTRA_SHARE_ID, shareId); - queueShareIntent(intent); - } - - private void queueShareIntent(Intent shareIntent) { - // Unshare the file - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(shareIntent); - - fileActivity.showLoadingDialog(fileActivity.getApplicationContext().getString(R.string.wait_a_moment)); - } - - /** - * Show an instance of {@link ShareType} for sharing or unsharing the {@link OCFile} received as parameter. - * - * @param file File to share or unshare. - */ - public void showShareFile(OCFile file) { - Intent intent = new Intent(fileActivity, ShareActivity.class); - intent.putExtra(FileActivity.EXTRA_FILE, file); - intent.putExtra(FileActivity.EXTRA_USER, fileActivity.getUser().orElseThrow(RuntimeException::new)); - fileActivity.startActivity(intent); - } - - /** - * Updates a public share on a file to set its label. Starts a request to do it in {@link OperationsService} - * - * @param label new label - */ - public void setLabelToPublicShare(OCShare share, String label) { - // Set password updating share - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_PUBLIC_SHARE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); - - queueShareIntent(updateShareIntent); - } - - /** - * Updates a share on a file to set its password. Starts a request to do it in {@link OperationsService} - * - * @param share File which share will be protected with a password. - * @param password Password to set for the public link; null or empty string to clear the current password - */ - public void setPasswordToShare(OCShare share, String password) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - if (TextUtils.isEmpty(share.getShareLink())) { - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_USER_SHARE); - } else { - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_PUBLIC_SHARE); - } - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password); - queueShareIntent(updateShareIntent); - } - - - /** - * Updates a public share on a file to set its expiration date. Starts a request to do it in - * {@link OperationsService} - * - * @param share {@link OCShare} instance which permissions will be updated. - * @param expirationTimeInMillis Expiration date to set. A negative value clears the current expiration date, - * leaving the link unrestricted. Zero makes no change. - */ - public void setExpirationDateToShare(OCShare share, long expirationTimeInMillis) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_USER_SHARE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, 0); - queueShareIntent(updateShareIntent); - } - - /** - * Updates a share on a file to set its access permissions. Starts a request to do it in {@link OperationsService} - * - * @param share {@link OCShare} instance which permissions will be updated. - * @param permissions New permissions to set. A value <= 0 makes no update. - */ - public void setPermissionsToShare(OCShare share, int permissions) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_USER_SHARE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions); - queueShareIntent(updateShareIntent); - } - - /** - * Updates a public share on a folder to set its editing permission. Starts a request to do it in - * {@link OperationsService} - * - * @param share {@link OCShare} instance which permissions will be updated. - * @param uploadPermission New state of the permission for editing the folder shared via link. - */ - public void setUploadPermissionsToPublicShare(OCShare share, boolean uploadPermission) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_USER_SHARE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - if (uploadPermission) { - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, 3); - } else { - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, 1); - } - - queueShareIntent(updateShareIntent); - } - - public void setHideFileDownloadPermissionsToPublicShare(OCShare share, boolean hideFileDownload) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_PUBLIC_SHARE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload); - - queueShareIntent(updateShareIntent); - } - - public void updateNoteToShare(OCShare share, String note) { - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_NOTE); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_NOTE, note); - - queueShareIntent(updateShareIntent); - } - - public void updateFilesDownloadLimit(OCShare share, int newLimit) { - Intent updateDownloadLimitIntent = new Intent(fileActivity, OperationsService.class); - updateDownloadLimitIntent.setAction(OperationsService.ACTION_UPDATE_FILES_DOWNLOAD_LIMIT); - updateDownloadLimitIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateDownloadLimitIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); - updateDownloadLimitIntent.putExtra(OperationsService.EXTRA_FILES_DOWNLOAD_LIMIT, newLimit); - - queueShareIntent(updateDownloadLimitIntent); - } - - /** - * Helper method to update the share information - * - * @param share {@link OCShare} instance which information will be updated. - * @param permissions Permissions to grant to sharee on the shared file. - * @param hideFileDownload true/false to hide file download - * @param password Password to set for the public link; null or empty string to clear the current - * password - * @param expirationTimeInMillis Expiration date to set. A negative value clears the current expiration date, - * leaving the link unrestricted. Zero makes no change. - * @param label new label - */ - public void updateShareInformation(OCShare share, - int permissions, - boolean hideFileDownload, - String password, - long expirationTimeInMillis, - String label) { - final var id = share.getId(); - final var attributes = share.getAttributes(); - - Log_OC.i(TAG, "-----AFTER UPDATE SHARE-----"); - Log_OC.i(TAG, "ID: " + id); - Log_OC.i(TAG, "Permission: " + permissions); - Log_OC.i(TAG, "Hide File Download: " + hideFileDownload); - Log_OC.i(TAG, "Label: " + label); - Log_OC.i(TAG, "Attributes: " + attributes); - - Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); - updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_INFO); - updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, id); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_REMOTE_ID, share.getRemoteId()); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); - updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes); - queueShareIntent(updateShareIntent); - } - - - public void sendShareFile(OCFile file, boolean hideNcSharingOptions) { - // Show dialog - FragmentManager fm = fileActivity.getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.addToBackStack(null); - - OCCapability capability = fileActivity.getStorageManager().getCapability(fileActivity.getAccount().name); - SendShareDialog mSendShareDialog = SendShareDialog.newInstance(file, hideNcSharingOptions, capability); - mSendShareDialog.setFileOperationsHelper(this); - if (fm.findFragmentByTag("TAG_SEND_SHARE_DIALOG") == null) { - mSendShareDialog.show(ft, "TAG_SEND_SHARE_DIALOG"); - } - } - - public void sendFiles(Set files) { - // Show dialog - FragmentManager fm = fileActivity.getSupportFragmentManager(); - FragmentTransaction ft = fm.beginTransaction(); - ft.addToBackStack(null); - - SendFilesDialog sendFilesDialog = SendFilesDialog.Companion.newInstance(files); - sendFilesDialog.show(ft, "TAG_SEND_SHARE_DIALOG"); - } - - public void sendShareFile(OCFile file) { - sendShareFile(file, !file.canReshare()); - } - - public void sendCachedImage(OCFile file, String packageName, String activityName) { - if (file != null) { - Context context = MainApp.getAppContext(); - Intent sendIntent = new Intent(Intent.ACTION_SEND); - // set MimeType - sendIntent.setType(file.getMimeType()); - sendIntent.setComponent(new ComponentName(packageName, activityName)); - sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://" + - context.getResources().getString(R.string.image_cache_provider_authority) + - file.getRemotePath())); - sendIntent.putExtra(Intent.ACTION_SEND, true); // Send Action - sendIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - - fileActivity.startActivity(Intent.createChooser(sendIntent, - context.getString(R.string.actionbar_send_file))); - } else { - Log_OC.wtf(TAG, "Trying to send a NULL OCFile"); - } - } - - public void setPictureAs(OCFile file, View view) { - if (file != null) { - Context context = MainApp.getAppContext(); - Intent intent = new Intent(Intent.ACTION_ATTACH_DATA); - Uri uri; - - try { - if (file.isDown()) { - File externalFile = new File(file.getStoragePath()); - - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - uri = FileProvider.getUriForFile(context, - context.getResources().getString(R.string.file_provider_authority), externalFile); - } else { - uri = Uri.parse(UriUtils.URI_CONTENT_SCHEME + - context.getResources().getString(R.string.image_cache_provider_authority) + - file.getRemotePath()); - } - - intent.setDataAndType(uri, file.getMimeType()); - intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - fileActivity.startActivityForResult(Intent.createChooser(intent, - fileActivity.getString(R.string.set_as)), - 200); - - intent.setDataAndType(uri, file.getMimeType()); - } catch (ActivityNotFoundException exception) { - SnackbarUtil.show(view, R.string.picture_set_as_no_app); - } - } else { - Log_OC.wtf(TAG, "Trying to send a NULL OCFile"); - } - } - - // region sync file or folder - public void syncFileOrFolder(OCFile file, boolean postDialogEvent, boolean syncAll) { - if (file.isFolder()) { - startSyncFolderIntent(file, syncAll); - } else { - queueSyncFileIntent(file, postDialogEvent); - } - } - - public void syncFileOrFolder(OCFile file) { - syncFileOrFolder(file, false, false); - } - - public void syncFolderIncludingNestedFiles(OCFile file) { - syncFileOrFolder(file, false, true); - } - - private void startSyncFolderIntent(ServerFileInterface file, boolean syncAll) { - final var intent = new Intent(fileActivity, OperationsService.class); - intent.setAction(OperationsService.ACTION_SYNC_FOLDER); - intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - intent.putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll); - - fileActivity.startService(intent); - } - - private void queueSyncFileIntent(ServerFileInterface file, boolean postDialog) { - final var intent = new Intent(fileActivity, OperationsService.class); - intent.setAction(OperationsService.ACTION_SYNC_FILE); - intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - intent.putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true); - intent.putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialog); - - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(intent); - } - // endregion - - public void toggleFavoriteFiles(Collection files, boolean shouldBeFavorite) { - List toToggle = new ArrayList<>(); - for (OCFile file : files) { - if (file.isFavorite() != shouldBeFavorite) { - toToggle.add(file); - } - } - - for (OCFile file : toToggle) { - toggleFavoriteFile(file, shouldBeFavorite); - } - } - - public void toggleFavoriteFile(ServerFileInterface file, boolean shouldBeFavorite) { - if (file.isFavorite() != shouldBeFavorite) { - EventBus.getDefault().post(new FavoriteEvent(file.getRemotePath(), shouldBeFavorite)); - } - } - - public void toggleEncryption(OCFile file, boolean shouldBeEncrypted) { - if (file.isEncrypted() == shouldBeEncrypted) { - Log_OC.d(TAG, "file already in wanted encryption state. " + - "isEncrypted: " + file.isEncrypted() + " should be encrypted: "+ shouldBeEncrypted); - return; - } - - if (file.isRootDirectory()) { - Log_OC.d(TAG, "toggle encryption triggered in root directory, this call is for creating encrypted folder"); - if (!(fileActivity instanceof FileDisplayActivity fda)) { - Log_OC.e(TAG, "file display activity is not active, cannot show create folder dialog"); - return; - } - - OCFileListFragment fragment = fda.getListOfFilesFragment(); - if (fragment == null) { - Log_OC.e(TAG, "file list fragment is null, cannot show create folder dialog"); - return; - } - - fragment.createFolder(true); - return; - } - - Log_OC.d(TAG, "toggling encryption for: " + file.getRemotePath()); - EventBus.getDefault().post(OCFileExtensionsKt.toEncryptionEvent(file, shouldBeEncrypted)); - } - - public void toggleFileLock(OCFile file, boolean shouldBeLocked) { - if (file.isLocked() != shouldBeLocked) { - EventBus.getDefault().post(new FileLockEvent(file.getRemotePath(), shouldBeLocked)); - } - } - - public void renameFile(ServerFileInterface file, String newFilename) { - Intent service = new Intent(fileActivity, OperationsService.class); - - service.setAction(OperationsService.ACTION_RENAME); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); - service.putExtra(OperationsService.EXTRA_NEWNAME, newFilename); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } - - - /** - * Start operations to delete one or several files - * - * @param files Files to delete - * @param onlyLocalCopy When 'true' only local copy of the files is removed; otherwise files are also deleted in the - * server. - * @param inBackground When 'true', do not show any loading dialog - */ - public void removeFiles(Collection files, boolean onlyLocalCopy, boolean inBackground) { - for (OCFile file : files) { - removeFile(file, onlyLocalCopy, inBackground); - } - } - - public void removeFile(OCFile file, boolean onlyLocalCopy, boolean inBackground) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_REMOVE); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_FILE, file); - service.putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy); - service.putExtra(OperationsService.EXTRA_IN_BACKGROUND, inBackground); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } - - public void createFolder(String remotePath, boolean encrypted) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_FOLDER); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath); - service.putExtra(OperationsService.EXTRA_ENCRYPTED, encrypted); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - /** - * Cancel the transference in downloads (files/folders) and file uploads - * - * @param file OCFile - */ - public void cancelTransference(OCFile file) { - User currentUser = fileActivity.getUser().orElseThrow(IllegalStateException::new); - if (file.isFolder()) { - OperationsService.OperationsServiceBinder opsBinder = - fileActivity.getOperationsServiceBinder(); - if (opsBinder != null) { - opsBinder.cancel(currentUser.toPlatformAccount(), file); - } - } - - final var fileDownloadHelper = FileDownloadHelper.Companion.instance(); - if (fileDownloadHelper.isDownloading(currentUser, file)) { - List files = fileActivity.getStorageManager().getAllFilesRecursivelyInsideFolder(file); - fileDownloadHelper.cancelPendingOrCurrentDownloads(currentUser, files); - } - - if (file.isFolder()) { - fileDownloadHelper.cancelFolderDownload(); - } - - final var fileUploadHelper = FileUploadHelper.Companion.instance(); - if (fileUploadHelper.isUploading(file.getRemotePath(), currentUser.getAccountName())) { - FileUploadWorker.Companion.cancelUpload(file.getRemotePath(), currentUser.getAccountName(), () -> { - fileUploadHelper.updateUploadStatus(file.getRemotePath(), currentUser.getAccountName(), UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED); - return Unit.INSTANCE; - }); - } - } - - public void moveOrCopyFiles(String action, final List filePaths, final OCFile targetFolder) { - for (String path : filePaths) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(action); - service.putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, targetFolder.getRemotePath()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, path); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - public void createAlbum(String albumName) { - // Create Album - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CREATE_ALBUM); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - public void addFileToAlbum(Collection files) { - final ArrayList paths = new ArrayList<>(files.size()); - for (OCFile file : files) { - paths.add(file.getRemotePath()); - } - - fileActivity.startActivity(AlbumsPickerActivity.Companion.intentForPickingAlbum(fileActivity, paths)); - } - - public void albumCopyFiles(final List filePaths, final String targetFolder) { - if (filePaths == null || filePaths.isEmpty()) { - return; - } - - connectivityService.isNetworkAndServerAvailable(result -> { - if (result) { - for (String path : filePaths) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_ALBUM_COPY_FILE); - service.putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, targetFolder); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, path); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - } - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } else { - SnackbarUtil.show(fileActivity, fileActivity.getString(R.string.offline_mode)); - } - return Unit.INSTANCE; - }); - } - - public void renameAlbum(String oldAlbumName, String newAlbumName) { - Intent service = new Intent(fileActivity, OperationsService.class); - - service.setAction(OperationsService.ACTION_RENAME_ALBUM); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_REMOTE_PATH, oldAlbumName); - service.putExtra(OperationsService.EXTRA_NEWNAME, newAlbumName); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - public void removeAlbum(String albumName) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_REMOVE_ALBUM); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - public void albumPublicShareLink(String albumName, boolean isCreateShare) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_PUBLIC_SHARE_LINK_ALBUM); - service.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); - service.putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName); - service.putExtra(OperationsService.EXTRA_CREATE_ALBUM_SHARE, isCreateShare); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)); - } - - public void exportFiles(Collection files, - Context context, - View view, - BackgroundJobManager backgroundJobManager) { - backgroundJobManager.startImmediateFilesExportJob(files); - } - - public long getOpIdWaitingFor() { - return mWaitingForOpId; - } - - - public void setOpIdWaitingFor(long waitingForOpId) { - mWaitingForOpId = waitingForOpId; - } - - /** - * Starts a check of the currently stored credentials for the given account. - * - * @param user user which credentials will be checked. - */ - public void checkCurrentCredentials(User user) { - Intent service = new Intent(fileActivity, OperationsService.class); - service.setAction(OperationsService.ACTION_CHECK_CURRENT_CREDENTIALS); - service.putExtra(OperationsService.EXTRA_ACCOUNT, user.toPlatformAccount()); - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); - - fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_checking_credentials)); - } - - public void uploadFromCamera(Activity activity, int requestCode, boolean isVideo) { - Intent intent; - if (isVideo) { - - // FIXME Not working on Emulator - intent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); - } else { - intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - } - - File cameraFile = createCameraFile(activity, isVideo); - - Uri cameraUri = FileProvider.getUriForFile(activity.getApplicationContext(), - activity.getResources().getString(R.string.file_provider_authority), cameraFile); - intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraUri); - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - - if (intent.resolveActivity(activity.getPackageManager()) != null) { - activity.startActivityForResult(intent, requestCode); - } else { - SnackbarUtil.show(activity, "No Camera found"); - } - } - - public static File createCameraFile(Activity activity, boolean isVideo) { - String directory = isVideo ? Environment.DIRECTORY_MOVIES : Environment.DIRECTORY_PICTURES; - File storageDir = activity.getExternalFilesDir(directory); - String fileName = isVideo ? "/directCameraUpload.mp4" : "/directCameraUpload.jpg"; - return new File(storageDir + fileName); - } - - public static String getCapturedImageName() { - return getTimestampedFileName(".jpg"); - } - - public static String getCapturedVideoName() { - return getTimestampedFileName(".mp4"); - } - - /** - * @param extension a file extension, including the dot - * @return a filename with the given extension, based on the current date and time. - */ - public static String getTimestampedFileName(final String extension) { - return new SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US).format(new Date()) + extension; - } - - /** - * @return -1 if no space could computed, otherwise available space in bytes - */ - public static Long getAvailableSpaceOnDevice() { - StatFs stat; - try { - stat = new StatFs(MainApp.getStoragePath()); - } catch (NullPointerException | IllegalArgumentException e) { - return -1L; - } - - return stat.getBlockSizeLong() * stat.getAvailableBlocksLong(); - } - - public static boolean isEndToEndEncryptionSetup(Context context, User user) { - ArbitraryDataProvider arbitraryDataProvider = new ArbitraryDataProviderImpl(context); - - String publicKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY); - String privateKey = arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY); - - return !publicKey.isEmpty() && !privateKey.isEmpty(); - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.kt b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.kt new file mode 100755 index 000000000000..669150aab334 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.kt @@ -0,0 +1,946 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-FileCopyrightText: 2023-2026 TSI-mc + * SPDX-FileCopyrightText: 2020 Chris Narkiewicz + * SPDX-FileCopyrightText: 2018-2020 Tobias Kaminsky + * SPDX-FileCopyrightText: 2018-2020 Andy Scherzinger + * SPDX-FileCopyrightText: 2016 Atsushi Matsuo + * SPDX-FileCopyrightText: 2015 ownCloud Inc. + * SPDX-FileCopyrightText: 2014 María Asensio Valverde + * SPDX-FileCopyrightText: 2014 David A. Velasco + * SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only) + */ +package com.owncloud.android.ui.helpers + +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Environment +import android.os.StatFs +import android.provider.MediaStore +import android.view.View +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import com.nextcloud.client.account.CurrentAccountProvider +import com.nextcloud.client.account.User +import com.nextcloud.client.jobs.BackgroundJobManager +import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.jobs.upload.FileUploadHelper +import com.nextcloud.client.jobs.upload.FileUploadWorker +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.utils.AppConstants +import com.nextcloud.utils.EditorUtils +import com.nextcloud.utils.SnackbarUtil.show +import com.nextcloud.utils.extensions.isLocalETagOutdated +import com.nextcloud.utils.extensions.toEncryptionEvent +import com.owncloud.android.MainApp +import com.owncloud.android.R +import com.owncloud.android.datamodel.ArbitraryDataProviderImpl +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.files.StreamMediaFileOperation +import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation +import com.owncloud.android.lib.resources.files.model.FileVersion +import com.owncloud.android.lib.resources.files.model.ServerFileInterface +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.operations.SynchronizeFileOperation +import com.owncloud.android.services.OperationsService +import com.owncloud.android.ui.activity.AlbumsPickerActivity +import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.ui.activity.ExternalSiteWebView +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.activity.RichDocumentsEditorWebView +import com.owncloud.android.ui.activity.ShareActivity +import com.owncloud.android.ui.activity.TextEditorWebView +import com.owncloud.android.ui.dialog.SendFilesDialog +import com.owncloud.android.ui.dialog.SendShareDialog +import com.owncloud.android.ui.events.FavoriteEvent +import com.owncloud.android.ui.events.FileLockEvent +import com.owncloud.android.ui.events.SyncEventFinished +import com.owncloud.android.utils.EncryptionUtils +import com.owncloud.android.utils.FileStorageUtils +import com.owncloud.android.utils.UriUtils +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Suppress("LargeClass", "DEPRECATION") +class FileOperationsHelper( + private val fileActivity: FileActivity, + private val currentAccount: CurrentAccountProvider, + private val connectivityService: ConnectivityService, + private val editorUtils: EditorUtils +) { + var opIdWaitingFor: Long = Long.MAX_VALUE + + // region Sync before preview + fun startSyncForFileAndIntent(file: OCFile, intent: Intent) { + fileActivity.lifecycleScope.launch(Dispatchers.IO) { + val user = fileActivity.user.orElseThrow { RuntimeException() } + val storageManager = FileDataStorageManager(user, fileActivity.contentResolver) + syncFileIfOutdated(file, user, storageManager) + postSyncEventFinished(intent) + } + } + + fun startSyncForOutdatedFileAndIntent(file: OCFile, intent: Intent) { + fileActivity.lifecycleScope.launch(Dispatchers.IO) { + val user = currentAccount.user + val storageManager = FileDataStorageManager(user, fileActivity.contentResolver) + if (!isDownloadedAndOutdated(storageManager.getFileById(file.fileId))) { + postSyncEventFinished(intent) + return@launch + } + + synchronizeFile(file, user, storageManager) { postSyncEventFinished(intent) } + } + } + + private fun syncFileIfOutdated(file: OCFile, user: User, storageManager: FileDataStorageManager) { + if (file.isInConflict) { + syncFileShowingProgress(file, user, storageManager) + return + } + + if (isParentRecentlySynced(file, storageManager)) { + return + } + + if (!connectivityService.connectivity.isConnected || connectivityService.isInternetWalled()) { + fileActivity.runOnUiThread { show(fileActivity, R.string.file_not_synced) } + return + } + + val etagResult = CheckEtagRemoteOperation(file.remotePath, file.etag).execute(user, fileActivity) + if (etagResult.code == ResultCode.ETAG_CHANGED) { + syncFileShowingProgress(file, user, storageManager) + } + } + + private fun isParentRecentlySynced(file: OCFile, storageManager: FileDataStorageManager): Boolean { + val parent = storageManager.getFileById(file.parentId) ?: return false + return parent.lastSyncDateForData + AppConstants.THIRTY_SECONDS_IN_MILLIS > System.currentTimeMillis() + } + + private fun syncFileShowingProgress(file: OCFile, user: User, storageManager: FileDataStorageManager) { + fileActivity.showLoadingDialog(fileActivity.getString(R.string.sync_in_progress)) + + val result = SynchronizeFileOperation(file, null, user, true, fileActivity, storageManager, false) + .execute(fileActivity) + + when { + result.code == ResultCode.SYNC_CONFLICT -> startConflictsResolveActivity(file, user) + + file.isDown -> { + FileStorageUtils.checkIfFileFinishedSaving(file) + if (!result.isSuccess) { + showFileNotSyncedMessage(FILE_NOT_SYNCED_AFTER_CONFLICT_CHECK_WAIT_MILLIS) + } + } + } + + fileActivity.dismissLoadingDialog() + } + + private fun synchronizeFile( + file: OCFile, + user: User, + storageManager: FileDataStorageManager, + onSynced: () -> Unit + ) { + val result = SynchronizeFileOperation(file, null, user, true, fileActivity, storageManager, false) + .execute(fileActivity) + + if (result.code == ResultCode.SYNC_CONFLICT) { + startConflictsResolveActivity(file, user) + return + } + + if (!result.isSuccess) { + showFileNotSyncedMessage(AppConstants.MILLIS_PER_SECOND.toLong()) + } + + onSynced() + } + + private fun isDownloadedAndOutdated(storedFile: OCFile?): Boolean = + storedFile != null && storedFile.isDown && storedFile.isLocalETagOutdated() + + private fun postSyncEventFinished(intent: Intent) { + EventBus.getDefault().post(SyncEventFinished(intent)) + } + + private fun startConflictsResolveActivity(file: OCFile, user: User) { + val intent = ConflictsResolveActivity.createIntent( + file, + user, + NO_CONFLICT_UPLOAD_ID, + Intent.FLAG_ACTIVITY_NEW_TASK, + fileActivity + ) + fileActivity.startActivity(intent) + } + + private fun showFileNotSyncedMessage(snackbarWaitMillis: Long) { + fileActivity.runOnUiThread { show(fileActivity, R.string.file_not_synced) } + + try { + Thread.sleep(snackbarWaitMillis) + } catch (_: InterruptedException) { + Log_OC.e(TAG, "Failed to sleep") + } + } + // endregion + + // region Open file + fun canOpenFile(file: OCFile): Boolean = hasAppToOpen(createOpenFileIntent(file)) + + fun openFile(file: OCFile?) { + if (file == null) { + Log_OC.e(TAG, "Trying to open a NULL OCFile") + return + } + + val openFileIntent = createOpenFileIntent(file) + if (!hasAppToOpen(openFileIntent)) { + openFileWithoutAvailableApps(file) + return + } + + fileActivity.lifecycleScope.launch(Dispatchers.IO) { syncAndOpenFile(file, openFileIntent) } + } + + fun openFileAsRichDocument(file: OCFile?, context: Context?) { + context ?: return + + val intent = Intent(context, RichDocumentsEditorWebView::class.java).apply { + putExtra(ExternalSiteWebView.EXTRA_TITLE, RICH_DOCUMENTS_EDITOR_TITLE) + putExtra(FileActivity.EXTRA_FILE, file) + putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + } + context.startActivity(intent) + } + + fun openRichWorkspaceWithTextEditor(file: OCFile?, url: String?, context: Context?) { + context ?: return + + val intent = Intent(context, TextEditorWebView::class.java).apply { + putExtra(ExternalSiteWebView.EXTRA_TITLE, TEXT_EDITOR_TITLE) + putExtra(ExternalSiteWebView.EXTRA_URL, url) + putExtra(FileActivity.EXTRA_FILE, file) + putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false) + } + context.startActivity(intent) + } + + private fun hasAppToOpen(intent: Intent): Boolean = fileActivity.packageManager + .queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER) + .isNotEmpty() + + private fun openFileWithoutAvailableApps(file: OCFile) { + val user = fileActivity.user.orElse(null) + if (user != null && editorUtils.isEditorAvailable(user, file.mimeType)) { + TextEditorWebView.startTextEditor(file, fileActivity) + return + } + + openRichDocumentFileWithoutAvailableApps(file) + } + + private fun openRichDocumentFileWithoutAvailableApps(file: OCFile) { + val capability = fileActivity.storageManager.getCapability(fileActivity.account.name) + val canOpenAsRichDocument = capability.richDocumentsMimeTypeList?.contains(file.mimeType) == true && + capability.richDocumentsDirectEditing.isTrue + + if (canOpenAsRichDocument) { + openFileAsRichDocument(file, fileActivity) + } else { + show(fileActivity, R.string.file_list_no_app_for_file_type) + } + } + + private fun syncAndOpenFile(file: OCFile, openFileIntent: Intent) { + val user = currentAccount.user + val storageManager = FileDataStorageManager(user, fileActivity.contentResolver) + if (isDownloadedAndUpToDate(storageManager.getFileById(file.fileId))) { + startOpenFileIntent(openFileIntent) + return + } + + synchronizeFile(file, user, storageManager) { startOpenFileIntent(openFileIntent) } + } + + private fun isDownloadedAndUpToDate(storedFile: OCFile?): Boolean = + storedFile != null && storedFile.isDown && !storedFile.isLocalETagOutdated() + + private fun startOpenFileIntent(openFileIntent: Intent) { + fileActivity.runOnUiThread { + try { + openFileIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + fileActivity.startActivity(openFileIntent) + } catch (_: ActivityNotFoundException) { + show(fileActivity, R.string.file_list_no_app_for_file_type) + } + } + } + + private fun createOpenFileIntent(file: OCFile): Intent { + val storagePath = file.storagePath + val fileUri = file.getExposedFileUri(fileActivity) + val extension = storagePath.substringAfterLast('.', missingDelimiterValue = "") + val guessedMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + + val openFileIntent = guessedMimeType?.let { Intent(Intent.ACTION_VIEW).setDataAndType(fileUri, it) } + ?: createIntentFromShortcutFile(storagePath, extension) + ?: Intent(Intent.ACTION_VIEW).setDataAndType(fileUri, file.mimeType) + + openFileIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + return openFileIntent + } + + private fun createIntentFromShortcutFile(storagePath: String, extension: String): Intent? { + val urlPattern = shortcutUrlPattern(extension) ?: return null + val url = findUrlInFile(storagePath, urlPattern) ?: return null + return Intent(Intent.ACTION_VIEW, url.toUri()) + } + + private fun shortcutUrlPattern(extension: String): Regex? = when { + extension.equals(FILE_EXTENSION_URL, ignoreCase = true) || + extension.equals(FILE_EXTENSION_DESKTOP, ignoreCase = true) -> URL_LINE_PATTERN + + extension.equals(FILE_EXTENSION_WEBLOC, ignoreCase = true) -> WEBLOC_STRING_PATTERN + + else -> null + } + + private fun findUrlInFile(storagePath: String, pattern: Regex): String? = try { + File(storagePath).useLines(Charsets.UTF_8) { lines -> + lines.firstNotNullOfOrNull { line -> pattern.find(line)?.groupValues?.get(1) } + } + } catch (e: IOException) { + Log_OC.d(TAG, e.message) + null + } + // endregion + + // region Stream media + fun streamMediaFile(file: OCFile) { + showWaitDialog() + val user = currentAccount.user + fileActivity.lifecycleScope.launch(Dispatchers.IO) { + val result = StreamMediaFileOperation(file.localId).execute(user, fileActivity) + fileActivity.dismissLoadingDialog() + + val streamUrl = result.data?.firstOrNull() as? String + if (!result.isSuccess || streamUrl == null) { + fileActivity.runOnUiThread { show(fileActivity, R.string.stream_not_possible_headline) } + return@launch + } + + val streamIntent = Intent(Intent.ACTION_VIEW).setDataAndType(streamUrl.toUri(), file.mimeType) + fileActivity.startActivity(Intent.createChooser(streamIntent, fileActivity.getString(R.string.stream))) + } + } + // endregion + + // region Share + fun shareFileViaPublicShare(file: OCFile?, password: String?) { + if (file == null) { + Log_OC.e(TAG, "Trying to share a NULL OCFile") + return + } + + showWaitDialog() + val intent = operationIntent(OperationsService.ACTION_CREATE_SHARE_VIA_LINK).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + if (!password.isNullOrEmpty()) { + putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password) + } + } + queueOperation(intent) + } + + fun shareFolderViaSecureFileDrop(file: OCFile) { + showWaitDialog() + val intent = operationIntent(OperationsService.ACTION_CREATE_SECURE_FILE_DROP).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + } + queueOperation(intent) + } + + fun getFileWithLink(file: OCFile, viewThemeUtils: ViewThemeUtils?) { + val shares = fileActivity.storageManager.getSharesByPathAndType(file.remotePath, ShareType.PUBLIC_LINK, "") + + when { + shares.size == SINGLE_LINK_SIZE -> + FileActivity.copyAndShareFileLink(fileActivity, file, shares[0].shareLink, viewThemeUtils) + + fileActivity is FileDisplayActivity -> fileActivity.showDetails(file, SHARING_TAB) + + else -> showShareFile(file) + } + + fileActivity.refreshList() + } + + fun shareFileWithSharee( + file: OCFile?, + shareeName: String?, + shareType: ShareType?, + permissions: Int, + hideFileDownload: Boolean, + password: String?, + expirationTimeInMillis: Long, + note: String?, + attributes: String?, + label: String?, + showLoadingDialog: Boolean + ) { + if (file == null) { + Log_OC.e(TAG, "Trying to share a NULL OCFile") + return + } + + if (showLoadingDialog) { + showWaitDialog() + } + + val intent = operationIntent(OperationsService.ACTION_CREATE_SHARE_WITH_SHAREE).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + putExtra(OperationsService.EXTRA_SHARE_WITH, shareeName) + putExtra(OperationsService.EXTRA_SHARE_TYPE, shareType) + putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions) + putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload) + putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password.orEmpty()) + putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis) + putExtra(OperationsService.EXTRA_SHARE_NOTE, note.orEmpty()) + putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, label.orEmpty()) + putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes) + } + queueOperation(intent) + } + + fun unShareShare(file: ServerFileInterface?, shareId: Long) { + file ?: return + + val intent = operationIntent(OperationsService.ACTION_UNSHARE).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + putExtra(OperationsService.EXTRA_SHARE_ID, shareId) + } + queueShareIntent(intent) + } + + fun showShareFile(file: OCFile?) { + val intent = Intent(fileActivity, ShareActivity::class.java).apply { + putExtra(FileActivity.EXTRA_FILE, file) + putExtra(FileActivity.EXTRA_USER, fileActivity.user.orElseThrow { RuntimeException() }) + } + fileActivity.startActivity(intent) + } + + fun setPasswordToShare(share: OCShare?, password: String?) { + share ?: return + + val action = if (share.shareLink.isNullOrEmpty()) { + OperationsService.ACTION_UPDATE_USER_SHARE + } else { + OperationsService.ACTION_UPDATE_PUBLIC_SHARE + } + val intent = shareUpdateIntent(action, share).apply { + putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password.orEmpty()) + } + queueShareIntent(intent) + } + + fun setPermissionsToShare(share: OCShare, permissions: Int) { + val intent = shareUpdateIntent(OperationsService.ACTION_UPDATE_USER_SHARE, share).apply { + putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions) + } + queueShareIntent(intent) + } + + fun updateNoteToShare(share: OCShare?, note: String?) { + share ?: return + + val intent = shareUpdateIntent(OperationsService.ACTION_UPDATE_SHARE_NOTE, share).apply { + putExtra(OperationsService.EXTRA_SHARE_NOTE, note) + } + queueShareIntent(intent) + } + + fun updateFilesDownloadLimit(share: OCShare?, newLimit: Int) { + share ?: return + + val intent = shareUpdateIntent(OperationsService.ACTION_UPDATE_FILES_DOWNLOAD_LIMIT, share).apply { + putExtra(OperationsService.EXTRA_FILES_DOWNLOAD_LIMIT, newLimit) + } + queueShareIntent(intent) + } + + fun updateShareInformation( + share: OCShare?, + permissions: Int, + hideFileDownload: Boolean, + password: String?, + expirationTimeInMillis: Long, + label: String? + ) { + share ?: return + + val attributes = share.attributes + + Log_OC.i(TAG, "-----AFTER UPDATE SHARE-----") + Log_OC.i(TAG, "ID: ${share.id}") + Log_OC.i(TAG, "Permission: $permissions") + Log_OC.i(TAG, "Hide File Download: $hideFileDownload") + Log_OC.i(TAG, "Label: $label") + Log_OC.i(TAG, "Attributes: $attributes") + + val intent = shareUpdateIntent(OperationsService.ACTION_UPDATE_SHARE_INFO, share).apply { + putExtra(OperationsService.EXTRA_SHARE_REMOTE_ID, share.remoteId) + putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions) + putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload) + putExtra(OperationsService.EXTRA_SHARE_PASSWORD, password) + putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis) + putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, label.orEmpty()) + putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes) + } + queueShareIntent(intent) + } + + private fun shareUpdateIntent(action: String, share: OCShare): Intent = operationIntent(action).apply { + putExtra(OperationsService.EXTRA_SHARE_ID, share.id) + } + + private fun queueShareIntent(intent: Intent) { + queueOperation(intent) + showWaitDialog() + } + // endregion + + // region Send and set as + fun sendShareFile(file: OCFile) { + sendShareFile(file, !file.canReshare()) + } + + fun sendShareFile(file: OCFile?, hideNcSharingOptions: Boolean) { + val fragmentManager = fileActivity.supportFragmentManager + if (fragmentManager.findFragmentByTag(TAG_SEND_SHARE_DIALOG) != null) { + return + } + + val transaction = fragmentManager.beginTransaction().addToBackStack(null) + val capability = fileActivity.storageManager.getCapability(fileActivity.account.name) + val sendShareDialog = SendShareDialog.newInstance(file, hideNcSharingOptions, capability) + sendShareDialog.setFileOperationsHelper(this) + sendShareDialog.show(transaction, TAG_SEND_SHARE_DIALOG) + } + + fun sendFiles(files: Set) { + val transaction = fileActivity.supportFragmentManager.beginTransaction().addToBackStack(null) + SendFilesDialog.newInstance(files).show(transaction, TAG_SEND_SHARE_DIALOG) + } + + fun sendCachedImage(file: OCFile?, packageName: String, activityName: String) { + if (file == null) { + Log_OC.wtf(TAG, "Trying to send a NULL OCFile") + return + } + + val context = MainApp.getAppContext() + val sendIntent = Intent(Intent.ACTION_SEND).apply { + type = file.mimeType + component = ComponentName(packageName, activityName) + putExtra(Intent.EXTRA_STREAM, imageCacheUri(context, file)) + putExtra(Intent.ACTION_SEND, true) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + fileActivity.startActivity(Intent.createChooser(sendIntent, context.getString(R.string.actionbar_send_file))) + } + + fun setPictureAs(file: OCFile?, view: View?) { + if (file == null) { + Log_OC.wtf(TAG, "Trying to send a NULL OCFile") + return + } + + try { + val intent = Intent(Intent.ACTION_ATTACH_DATA).apply { + setDataAndType(pictureUri(file), file.mimeType) + flags = Intent.FLAG_GRANT_READ_URI_PERMISSION + } + val chooser = Intent.createChooser(intent, fileActivity.getString(R.string.set_as)) + fileActivity.startActivityForResult(chooser, REQUEST_CODE_SET_PICTURE_AS) + } catch (_: ActivityNotFoundException) { + show(view, R.string.picture_set_as_no_app) + } + } + + private fun pictureUri(file: OCFile): Uri { + val context = MainApp.getAppContext() + if (!file.isDown) { + return imageCacheUri(context, file) + } + + val authority = context.getString(R.string.file_provider_authority) + return FileProvider.getUriForFile(context, authority, File(file.storagePath)) + } + + private fun imageCacheUri(context: Context, file: OCFile): Uri { + val authority = context.getString(R.string.image_cache_provider_authority) + return "${UriUtils.URI_CONTENT_SCHEME}$authority${file.remotePath}".toUri() + } + // endregion + + // region Sync file or folder + fun syncFileOrFolder(file: OCFile, postDialogEvent: Boolean = false, syncAll: Boolean = false) { + if (file.isFolder) { + startSyncFolderIntent(file, syncAll) + } else { + queueSyncFileIntent(file, postDialogEvent) + } + } + + fun syncFolderIncludingNestedFiles(file: OCFile) { + syncFileOrFolder(file, postDialogEvent = false, syncAll = true) + } + + private fun startSyncFolderIntent(file: ServerFileInterface, syncAll: Boolean) { + val intent = operationIntent(OperationsService.ACTION_SYNC_FOLDER).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + putExtra(OperationsService.EXTRA_SYNC_ALL, syncAll) + } + fileActivity.startService(intent) + } + + private fun queueSyncFileIntent(file: ServerFileInterface, postDialog: Boolean) { + val intent = operationIntent(OperationsService.ACTION_SYNC_FILE).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + putExtra(OperationsService.EXTRA_SYNC_FILE_CONTENTS, true) + putExtra(OperationsService.EXTRA_POST_DIALOG_EVENT, postDialog) + } + queueOperation(intent) + } + // endregion + + // region Toggle favorite, encryption and lock + fun toggleFavoriteFiles(files: Collection, shouldBeFavorite: Boolean) { + files + .filter { it.isFavorite != shouldBeFavorite } + .forEach { toggleFavoriteFile(it, shouldBeFavorite) } + } + + fun toggleFavoriteFile(file: ServerFileInterface, shouldBeFavorite: Boolean) { + if (file.isFavorite != shouldBeFavorite) { + EventBus.getDefault().post(FavoriteEvent(file.remotePath, shouldBeFavorite)) + } + } + + fun toggleEncryption(file: OCFile, shouldBeEncrypted: Boolean) { + if (file.isEncrypted == shouldBeEncrypted) { + Log_OC.d( + TAG, + "file already in wanted encryption state. " + + "isEncrypted: ${file.isEncrypted} should be encrypted: $shouldBeEncrypted" + ) + return + } + + if (file.isRootDirectory) { + showCreateEncryptedFolderDialog() + return + } + + Log_OC.d(TAG, "toggling encryption for: ${file.remotePath}") + EventBus.getDefault().post(file.toEncryptionEvent(shouldBeEncrypted)) + } + + private fun showCreateEncryptedFolderDialog() { + Log_OC.d(TAG, "toggle encryption triggered in root directory, this call is for creating encrypted folder") + if (fileActivity !is FileDisplayActivity) { + Log_OC.e(TAG, "file display activity is not active, cannot show create folder dialog") + return + } + + val fragment = fileActivity.listOfFilesFragment + if (fragment == null) { + Log_OC.e(TAG, "file list fragment is null, cannot show create folder dialog") + return + } + + fragment.createFolder(true) + } + + fun toggleFileLock(file: OCFile, shouldBeLocked: Boolean) { + if (file.isLocked != shouldBeLocked) { + EventBus.getDefault().post(FileLockEvent(file.remotePath, shouldBeLocked)) + } + } + // endregion + + // region File operations + fun renameFile(file: ServerFileInterface?, newFilename: String?) { + file ?: return + + val intent = operationIntent(OperationsService.ACTION_RENAME).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, file.remotePath) + putExtra(OperationsService.EXTRA_NEWNAME, newFilename) + } + queueOperation(intent) + } + + fun removeFiles(files: Collection, onlyLocalCopy: Boolean, inBackground: Boolean) { + files.forEach { removeFile(it, onlyLocalCopy, inBackground) } + } + + private fun removeFile(file: OCFile?, onlyLocalCopy: Boolean, inBackground: Boolean) { + val intent = operationIntent(OperationsService.ACTION_REMOVE).apply { + putExtra(OperationsService.EXTRA_FILE, file) + putExtra(OperationsService.EXTRA_REMOVE_ONLY_LOCAL, onlyLocalCopy) + putExtra(OperationsService.EXTRA_IN_BACKGROUND, inBackground) + } + queueOperation(intent) + } + + fun createFolder(remotePath: String?, encrypted: Boolean) { + val intent = operationIntent(OperationsService.ACTION_CREATE_FOLDER).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, remotePath) + putExtra(OperationsService.EXTRA_ENCRYPTED, encrypted) + } + queueOperationAndShowWaitDialog(intent) + } + + fun restoreFileVersion(fileVersion: FileVersion?) { + if (fileVersion == null) { + Log_OC.e(TAG, "Trying to restore a NULL FileVersion") + return + } + + showWaitDialog() + val intent = operationIntent(OperationsService.ACTION_RESTORE_VERSION).apply { + putExtra(OperationsService.EXTRA_FILE_VERSION, fileVersion) + } + queueOperation(intent) + } + + fun moveOrCopyFiles(action: String?, filePaths: List, targetFolder: OCFile?) { + targetFolder ?: return + + filePaths.forEach { path -> + val intent = operationIntent(action).apply { + putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, targetFolder.remotePath) + putExtra(OperationsService.EXTRA_REMOTE_PATH, path) + } + queueOperation(intent) + } + showWaitDialog() + } + + fun cancelTransference(file: OCFile) { + val currentUser = fileActivity.user.orElseThrow { IllegalStateException() } + if (file.isFolder) { + fileActivity.operationsServiceBinder?.cancel(currentUser.toPlatformAccount(), file) + } + + val fileDownloadHelper = FileDownloadHelper.instance() + if (fileDownloadHelper.isDownloading(currentUser, file)) { + val files = fileActivity.storageManager.getAllFilesRecursivelyInsideFolder(file) + fileDownloadHelper.cancelPendingOrCurrentDownloads(currentUser, files) + } + + if (file.isFolder) { + fileDownloadHelper.cancelFolderDownload() + } + + val accountName = currentUser.accountName + val fileUploadHelper = FileUploadHelper.instance() + if (fileUploadHelper.isUploading(file.remotePath, accountName)) { + FileUploadWorker.cancelUpload(file.remotePath, accountName) { + fileUploadHelper.updateUploadStatus( + file.remotePath, + accountName, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED + ) + } + } + } + + fun exportFiles(files: Collection, backgroundJobManager: BackgroundJobManager) { + backgroundJobManager.startImmediateFilesExportJob(files) + } + // endregion + + // region Albums + fun createAlbum(albumName: String?) { + val intent = operationIntent(OperationsService.ACTION_CREATE_ALBUM).apply { + putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName) + } + queueOperationAndShowWaitDialog(intent) + } + + fun addFileToAlbum(files: Collection) { + val paths = files.mapTo(ArrayList(files.size)) { it.remotePath } + fileActivity.startActivity(AlbumsPickerActivity.intentForPickingAlbum(fileActivity, paths)) + } + + fun albumCopyFiles(filePaths: List?, targetFolder: String?) { + if (filePaths.isNullOrEmpty()) { + return + } + + connectivityService.isNetworkAndServerAvailable { isAvailable -> + if (!isAvailable) { + show(fileActivity, fileActivity.getString(R.string.offline_mode)) + return@isNetworkAndServerAvailable + } + + filePaths.forEach { path -> + val intent = operationIntent(OperationsService.ACTION_ALBUM_COPY_FILE).apply { + putExtra(OperationsService.EXTRA_NEW_PARENT_PATH, targetFolder) + putExtra(OperationsService.EXTRA_REMOTE_PATH, path) + } + queueOperation(intent) + } + showWaitDialog() + } + } + + fun renameAlbum(oldAlbumName: String?, newAlbumName: String?) { + val intent = operationIntent(OperationsService.ACTION_RENAME_ALBUM).apply { + putExtra(OperationsService.EXTRA_REMOTE_PATH, oldAlbumName) + putExtra(OperationsService.EXTRA_NEWNAME, newAlbumName) + } + queueOperationAndShowWaitDialog(intent) + } + + fun removeAlbum(albumName: String?) { + val intent = operationIntent(OperationsService.ACTION_REMOVE_ALBUM).apply { + putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName) + } + queueOperationAndShowWaitDialog(intent) + } + + fun albumPublicShareLink(albumName: String?, isCreateShare: Boolean) { + val intent = operationIntent(OperationsService.ACTION_PUBLIC_SHARE_LINK_ALBUM).apply { + putExtra(OperationsService.EXTRA_ALBUM_NAME, albumName) + putExtra(OperationsService.EXTRA_CREATE_ALBUM_SHARE, isCreateShare) + } + queueOperationAndShowWaitDialog(intent) + } + // endregion + + // region Camera upload + fun uploadFromCamera(activity: Activity, requestCode: Int, isVideo: Boolean) { + val action = if (isVideo) MediaStore.ACTION_VIDEO_CAPTURE else MediaStore.ACTION_IMAGE_CAPTURE + val cameraUri = FileProvider.getUriForFile( + activity.applicationContext, + activity.getString(R.string.file_provider_authority), + createCameraFile(activity, isVideo) + ) + val intent = Intent(action).apply { + putExtra(MediaStore.EXTRA_OUTPUT, cameraUri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + } + + if (intent.resolveActivity(activity.packageManager) == null) { + show(activity, R.string.upload_direct_camera_not_found) + return + } + + activity.startActivityForResult(intent, requestCode) + } + // endregion + + // region Operations service + private fun operationIntent(action: String?): Intent = Intent(fileActivity, OperationsService::class.java).apply { + this.action = action + putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.account) + } + + private fun queueOperation(intent: Intent) { + opIdWaitingFor = fileActivity.operationsServiceBinder.queueNewOperation(intent) + } + + private fun queueOperationAndShowWaitDialog(intent: Intent) { + queueOperation(intent) + showWaitDialog() + } + + private fun showWaitDialog() { + fileActivity.showLoadingDialog(fileActivity.getString(R.string.wait_a_moment)) + } + // endregion + + companion object { + private val TAG = FileOperationsHelper::class.java.simpleName + private val URL_LINE_PATTERN = Regex("^URL=(.+)$") + private val WEBLOC_STRING_PATTERN = Regex("(.+)") + private const val FILE_EXTENSION_URL = "url" + private const val FILE_EXTENSION_DESKTOP = "desktop" + private const val FILE_EXTENSION_WEBLOC = "webloc" + private const val TAG_SEND_SHARE_DIALOG = "TAG_SEND_SHARE_DIALOG" + private const val RICH_DOCUMENTS_EDITOR_TITLE = "Collabora" + private const val TEXT_EDITOR_TITLE = "Text" + private const val SINGLE_LINK_SIZE = 1 + private const val SHARING_TAB = 1 + private const val NO_CONFLICT_UPLOAD_ID = -1L + private const val REQUEST_CODE_SET_PICTURE_AS = 200 + private const val FILE_NOT_SYNCED_AFTER_CONFLICT_CHECK_WAIT_MILLIS = 3L * AppConstants.MILLIS_PER_SECOND + private const val UNKNOWN_AVAILABLE_SPACE = -1L + private const val TIMESTAMP_FILE_NAME_PATTERN = "yyyy-MM-dd_HHmmss" + private const val CAMERA_PHOTO_FILE_NAME = "/directCameraUpload.jpg" + private const val CAMERA_VIDEO_FILE_NAME = "/directCameraUpload.mp4" + private const val PHOTO_EXTENSION = ".jpg" + private const val VIDEO_EXTENSION = ".mp4" + + fun createCameraFile(activity: Activity, isVideo: Boolean): File { + val directory = if (isVideo) Environment.DIRECTORY_MOVIES else Environment.DIRECTORY_PICTURES + val fileName = if (isVideo) CAMERA_VIDEO_FILE_NAME else CAMERA_PHOTO_FILE_NAME + return File("${activity.getExternalFilesDir(directory)}$fileName") + } + + fun getCapturedImageName(): String = getTimestampedFileName(PHOTO_EXTENSION) + + fun getCapturedVideoName(): String = getTimestampedFileName(VIDEO_EXTENSION) + + fun getTimestampedFileName(extension: String): String = + SimpleDateFormat(TIMESTAMP_FILE_NAME_PATTERN, Locale.US).format(Date()) + extension + + fun getAvailableSpaceOnDevice(): Long { + val storagePath = MainApp.getStoragePath() ?: return UNKNOWN_AVAILABLE_SPACE + val stat = try { + StatFs(storagePath) + } catch (_: IllegalArgumentException) { + return UNKNOWN_AVAILABLE_SPACE + } + + return stat.blockSizeLong * stat.availableBlocksLong + } + + @JvmStatic + fun isEndToEndEncryptionSetup(context: Context?, user: User?): Boolean { + val arbitraryDataProvider = ArbitraryDataProviderImpl(context) + return arbitraryDataProvider.getValue(user, EncryptionUtils.PUBLIC_KEY).isNotEmpty() && + arbitraryDataProvider.getValue(user, EncryptionUtils.PRIVATE_KEY).isNotEmpty() + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31901ff1e355..f3d6f60bf3e1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1055,6 +1055,7 @@ Do you want to take a photo or video? Photo Video + No camera found Failed to upload taken media Scan document from camera Upload content from other apps