read files from Andoid/data and Android/obb

This commit is contained in:
darthpaul 2021-10-24 11:01:32 +01:00
parent 16120313b2
commit 08fbc4a0cd
7 changed files with 307 additions and 64 deletions

View file

@ -221,12 +221,37 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
} }
Log.i(TAG, "onActivityResult: partition=$partition") 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) 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) { 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)) 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) { if (resultData.dataString == baseConfig.OTGTreeUri) {
toast(R.string.sd_card_usb_same) toast(R.string.sd_card_usb_same)
return return
@ -247,7 +272,7 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) { 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)) val isProperPartition = partition.isEmpty() || !sdOtgPattern.matcher(partition).matches() || (sdOtgPattern.matcher(partition).matches() && resultData.dataString!!.contains(partition))
if (isProperOTGFolder(resultData.data!!) && isProperPartition) { if (isProperOTGFolder(resultData.data!!) && isProperPartition) {
if (resultData.dataString == baseConfig.treeUri) { if (resultData.dataString == baseConfig.sdTreeUri) {
funAfterSAFPermission?.invoke(false) funAfterSAFPermission?.invoke(false)
toast(R.string.sd_card_usb_same) toast(R.string.sd_card_usb_same)
return return
@ -277,7 +302,7 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
private fun saveTreeUri(resultData: Intent) { private fun saveTreeUri(resultData: Intent) {
val treeUri = resultData.data 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 val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
applicationContext.contentResolver.takePersistableUriPermission(treeUri!!, takeFlags) 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 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") 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) { fun handleOTGPermission(callback: (success: Boolean) -> Unit) {
if (baseConfig.OTGTreeUri.isNotEmpty()) { if (baseConfig.OTGTreeUri.isNotEmpty()) {
callback(true) callback(true)

View file

@ -36,13 +36,13 @@ import com.simplemobiletools.commons.dialogs.*
import com.simplemobiletools.commons.helpers.* import com.simplemobiletools.commons.helpers.*
import com.simplemobiletools.commons.models.* import com.simplemobiletools.commons.models.*
import com.simplemobiletools.commons.views.MyTextView import com.simplemobiletools.commons.views.MyTextView
import kotlinx.android.synthetic.main.dialog_title.view.*
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.* import java.util.*
import kotlin.collections.HashMap import kotlin.collections.HashMap
import kotlinx.android.synthetic.main.dialog_title.view.*
fun AppCompatActivity.updateActionBarTitle(text: String, color: Int = baseConfig.primaryColor) { fun AppCompatActivity.updateActionBarTitle(text: String, color: Int = baseConfig.primaryColor) {
supportActionBar?.title = Html.fromHtml("<font color='${color.getContrastColor().toHex()}'>$text</font>") supportActionBar?.title = Html.fromHtml("<font color='${color.getContrastColor().toHex()}'>$text</font>")
@ -117,14 +117,14 @@ fun Activity.isAppInstalledOnSDCard(): Boolean = try {
} }
fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean { 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 { runOnUiThread {
if (!isDestroyed && !isFinishing) { if (!isDestroyed && !isFinishing) {
WritePermissionDialog(this, false) { WritePermissionDialog(this, false) {
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply { Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
putExtra("android.content.extra.SHOW_ADVANCED", true) putExtra("android.content.extra.SHOW_ADVANCED", true)
try { try {
startActivityForResult(this, OPEN_DOCUMENT_TREE) startActivityForResult(this, OPEN_DOCUMENT_TREE_SD)
checkedDocumentPath = path checkedDocumentPath = path
return@apply return@apply
} catch (e: Exception) { } catch (e: Exception) {
@ -132,7 +132,38 @@ fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean {
} }
try { 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 checkedDocumentPath = path
} catch (e: Exception) { } catch (e: Exception) {
toast(R.string.unknown_error_occurred) toast(R.string.unknown_error_occurred)
@ -496,7 +527,7 @@ fun BaseSimpleActivity.deleteFoldersBg(folders: List<FileDirItem>, deleteMediaOn
var wasSuccess = false var wasSuccess = false
var needPermissionForPath = "" var needPermissionForPath = ""
for (folder in folders) { for (folder in folders) {
if (needsStupidWritePermissions(folder.path) && baseConfig.treeUri.isEmpty()) { if (needsStupidWritePermissions(folder.path) && baseConfig.sdTreeUri.isEmpty()) {
needPermissionForPath = folder.path needPermissionForPath = folder.path
break break
} }
@ -818,7 +849,7 @@ fun BaseSimpleActivity.getFileOutputStream(fileDirItem: FileDirItem, allowCreati
fun BaseSimpleActivity.showFileCreateError(path: String) { fun BaseSimpleActivity.showFileCreateError(path: String) {
val error = String.format(getString(R.string.could_not_create_file), path) val error = String.format(getString(R.string.could_not_create_file), path)
baseConfig.treeUri = "" baseConfig.sdTreeUri = ""
showErrorToast(error) showErrorToast(error)
} }

View file

@ -12,8 +12,8 @@ import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.DocumentsContract import android.provider.DocumentsContract
import android.provider.DocumentsContract.Document
import android.provider.MediaStore.* import android.provider.MediaStore.*
import android.provider.OpenableColumns
import android.text.TextUtils import android.text.TextUtils
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
@ -26,13 +26,19 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.net.URLDecoder 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 import java.util.regex.Pattern
// http://stackoverflow.com/a/40582634/1967672 // http://stackoverflow.com/a/40582634/1967672
fun Context.getSDCardPath(): String { fun Context.getSDCardPath(): String {
val directories = getStorageDirectories().filter { 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) val fullSDpattern = Pattern.compile(SD_OTG_PATTERN)
@ -121,12 +127,14 @@ fun Context.getStorageDirectories(): Array<String> {
} }
fun Context.getHumanReadablePath(path: String): String { fun Context.getHumanReadablePath(path: String): String {
return getString(when (path) { return getString(
"/" -> R.string.root when (path) {
internalStoragePath -> R.string.internal "/" -> R.string.root
otgPath -> R.string.usb internalStoragePath -> R.string.internal
else -> R.string.sd_card otgPath -> R.string.usb
}) else -> R.string.sd_card
}
)
} }
fun Context.humanizePath(path: String): String { 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.isPathOnSD(path: String) = sdCardPath.isNotEmpty() && path.startsWith(sdCardPath)
fun Context.isPathOnOTG(path: String) = otgPath.isNotEmpty() && path.startsWith(otgPath) 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<String> {
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 // 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() 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.isSDCardSetAsDefaultStorage() = sdCardPath.isNotEmpty() && Environment.getExternalStorageDirectory().absolutePath.equals(sdCardPath, true)
fun Context.hasProperStoredTreeUri(isOTG: Boolean): Boolean { 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 } val hasProperUri = contentResolver.persistedUriPermissions.any { it.uri.toString() == uri }
if (!hasProperUri) { if (!hasProperUri) {
if (isOTG) { if (isOTG) {
baseConfig.OTGTreeUri = "" baseConfig.OTGTreeUri = ""
} else { } else {
baseConfig.treeUri = "" baseConfig.sdTreeUri = ""
} }
} }
return hasProperUri 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 { fun Context.isAStorageRootFolder(path: String): Boolean {
val trimmed = path.trimEnd('/') val trimmed = path.trimEnd('/')
return trimmed.isEmpty() || trimmed.equals(internalStoragePath, true) || trimmed.equals(sdCardPath, true) || trimmed.equals(otgPath, true) 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 relativePath = Uri.encode(path.substring(baseConfig.sdCardPath.length).trim('/'))
val externalPathPart = baseConfig.sdCardPath.split("/").lastOrNull(String::isNotEmpty)?.trim('/') ?: return null 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)) return DocumentFile.fromSingleUri(this, Uri.parse(fullUri))
} }
@ -230,7 +257,7 @@ fun Context.getDocumentFile(path: String): DocumentFile? {
} }
return try { 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) var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val parts = relativePath.split("/").filter { it.isNotEmpty() } val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) { for (part in parts) {
@ -447,60 +474,90 @@ fun Context.getOTGItems(path: String, shouldShowHidden: Boolean, getProperFileSi
callback(items) callback(items)
} }
const val MIME_TYPE_IS_DIRECTORY = "vnd.android.document/directory"
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun Context.getStorageItems(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList<FileDirItem>) -> Unit) { fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>() val items = ArrayList<FileDirItem>()
val treeUri = baseConfig.treeUri val rootDocumentFile = try {
val document = getFastDocumentFile(path) DocumentFile.fromTreeUri(applicationContext, baseConfig.primaryTreeUri.toUri())
val files = document?.listFiles()
val childrenUri = try {
DocumentsContract.buildChildDocumentsUriUsingTree(treeUri.toUri(), document?.uri.toString())
} catch (e: Exception) { } catch (e: Exception) {
showErrorToast(e) showErrorToast(e)
baseConfig.treeUri = "" baseConfig.primaryTreeUri = ""
null null
} }
if (childrenUri == null) {
if (rootDocumentFile == null) {
callback(items) callback(items)
return return
} }
contentResolver.query(childrenUri, null, null, null) val treeUri = baseConfig.primaryTreeUri.toUri()
?.use { cursor -> // uri should be a concatenation of the tree uri and the path without the internal storage path
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val relativePath = path.substring(baseConfig.internalStoragePath.length).trim('/')
val mimeIndex = cursor.getColumnIndex("mime_type") val documentId = "primary:$relativePath"
while (cursor.moveToNext()) { val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
val name = cursor.getString(nameIndex) ?: continue 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(".")) { if (!shouldShowHidden && name.startsWith(".")) {
continue continue
} }
val mimeType = cursor.getString(mimeIndex) val decodedPath = internalStoragePath + "/" + URLDecoder.decode(filePath, "UTF-8")
val isDirectory = mimeType == MIME_TYPE_IS_DIRECTORY
val fileSize = when { val fileSize = when {
getProperFileSize -> 0L getProperFileSize -> getFileSize(treeUri, documentId)
isDirectory -> 0L isDirectory -> 0L
else -> 0L else -> getFileSize(treeUri, documentId)
} }
val childrenCount = if (isDirectory) { val childrenCount = if (isDirectory) {
0 getChildrenCount(treeUri, documentId)
} else { } else {
0 0
} }
val lastModified = 0L val lastModified = System.currentTimeMillis()
val fileDirItem = FileDirItem(path, name, isDirectory, childrenCount, fileSize, lastModified) val fileDirItem = FileDirItem(decodedPath, name, isDirectory, childrenCount, fileSize, lastModified)
items.add(fileDirItem) items.add(fileDirItem)
} } while (cursor.moveToNext())
} }
}
callback(items) 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) { fun Context.trySAFFileDelete(fileDirItem: FileDirItem, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
var fileDeleted = tryFastDocumentDelete(fileDirItem.path, allowDeleteFolder) var fileDeleted = tryFastDocumentDelete(fileDirItem.path, allowDeleteFolder)
if (!fileDeleted) { if (!fileDeleted) {
@ -509,7 +566,7 @@ fun Context.trySAFFileDelete(fileDirItem: FileDirItem, allowDeleteFolder: Boolea
try { try {
fileDeleted = (document.isFile || allowDeleteFolder) && DocumentsContract.deleteDocument(applicationContext.contentResolver, document.uri) fileDeleted = (document.isFile || allowDeleteFolder) && DocumentsContract.deleteDocument(applicationContext.contentResolver, document.uri)
} catch (ignored: Exception) { } catch (ignored: Exception) {
baseConfig.treeUri = "" baseConfig.sdTreeUri = ""
baseConfig.sdCardPath = "" baseConfig.sdCardPath = ""
} }
} }

View file

@ -55,7 +55,9 @@ import com.simplemobiletools.commons.models.SharedTheme
import com.simplemobiletools.commons.views.* import com.simplemobiletools.commons.views.*
import java.io.File import java.io.File
import java.text.SimpleDateFormat 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) fun Context.getSharedPrefs() = getSharedPreferences(PREFS_KEY, Context.MODE_PRIVATE)
@ -489,7 +491,7 @@ fun Context.updateSDCardPath() {
val oldPath = baseConfig.sdCardPath val oldPath = baseConfig.sdCardPath
baseConfig.sdCardPath = getSDCardPath() baseConfig.sdCardPath = getSDCardPath()
if (oldPath != baseConfig.sdCardPath) { if (oldPath != baseConfig.sdCardPath) {
baseConfig.treeUri = "" baseConfig.sdTreeUri = ""
} }
} }
} }

View file

@ -7,7 +7,9 @@ import com.simplemobiletools.commons.extensions.getInternalStoragePath
import com.simplemobiletools.commons.extensions.getSDCardPath import com.simplemobiletools.commons.extensions.getSDCardPath
import com.simplemobiletools.commons.extensions.getSharedPrefs import com.simplemobiletools.commons.extensions.getSharedPrefs
import java.text.SimpleDateFormat 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) { open class BaseConfig(val context: Context) {
protected val prefs = context.getSharedPrefs() protected val prefs = context.getSharedPrefs()
@ -24,9 +26,13 @@ open class BaseConfig(val context: Context) {
get() = prefs.getInt(LAST_VERSION, 0) get() = prefs.getInt(LAST_VERSION, 0)
set(lastVersion) = prefs.edit().putInt(LAST_VERSION, lastVersion).apply() set(lastVersion) = prefs.edit().putInt(LAST_VERSION, lastVersion).apply()
var treeUri: String var primaryTreeUri: String
get() = prefs.getString(TREE_URI, "")!! get() = prefs.getString(PRIMARY_TREE_URI, "")!!
set(uri) = prefs.edit().putString(TREE_URI, uri).apply() 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 var OTGTreeUri: String
get() = prefs.getString(OTG_TREE_URI, "")!! get() = prefs.getString(OTG_TREE_URI, "")!!

View file

@ -8,7 +8,8 @@ import android.os.Looper
import android.util.Log import android.util.Log
import com.simplemobiletools.commons.R import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.overloads.times 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_NAME = "app_name"
const val APP_LICENSES = "app_licenses" const val APP_LICENSES = "app_licenses"
@ -61,7 +62,8 @@ const val YEAR_SECONDS = YEAR_MINUTES * 60
const val PREFS_KEY = "Prefs" const val PREFS_KEY = "Prefs"
const val APP_RUN_COUNT = "app_run_count" const val APP_RUN_COUNT = "app_run_count"
const val LAST_VERSION = "last_version" 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 OTG_TREE_URI = "otg_tree_uri_2"
const val SD_CARD_PATH = "sd_card_path_2" const val SD_CARD_PATH = "sd_card_path_2"
const val OTG_REAL_PATH = "otg_real_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 const val LICENSE_APNG = 268435456
// global intents // 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_OTG = 1001
const val OPEN_DOCUMENT_TREE_SD = 1002
const val REQUEST_SET_AS = 1002 const val REQUEST_SET_AS = 1002
const val REQUEST_EDIT_IMAGE = 1003 const val REQUEST_EDIT_IMAGE = 1003
const val SELECT_EXPORT_SETTINGS_FILE_INTENT = 1004 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_12 = "hh:mm a"
const val TIME_FORMAT_24 = "HH:mm" const val TIME_FORMAT_24 = "HH:mm"
//storage
const val ANDROID_DIR = "/Android"
val appIconColorStrings = arrayListOf( val appIconColorStrings = arrayListOf(
".Red", ".Red",
".Pink", ".Pink",

View file

@ -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
}
}