diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt
index 6fbe816d2..6cb37ca49 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt
@@ -221,12 +221,37 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
}
Log.i(TAG, "onActivityResult: partition=$partition")
+ Log.i(TAG, "onActivityResult: checkedDocumentPath=$checkedDocumentPath")
+ Log.i(TAG, "onActivityResult: treeUri=${resultData?.data}")
val sdOtgPattern = Pattern.compile(SD_OTG_SHORT)
- if (requestCode == OPEN_DOCUMENT_TREE) {
+ if (requestCode == OPEN_DOCUMENT_TREE_PRIMARY) {
+ if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
+ if (isProperInternalRoot(resultData.data!!)) {
+ if (resultData.dataString == baseConfig.primaryTreeUri) {
+ toast(R.string.sd_card_usb_same)
+ return
+ }
+
+ val treeUri = resultData.data
+ baseConfig.primaryTreeUri = treeUri.toString()
+
+ val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ applicationContext.contentResolver.takePersistableUriPermission(treeUri!!, takeFlags)
+ funAfterSAFPermission?.invoke(true)
+ funAfterSAFPermission = null
+ } else {
+ toast(R.string.wrong_root_selected)
+ val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
+ startActivityForResult(intent, requestCode)
+ }
+ } else {
+ funAfterSAFPermission?.invoke(false)
+ }
+ } else if (requestCode == OPEN_DOCUMENT_TREE_SD) {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val isProperPartition = partition.isEmpty() || !sdOtgPattern.matcher(partition).matches() || (sdOtgPattern.matcher(partition).matches() && resultData.dataString!!.contains(partition))
- if (isAndroidDataRoot(checkedDocumentPath) || (isProperSDFolder(resultData.data!!) && isProperPartition)) {
+ if (isProperSDFolder(resultData.data!!) && isProperPartition) {
if (resultData.dataString == baseConfig.OTGTreeUri) {
toast(R.string.sd_card_usb_same)
return
@@ -247,7 +272,7 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
val isProperPartition = partition.isEmpty() || !sdOtgPattern.matcher(partition).matches() || (sdOtgPattern.matcher(partition).matches() && resultData.dataString!!.contains(partition))
if (isProperOTGFolder(resultData.data!!) && isProperPartition) {
- if (resultData.dataString == baseConfig.treeUri) {
+ if (resultData.dataString == baseConfig.sdTreeUri) {
funAfterSAFPermission?.invoke(false)
toast(R.string.sd_card_usb_same)
return
@@ -277,7 +302,7 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
private fun saveTreeUri(resultData: Intent) {
val treeUri = resultData.data
- baseConfig.treeUri = treeUri.toString()
+ baseConfig.sdTreeUri = treeUri.toString()
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
applicationContext.contentResolver.takePersistableUriPermission(treeUri!!, takeFlags)
@@ -287,7 +312,10 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
private fun isProperOTGFolder(uri: Uri) = isExternalStorageDocument(uri) && isRootUri(uri) && !isInternalStorage(uri)
- private fun isRootUri(uri: Uri) = DocumentsContract.getTreeDocumentId(uri).endsWith(":")
+ @SuppressLint("NewApi")
+ private fun isProperInternalRoot(uri: Uri) = isExternalStorageDocument(uri) && isRootUri(uri) && isInternalStorage(uri)
+
+ private fun isRootUri(uri: Uri) = uri.lastPathSegment?.endsWith(":") ?: false
private fun isInternalStorage(uri: Uri) = isExternalStorageDocument(uri) && DocumentsContract.getTreeDocumentId(uri).contains("primary")
@@ -354,6 +382,19 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
}
}
+ fun handlePrimarySAFDialog(path: String, callback: (success: Boolean) -> Unit): Boolean {
+ return if (!packageName.startsWith("com.simplemobiletools")) {
+ callback(true)
+ false
+ } else if (isShowingSAFPrimaryDialog(path)) {
+ funAfterSAFPermission = callback
+ true
+ } else {
+ callback(true)
+ false
+ }
+ }
+
fun handleOTGPermission(callback: (success: Boolean) -> Unit) {
if (baseConfig.OTGTreeUri.isNotEmpty()) {
callback(true)
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt
index bcabfd070..bdae8ded0 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt
@@ -36,13 +36,13 @@ import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.*
import com.simplemobiletools.commons.views.MyTextView
-import kotlinx.android.synthetic.main.dialog_title.view.*
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.OutputStream
import java.util.*
import kotlin.collections.HashMap
+import kotlinx.android.synthetic.main.dialog_title.view.*
fun AppCompatActivity.updateActionBarTitle(text: String, color: Int = baseConfig.primaryColor) {
supportActionBar?.title = Html.fromHtml("$text")
@@ -117,14 +117,14 @@ fun Activity.isAppInstalledOnSDCard(): Boolean = try {
}
fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean {
- return if (((isPathOnSD(path) || isAndroidDataRoot(path)) && !isSDCardSetAsDefaultStorage() && (baseConfig.treeUri.isEmpty() || !hasProperStoredTreeUri(false)))) {
+ return if ((isPathOnSD(path) && !isSDCardSetAsDefaultStorage() && (baseConfig.sdTreeUri.isEmpty() || !hasProperStoredTreeUri(false)))) {
runOnUiThread {
if (!isDestroyed && !isFinishing) {
WritePermissionDialog(this, false) {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true)
try {
- startActivityForResult(this, OPEN_DOCUMENT_TREE)
+ startActivityForResult(this, OPEN_DOCUMENT_TREE_SD)
checkedDocumentPath = path
return@apply
} catch (e: Exception) {
@@ -132,7 +132,38 @@ fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean {
}
try {
- startActivityForResult(this, OPEN_DOCUMENT_TREE)
+ startActivityForResult(this, OPEN_DOCUMENT_TREE_SD)
+ checkedDocumentPath = path
+ } catch (e: Exception) {
+ toast(R.string.unknown_error_occurred)
+ }
+ }
+ }
+ }
+ }
+ true
+ } else {
+ false
+ }
+}
+
+fun BaseSimpleActivity.isShowingSAFPrimaryDialog(path: String): Boolean {
+ return if (isSAFOnlyRoot(path) && (baseConfig.primaryTreeUri.isEmpty() || !hasProperStoredPrimaryTreeUri())) {
+ runOnUiThread {
+ if (!isDestroyed && !isFinishing) {
+ WritePermissionDialog(this, false) {
+ Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
+ putExtra("android.content.extra.SHOW_ADVANCED", true)
+ try {
+ startActivityForResult(this, OPEN_DOCUMENT_TREE_PRIMARY)
+ checkedDocumentPath = path
+ return@apply
+ } catch (e: Exception) {
+ type = "*/*"
+ }
+
+ try {
+ startActivityForResult(this, OPEN_DOCUMENT_TREE_PRIMARY)
checkedDocumentPath = path
} catch (e: Exception) {
toast(R.string.unknown_error_occurred)
@@ -496,7 +527,7 @@ fun BaseSimpleActivity.deleteFoldersBg(folders: List, deleteMediaOn
var wasSuccess = false
var needPermissionForPath = ""
for (folder in folders) {
- if (needsStupidWritePermissions(folder.path) && baseConfig.treeUri.isEmpty()) {
+ if (needsStupidWritePermissions(folder.path) && baseConfig.sdTreeUri.isEmpty()) {
needPermissionForPath = folder.path
break
}
@@ -818,7 +849,7 @@ fun BaseSimpleActivity.getFileOutputStream(fileDirItem: FileDirItem, allowCreati
fun BaseSimpleActivity.showFileCreateError(path: String) {
val error = String.format(getString(R.string.could_not_create_file), path)
- baseConfig.treeUri = ""
+ baseConfig.sdTreeUri = ""
showErrorToast(error)
}
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt
index 065d039f1..29c468966 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context-storage.kt
@@ -12,8 +12,8 @@ import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.provider.DocumentsContract
+import android.provider.DocumentsContract.Document
import android.provider.MediaStore.*
-import android.provider.OpenableColumns
import android.text.TextUtils
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
@@ -26,13 +26,19 @@ import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.net.URLDecoder
-import java.util.*
+import java.util.ArrayList
+import java.util.Collections
+import java.util.HashMap
+import java.util.HashSet
import java.util.regex.Pattern
// http://stackoverflow.com/a/40582634/1967672
fun Context.getSDCardPath(): String {
val directories = getStorageDirectories().filter {
- !it.equals(getInternalStoragePath()) && !it.equals("/storage/emulated/0", true) && (baseConfig.OTGPartition.isEmpty() || !it.endsWith(baseConfig.OTGPartition))
+ !it.equals(getInternalStoragePath()) && !it.equals(
+ "/storage/emulated/0",
+ true
+ ) && (baseConfig.OTGPartition.isEmpty() || !it.endsWith(baseConfig.OTGPartition))
}
val fullSDpattern = Pattern.compile(SD_OTG_PATTERN)
@@ -121,12 +127,14 @@ fun Context.getStorageDirectories(): Array {
}
fun Context.getHumanReadablePath(path: String): String {
- return getString(when (path) {
- "/" -> R.string.root
- internalStoragePath -> R.string.internal
- otgPath -> R.string.usb
- else -> R.string.sd_card
- })
+ return getString(
+ when (path) {
+ "/" -> R.string.root
+ internalStoragePath -> R.string.internal
+ otgPath -> R.string.usb
+ else -> R.string.sd_card
+ }
+ )
}
fun Context.humanizePath(path: String): String {
@@ -138,14 +146,24 @@ fun Context.humanizePath(path: String): String {
}
}
-fun Context.getInternalStoragePath() = if (File("/storage/emulated/0").exists()) "/storage/emulated/0" else Environment.getExternalStorageDirectory().absolutePath.trimEnd('/')
+fun Context.getInternalStoragePath() =
+ if (File("/storage/emulated/0").exists()) "/storage/emulated/0" else Environment.getExternalStorageDirectory().absolutePath.trimEnd('/')
fun Context.isPathOnSD(path: String) = sdCardPath.isNotEmpty() && path.startsWith(sdCardPath)
fun Context.isPathOnOTG(path: String) = otgPath.isNotEmpty() && path.startsWith(otgPath)
-fun Context.isAndroidDataRoot(path: String) = (path == (internalStoragePath.plus(ANDROID_DIR)) || path == otgPath.plus(ANDROID_DIR) || path == sdCardPath.plus(ANDROID_DIR))
+val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf("/Android")
+fun Context.getSAFOnlyDirs(): List {
+ return DIRS_ACCESSIBLE_ONLY_WITH_SAF.map { "$internalStoragePath$it" }
+}
+
+fun Context.isSAFOnlyRoot(path: String): Boolean {
+ val dirs = getSAFOnlyDirs()
+ val result = dirs.any { path.startsWith(it) }
+ return result
+}
// no need to use DocumentFile if an SD card is set as the default storage
fun Context.needsStupidWritePermissions(path: String) = (isPathOnSD(path) || isPathOnOTG(path)) && !isSDCardSetAsDefaultStorage()
@@ -153,18 +171,27 @@ fun Context.needsStupidWritePermissions(path: String) = (isPathOnSD(path) || isP
fun Context.isSDCardSetAsDefaultStorage() = sdCardPath.isNotEmpty() && Environment.getExternalStorageDirectory().absolutePath.equals(sdCardPath, true)
fun Context.hasProperStoredTreeUri(isOTG: Boolean): Boolean {
- val uri = if (isOTG) baseConfig.OTGTreeUri else baseConfig.treeUri
+ val uri = if (isOTG) baseConfig.OTGTreeUri else baseConfig.sdTreeUri
val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
if (!hasProperUri) {
if (isOTG) {
baseConfig.OTGTreeUri = ""
} else {
- baseConfig.treeUri = ""
+ baseConfig.sdTreeUri = ""
}
}
return hasProperUri
}
+fun Context.hasProperStoredPrimaryTreeUri(): Boolean {
+ val uri = baseConfig.primaryTreeUri
+ val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
+ if (!hasProperUri) {
+ baseConfig.primaryTreeUri = ""
+ }
+ return hasProperUri
+}
+
fun Context.isAStorageRootFolder(path: String): Boolean {
val trimmed = path.trimEnd('/')
return trimmed.isEmpty() || trimmed.equals(internalStoragePath, true) || trimmed.equals(sdCardPath, true) || trimmed.equals(otgPath, true)
@@ -202,7 +229,7 @@ fun Context.getFastDocumentFile(path: String): DocumentFile? {
val relativePath = Uri.encode(path.substring(baseConfig.sdCardPath.length).trim('/'))
val externalPathPart = baseConfig.sdCardPath.split("/").lastOrNull(String::isNotEmpty)?.trim('/') ?: return null
- val fullUri = "${baseConfig.treeUri}/document/$externalPathPart%3A$relativePath"
+ val fullUri = "${baseConfig.sdTreeUri}/document/$externalPathPart%3A$relativePath"
return DocumentFile.fromSingleUri(this, Uri.parse(fullUri))
}
@@ -230,7 +257,7 @@ fun Context.getDocumentFile(path: String): DocumentFile? {
}
return try {
- val treeUri = Uri.parse(if (isOTG) baseConfig.OTGTreeUri else baseConfig.treeUri)
+ val treeUri = Uri.parse(if (isOTG) baseConfig.OTGTreeUri else baseConfig.sdTreeUri)
var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) {
@@ -447,60 +474,90 @@ fun Context.getOTGItems(path: String, shouldShowHidden: Boolean, getProperFileSi
callback(items)
}
-const val MIME_TYPE_IS_DIRECTORY = "vnd.android.document/directory"
@RequiresApi(Build.VERSION_CODES.O)
-fun Context.getStorageItems(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList) -> Unit) {
+fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList) -> Unit) {
val items = ArrayList()
- val treeUri = baseConfig.treeUri
- val document = getFastDocumentFile(path)
- val files = document?.listFiles()
- val childrenUri = try {
- DocumentsContract.buildChildDocumentsUriUsingTree(treeUri.toUri(), document?.uri.toString())
+ val rootDocumentFile = try {
+ DocumentFile.fromTreeUri(applicationContext, baseConfig.primaryTreeUri.toUri())
} catch (e: Exception) {
showErrorToast(e)
- baseConfig.treeUri = ""
+ baseConfig.primaryTreeUri = ""
null
}
- if (childrenUri == null) {
+
+ if (rootDocumentFile == null) {
callback(items)
return
}
- contentResolver.query(childrenUri, null, null, null)
- ?.use { cursor ->
- val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
- val mimeIndex = cursor.getColumnIndex("mime_type")
- while (cursor.moveToNext()) {
- val name = cursor.getString(nameIndex) ?: continue
+ val treeUri = baseConfig.primaryTreeUri.toUri()
+ // uri should be a concatenation of the tree uri and the path without the internal storage path
+ val relativePath = path.substring(baseConfig.internalStoragePath.length).trim('/')
+ val documentId = "primary:$relativePath"
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
+ val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE)
+
+ val rawCursor = contentResolver.query(childrenUri, projection, null, null)!!
+ val cursor = ExternalStorageProviderHack.transformQueryResult(childrenUri, rawCursor)
+ cursor.use {
+ if (cursor.moveToFirst()) {
+ do {
+ val documentId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
+ val name = cursor.getStringValue(Document.COLUMN_DISPLAY_NAME)
+ val mimeType = cursor.getStringValue(Document.COLUMN_MIME_TYPE)
+ val isDirectory = mimeType == Document.MIME_TYPE_DIR
+ val filePath = documentId.substring("primary:".length)
if (!shouldShowHidden && name.startsWith(".")) {
continue
}
- val mimeType = cursor.getString(mimeIndex)
- val isDirectory = mimeType == MIME_TYPE_IS_DIRECTORY
-
+ val decodedPath = internalStoragePath + "/" + URLDecoder.decode(filePath, "UTF-8")
val fileSize = when {
- getProperFileSize -> 0L
+ getProperFileSize -> getFileSize(treeUri, documentId)
isDirectory -> 0L
- else -> 0L
+ else -> getFileSize(treeUri, documentId)
}
val childrenCount = if (isDirectory) {
- 0
+ getChildrenCount(treeUri, documentId)
} else {
0
}
- val lastModified = 0L
- val fileDirItem = FileDirItem(path, name, isDirectory, childrenCount, fileSize, lastModified)
+ val lastModified = System.currentTimeMillis()
+ val fileDirItem = FileDirItem(decodedPath, name, isDirectory, childrenCount, fileSize, lastModified)
items.add(fileDirItem)
- }
+ } while (cursor.moveToNext())
}
-
+ }
callback(items)
}
+fun Context.getChildrenCount(treeUri: Uri, documentId: String): Int {
+ val projection = arrayOf(Document.COLUMN_DOCUMENT_ID)
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
+ val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
+ val cursor = ExternalStorageProviderHack.transformQueryResult(childrenUri, rawCursor)
+ val count = cursor.count
+ return count
+}
+
+fun Context.getFileSize(treeUri: Uri, documentId: String): Long {
+ val projection = arrayOf(Document.COLUMN_SIZE)
+ val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
+ val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
+ val cursor = ExternalStorageProviderHack.transformQueryResult(childrenUri, rawCursor)
+ var size = 0L
+ cursor.use { c ->
+ if (c.moveToFirst()) {
+ size = c.getLongValue(Document.COLUMN_SIZE)
+ }
+ }
+ return size
+}
+
+
fun Context.trySAFFileDelete(fileDirItem: FileDirItem, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
var fileDeleted = tryFastDocumentDelete(fileDirItem.path, allowDeleteFolder)
if (!fileDeleted) {
@@ -509,7 +566,7 @@ fun Context.trySAFFileDelete(fileDirItem: FileDirItem, allowDeleteFolder: Boolea
try {
fileDeleted = (document.isFile || allowDeleteFolder) && DocumentsContract.deleteDocument(applicationContext.contentResolver, document.uri)
} catch (ignored: Exception) {
- baseConfig.treeUri = ""
+ baseConfig.sdTreeUri = ""
baseConfig.sdCardPath = ""
}
}
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt
index f402a594e..adcc89660 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt
@@ -55,7 +55,9 @@ import com.simplemobiletools.commons.models.SharedTheme
import com.simplemobiletools.commons.views.*
import java.io.File
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.ArrayList
+import java.util.Date
+import java.util.Locale
fun Context.getSharedPrefs() = getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE)
@@ -489,7 +491,7 @@ fun Context.updateSDCardPath() {
val oldPath = baseConfig.sdCardPath
baseConfig.sdCardPath = getSDCardPath()
if (oldPath != baseConfig.sdCardPath) {
- baseConfig.treeUri = ""
+ baseConfig.sdTreeUri = ""
}
}
}
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/BaseConfig.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/BaseConfig.kt
index 620b12b8b..5b64915b9 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/BaseConfig.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/BaseConfig.kt
@@ -7,7 +7,9 @@ import com.simplemobiletools.commons.extensions.getInternalStoragePath
import com.simplemobiletools.commons.extensions.getSDCardPath
import com.simplemobiletools.commons.extensions.getSharedPrefs
import java.text.SimpleDateFormat
-import java.util.*
+import java.util.Calendar
+import java.util.HashSet
+import java.util.Locale
open class BaseConfig(val context: Context) {
protected val prefs = context.getSharedPrefs()
@@ -24,9 +26,13 @@ open class BaseConfig(val context: Context) {
get() = prefs.getInt(LAST_VERSION, 0)
set(lastVersion) = prefs.edit().putInt(LAST_VERSION, lastVersion).apply()
- var treeUri: String
- get() = prefs.getString(TREE_URI, "")!!
- set(uri) = prefs.edit().putString(TREE_URI, uri).apply()
+ var primaryTreeUri: String
+ get() = prefs.getString(PRIMARY_TREE_URI, "")!!
+ set(uri) = prefs.edit().putString(PRIMARY_TREE_URI, uri).apply()
+
+ var sdTreeUri: String
+ get() = prefs.getString(SD_TREE_URI, "")!!
+ set(uri) = prefs.edit().putString(SD_TREE_URI, uri).apply()
var OTGTreeUri: String
get() = prefs.getString(OTG_TREE_URI, "")!!
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt
index 6e3c010c1..2084be226 100644
--- a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/Constants.kt
@@ -8,7 +8,8 @@ import android.os.Looper
import android.util.Log
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.overloads.times
-import java.util.*
+import java.util.HashMap
+import java.util.LinkedHashMap
const val APP_NAME = "app_name"
const val APP_LICENSES = "app_licenses"
@@ -61,7 +62,8 @@ const val YEAR_SECONDS = YEAR_MINUTES * 60
const val PREFS_KEY = "Prefs"
const val APP_RUN_COUNT = "app_run_count"
const val LAST_VERSION = "last_version"
-const val TREE_URI = "tree_uri_2"
+const val SD_TREE_URI = "tree_uri_2"
+const val PRIMARY_TREE_URI = "primary_tree_uri_2"
const val OTG_TREE_URI = "otg_tree_uri_2"
const val SD_CARD_PATH = "sd_card_path_2"
const val OTG_REAL_PATH = "otg_real_path_2"
@@ -183,8 +185,9 @@ const val LICENSE_SMS_MMS = 134217728
const val LICENSE_APNG = 268435456
// global intents
-const val OPEN_DOCUMENT_TREE = 1000
+const val OPEN_DOCUMENT_TREE_PRIMARY = 1000
const val OPEN_DOCUMENT_TREE_OTG = 1001
+const val OPEN_DOCUMENT_TREE_SD = 1002
const val REQUEST_SET_AS = 1002
const val REQUEST_EDIT_IMAGE = 1003
const val SELECT_EXPORT_SETTINGS_FILE_INTENT = 1004
@@ -305,9 +308,6 @@ const val DATE_FORMAT_FOURTEEN = "yy/MM/dd"
const val TIME_FORMAT_12 = "hh:mm a"
const val TIME_FORMAT_24 = "HH:mm"
-//storage
-const val ANDROID_DIR = "/Android"
-
val appIconColorStrings = arrayListOf(
".Red",
".Pink",
diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/ExternalStorageProviderHack.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/ExternalStorageProviderHack.kt
new file mode 100644
index 000000000..849832892
--- /dev/null
+++ b/commons/src/main/kotlin/com/simplemobiletools/commons/helpers/ExternalStorageProviderHack.kt
@@ -0,0 +1,106 @@
+package com.simplemobiletools.commons.helpers
+
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.database.MergeCursor
+import android.net.Uri
+import android.provider.DocumentsContract
+import com.simplemobiletools.commons.extensions.getStringValue
+
+// On Android 11, ExternalStorageProvider no longer returns Android/data and Android/obb as children
+// of the Android directory on primary storage. However, the two child directories are actually
+// still accessible.
+// https://github.com/zhanghai/MaterialFiles/blob/master/app/src/main/java/me/zhanghai/android/files/provider/document/resolver/ExternalStorageProviderPrimaryAndroidDataHack.kt
+object ExternalStorageProviderHack {
+ private const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
+ private const val EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DOCUMENT_ID = "primary:Android"
+ private const val EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DATA_DOCUMENT_ID =
+ "primary:Android/data"
+ private const val EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DATA_DISPLAY_NAME = "data"
+ private const val EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_OBB_DOCUMENT_ID =
+ "primary:Android/obb"
+ private const val EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_OBB_DISPLAY_NAME = "obb"
+
+ private val CHILD_DOCUMENTS_CURSOR_COLUMN_NAMES = arrayOf(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ DocumentsContract.Document.COLUMN_SIZE,
+ )
+
+ fun transformQueryResult(uri: Uri, cursor: Cursor): Cursor {
+ val documentId = DocumentsContract.getDocumentId(uri)
+ if (uri.authority == EXTERNAL_STORAGE_PROVIDER_AUTHORITY && documentId == EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DOCUMENT_ID) {
+ var hasDataRow = false
+ var hasObbRow = false
+ try {
+ while (cursor.moveToNext()) {
+ when (cursor.getStringValue(DocumentsContract.Document.COLUMN_DOCUMENT_ID)) {
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DATA_DOCUMENT_ID ->
+ hasDataRow = true
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_OBB_DOCUMENT_ID ->
+ hasObbRow = true
+ }
+ if (hasDataRow && hasObbRow) {
+ break
+ }
+ }
+ } finally {
+ cursor.moveToPosition(-1)
+ }
+ if (hasDataRow && hasObbRow) {
+ return cursor
+ }
+ val extraCursor = MatrixCursor(CHILD_DOCUMENTS_CURSOR_COLUMN_NAMES)
+ if (!hasDataRow) {
+ extraCursor.newRow()
+ .add(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DATA_DOCUMENT_ID
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_DATA_DISPLAY_NAME
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.MIME_TYPE_DIR
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ System.currentTimeMillis()
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_SIZE,
+ 0L
+ )
+ }
+ if (!hasObbRow) {
+ extraCursor.newRow()
+ .add(
+ DocumentsContract.Document.COLUMN_DOCUMENT_ID,
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_OBB_DOCUMENT_ID
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_DISPLAY_NAME,
+ EXTERNAL_STORAGE_PROVIDER_PRIMARY_ANDROID_OBB_DISPLAY_NAME
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_MIME_TYPE,
+ DocumentsContract.Document.MIME_TYPE_DIR
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_LAST_MODIFIED,
+ System.currentTimeMillis()
+ )
+ .add(
+ DocumentsContract.Document.COLUMN_SIZE,
+ 0L
+ )
+ }
+ return MergeCursor(arrayOf(cursor, extraCursor))
+ }
+ return cursor
+ }
+}