diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4735783704..7ec487a539 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -111,6 +111,14 @@ + + + + + + + diff --git a/app/src/main/java/com/amaze/filemanager/filesystem/files/UriUtils.kt b/app/src/main/java/com/amaze/filemanager/filesystem/files/UriUtils.kt new file mode 100644 index 0000000000..e5037c74f2 --- /dev/null +++ b/app/src/main/java/com/amaze/filemanager/filesystem/files/UriUtils.kt @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2014-2024 Arpit Khurana , Vishal Nehra , + * Emmanuel Messulam, Raymond Lai and Contributors. + * + * This file is part of Amaze File Manager. + * + * Amaze File Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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 com.amaze.filemanager.filesystem.files + +import android.content.ContentUris +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import android.text.TextUtils +import java.io.File + +/** + * Tries to find the path of the file that is identified with [uri]. + * If the path cannot be found, returns null. + * + * Adapted from: https://github.com/saparkhid/AndroidFileNamePicker/blob/main/javautil/FileUtils.java + */ +fun fromUri( + uri: Uri, + context: Context, +): String? { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val fullPath = getPathFromExtSD(split) + return if (fullPath !== "") { + fullPath + } else { + null + } + } + + // DownloadsProvider + if (isDownloadsDocument(uri)) { + return getPathFromDownloads(uri, context) + } + + // MediaProvider + if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":") + val contentUri = + when (split[0]) { + "image" -> { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } + "video" -> { + MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } + "audio" -> { + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + "document" -> { + MediaStore.Files.getContentUri("external") + } + else -> return getDataColumn(context, uri, null, null) + } + val selection = "_id=?" + val selectionArgs = + arrayOf( + split[1], + ) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + if ("content".equals(uri.scheme, ignoreCase = true)) { + if (isGooglePhotosUri(uri)) { + return uri.lastPathSegment + } + val path = getDataColumn(context, uri, null, null) + if (path != null) { + return path + } else if (fileExists(uri.path)) { + // Check if the full path is the uri path + return uri.path + } else { + // Check if the full path is contained in the uri path + return getPathInUri(uri) + } + } + if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null +} + +private fun fileExists(filePath: String?): Boolean { + if (filePath == null) return false + + val file = File(filePath) + return file.exists() +} + +private fun getPathFromExtSD(pathData: List): String? { + val type = pathData[0] + val relativePath = File.separator + pathData[1] + var fullPath: String? = null + // on my Sony devices (4.4.4 & 5.1.1), `type` is a dynamic string + // something like "71F8-2C0A", some kind of unique id per storage + // don't know any API that can get the root path of that storage based on its id. + // + // so no "primary" type, but let the check here for other devices + if ("primary".equals(type, ignoreCase = true)) { + fullPath = Environment.getExternalStorageDirectory().toString() + relativePath + if (fileExists(fullPath)) { + return fullPath + } + } + if ("home".equals(type, ignoreCase = true)) { + fullPath = "/storage/emulated/0/Documents$relativePath" + if (fileExists(fullPath)) { + return fullPath + } + } + + // Adapted from: https://stackoverflow.com/questions/42110882/get-real-path-from-uri-of-file-in-sdcard-marshmallow + fullPath = "/storage/$type$relativePath" + return if (fileExists(fullPath)) { + fullPath + } else { + null + } +} + +private fun getPathFromDownloads( + uri: Uri, + context: Context, +): String? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Try to use ContentResolver to get the file name + context.contentResolver.query( + uri, + arrayOf(MediaStore.MediaColumns.DISPLAY_NAME), + null, + null, + null, + ).use { cursor -> + if (cursor != null && cursor.moveToFirst()) { + val fileName = + cursor.getString( + cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME), + ) + val path = + Environment.getExternalStorageDirectory() + .toString() + "/Download/" + fileName + if (!TextUtils.isEmpty(path)) { + return path + } + } + } + val id = DocumentsContract.getDocumentId(uri) + if (!TextUtils.isEmpty(id)) { + if (id.startsWith("raw:")) { + return id.replaceFirst("raw:", "") + } + val contentUriPrefixesToTry = + arrayOf( + "content://downloads/public_downloads", + "content://downloads/my_downloads", + ) + // Try to guess full path with frequently used download paths + for (contentUriPrefix in contentUriPrefixesToTry) { + return try { + val contentUri = + ContentUris.withAppendedId( + Uri.parse(contentUriPrefix), + java.lang.Long.valueOf(id), + ) + getDataColumn(context, contentUri, null, null) + } catch (e: NumberFormatException) { + // In Android 8 and Android P the id is not a number + uri.path!!.replaceFirst("^/document/raw:", "") + .replaceFirst("^raw:", "") + } + } + } + } else { + val id = DocumentsContract.getDocumentId(uri) + if (id.startsWith("raw:")) { + return id.replaceFirst("raw:", "") + } + return try { + val contentUri = + ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id), + ) + getDataColumn(context, contentUri, null, null) + } catch (e: NumberFormatException) { + null + } + } + return null +} + +private fun getDataColumn( + context: Context, + uri: Uri, + selection: String?, + selectionArgs: Array?, +): String? { + val column = MediaStore.Files.FileColumns.DATA + val projection = arrayOf(column) + + context.contentResolver.query( + uri, + projection, + selection, + selectionArgs, + null, + ).use { cursor -> + if (cursor != null && cursor.moveToFirst()) { + val index: Int = cursor.getColumnIndex(column) + return if (index >= 0) { + cursor.getString(index) + } else { + null + } + } + } + return null +} + +private fun getPathInUri(uri: Uri): String? { + // As last resort, check if the full path is somehow contained in the uri path + val uriPath = uri.path ?: return null + // Some common path prefixes + val pathPrefixes = listOf("/storage", "/external_files") + for (prefix in pathPrefixes) { + if (uriPath.contains(prefix)) { + // make sure path starts with storage + val pathInUri = "/storage${uriPath.substring( + uriPath.indexOf(prefix) + prefix.length, + )}" + if (fileExists(pathInUri)) { + return pathInUri + } + } + } + return null +} + +private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority +} + +private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority +} + +private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority +} + +private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority +} diff --git a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java index fc6947f172..3ecd62ea3d 100644 --- a/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java +++ b/app/src/main/java/com/amaze/filemanager/ui/activities/MainActivity.java @@ -106,6 +106,7 @@ import com.amaze.filemanager.filesystem.PasteHelper; import com.amaze.filemanager.filesystem.RootHelper; import com.amaze.filemanager.filesystem.files.FileUtils; +import com.amaze.filemanager.filesystem.files.UriUtilsKt; import com.amaze.filemanager.filesystem.ftp.NetCopyClientConnectionPool; import com.amaze.filemanager.filesystem.ftp.NetCopyConnectionInfo; import com.amaze.filemanager.filesystem.ssh.SshClientUtils; @@ -637,6 +638,25 @@ private void checkForExternalIntent(Intent intent) { * http://teamamaze.xyz/open_file?path=path-to-file */ path = Utils.sanitizeInput(uri.getQueryParameter("path")); + } else if (ContentResolver.SCHEME_CONTENT.equals(uri.getScheme()) + || ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { + File fromUri = null; + try { + String path = UriUtilsKt.fromUri(uri, this); + if (path != null) { + fromUri = new File(path); + } + } catch (Exception ignored) { + } + + if (fromUri != null && fromUri.getParent() != null) { + path = Utils.sanitizeInput(fromUri.getParent()); + scrollToFileName = Utils.sanitizeInput(fromUri.getName()); + } else { + Toast.makeText(this, getString(R.string.error_file_not_found), Toast.LENGTH_LONG).show(); + path = null; + scrollToFileName = null; + } } else { LOG.warn(getString(R.string.error_cannot_find_way_open)); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d3f97ae2a..ca493642ba 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -794,6 +794,7 @@ You only need to do this once, until the next time you select a new location for Share logs Share captured logs via email / telegram Open with Amaze + Show in Amaze Confirmation Are you sure you want to open following file?\n\nName:\n%s\n\nLocation:\n%s\n\nSize:\n%s\n\nMD5:\n%s\n\nSHA256:\n%s\n\n Per Google Play policy mandates, apps are not allowed to update itself on its own. Please update app from Google Play.