From 777c0fff4b6ab822593cf934e656ecbd94a42cfe Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Wed, 2 Oct 2024 18:55:42 +0530 Subject: [PATCH] Fixed: In Samsung tablet of Android 14, downloaded files can not be opened via file picker or deep linking. * When we open a file from different browsers, they provide a URI through their own file provider, and the content resolver cannot retrieve the file path for these types of URIs. To fix this issue, we have introduced a fallback method that returns the exact path of the file located in the Downloads folder. * Another issue we encountered on tablets is that the URIs are different from those on regular mobile devices, and our `documentProviderContentQuery` method could not return the path for these types of URIs from the Downloads folder. To fix this issue, we used our fallback method to retrieve the file path for these URIs. --- .../files/FileUtilsInstrumentationTest.kt | 9 +++ .../kiwixmobile/core/utils/files/FileUtils.kt | 81 ++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt index 9c7013878..9c0a42d5c 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/utils/files/FileUtilsInstrumentationTest.kt @@ -406,6 +406,15 @@ class FileUtilsInstrumentationTest { Uri.parse( "${downloadUriPrefix}0" ) + ), + DummyUrlData( + null, + null, + null, + null, + Uri.parse( + "${downloadDocumentUriPrefix}msf%3A1000000057" + ) ) ) context?.let { context -> diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt index 977e96535..b95bf9d42 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/utils/files/FileUtils.kt @@ -21,10 +21,13 @@ import android.annotation.SuppressLint import android.content.ContentUris import android.content.Context import android.content.res.AssetFileDescriptor +import android.database.Cursor import android.net.Uri import android.os.Build import android.os.Environment +import android.os.storage.StorageManager import android.provider.DocumentsContract +import android.provider.MediaStore import android.webkit.URLUtil import androidx.core.content.ContextCompat import kotlinx.coroutines.CoroutineScope @@ -143,7 +146,7 @@ object FileUtils { } } else if (uri.scheme != null) { if ("content".equals(uri.scheme, ignoreCase = true)) { - return contentQuery(context, uri) + return getFilePathOfContentUri(context, uri) } else if ("file".equals(uri.scheme, ignoreCase = true)) { return uri.path } @@ -154,6 +157,73 @@ object FileUtils { return null } + private fun getFilePathOfContentUri(context: Context, uri: Uri): String? { + val filePath = contentQuery(context, uri) + return if (!filePath.isNullOrEmpty()) { + filePath + } else { + // Fallback method to get the actual path of the URI. This will be called + // when contentQuery returns null, especially in cases where the user directly clicks + // on the downloaded file within browsers (since different browsers provide URIs with their + // own file providers, the content resolver cannot directly retrieve paths for those URIs). + val fileName = getFileNameFromUri(context, uri) + getFilePathFromFileName(context, fileName) + } + } + + private fun getFilePathFromFileName(context: Context, fileName: String?): String? { + var filePath: String? = null + getStorageVolumesList(context).forEach { volume -> + val file = File("$volume/Download/$fileName") + if (file.isFileExist()) { + filePath = file.path + } + } + return filePath + } + + private fun getStorageVolumesList(context: Context): HashSet { + val storageVolumes = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager + val storageVolumesList = HashSet() + storageVolumes.storageVolumes.filterNotNull().forEach { + if (it.isPrimary) { + storageVolumesList.add("${Environment.getExternalStorageDirectory()}/") + } else { + val externalStorageName = it.uuid?.let { uuid -> + "/$uuid/" + } ?: kotlin.run { + "/${it.getDescription(context)}/" + } + storageVolumesList.add(externalStorageName) + } + } + return storageVolumesList + } + + private fun getFileNameFromUri(context: Context, uri: Uri?): String? { + if (uri == null) return null + var cursor: Cursor? = null + val projection = arrayOf( + MediaStore.MediaColumns.DISPLAY_NAME + ) + return try { + cursor = context.contentResolver.query( + uri, projection, null, null, + null + ) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME) + cursor.getString(index) + } else { + null + } + } catch (ignore: Exception) { + null + } finally { + cursor?.close() + } + } + fun documentProviderContentQuery( context: Context, uri: Uri, @@ -183,7 +253,14 @@ object FileUtils { actualDocumentId, contentUriPrefixes, documentsContractWrapper - ) + ) ?: kotlin.run { + // Fallback method to get the actual path of the URI. This will be called + // when queryForActualPath returns null, especially in cases where the user directly opens + // the file from the file manager in the downloads folder, and the URI contains a different + // document ID (particularly on tablets). See https://github.com/kiwix/kiwix-android/issues/4008 + val fileName = getFileNameFromUri(context, uri) + getFilePathFromFileName(context, fileName) + } } private fun queryForActualPath(