|
| 1 | +package com.osfans.trime.util |
| 2 | + |
| 3 | +import android.content.Context |
| 4 | +import android.net.Uri |
| 5 | +import android.os.Environment |
| 6 | +import android.os.storage.StorageManager |
| 7 | +import android.provider.DocumentsContract |
| 8 | +import timber.log.Timber |
| 9 | +import java.io.File |
| 10 | +import java.lang.reflect.Array |
| 11 | + |
| 12 | +/** |
| 13 | + * Utils for dealing with Storage Access Framework URIs. |
| 14 | + */ |
| 15 | +object UriUtils { |
| 16 | + |
| 17 | + private const val HOME_VOLUME_NAME = "home" |
| 18 | + private const val DOWNLOADS_VOLUME_NAME = "downloads" |
| 19 | + private const val PRIMARY_VOLUME_NAME = "primary" |
| 20 | + |
| 21 | + @JvmStatic |
| 22 | + fun Uri?.toFile(): File? { |
| 23 | + this ?: return null |
| 24 | + |
| 25 | + // Determine volume id, e.g. "home", "downloads", ... |
| 26 | + val docId = DocumentsContract.getDocumentId(this) |
| 27 | + val docIdSplit = docId.split(':') |
| 28 | + val volumeId = if (docIdSplit.isNotEmpty()) docIdSplit[0] else return null |
| 29 | + Timber.d("docId: $docId, volumeId: $volumeId") |
| 30 | + |
| 31 | + // Handle Uri referring to internal or external storage |
| 32 | + val volumePath = runCatching { |
| 33 | + when (volumeId) { |
| 34 | + HOME_VOLUME_NAME -> { |
| 35 | + Timber.v("Volume path: isHomeVolume") |
| 36 | + @Suppress("DEPRECATION") |
| 37 | + // Reading the environment var avoids hard coding the case of the "documents" folder. |
| 38 | + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS).absolutePath |
| 39 | + } |
| 40 | + DOWNLOADS_VOLUME_NAME -> { |
| 41 | + Timber.v("Volume path: isDownloadsVolume") |
| 42 | + @Suppress("DEPRECATION") |
| 43 | + // Reading the environment var avoids hard coding the case of the "downloads" folder. |
| 44 | + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath |
| 45 | + } |
| 46 | + else -> { |
| 47 | + val sm = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager |
| 48 | + val storageVolume = Class.forName("android.os.storage.StorageVolume") |
| 49 | + val getVolumeList = sm::class.java.getMethod("getVolumeList") |
| 50 | + val getUuid = storageVolume.getMethod("getUuid") |
| 51 | + val getPath = storageVolume.getMethod("getPath") |
| 52 | + val isPrimary = storageVolume.getMethod("isPrimary") |
| 53 | + |
| 54 | + val result = getVolumeList.invoke(sm) |
| 55 | + val resultSize = Array.getLength(result!!) |
| 56 | + |
| 57 | + var final: String? = null |
| 58 | + for (i in 0 until resultSize) { |
| 59 | + val e = Array.get(result, i) |
| 60 | + val uuid = getUuid.invoke(e) as? String |
| 61 | + val primary = isPrimary.invoke(e) as Boolean |
| 62 | + val isPrimaryVolume = primary && PRIMARY_VOLUME_NAME == volumeId |
| 63 | + val isExternalVolume = uuid == volumeId |
| 64 | + |
| 65 | + Timber.d( |
| 66 | + "Found volume: UUID = $uuid, " + |
| 67 | + "volumeId = $volumeId, " + |
| 68 | + "primary = $primary, " + |
| 69 | + "isPrimaryVolume = $isPrimaryVolume, " + |
| 70 | + "isExternalVolume = $isExternalVolume" |
| 71 | + ) |
| 72 | + |
| 73 | + if (isPrimaryVolume || isExternalVolume) { |
| 74 | + Timber.v("Volume path: isPrimaryVolume OR isExternalVolume") |
| 75 | + // Return path if the correct volume corresponding to volumeId was found |
| 76 | + final = getPath.invoke(e) as String |
| 77 | + break |
| 78 | + } |
| 79 | + } |
| 80 | + final ?: return File(File.separator) |
| 81 | + } |
| 82 | + } |
| 83 | + }.mapCatching { |
| 84 | + if (it.endsWith(File.separator)) |
| 85 | + it.substringBeforeLast(File.separator) |
| 86 | + else it |
| 87 | + }.getOrElse { |
| 88 | + Timber.w(it, "volumePath: EXCEPTION on parsing!") |
| 89 | + Timber.e("volumePath: parse failed, volumeId = $volumeId") |
| 90 | + return File(File.separator) |
| 91 | + } |
| 92 | + |
| 93 | + val docTreeId = DocumentsContract.getTreeDocumentId(this) |
| 94 | + val docTreeIdSplit = docTreeId.split(':') |
| 95 | + val documentPath = if (docTreeIdSplit.size >= 2) { |
| 96 | + docTreeIdSplit[1].let { |
| 97 | + if (it.endsWith(File.separator)) |
| 98 | + it.substringBeforeLast(File.separator) |
| 99 | + else it |
| 100 | + } |
| 101 | + } else { |
| 102 | + "" |
| 103 | + } |
| 104 | + Timber.d("docTreeId: $docTreeId, documentPath: $documentPath") |
| 105 | + |
| 106 | + return if (documentPath.isNotEmpty()) { |
| 107 | + if (documentPath.startsWith(File.separator)) { |
| 108 | + File("$volumePath$documentPath") |
| 109 | + } else { |
| 110 | + File("$volumePath${File.separator}$documentPath") |
| 111 | + } |
| 112 | + } else { |
| 113 | + File(volumePath) |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + @JvmStatic |
| 118 | + fun File?.toUri(): Uri? { |
| 119 | + return runCatching { |
| 120 | + val externalFileDirs = appContext.getExternalFilesDirs(null) |
| 121 | + .filter { it != appContext.getExternalFilesDir(null) } |
| 122 | + if (externalFileDirs.isEmpty()) { |
| 123 | + Timber.w("Could not determine app's private files directory on external storage") |
| 124 | + return null |
| 125 | + } |
| 126 | + |
| 127 | + val absPath = externalFileDirs[0].absolutePath |
| 128 | + val segments = absPath.split('/') |
| 129 | + if (segments.size < 2) { |
| 130 | + Timber.w("Could not extract volumeId from app's private files path '$absPath'") |
| 131 | + return null |
| 132 | + } |
| 133 | + |
| 134 | + val volumeId = segments[2] |
| 135 | + return Uri.parse("content://com.android.externalstorage.documents/document/$volumeId%3A/") |
| 136 | + }.getOrDefault(null) |
| 137 | + } |
| 138 | +} |
0 commit comments