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