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, + ) + ) + } +}