Improved USB and External Hard Drive Path Retrieval for Android 10 and Above.

* The getExternalFilesDirs method only returns the USB path for devices running Android 9 (API 28) and below.
* On Android 10 (API 29) and above, this method returns null when accessing external storage like USB sticks.
* To work around this limitation, we previously appended the /mnt/media_rw/ path manually. While this worked in most cases, it was unreliable across different devices, as the mounted path could vary.
* To ensure accurate retrieval of external storage paths, we have switched to the StorageService API.
* This API, introduced in Android 11 (API 30), directly provides the actual mount paths for all external storage devices, including SD cards, USB sticks, and external hard drives.
* For Android 9 and below, getExternalFilesDirs continues to work as expected, so we use it where applicable.
* This improvement ensures that USB and SD card paths are retrieved correctly on modern Android devices.
* Added debugging logs to capture errors when opening a file in the reader.
* These logs will help us diagnose issues when users share a diagnostic report.
This commit is contained in:
MohitMaliFtechiz 2025-02-10 17:39:43 +05:30 committed by Kelson
parent e6f378e165
commit 6cdc8f23d4
3 changed files with 94 additions and 15 deletions

View File

@ -27,6 +27,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -90,6 +91,7 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_DOWN import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_DOWN
import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_UP import org.kiwix.kiwixmobile.core.utils.SimpleRecyclerViewScrollListener.Companion.SCROLL_UP
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower
import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog
import org.kiwix.kiwixmobile.core.utils.files.FileUtils import org.kiwix.kiwixmobile.core.utils.files.FileUtils
@ -456,11 +458,17 @@ class LocalLibraryFragment : BaseFragment(), CopyMoveFileHandler.FileCopyMoveCal
requireActivity().applicationContext, uri requireActivity().applicationContext, uri
) )
if (filePath == null || !File(filePath).isFileExist()) { if (filePath == null || !File(filePath).isFileExist()) {
Log.e(
TAG_KIWIX,
"The Selected ZIM file not found in the storage. File Uri = $uri\n" +
"Retrieved Path = $filePath"
)
activity.toast(getString(string.error_file_not_found, "$uri")) activity.toast(getString(string.error_file_not_found, "$uri"))
return null return null
} }
val file = File(filePath) val file = File(filePath)
return if (!FileUtils.isValidZimFile(file.path)) { return if (!FileUtils.isValidZimFile(file.path)) {
Log.e(TAG_KIWIX, "Selected ZIM file is not a valid ZIM file. File path = ${file.path}")
activity.toast(getString(string.error_file_invalid, file.path)) activity.toast(getString(string.error_file_invalid, file.path))
null null
} else { } else {

View File

@ -88,8 +88,14 @@ class ZimFileReader constructor(
null null
} }
} catch (ignore: JNIKiwixException) { } catch (ignore: JNIKiwixException) {
Log.e(
TAG,
"Error in creating ZimFileReader," +
" there is an JNI exception happens: $ignore"
)
null null
} catch (ignore: Exception) { // for handing the error, if any zim file is corrupted } catch (ignore: Exception) { // for handing the error, if any zim file is corrupted
Log.e(TAG, "Error in creating ZimFileReader: $ignore")
null null
} }
} }

View File

