DroidFS/app/src/main/java/sushi/hardcore/droidfs/util/PathUtils.kt

245 lines
9.3 KiB
Kotlin
Raw Normal View History

2020-11-03 17:22:09 +01:00
package sushi.hardcore.droidfs.util
2022-03-06 14:45:52 +01:00
import android.content.ActivityNotFoundException
2020-11-03 17:22:09 +01:00
import android.content.Context
import android.net.Uri
import android.os.Build
2023-02-01 19:08:14 +01:00
import android.os.Environment
2020-11-03 17:22:09 +01:00
import android.os.storage.StorageManager
import android.provider.DocumentsContract
import android.provider.OpenableColumns
2023-02-01 19:08:14 +01:00
import android.util.Log
2022-03-06 14:45:52 +01:00
import androidx.activity.result.ActivityResultLauncher
2020-11-03 17:22:09 +01:00
import androidx.core.content.ContextCompat
2022-03-06 14:45:52 +01:00
import sushi.hardcore.droidfs.R
2023-02-28 22:50:59 +01:00
import sushi.hardcore.droidfs.Theme
2022-03-06 14:45:52 +01:00
import sushi.hardcore.droidfs.widgets.CustomAlertDialogBuilder
2020-11-03 17:22:09 +01:00
import java.io.File
import java.text.DecimalFormat
import kotlin.math.log10
2022-06-18 21:13:16 +02:00
import kotlin.math.max
2020-11-03 17:22:09 +01:00
import kotlin.math.pow
object PathUtils {
2022-06-18 21:13:16 +02:00
const val SEPARATOR = '/'
2023-02-01 19:08:14 +01:00
const val PATH_RESOLVER_TAG = "PATH RESOLVER"
2022-06-18 21:13:16 +02:00
2020-11-03 17:22:09 +01:00
fun getParentPath(path: String): String {
2022-06-18 21:13:16 +02:00
val strippedPath = if (path.endsWith(SEPARATOR)) {
path.substring(0, max(1, path.length - 1))
2020-11-03 17:22:09 +01:00
} else {
2022-06-18 21:13:16 +02:00
path
}
return if (strippedPath.count { it == SEPARATOR } <= 1) {
SEPARATOR.toString()
} else {
strippedPath.substring(0, strippedPath.lastIndexOf(SEPARATOR))
2020-11-03 17:22:09 +01:00
}
}
fun pathJoin(vararg strings: String): String {
val result = StringBuilder()
for (element in strings) {
if (element.isNotEmpty()) {
2022-06-18 21:13:16 +02:00
if (!element.startsWith(SEPARATOR) && result.last() != SEPARATOR) {
result.append(SEPARATOR)
2020-11-03 17:22:09 +01:00
}
2022-06-18 21:13:16 +02:00
result.append(element)
2020-11-03 17:22:09 +01:00
}
}
2022-06-18 21:13:16 +02:00
return result.toString()
2020-11-03 17:22:09 +01:00
}
fun getRelativePath(parentPath: String, childPath: String): String {
2022-06-18 21:13:16 +02:00
return childPath.substring(parentPath.length + if (parentPath.endsWith(SEPARATOR) || childPath.length == parentPath.length) {
0
} else {
1
})
2020-11-03 17:22:09 +01:00
}
fun isChildOf(childPath: String, parentPath: String): Boolean {
if (parentPath.length > childPath.length){
return false
}
return childPath.substring(0, parentPath.length) == parentPath
}
2020-11-03 17:22:09 +01:00
fun getFilenameFromURI(context: Context, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
}
}
if (result == null) {
result = uri.path
result?.let {
2022-06-18 21:13:16 +02:00
val cut = it.lastIndexOf(SEPARATOR)
2020-11-03 17:22:09 +01:00
if (cut != -1) {
result = it.substring(cut + 1)
}
}
}
return result
}
2022-03-23 16:35:13 +01:00
private val units = arrayOf("B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
2020-11-03 17:22:09 +01:00
fun formatSize(size: Long): String {
if (size <= 0) {
return "0 B"
}
2021-12-20 20:24:07 +01:00
val digitGroups = (log10(size.toDouble()) / log10(1000.0)).toInt()
return DecimalFormat("#,##0.#").format(size / 1000.0.pow(digitGroups.toDouble())
2020-11-03 17:22:09 +01:00
) + " " + units[digitGroups]
}
fun getPackageDataFolder(context: Context): String {
return "Android/data/${context.packageName}/"
2020-11-03 17:22:09 +01:00
}
2023-02-01 19:08:14 +01:00
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
}
2023-02-01 19:08:14 +01:00
}
}
}
} catch (e: Exception) {
e.printStackTrace()
2023-02-01 19:08:14 +01:00
}
Log.d(PATH_RESOLVER_TAG, "mount processing failed")
2023-02-01 19:08:14 +01:00
}
return null
}
private fun getExternalStoragesPaths(context: Context): List<String> {
2020-11-03 17:22:09 +01:00
val externalPaths: MutableList<String> = ArrayList()
ContextCompat.getExternalFilesDirs(context, null).forEach {
2023-02-01 19:08:14 +01:00
if (Environment.isExternalStorageRemovable(it)) {
val rootPath = it.path.substring(0, it.path.indexOf(getPackageDataFolder(context)+"files"))
2020-11-03 17:22:09 +01:00
externalPaths.add(rootPath)
}
}
return externalPaths
}
fun isPathOnExternalStorage(path: String, context: Context): Boolean {
2023-02-01 19:08:14 +01:00
getExternalStoragesPaths(context).forEach {
2020-11-03 17:22:09 +01:00
if (path.startsWith(it)){
return true
}
}
return false
}
private const val PRIMARY_VOLUME_NAME = "primary"
2023-02-01 19:08:14 +01:00
fun getFullPathFromTreeUri(treeUri: Uri, context: Context): String? {
2020-11-03 17:22:09 +01:00
if ("content".equals(treeUri.scheme, ignoreCase = true)) {
val vId = getVolumeIdFromTreeUri(treeUri)
2023-02-01 19:08:14 +01:00
Log.d(PATH_RESOLVER_TAG, "Volume Id: $vId")
var volumePath = getVolumePath(vId ?: return null, context)
Log.d(PATH_RESOLVER_TAG, "Volume Path: $volumePath")
if (volumePath == null) {
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")
2020-11-03 17:22:09 +01:00
return if (documentPath.isNotEmpty()) {
2023-02-01 19:08:14 +01:00
pathJoin(volumePath!!, documentPath)
2020-11-03 17:22:09 +01:00
} else volumePath
} else if ("file".equals(treeUri.scheme, ignoreCase = true)) {
return treeUri.path
}
return null
}
2023-02-01 19:08:14 +01:00
private fun getVolumePath(volumeId: String, context: Context): String? {
2020-11-03 17:22:09 +01:00
return try {
val mStorageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume")
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
}
}
private fun getVolumeIdFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split = docId.split(":").toTypedArray()
return if (split.isNotEmpty()) split[0] else null
}
private fun getDocumentPathFromTreeUri(treeUri: Uri): String? {
val docId = DocumentsContract.getTreeDocumentId(treeUri)
val split: Array<String?> = docId.split(":").toTypedArray()
return if (split.size >= 2 && split[1] != null) split[1] else File.separator
}
fun recursiveRemoveDirectory(rootDirectory: File): Boolean {
rootDirectory.listFiles()?.forEach { item ->
if (item.isDirectory) {
if (!recursiveRemoveDirectory(item)){
return false
}
} else {
if (!item.delete()) {
return false
}
}
}
return rootDirectory.delete()
}
2022-03-06 14:45:52 +01:00
2023-02-28 22:50:59 +01:00
fun safePickDirectory(directoryPicker: ActivityResultLauncher<Uri?>, context: Context, theme: Theme) {
2022-03-06 14:45:52 +01:00
try {
directoryPicker.launch(null)
} catch (e: ActivityNotFoundException) {
2023-02-28 22:50:59 +01:00
CustomAlertDialogBuilder(context, theme)
2022-03-06 14:45:52 +01:00
.setTitle(R.string.error)
.setMessage(R.string.open_tree_failed)
.setPositiveButton(R.string.ok, null)
.show()
}
}
2020-11-03 17:22:09 +01:00
}