diff --git a/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt b/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt index a4f61c7325f9..632de0e1c459 100644 --- a/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/files/FileMenuFilterIT.kt @@ -330,6 +330,39 @@ class FileMenuFilterIT : AbstractIT() { } } + @Test + fun filter_openWithOnlyOffice_onlyForDownloadedFiles() { + every { + mockArbitraryDataProvider.getValue(any(), ArbitraryDataProvider.DIRECT_EDITING) + } returns ONLYOFFICE_DIRECT_EDITING_JSON + + configureCapability(OCCapability()) + + val downloadedFile = OCFile("/downloaded.docx").apply { + mimeType = OFFICE_MIMETYPE + storagePath = getDummyFile("downloaded.docx").absolutePath + } + val onlineOnlyFile = OCFile("/online.docx").apply { + mimeType = OFFICE_MIMETYPE + } + + launchActivity().use { + it.onActivity { activity -> + val filterFactory = FileMenuFilter.Factory(mockStorageManager, activity, editorUtils) + + val downloadedToHide = filterFactory + .newInstance(downloadedFile, mockComponentsGetter, true, user) + .getToHide(false) + assertFalse(downloadedToHide.contains(R.id.action_open_in_web_editor)) + + val onlineToHide = filterFactory + .newInstance(onlineOnlyFile, mockComponentsGetter, true, user) + .getToHide(false) + assertTrue(onlineToHide.contains(R.id.action_open_in_web_editor)) + } + } + } + private data class ExpectedLockVisibilities(val lockFile: Boolean, val unlockFile: Boolean) private fun configureCapability(capability: OCCapability) { @@ -363,4 +396,25 @@ class FileMenuFilterIT : AbstractIT() { } } } + + companion object { + private const val OFFICE_MIMETYPE = + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + + private val ONLYOFFICE_DIRECT_EDITING_JSON = + """ + { + "editors": { + "onlyoffice": { + "id": "onlyoffice", + "name": "OnlyOffice", + "mimetypes": [], + "optionalMimetypes": ["$OFFICE_MIMETYPE"], + "secure": false + } + }, + "creators": {} + } + """.trimIndent() + } } diff --git a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt index f30dbefbe86a..96e7b63a21d3 100644 --- a/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt +++ b/app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt @@ -48,6 +48,7 @@ enum class FileAction( SEND_SHARE_FILE(R.id.action_send_share_file, R.string.action_send_share, R.drawable.ic_share), SEND_FILE(R.id.action_send_file, R.string.common_send, R.drawable.ic_share), OPEN_FILE_WITH(R.id.action_open_file_with, R.string.actionbar_open_with, R.drawable.ic_external), + OPEN_IN_WEB_EDITOR(R.id.action_open_in_web_editor, R.string.action_open_in_web_editor, R.drawable.file_doc), STREAM_MEDIA(R.id.action_stream_media, R.string.stream, R.drawable.ic_play_arrow), SET_AS_WALLPAPER(R.id.action_set_as_wallpaper, R.string.set_picture_as, R.drawable.ic_wallpaper), @@ -87,6 +88,7 @@ enum class FileAction( SEND_SHARE_FILE, SEND_FILE, OPEN_FILE_WITH, + OPEN_IN_WEB_EDITOR, SYNC_FOLDER ) diff --git a/app/src/main/java/com/nextcloud/utils/EditorUtils.kt b/app/src/main/java/com/nextcloud/utils/EditorUtils.kt index 3c1630e64308..8224fa316ca1 100644 --- a/app/src/main/java/com/nextcloud/utils/EditorUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/EditorUtils.kt @@ -1,6 +1,7 @@ /* * Nextcloud - Android Client * + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-FileCopyrightText: 2022 Álvaro Brey * SPDX-FileCopyrightText: 2022 Nextcloud GmbH * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only @@ -16,17 +17,42 @@ import javax.inject.Inject class EditorUtils @Inject constructor(private val arbitraryDataProvider: ArbitraryDataProvider) { + /** + * Returns an editor matching only the strictly supported mimetypes. + */ fun getEditor(user: User?, mimeType: String?): Editor? { - val json = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.DIRECT_EDITING) - if (json.isEmpty()) { - return null - } - val editors = Gson().fromJson(json, DirectEditing::class.java).editors.values + val editors = getEditors(user) ?: return null + return editors.firstOrNull { mimeType in it.mimetypes } + } + + /** + * Returns an editor matching the supported mimetypes or, as a fallback, the optional ones. + */ + fun getAvailableEditor(user: User?, mimeType: String?): Editor? { + val editors = getEditors(user) ?: return null return editors.firstOrNull { mimeType in it.mimetypes } ?: editors.firstOrNull { mimeType in it.optionalMimetypes } } - fun isEditorAvailable(user: User?, mimeType: String?): Boolean = getEditor(user, mimeType) != null + /** + * Returns supported mimetypes along with optional ones + */ + fun isEditorAvailable(user: User?, mimeType: String?): Boolean = getAvailableEditor(user, mimeType) != null + + /** + * Returns true when an office editor (e.g. OnlyOffice) can open the mimetype, + * either as a supported or optional mimetype. + */ + fun isOfficeEditorAvailable(user: User?, mimeType: String?): Boolean { + val editors = getEditors(user) ?: return false + return editors.any { usesOfficeUserAgent(it) && (mimeType in it.mimetypes || mimeType in it.optionalMimetypes) } + } + + private fun getEditors(user: User?): Collection? { + val json = arbitraryDataProvider.getValue(user, ArbitraryDataProvider.DIRECT_EDITING) + if (json.isEmpty()) return null + return Gson().fromJson(json, DirectEditing::class.java).editors.values + } fun usesOfficeUserAgent(editor: Editor?): Boolean = editor?.id in OFFICE_EDITOR_IDS diff --git a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java index 9f29055f94fe..5df186df3931 100644 --- a/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java +++ b/app/src/main/java/com/owncloud/android/files/FileMenuFilter.java @@ -158,6 +158,7 @@ private List filter(boolean inSingleFileFragment) { filterSelectAll(toHide, inSingleFileFragment); filterDeselectAll(toHide, inSingleFileFragment); filterOpenWith(toHide, synchronizing); + filterOpenInWebEditor(toHide); filterCancelSync(toHide, synchronizing); filterSync(toHide, synchronizing); filterShareFile(toHide, capability); @@ -313,9 +314,15 @@ private void filterEdit( */ @NextcloudServer(max = 18) private boolean isRichDocumentEditingSupported(OCCapability capability, String mimeType) { + final var mimeTypeList = capability.getRichDocumentsMimeTypeList(); + final var isMimeTypeListContainsMimeType = (mimeTypeList != null && mimeTypeList.contains(mimeType)); + + final var optionalMimeTypeList = capability.getRichDocumentsOptionalMimeTypeList(); + final var isOptionalMimeTypeListContainsMimeType = (optionalMimeTypeList != null && + optionalMimeTypeList.contains(mimeType)); + return isSingleFile() && - (capability.getRichDocumentsMimeTypeList().contains(mimeType) || - capability.getRichDocumentsOptionalMimeTypeList().contains(mimeType)) && + (isMimeTypeListContainsMimeType || isOptionalMimeTypeListContainsMimeType) && capability.getRichDocumentsDirectEditing().isTrue(); } @@ -339,6 +346,19 @@ private void filterOpenWith(Collection toHide, boolean synchronizing) { } } + private void filterOpenInWebEditor(Collection toHide) { + if (!isSingleFile()) { + toHide.add(R.id.action_open_in_web_editor); + return; + } + + OCFile file = files.iterator().next(); + boolean canOpenLocally = file.isDown() && !file.isEncrypted(); + if (!canOpenLocally || !editorUtils.isOfficeEditorAvailable(user, file.getMimeType())) { + toHide.add(R.id.action_open_in_web_editor); + } + } + private void filterDeselectAll(List toHide, boolean inSingleFileFragment) { if (inSingleFileFragment) { // Always hide in single file fragments diff --git a/app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt b/app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt index 2f950420b6ac..752f5720696f 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/TextEditorWebView.kt @@ -9,6 +9,8 @@ package com.owncloud.android.ui.activity import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import androidx.core.net.toUri import androidx.webkit.WebSettingsCompat import androidx.webkit.WebViewFeature @@ -16,6 +18,7 @@ import com.nextcloud.android.common.ui.util.PlatformThemeUtil import com.nextcloud.client.appinfo.AppInfo import com.nextcloud.client.device.DeviceInfo import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile import com.owncloud.android.ui.asynctasks.TextEditorLoadUrlTask import com.owncloud.android.utils.DisplayUtils import javax.inject.Inject @@ -27,6 +30,21 @@ class TextEditorWebView : EditorWebView() { @Inject lateinit var deviceInfo: DeviceInfo + companion object { + fun startTextEditor(file: OCFile?, context: Context?) { + val context = context ?: return + val file = file ?: return + + Intent(context, TextEditorWebView::class.java).apply { + putExtra(EXTRA_TITLE, "Text") + putExtra(EXTRA_FILE, file) + putExtra(EXTRA_SHOW_SIDEBAR, false) + }.also { + context.startActivity(it) + } + } + } + @SuppressLint("AddJavascriptInterface") // suppress warning as webview is only used > Lollipop override fun postOnCreate() { super.postOnCreate() @@ -37,7 +55,7 @@ class TextEditorWebView : EditorWebView() { } user.ifPresent { - val editor = editorUtils.getEditor(it, file?.mimeType) + val editor = editorUtils.getAvailableEditor(it, file?.mimeType) if (editorUtils.usesOfficeUserAgent(editor)) { webView.settings.userAgentString = generateOfficeUserAgent() diff --git a/app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java b/app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java index 14c18dba9b72..360e8a347316 100644 --- a/app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java +++ b/app/src/main/java/com/owncloud/android/ui/asynctasks/TextEditorLoadUrlTask.java @@ -41,7 +41,7 @@ protected String doInBackground(Void... voids) { return ""; } - Editor editor = editorUtils.getEditor(user, file.getMimeType()); + Editor editor = editorUtils.getAvailableEditor(user, file.getMimeType()); if (editor == null) { return ""; diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index f9b959d2ecfc..af167aff68f3 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -91,6 +91,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.FolderPickerActivity; import com.owncloud.android.ui.activity.OnEnforceableRefreshListener; +import com.owncloud.android.ui.activity.TextEditorWebView; import com.owncloud.android.ui.activity.UploadFilesActivity; import com.owncloud.android.ui.adapter.CommonOCFileListAdapterInterface; import com.owncloud.android.ui.adapter.OCFileListAdapter; @@ -148,7 +149,6 @@ import androidx.fragment.app.FragmentManager; import androidx.media3.common.util.UnstableApi; import kotlin.Unit; -import kotlin.jvm.functions.Function1; import static com.owncloud.android.datamodel.OCFile.ROOT_PATH; import static com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment.SETUP_ENCRYPTION_DIALOG_TAG; @@ -1220,8 +1220,8 @@ private void handlePendingDownloadFile(OCFile file) { } else if (PreviewMediaActivity.Companion.canBePreviewed(file) && !file.isEncrypted() && mContainerActivity instanceof FileDisplayActivity fda) { setFabVisible(false); fda.startMediaPreview(file, 0, true, true, true, true); - } else if (editorUtils.isEditorAvailable(accountManager.getUser(), file.getMimeType()) && !file.isEncrypted()) { - mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(file, getContext()); + } else if (editorUtils.getEditor(accountManager.getUser(), file.getMimeType()) != null && !file.isEncrypted()) { + TextEditorWebView.Companion.startTextEditor(file, getContext()); } else if (capability.getRichDocumentsMimeTypeList() != null && capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) && capability.getRichDocumentsDirectEditing().isTrue() && !file.isEncrypted()) { @@ -1355,14 +1355,16 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi } else if (itemId == R.id.action_open_file_with) { mContainerActivity.getFileOperationsHelper().openFile(singleFile); return true; + } else if (itemId == R.id.action_open_in_web_editor) { + TextEditorWebView.Companion.startTextEditor(singleFile, getContext()); + return true; } else if (itemId == R.id.action_stream_media) { mContainerActivity.getFileOperationsHelper().streamMediaFile(singleFile); return true; } else if (itemId == R.id.action_edit) { // should not be necessary, as menu item is filtered, but better play safe - if (editorUtils.isEditorAvailable(accountManager.getUser(), - singleFile.getMimeType())) { - mContainerActivity.getFileOperationsHelper().openFileWithTextEditor(singleFile, getContext()); + if (editorUtils.isEditorAvailable(accountManager.getUser(), singleFile.getMimeType())) { + TextEditorWebView.Companion.startTextEditor(singleFile,getContext()); } else if (EditImageActivity.Companion.canBePreviewed(singleFile)) { ((FileDisplayActivity) mContainerActivity).startImageEditor(singleFile); } else { 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 index a1b22caa96e5..86d326d5c6ae 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -291,11 +291,12 @@ public void openFile(OCFile file) { List availableApps = fileActivity.getPackageManager(). queryIntentActivities(openFileWithIntent, PackageManager.GET_RESOLVED_FILTER); + // first always try to use available apps if (availableApps.isEmpty()) { Optional optionalUser = fileActivity.getUser(); if (optionalUser.isPresent() && editorUtils.isEditorAvailable(optionalUser.get(), file.getMimeType())) { - openFileWithTextEditor(file, fileActivity); + TextEditorWebView.Companion.startTextEditor(file, fileActivity); return; } @@ -364,14 +365,6 @@ public void openFileAsRichDocument(OCFile file, Context context) { context.startActivity(collaboraWebViewIntent); } - public void openFileWithTextEditor(OCFile file, Context context) { - Intent textEditorIntent = new Intent(context, TextEditorWebView.class); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text"); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_FILE, file); - textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_SHOW_SIDEBAR, false); - context.startActivity(textEditorIntent); - } - public void openRichWorkspaceWithTextEditor(OCFile file, String url, Context context) { Intent textEditorIntent = new Intent(context, TextEditorWebView.class); textEditorIntent.putExtra(ExternalSiteWebView.EXTRA_TITLE, "Text"); diff --git a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java index 0b32cc0a9a92..dd6240c4cf2e 100644 --- a/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/preview/PreviewTextFileFragment.java @@ -27,6 +27,7 @@ import com.owncloud.android.datamodel.OCFile; import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.ui.activity.FileActivity; +import com.owncloud.android.ui.activity.TextEditorWebView; import com.owncloud.android.ui.dialog.ConfirmationDialogFragment; import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; import com.owncloud.android.utils.DisplayUtils; @@ -310,7 +311,7 @@ private void onFileActionChosen(final int itemId) { } else if(itemId == R.id.action_cancel_sync){ containerActivity.getFileOperationsHelper().cancelTransference(getFile()); } else if (itemId == R.id.action_edit) { - containerActivity.getFileOperationsHelper().openFileWithTextEditor(getFile(), getContext()); + TextEditorWebView.Companion.startTextEditor(getFile(), getContext()); } } diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 34075e4237cf..31a58feb98e8 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -23,6 +23,7 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7734d4ce2a34..7c03dc35e5f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1194,6 +1194,7 @@ Internal share link only works for users with access to this file Share internal link Edit + Open in web editor Failed to start editor Add folder description We couldnt locate the file on server. Another user may have deleted the file diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys index fa31fb65640b..c4a3b87a62e2 100644 --- a/gradle/verification-keyring.keys +++ b/gradle/verification-keyring.keys @@ -1,3 +1,116 @@ +pub 16968D251E760606 +uid Ben Fortuna + +sub A919E79123E17CA3 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +XZ4Bscfi9g7eUg== +=5zwj +-----END PGP PUBLIC KEY BLOCK----- + +pub 72385FF0AF338D52 +uid Stephen Colebourne (CODE SIGNING KEY) + +sub 458AAC45B5189772 +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +wiMcuA7Wf7wF6Yt6o2KOu+jNqT7oglS+Bpw= +=iGvG +-----END PGP PUBLIC KEY BLOCK----- + pub 80C08B1C29100955 uid Jake Wharton diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 8bd5357e380b..f1e276e4696d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -31258,6 +31258,11 @@ + + + + + @@ -38705,6 +38710,11 @@ + + + + +