@ -27,9 +27,11 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.Environment.DIRECTORY_DOWNLOADS import android.os.Environment.DIRECTORY_DOWNLOADS
import android.os.storage.StorageManager import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.annotation.RequiresApi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -43,6 +45,7 @@ import org.kiwix.kiwixmobile.core.extensions.deleteFile
import org.kiwix.kiwixmobile.core.extensions.isFileExist import org.kiwix.kiwixmobile.core.extensions.isFileExist
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.TAG_KIWIX
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@ -120,6 +123,7 @@ object FileUtils {
context: Context, context: Context,
uri: Uri uri: Uri
): String? { ): String? {
Log.e(TAG_KIWIX, "Trying to get the ZIM file path for Uri = $uri")
if (DocumentsContract.isDocumentUri(context, uri)) { if (DocumentsContract.isDocumentUri(context, uri)) {
if ("com.android.externalstorage.documents" == uri.authority) { if ("com.android.externalstorage.documents" == uri.authority) {
val documentId = DocumentsContract.getDocumentId(uri) val documentId = DocumentsContract.getDocumentId(uri)
@ -129,10 +133,10 @@ object FileUtils {
return "${Environment.getExternalStorageDirectory()}/${documentId[1]}" return "${Environment.getExternalStorageDirectory()}/${documentId[1]}"
} }
return try { return try {
var sdCardOrUsbMainPath = getSdCardOrUSBMainPath(context, documentId[0]) val sdCardOrUsbMainPath = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
if (sdCardOrUsbMainPath == null) { getSDCardOrUSBMainPathForAndroid10AndAbove(context, documentId[0])
// USB sticks are mounted under the `/mnt/media_rw` directory. } else {
sdCardOrUsbMainPath = "/mnt/media_rw/${documentId[0]}" getSdCardOrUSBMainPathForAndroid9AndBelow(context, documentId[0])
} }
"$sdCardOrUsbMainPath/${documentId[1]}" "$sdCardOrUsbMainPath/${documentId[1]}"
} catch (ignore: Exception) { } catch (ignore: Exception) {
@ -157,6 +161,24 @@ object FileUtils {
return null return null
} }
/**
* Retrieves the main storage path for a given external storage device (SD card, USB stick, or external hard drive).
*
* @param context The application context.
* @param storageName The name of the storage (e.g., "sdcard" or "usbstick").
* @return The main storage path for the given storage name, or `null` if not found.
*
* This method leverages `getStorageVolumesList`, which directly provides the storage path
* for USB and other mounted devices on Android 10 (API 29) and above.
*
* For Android 9 (API 28) and below, refer to `getSdCardOrUSBMainPath` for retrieving the USB path.
*
* @see getSdCardOrUSBMainPathForAndroid9AndBelow
*/
@RequiresApi(Build.VERSION_CODES.Q)
private fun getSDCardOrUSBMainPathForAndroid10AndAbove(context: Context, storageName: String) =
getStorageVolumesList(context).firstOrNull { it.contains(storageName) }
/** /**
* Retrieves the file path from a given content URI. This method first attempts to get the path * Retrieves the file path from a given content URI. This method first attempts to get the path
* using the content resolver (via `contentQuery`). If that returns null or empty, it falls back * using the content resolver (via `contentQuery`). If that returns null or empty, it falls back
@ -196,24 +218,64 @@ object FileUtils {
return actualFilePath return actualFilePath
} }
/**
* Retrieves a list of storage volume paths available on the device.
*
* This method uses the `StorageManager` system service to obtain a list of storage volumes,
* including internal storage, SD cards, and USB devices. The method accounts for API differences:
* - On Android 11 (API 30) and above, it directly retrieves the storage path using `StorageVolume.directory`.
* - On Android 10 (API 29) and below, it constructs the storage path based on the volume's UUID or description.
*
* @param context The application context used to access system services.
* @return A `HashSet<String>` containing paths of available storage volumes.
*/
private fun getStorageVolumesList(context: Context): HashSet<String> { private fun getStorageVolumesList(context: Context): HashSet<String> {
val storageVolumes = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager val storageVolumes = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumesList = HashSet<String>() val storageVolumesList = HashSet<String>()
storageVolumes.storageVolumes.filterNotNull().forEach { storageVolumes.storageVolumes.filterNotNull().forEach {
if (it.isPrimary) { storageVolumesList.add(getStoragePath(context, it))
storageVolumesList.add("${Environment.getExternalStorageDirectory()}/")
} else {
val externalStorageName = it.uuid?.let { uuid ->
"/$uuid/"
} ?: kotlin.run {
"/${it.getDescription(context)}/"
}
storageVolumesList.add("/storage$externalStorageName")
}
} }
return storageVolumesList return storageVolumesList
} }
/**
* Determines the appropriate storage path for a given volume.
*
* This method retrieves the storage path based on the Android version:
* - **Android 11+ (API 30+)**: Directly retrieves the storage path from `StorageVolume.directory`.
* - **Primary storage (Internal storage)**: Returns the path using `Environment.getExternalStorageDirectory()`.
* - **External storage (SD card, USB, etc.)**:
* - If the volume has a UUID, constructs the path using `/storage/{UUID}/`.
* - If no UUID is available, falls back to using the volume description.
*
* @param context The application context used for accessing volume descriptions.
* @param volume The `StorageVolume` whose path needs to be determined.
* @return The storage path as a `String`.
*/
private fun getStoragePath(context: Context, volume: StorageVolume): String {
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
// On Android 11 (API 30) and above, return the storage path directly.
"${volume.directory?.path}"
}
volume.isPrimary -> {
// If this is the primary internal storage, return the default external storage directory.
"${Environment.getExternalStorageDirectory()}/"
}
else -> {
// If this is an external storage device, construct the path using UUID or description.
val externalStorageName = volume.uuid?.let { uuid ->
"/$uuid/"
} ?: "/${volume.getDescription(context)}/"
// On Android 10 and below, external storage devices are mounted under `/storage`.
"/storage$externalStorageName"
}
}
}
private fun getFileNameFromUri(context: Context, uri: Uri): String? { private fun getFileNameFromUri(context: Context, uri: Uri): String? {
var cursor: Cursor? = null var cursor: Cursor? = null
val projection = arrayOf( val projection = arrayOf(
@ -510,9 +572,12 @@ object FileUtils {
* @return The main storage path for the given storage name, * @return The main storage path for the given storage name,
* or null if the path is a USB path on Android 10 and above * or null if the path is a USB path on Android 10 and above
* (due to limitations in `context.getExternalFilesDirs("")` behavior). * (due to limitations in `context.getExternalFilesDirs("")` behavior).
*
* To get the SD card or USB main path for Android 10 and above refer to:
* @See getSDCardOrUSBMainPathForAndroid10AndAbove
*/ */
@JvmStatic @JvmStatic
fun getSdCardOrUSBMainPath(context: Context, storageName: String) = fun getSdCardOrUSBMainPathForAndroid9AndBelow(context: Context, storageName: String) =
context.getExternalFilesDirs("") context.getExternalFilesDirs("")
.firstOrNull { it.path.contains(storageName) } .firstOrNull { it.path.contains(storageName) }
?.path?.substringBefore(context.getString(R.string.android_directory_seperator)) ?.path?.substringBefore(context.getString(R.string.android_directory_seperator))