Improve PathUtils.getFullPathFromTreeUri()
This commit is contained in:
parent
ae0e23acc9
commit
4d6b043a8a
@ -100,42 +100,6 @@ object PathUtils {
|
|||||||
return "Android/data/${context.packageName}/"
|
return "Android/data/${context.packageName}/"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExternalStoragePath(context: Context, name: String): String? {
|
|
||||||
for (dir in ContextCompat.getExternalFilesDirs(context, null)) {
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "External dir: $dir")
|
|
||||||
if (Environment.isExternalStorageRemovable(dir)) {
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "isExternalStorageRemovable")
|
|
||||||
val path = dir.path.split("/Android")[0]
|
|
||||||
if (File(path).name == name) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "getExternalFilesDirs failed")
|
|
||||||
// Don't risk to be killed by SELinux on newer Android versions
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
|
||||||
try {
|
|
||||||
val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() }
|
|
||||||
process.inputStream.readBytes().decodeToString().split("\n").forEach { line ->
|
|
||||||
if (line.startsWith("/dev/block/vold")) {
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "mount: $line")
|
|
||||||
val fields = line.split(" ")
|
|
||||||
if (fields.size >= 3) {
|
|
||||||
val path = fields[2]
|
|
||||||
if (File(path).name == name) {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "mount processing failed")
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getExternalStoragesPaths(context: Context): List<String> {
|
private fun getExternalStoragesPaths(context: Context): List<String> {
|
||||||
val externalPaths: MutableList<String> = ArrayList()
|
val externalPaths: MutableList<String> = ArrayList()
|
||||||
ContextCompat.getExternalFilesDirs(context, null).forEach {
|
ContextCompat.getExternalFilesDirs(context, null).forEach {
|
||||||
@ -156,21 +120,17 @@ object PathUtils {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val PRIMARY_VOLUME_NAME = "primary"
|
|
||||||
fun getFullPathFromTreeUri(treeUri: Uri, context: Context): String? {
|
fun getFullPathFromTreeUri(treeUri: Uri, context: Context): String? {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "treeUri: $treeUri")
|
||||||
if ("content".equals(treeUri.scheme, ignoreCase = true)) {
|
if ("content".equals(treeUri.scheme, ignoreCase = true)) {
|
||||||
val vId = getVolumeIdFromTreeUri(treeUri)
|
val docId = DocumentsContract.getTreeDocumentId(treeUri)
|
||||||
Log.d(PATH_RESOLVER_TAG, "Volume Id: $vId")
|
Log.d(PATH_RESOLVER_TAG, "Document Id: $docId")
|
||||||
var volumePath = getVolumePath(vId ?: return null, context)
|
val split: Array<String?> = docId.split(":").toTypedArray()
|
||||||
|
val volumeId = if (split.isNotEmpty()) split[0] else null
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "Volume Id: $volumeId")
|
||||||
|
val volumePath = getVolumePath(volumeId ?: return null, context)
|
||||||
Log.d(PATH_RESOLVER_TAG, "Volume Path: $volumePath")
|
Log.d(PATH_RESOLVER_TAG, "Volume Path: $volumePath")
|
||||||
if (volumePath == null) {
|
val documentPath = if (split.size >= 2 && split[1] != null) split[1]!! else File.separator
|
||||||
volumePath = if (vId == "primary") {
|
|
||||||
Environment.getExternalStorageDirectory().path
|
|
||||||
} else {
|
|
||||||
getExternalStoragePath(context, vId) ?: "/storage/$vId"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val documentPath = getDocumentPathFromTreeUri(treeUri)!!
|
|
||||||
Log.d(PATH_RESOLVER_TAG, "Document Path: $documentPath")
|
Log.d(PATH_RESOLVER_TAG, "Document Path: $documentPath")
|
||||||
return if (documentPath.isNotEmpty()) {
|
return if (documentPath.isNotEmpty()) {
|
||||||
pathJoin(volumePath!!, documentPath)
|
pathJoin(volumePath!!, documentPath)
|
||||||
@ -181,39 +141,92 @@ object PathUtils {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val PRIMARY_VOLUME_NAME = "primary"
|
||||||
private fun getVolumePath(volumeId: String, context: Context): String? {
|
private fun getVolumePath(volumeId: String, context: Context): String? {
|
||||||
return try {
|
if (volumeId == PRIMARY_VOLUME_NAME) {
|
||||||
val mStorageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
// easy case
|
||||||
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
|
return Environment.getExternalStorageDirectory().path
|
||||||
val getVolumeList = mStorageManager.javaClass.getMethod("getVolumeList")
|
|
||||||
val getUuid = storageVolumeClazz.getMethod("getUuid")
|
|
||||||
val getPath = storageVolumeClazz.getMethod("getPath")
|
|
||||||
val isPrimary = storageVolumeClazz.getMethod("isPrimary")
|
|
||||||
val result = getVolumeList.invoke(mStorageManager)
|
|
||||||
val length = java.lang.reflect.Array.getLength(result!!)
|
|
||||||
for (i in 0 until length) {
|
|
||||||
val storageVolumeElement = java.lang.reflect.Array.get(result, i)
|
|
||||||
val uuid = getUuid.invoke(storageVolumeElement)
|
|
||||||
val primary = isPrimary.invoke(storageVolumeElement) as Boolean
|
|
||||||
if (primary && PRIMARY_VOLUME_NAME == volumeId) return getPath.invoke(storageVolumeElement) as String
|
|
||||||
if (uuid == volumeId) return getPath.invoke(storageVolumeElement) as String
|
|
||||||
}
|
|
||||||
null
|
|
||||||
} catch (ex: Exception) {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
// external storage
|
||||||
|
// First strategy: StorageManager.getStorageVolumes()
|
||||||
private fun getVolumeIdFromTreeUri(treeUri: Uri): String? {
|
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
|
||||||
val docId = DocumentsContract.getTreeDocumentId(treeUri)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||||
val split = docId.split(":").toTypedArray()
|
// API is public on Android 11 and higher
|
||||||
return if (split.isNotEmpty()) split[0] else null
|
storageManager.storageVolumes.forEach { storage ->
|
||||||
}
|
Log.d(PATH_RESOLVER_TAG, "StorageVolume: ${storage.uuid} ${storage.directory}")
|
||||||
|
if (volumeId.contentEquals(storage.uuid, true)) {
|
||||||
private fun getDocumentPathFromTreeUri(treeUri: Uri): String? {
|
storage.directory?.let {
|
||||||
val docId = DocumentsContract.getTreeDocumentId(treeUri)
|
return it.absolutePath
|
||||||
val split: Array<String?> = docId.split(":").toTypedArray()
|
}
|
||||||
return if (split.size >= 2 && split[1] != null) split[1] else File.separator
|
}
|
||||||
|
}
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "StorageManager failed")
|
||||||
|
} else {
|
||||||
|
// Before Android 11, we try reflection
|
||||||
|
try {
|
||||||
|
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
|
||||||
|
val getVolumeList = storageManager.javaClass.getMethod("getVolumeList")
|
||||||
|
val getUuid = storageVolumeClazz.getMethod("getUuid")
|
||||||
|
val getPath = storageVolumeClazz.getMethod("getPath")
|
||||||
|
val result = getVolumeList.invoke(storageManager)
|
||||||
|
val length = java.lang.reflect.Array.getLength(result!!)
|
||||||
|
for (i in 0 until length) {
|
||||||
|
val storageVolumeElement = java.lang.reflect.Array.get(result, i)
|
||||||
|
val uuid = getUuid.invoke(storageVolumeElement)
|
||||||
|
if (uuid == volumeId) return getPath.invoke(storageVolumeElement) as String
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "StorageManager reflection failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Second strategy: Context.getExternalFilesDirs()
|
||||||
|
for (dir in ContextCompat.getExternalFilesDirs(context, null)) {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "External dir: $dir")
|
||||||
|
if (Environment.isExternalStorageRemovable(dir)) {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "isExternalStorageRemovable")
|
||||||
|
val path = dir.path.split("/Android")[0]
|
||||||
|
if (File(path).name == volumeId) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "getExternalFilesDirs failed")
|
||||||
|
// Third strategy: parsing the output of mount
|
||||||
|
// Don't risk to be killed by SELinux on newer Android versions
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
|
||||||
|
try {
|
||||||
|
val process = ProcessBuilder("mount").redirectErrorStream(true).start().apply { waitFor() }
|
||||||
|
process.inputStream.readBytes().decodeToString().split("\n").forEach { line ->
|
||||||
|
if (line.startsWith("/dev/block/vold")) {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "mount: $line")
|
||||||
|
val fields = line.split(" ")
|
||||||
|
if (fields.size >= 3) {
|
||||||
|
val path = fields[2]
|
||||||
|
if (File(path).name == volumeId) {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "mount processing failed")
|
||||||
|
}
|
||||||
|
// Fourth strategy: guessing
|
||||||
|
val directories = listOf("/storage/", "/mnt/media_rw/").map { File(it + volumeId) }
|
||||||
|
listOf(File::canWrite, File::canRead, File::isDirectory).forEach { check ->
|
||||||
|
directories.find { dir ->
|
||||||
|
if (check(dir)) {
|
||||||
|
Log.d(PATH_RESOLVER_TAG, "$dir: ${check.name}")
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}?.let { return it.path }
|
||||||
|
}
|
||||||
|
// Fifth strategy: fail
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recursiveRemoveDirectory(rootDirectory: File): Boolean {
|
fun recursiveRemoveDirectory(rootDirectory: File): Boolean {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user