Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,39 @@ class FileMenuFilterIT : AbstractIT() {
}
}

@Test
fun filter_openWithOnlyOffice_onlyForDownloadedFiles() {
every {
mockArbitraryDataProvider.getValue(any<User>(), 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<TestActivity>().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_with_office))

val onlineToHide = filterFactory
.newInstance(onlineOnlyFile, mockComponentsGetter, true, user)
.getToHide(false)
assertTrue(onlineToHide.contains(R.id.action_open_with_office))
}
}
}

private data class ExpectedLockVisibilities(val lockFile: Boolean, val unlockFile: Boolean)

private fun configureCapability(capability: OCCapability) {
Expand Down Expand Up @@ -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()
}
}
2 changes: 2 additions & 0 deletions app/src/main/java/com/nextcloud/ui/fileactions/FileAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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_WITH_OFFICE(R.id.action_open_with_office, R.string.action_open_with_office, 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),

Expand Down Expand Up @@ -87,6 +88,7 @@ enum class FileAction(
SEND_SHARE_FILE,
SEND_FILE,
OPEN_FILE_WITH,
OPEN_WITH_OFFICE,
SYNC_FOLDER
)

Expand Down
38 changes: 32 additions & 6 deletions app/src/main/java/com/nextcloud/utils/EditorUtils.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
Expand All @@ -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<Editor>? {
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

Expand Down
24 changes: 22 additions & 2 deletions app/src/main/java/com/owncloud/android/files/FileMenuFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ private List<Integer> filter(boolean inSingleFileFragment) {
filterSelectAll(toHide, inSingleFileFragment);
filterDeselectAll(toHide, inSingleFileFragment);
filterOpenWith(toHide, synchronizing);
filterOpenWithOffice(toHide);
filterCancelSync(toHide, synchronizing);
filterSync(toHide, synchronizing);
filterShareFile(toHide, capability);
Expand Down Expand Up @@ -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();
}

Expand All @@ -339,6 +346,19 @@ private void filterOpenWith(Collection<Integer> toHide, boolean synchronizing) {
}
}

private void filterOpenWithOffice(Collection<Integer> toHide) {
if (!isSingleFile()) {
toHide.add(R.id.action_open_with_office);
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_with_office);
}
}

private void filterDeselectAll(List<Integer> toHide, boolean inSingleFileFragment) {
if (inSingleFileFragment) {
// Always hide in single file fragments
Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
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
import com.nextcloud.android.common.ui.util.PlatformThemeUtil
import com.nextcloud.client.appinfo.AppInfo
import com.nextcloud.client.device.DeviceInfo
import com.nextcloud.utils.EditorUtils
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 com.owncloud.android.utils.theme.ThemeUtils
import javax.inject.Inject

class TextEditorWebView : EditorWebView() {
Expand All @@ -28,11 +30,20 @@ class TextEditorWebView : EditorWebView() {
@Inject
lateinit var deviceInfo: DeviceInfo

@Inject
lateinit var themeUtils: ThemeUtils

@Inject
lateinit var editorUtils: EditorUtils
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() {
Expand All @@ -43,7 +54,7 @@ class TextEditorWebView : EditorWebView() {
finish()
}

val editor = editorUtils.getEditor(user.get(), file?.mimeType)
val editor = editorUtils.getAvailableEditor(user.get(), file?.mimeType)

if (editorUtils.usesOfficeUserAgent(editor)) {
webView.settings.userAgentString = generateOfficeUserAgent()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -147,7 +148,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;
Expand Down Expand Up @@ -1216,8 +1216,8 @@ private void handlePendingDownloadFile(OCFile file) {
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()) {
Expand Down Expand Up @@ -1351,14 +1351,16 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set<OCFile> checkedFi
} else if (itemId == R.id.action_open_file_with) {
mContainerActivity.getFileOperationsHelper().openFile(singleFile);
return true;
} else if (itemId == R.id.action_open_with_office) {
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,12 @@ public void openFile(OCFile file) {
List<ResolveInfo> availableApps = fileActivity.getPackageManager().
queryIntentActivities(openFileWithIntent, PackageManager.GET_RESOLVED_FILTER);

// first always try to use available apps
if (availableApps.isEmpty()) {
Optional<User> optionalUser = fileActivity.getUser();

if (optionalUser.isPresent() && editorUtils.isEditorAvailable(optionalUser.get(), file.getMimeType())) {
openFileWithTextEditor(file, fileActivity);
TextEditorWebView.Companion.startTextEditor(file, fileActivity);
return;
}

Expand Down Expand Up @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}

Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/ids.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<item name="action_send_share_file" type="id"/>
<item name="action_send_file" type="id"/>
<item name="action_open_file_with" type="id"/>
<item name="action_open_with_office" type="id"/>
<item name="action_sync_file" type="id"/>
<item name="action_sync_all_files" type="id"/>
<item name="action_cancel_sync" type="id"/>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,7 @@
<string name="share_internal_link_to_file_text">Internal share link only works for users with access to this file</string>
<string name="share_internal_link">Share internal link</string>
<string name="action_edit">Edit</string>
<string name="action_open_with_office">Open with office</string>
<string name="failed_to_start_editor">Failed to start editor</string>
<string name="create_rich_workspace">Add folder description</string>
<string name="uploader_file_not_found_on_server_message">We couldnt locate the file on server. Another user may have deleted the file</string>
Expand Down
Loading