diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index cbed845c9..18fcb63ea 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -170,7 +170,7 @@ class MediaStoreImageProvider : ImageProvider() { return future } - if (requireAccessPermission(path)) { + if (requireAccessPermission(context, path)) { // if the file is on SD card, calling the content resolver `delete()` removes the entry from the Media Store // but it doesn't delete the file, even if the app has the permission try { diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt index 4fd2eff21..c78e0efee 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/PermissionManager.kt @@ -127,7 +127,7 @@ object PermissionManager { val accessibleDirs = HashSet(getGrantedDirs(context)) // from Android R, we no longer have access permission by default on primary volume if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { - accessibleDirs.add(StorageUtils.primaryVolumePath) + accessibleDirs.add(StorageUtils.getPrimaryVolumePath(context)) } Log.d(LOG_TAG, "getAccessibleDirs accessibleDirs=$accessibleDirs") return accessibleDirs diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt index d120dc1c2..ab0ff3ee3 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/StorageUtils.kt @@ -6,7 +6,6 @@ import android.content.Context import android.media.MediaMetadataRetriever 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 @@ -36,13 +35,13 @@ object StorageUtils { // primary volume path, with trailing "/" private var mPrimaryVolumePath: String? = null - val primaryVolumePath: String - get() { - if (mPrimaryVolumePath == null) { - mPrimaryVolumePath = findPrimaryVolumePath() - } - return mPrimaryVolumePath!! + @JvmStatic + fun getPrimaryVolumePath(context: Context): String { + if (mPrimaryVolumePath == null) { + mPrimaryVolumePath = findPrimaryVolumePath(context) } + return mPrimaryVolumePath!! + } @JvmStatic fun getVolumePaths(context: Context): Array { @@ -76,8 +75,17 @@ object StorageUtils { return pathSteps.iterator() } - private fun findPrimaryVolumePath(): String { - return ensureTrailingSeparator(Environment.getExternalStorageDirectory().absolutePath) + private fun findPrimaryVolumePath(context: Context): String? { + // we want: + // /storage/emulated/0/ + // `Environment.getExternalStorageDirectory()` (deprecated) yields: + // /storage/emulated/0 + // `context.getExternalFilesDir(null)` yields: + // /storage/emulated/0/Android/data/{package_name}/files + return context.getExternalFilesDir(null)?.let { + val appSpecificPath = it.absolutePath + return appSpecificPath.substring(0, appSpecificPath.indexOf("Android/data")) + } } @SuppressLint("ObsoleteSdkInt") @@ -126,10 +134,10 @@ object StorageUtils { } } else { // Device has emulated storage; external storage paths should have userId burned into them. - val path = Environment.getExternalStorageDirectory().absolutePath - val rawUserId = path.split(File.separator).lastOrNull()?.takeIf { TextUtils.isDigitsOnly(it) } ?: "" - // /storage/emulated/0[1,2,...] - if (TextUtils.isEmpty(rawUserId)) { + // /storage/emulated/[0,1,2,...]/ + val path = getPrimaryVolumePath(context) + val rawUserId = path.split(File.separator).lastOrNull(String::isNotEmpty)?.takeIf { TextUtils.isDigitsOnly(it) } ?: "" + if (rawUserId.isEmpty()) { paths.add(rawEmulatedStorageTarget) } else { paths.add(rawEmulatedStorageTarget + File.separator + rawUserId) @@ -145,30 +153,29 @@ object StorageUtils { } // return physicalPaths based on phone model - private val physicalPaths: Array - @SuppressLint("SdCardPath") - get() = arrayOf( - "/storage/sdcard0", - "/storage/sdcard1", //Motorola Xoom - "/storage/extsdcard", //Samsung SGS3 - "/storage/sdcard0/external_sdcard", //User request - "/mnt/extsdcard", - "/mnt/sdcard/external_sd", //Samsung galaxy family - "/mnt/external_sd", - "/mnt/media_rw/sdcard1", //4.4.2 on CyanogenMod S3 - "/removable/microsd", //Asus transformer prime - "/mnt/emmc", - "/storage/external_SD", //LG - "/storage/ext_sd", //HTC One Max - "/storage/removable/sdcard1", //Sony Xperia Z1 - "/data/sdext", - "/data/sdext2", - "/data/sdext3", - "/data/sdext4", - "/sdcard1", //Sony Xperia Z - "/sdcard2", //HTC One M8s - "/storage/microsd" //ASUS ZenFone 2 - ) + @SuppressLint("SdCardPath") + private val physicalPaths = arrayOf( + "/storage/sdcard0", + "/storage/sdcard1", //Motorola Xoom + "/storage/extsdcard", //Samsung SGS3 + "/storage/sdcard0/external_sdcard", //User request + "/mnt/extsdcard", + "/mnt/sdcard/external_sd", //Samsung galaxy family + "/mnt/external_sd", + "/mnt/media_rw/sdcard1", //4.4.2 on CyanogenMod S3 + "/removable/microsd", //Asus transformer prime + "/mnt/emmc", + "/storage/external_SD", //LG + "/storage/ext_sd", //HTC One Max + "/storage/removable/sdcard1", //Sony Xperia Z1 + "/data/sdext", + "/data/sdext2", + "/data/sdext3", + "/data/sdext4", + "/sdcard1", //Sony Xperia Z + "/sdcard2", //HTC One M8s + "/storage/microsd" //ASUS ZenFone 2 + ) /** * Volume tree URIs @@ -194,7 +201,7 @@ object StorageUtils { private fun getVolumePathFromTreeUriUuid(context: Context, uuid: String): String? { if (uuid == "primary") { - return primaryVolumePath + return getPrimaryVolumePath(context) } val sm = context.getSystemService(StorageManager::class.java) if (sm != null) { @@ -258,7 +265,7 @@ object StorageUtils { @JvmStatic fun getDocumentFile(context: Context, anyPath: String, mediaUri: Uri): DocumentFileCompat? { try { - if (requireAccessPermission(anyPath)) { + if (requireAccessPermission(context, anyPath)) { // need a document URI (not a media content URI) to open a `DocumentFile` output stream if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isMediaStoreContentUri(mediaUri)) { // cleanest API to get it @@ -283,7 +290,7 @@ object StorageUtils { @JvmStatic fun createDirectoryIfAbsent(context: Context, dirPath: String): DocumentFileCompat? { val cleanDirPath = ensureTrailingSeparator(dirPath) - return if (requireAccessPermission(cleanDirPath)) { + return if (requireAccessPermission(context, cleanDirPath)) { val grantedDir = getGrantedDirForPath(context, cleanDirPath) ?: return null val rootTreeUri = convertDirPathToTreeUri(context, grantedDir) ?: return null var parentFile: DocumentFileCompat? = DocumentFileCompat.fromTreeUri(context, rootTreeUri) ?: return null @@ -357,12 +364,12 @@ object StorageUtils { */ @JvmStatic - fun requireAccessPermission(anyPath: String): Boolean { + fun requireAccessPermission(context: Context, anyPath: String): Boolean { // on Android R, we should always require access permission, even on primary volume if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { return true } - val onPrimaryVolume = anyPath.startsWith(primaryVolumePath) + val onPrimaryVolume = anyPath.startsWith(getPrimaryVolumePath(context)) return !onPrimaryVolume }