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
17 changes: 17 additions & 0 deletions opencloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,23 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REORDER_TASKS" />

<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
<data
android:mimeType="*/*"
android:scheme="content" />
</intent>
<intent>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />
</intent>
<intent>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<data android:mimeType="*/*" />
</intent>
</queries>

<application
android:name=".MainApp"
android:allowBackup="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -456,8 +456,10 @@ fun FragmentActivity.sendDownloadedFilesByShareSheet(ocFiles: List<OCFile>) {
}

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
Expand Down Expand Up @@ -60,6 +61,7 @@ public class MimetypeIconUtil {
/** Mapping: mime type for file extension. */
private static final Map<String, List<String>> FILE_EXTENSION_TO_MIMETYPE_MAPPING =
new HashMap<String, List<String>>();
public static final String UNKNOWN_MIME_TYPE = "application/octet-stream";

static {
populateFileExtensionMimeTypeMapping();
Expand Down Expand Up @@ -95,11 +97,34 @@ public static int getFileTypeIconId(String mimetype, String filename) {
public static String getBestMimeTypeByFilename(String filename) {
List<String> 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.
*
Expand Down Expand Up @@ -151,7 +176,8 @@ private static List<String> 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 {
Expand All @@ -167,8 +193,16 @@ private static List<String> 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);
}

/**
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

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