diff --git a/opencloudApp/src/main/AndroidManifest.xml b/opencloudApp/src/main/AndroidManifest.xml
index 9e456753a0..d12293a40c 100644
--- a/opencloudApp/src/main/AndroidManifest.xml
+++ b/opencloudApp/src/main/AndroidManifest.xml
@@ -45,6 +45,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
) {
}
fun Activity.openOCFile(ocFile: OCFile) {
+ val finalMimeType = MimetypeIconUtil.getBestMimeTypeForOpen(ocFile.mimeType, ocFile.fileName)
+
val intentForSavedMimeType = Intent(Intent.ACTION_VIEW).apply {
- setDataAndType(getExposedFileUriForOCFile(this@openOCFile, ocFile), ocFile.mimeType)
+ setDataAndType(getExposedFileUriForOCFile(this@openOCFile, ocFile), finalMimeType)
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
if (ocFile.hasWritePermission) {
flags = flags or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
diff --git a/opencloudApp/src/main/java/eu/opencloud/android/ui/helpers/FileOperationsHelper.java b/opencloudApp/src/main/java/eu/opencloud/android/ui/helpers/FileOperationsHelper.java
index dbaaf9cb09..331ebe16f7 100644
--- a/opencloudApp/src/main/java/eu/opencloud/android/ui/helpers/FileOperationsHelper.java
+++ b/opencloudApp/src/main/java/eu/opencloud/android/ui/helpers/FileOperationsHelper.java
@@ -31,7 +31,6 @@
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
-import android.webkit.MimeTypeMap;
import eu.opencloud.android.R;
import eu.opencloud.android.domain.files.model.OCFile;
@@ -43,6 +42,7 @@
import eu.opencloud.android.ui.activity.FileActivity;
import eu.opencloud.android.usecases.synchronization.SynchronizeFileUseCase;
import eu.opencloud.android.usecases.synchronization.SynchronizeFolderUseCase;
+import eu.opencloud.android.utils.MimetypeIconUtil;
import eu.opencloud.android.utils.UriUtilsKt;
import kotlin.Lazy;
import org.jetbrains.annotations.NotNull;
@@ -74,48 +74,20 @@ private Intent getIntentForSavedMimeType(Uri data, String type, boolean hasWrite
return intentForSavedMimeType;
}
- private Intent getIntentForGuessedMimeType(String storagePath, String type, Uri data, boolean hasWritePermission) {
- Intent intentForGuessedMimeType = null;
-
- if (storagePath != null && storagePath.lastIndexOf('.') >= 0) {
- String guessedMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(storagePath.substring(storagePath.lastIndexOf('.') + 1));
-
- if (guessedMimeType != null && !guessedMimeType.equals(type)) {
- intentForGuessedMimeType = new Intent(Intent.ACTION_VIEW);
- intentForGuessedMimeType.setDataAndType(data, guessedMimeType);
- int flags = Intent.FLAG_GRANT_READ_URI_PERMISSION;
- if (hasWritePermission) {
- flags = flags | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
- }
- intentForGuessedMimeType.setFlags(flags);
- }
- }
- return intentForGuessedMimeType;
- }
-
public void openFile(OCFile ocFile) {
if (ocFile != null) {
+ String finalMimeType = MimetypeIconUtil.getBestMimeTypeForOpen(ocFile.getMimeType(), ocFile.getFileName());
Intent intentForSavedMimeType = getIntentForSavedMimeType(UriUtilsKt.INSTANCE.getExposedFileUriForOCFile(mFileActivity, ocFile),
- ocFile.getMimeType(), ocFile.getHasWritePermission());
-
- Intent intentForGuessedMimeType = getIntentForGuessedMimeType(ocFile.getStoragePath(), ocFile.getMimeType(),
- UriUtilsKt.INSTANCE.getExposedFileUriForOCFile(mFileActivity, ocFile), ocFile.getHasWritePermission());
+ finalMimeType, ocFile.getHasWritePermission());
- openFileWithIntent(intentForSavedMimeType, intentForGuessedMimeType);
+ openFileWithIntent(intentForSavedMimeType);
} else {
Timber.e("Trying to open a NULL OCFile");
}
}
- private void openFileWithIntent(Intent intentForSavedMimeType, Intent intentForGuessedMimeType) {
- Intent openFileWithIntent;
-
- if (intentForGuessedMimeType != null) {
- openFileWithIntent = intentForGuessedMimeType;
- } else {
- openFileWithIntent = intentForSavedMimeType;
- }
+ private void openFileWithIntent(Intent openFileWithIntent) {
try {
mFileActivity.startActivity(Intent.createChooser(openFileWithIntent, mFileActivity.getString(R.string.actionbar_open_with)));
} catch (ActivityNotFoundException anfe) {
diff --git a/opencloudApp/src/main/java/eu/opencloud/android/utils/MimetypeIconUtil.java b/opencloudApp/src/main/java/eu/opencloud/android/utils/MimetypeIconUtil.java
index ebea8ee3ad..0171c47a91 100644
--- a/opencloudApp/src/main/java/eu/opencloud/android/utils/MimetypeIconUtil.java
+++ b/opencloudApp/src/main/java/eu/opencloud/android/utils/MimetypeIconUtil.java
@@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
/**
@@ -60,6 +61,7 @@ public class MimetypeIconUtil {
/** Mapping: mime type for file extension. */
private static final Map> FILE_EXTENSION_TO_MIMETYPE_MAPPING =
new HashMap>();
+ public static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
static {
populateFileExtensionMimeTypeMapping();
@@ -95,11 +97,34 @@ public static int getFileTypeIconId(String mimetype, String filename) {
public static String getBestMimeTypeByFilename(String filename) {
List candidates = determineMimeTypesByFilename(filename);
if (candidates == null || candidates.size() < 1) {
- return "application/octet-stream";
+ return UNKNOWN_MIME_TYPE;
}
return candidates.get(0);
}
+ /**
+ * Returns the server MIME type unless it is generic, then falls back to the file extension.
+ *
+ * @param serverMimeType MIME type reported by the server
+ * @param filename Name of file
+ * @return MIME type to use when opening the file
+ */
+ public static String getBestMimeTypeForOpen(String serverMimeType, String filename) {
+ if (!isGenericMimeType(serverMimeType)) {
+ return serverMimeType;
+ }
+
+ String mimeTypeFromFilename = getBestMimeTypeByFilename(filename);
+ if (isGenericMimeType(mimeTypeFromFilename)) {
+ return UNKNOWN_MIME_TYPE;
+ }
+ return mimeTypeFromFilename;
+ }
+
+ private static boolean isGenericMimeType(String mimeType) {
+ return mimeType == null || mimeType.trim().isEmpty() || UNKNOWN_MIME_TYPE.equals(mimeType) || "*/*".equals(mimeType);
+ }
+
/**
* determines the icon based on the mime type.
*
@@ -151,7 +176,8 @@ private static List determineMimeTypesByFilename(String filename) {
return mimeTypeList;
} else {
// try detecting the mime type via android itself
- String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+ MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+ String mimeType = mimeTypeMap != null ? mimeTypeMap.getMimeTypeFromExtension(fileExtension) : null;
if (mimeType != null) {
return Collections.singletonList(mimeType);
} else {
@@ -167,8 +193,16 @@ private static List determineMimeTypesByFilename(String filename) {
* @return the file extension
*/
private static String getExtension(String filename) {
- String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
- return extension;
+ if (filename == null || filename.isEmpty()) {
+ return "";
+ }
+
+ int extensionStart = filename.lastIndexOf(".");
+ if (extensionStart < 0 || extensionStart == filename.length() - 1) {
+ return "";
+ }
+
+ return filename.substring(extensionStart + 1).toLowerCase(Locale.ROOT);
}
/**
@@ -186,7 +220,7 @@ private static void populateMimeTypeIconMapping() {
MIMETYPE_TO_ICON_MAPPING.put("application/msexcel", R.drawable.file_xls);
MIMETYPE_TO_ICON_MAPPING.put("application/mspowerpoint", R.drawable.file_ppt);
MIMETYPE_TO_ICON_MAPPING.put("application/msword", R.drawable.file_doc);
- MIMETYPE_TO_ICON_MAPPING.put("application/octet-stream", R.drawable.file);
+ MIMETYPE_TO_ICON_MAPPING.put(UNKNOWN_MIME_TYPE, R.drawable.file);
MIMETYPE_TO_ICON_MAPPING.put("application/postscript", R.drawable.file_image);
MIMETYPE_TO_ICON_MAPPING.put("application/pdf", R.drawable.file_pdf);
MIMETYPE_TO_ICON_MAPPING.put("application/rss+xml", R.drawable.file_code);
diff --git a/opencloudApp/src/test/java/eu/opencloud/android/utils/MimetypeIconUtilTest.kt b/opencloudApp/src/test/java/eu/opencloud/android/utils/MimetypeIconUtilTest.kt
new file mode 100644
index 0000000000..3deaf79d3d
--- /dev/null
+++ b/opencloudApp/src/test/java/eu/opencloud/android/utils/MimetypeIconUtilTest.kt
@@ -0,0 +1,125 @@
+/**
+ * openCloud Android client application
+ *
+ * Copyright (C) 2026 openCloud GmbH.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package eu.opencloud.android.utils
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class MimetypeIconUtilTest {
+
+ @Test
+ fun `getBestMimeTypeForOpen keeps specific server mime type`() {
+ val mimeType = MimetypeIconUtil.getBestMimeTypeForOpen(
+ "application/pdf",
+ "Invoice.PDF",
+ )
+
+ assertEquals("application/pdf", mimeType)
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen uses file extension when server mime type is generic`() {
+ val mimeType = MimetypeIconUtil.getBestMimeTypeForOpen(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ "Invoice.PDF",
+ )
+
+ assertEquals("application/pdf", mimeType)
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen uses file extension when server mime type is wildcard`() {
+ val mimeType = MimetypeIconUtil.getBestMimeTypeForOpen(
+ "*/*",
+ "Invoice.PDF",
+ )
+
+ assertEquals("application/pdf", mimeType)
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen uses file extension when server mime type is null or blank`() {
+ assertEquals(
+ "application/pdf",
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ null,
+ "Invoice.PDF",
+ )
+ )
+ assertEquals(
+ "application/pdf",
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ "",
+ "Invoice.PDF",
+ )
+ )
+ assertEquals(
+ "application/pdf",
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ " ",
+ "Invoice.PDF",
+ )
+ )
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen does not override specific server mime type`() {
+ val mimeType = MimetypeIconUtil.getBestMimeTypeForOpen(
+ "text/markdown",
+ "Invoice.PDF",
+ )
+
+ assertEquals("text/markdown", mimeType)
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen falls back to unknown mime type for unknown extension`() {
+ val mimeType = MimetypeIconUtil.getBestMimeTypeForOpen(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ "Invoice.notarealextension",
+ )
+
+ assertEquals(MimetypeIconUtil.UNKNOWN_MIME_TYPE, mimeType)
+ }
+
+ @Test
+ fun `getBestMimeTypeForOpen handles filenames without extensions`() {
+ assertEquals(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ "Invoice",
+ )
+ )
+ assertEquals(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ "",
+ )
+ )
+ assertEquals(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ MimetypeIconUtil.getBestMimeTypeForOpen(
+ MimetypeIconUtil.UNKNOWN_MIME_TYPE,
+ null,
+ )
+ )
+ }
+}