handle scoped storage changes

This commit is contained in:
darthpaul 2021-11-02 21:54:00 +00:00
parent df2fc3f38a
commit a781514567
12 changed files with 374 additions and 154 deletions

View file

@ -95,14 +95,10 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
updateNavigationBarColor()
}
override fun onStop() {
super.onStop()
actionOnPermission = null
}
override fun onDestroy() {
super.onDestroy()
funAfterSAFPermission = null
actionOnPermission = null
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -223,14 +219,14 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
Log.i(TAG, "onActivityResult: partition=$partition")
Log.i(TAG, "onActivityResult: checkedDocumentPath=$checkedDocumentPath")
Log.i(TAG, "onActivityResult: treeUri=${resultData?.data}")
Log.i(TAG, "onActivityResult: tree documentId=${DocumentsContract.getTreeDocumentId(resultData?.data)}")
Log.i(TAG, "onActivityResult: tree documentId=${resultData?.data?.let { DocumentsContract.getTreeDocumentId(it) }}")
val sdOtgPattern = Pattern.compile(SD_OTG_SHORT)
if (requestCode == OPEN_DOCUMENT_TREE_PRIMARY) {
if (resultCode == Activity.RESULT_OK && resultData != null && resultData.data != null) {
if (isProperInternalAndroidRoot(resultData.data!!)) {
if (resultData.dataString == baseConfig.primaryAndroidTreeUri) {
toast(R.string.sd_card_usb_same)
if (resultData.dataString == baseConfig.OTGTreeUri || resultData.dataString == baseConfig.sdTreeUri) {
toast("Select internal storage")
return
}
@ -447,7 +443,7 @@ abstract class BaseSimpleActivity : AppCompatActivity() {
if (isCopyOperation) {
startCopyMove(fileDirItems, destination, isCopyOperation, copyPhotoVideoOnly, copyHidden)
} else {
if (isPathOnOTG(source) || isPathOnOTG(destination) || isPathOnSD(source) || isPathOnSD(destination) || fileDirItems.first().isDirectory) {
if (isPathOnOTG(source) || isPathOnOTG(destination) || isPathOnSD(source) || isPathOnSD(destination) || isRestrictedAndroidDir(source) || isRestrictedAndroidDir(destination) || fileDirItems.first().isDirectory) {
handleSAFDialog(source) {
if (it) {
startCopyMove(fileDirItems, destination, isCopyOperation, copyPhotoVideoOnly, copyHidden)

View file

@ -18,11 +18,16 @@ import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.getFilePlaceholderDrawables
import com.simplemobiletools.commons.models.FileDirItem
import com.simplemobiletools.commons.views.MyRecyclerView
import kotlinx.android.synthetic.main.filepicker_list_item.view.*
import java.util.*
import java.util.HashMap
import java.util.Locale
import kotlinx.android.synthetic.main.filepicker_list_item.view.list_item_details
import kotlinx.android.synthetic.main.filepicker_list_item.view.list_item_icon
import kotlinx.android.synthetic.main.filepicker_list_item.view.list_item_name
class FilepickerItemsAdapter(activity: BaseSimpleActivity, val fileDirItems: List<FileDirItem>, recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) {
class FilepickerItemsAdapter(
activity: BaseSimpleActivity, val fileDirItems: List<FileDirItem>, recyclerView: MyRecyclerView,
itemClick: (Any) -> Unit
) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) {
private lateinit var fileDrawable: Drawable
private lateinit var folderDrawable: Drawable
@ -110,13 +115,15 @@ class FilepickerItemsAdapter(activity: BaseSimpleActivity, val fileDirItems: Lis
}
if (!activity.isDestroyed && !activity.isFinishing) {
if (activity.isRestrictedAndroidDir(path)) {
itemToLoad = activity.getPrimaryAndroidSAFUri(path)
} else if (hasOTGConnected && itemToLoad is String && activity.isPathOnOTG(itemToLoad)) {
itemToLoad = itemToLoad.getOTGPublicPath(activity)
}
if (itemToLoad.toString().isGif()) {
Glide.with(activity).asBitmap().load(itemToLoad).apply(options).into(list_item_icon)
} else {
if (hasOTGConnected && itemToLoad is String && activity.isPathOnOTG(itemToLoad)) {
itemToLoad = itemToLoad.getOTGPublicPath(activity)
}
Glide.with(activity)
.load(itemToLoad)
.transition(withCrossFade())

View file

@ -22,7 +22,8 @@ import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.lang.ref.WeakReference
import java.util.*
import java.util.ArrayList
import java.util.LinkedHashMap
class CopyMoveTask(
val activity: BaseSimpleActivity, val copyOnly: Boolean, val copyMediaOnly: Boolean, val conflictResolutions: LinkedHashMap<String, Int>,
@ -189,6 +190,21 @@ class CopyMoveTask(
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
} else if (activity.isRestrictedAndroidDir(source.path)) {
activity.getStorageItemsWithTreeUri(source.path, true) { files ->
for (child in files) {
val newPath = "$destinationPath/${child.name}"
if (activity.getDoesFilePathExist(newPath)) {
continue
}
val oldPath = "${source.path}/${child.name}"
val oldFileDirItem = FileDirItem(oldPath, child.name, child.isDirectory, 0, child.size)
val newFileDirItem = FileDirItem(newPath, child.name, child.isDirectory)
copy(oldFileDirItem, newFileDirItem)
}
mTransferredFiles.add(source)
}
} else {
val children = File(source.path).list()
for (child in children) {
@ -264,6 +280,7 @@ class CopyMoveTask(
}
}
} catch (e: Exception) {
e.printStackTrace()
activity.showErrorToast(e)
} finally {
inputStream?.close()

View file

@ -5,8 +5,8 @@ import androidx.appcompat.app.AlertDialog
import com.simplemobiletools.commons.R
import com.simplemobiletools.commons.activities.BaseSimpleActivity
import com.simplemobiletools.commons.extensions.*
import kotlinx.android.synthetic.main.dialog_create_new_folder.view.*
import java.io.File
import kotlinx.android.synthetic.main.dialog_create_new_folder.view.*
class CreateNewFolderDialog(val activity: BaseSimpleActivity, val path: String, val callback: (path: String) -> Unit) {
init {
@ -57,6 +57,7 @@ class CreateNewFolderDialog(val activity: BaseSimpleActivity, val path: String,
}
}
}
activity.isRestrictedAndroidDir(path) && activity.createSAFOnlyDirectory(path) -> sendSuccess(alertDialog, path)
File(path).mkdirs() -> sendSuccess(alertDialog, path)
else -> activity.toast(R.string.unknown_error_occurred)
}

View file

@ -28,15 +28,17 @@ import java.util.*
* @param showFAB toggle the displaying of a Floating Action Button for creating new folders
* @param callback the callback used for returning the selected file/folder
*/
class FilePickerDialog(val activity: BaseSimpleActivity,
var currPath: String = Environment.getExternalStorageDirectory().toString(),
val pickFile: Boolean = true,
var showHidden: Boolean = false,
val showFAB: Boolean = false,
val canAddShowHiddenButton: Boolean = false,
val forceShowRoot: Boolean = false,
val showFavoritesButton: Boolean = false,
val callback: (pickedPath: String) -> Unit) : Breadcrumbs.BreadcrumbsListener {
class FilePickerDialog(
val activity: BaseSimpleActivity,
var currPath: String = Environment.getExternalStorageDirectory().toString(),
val pickFile: Boolean = true,
var showHidden: Boolean = false,
val showFAB: Boolean = false,
val canAddShowHiddenButton: Boolean = false,
val forceShowRoot: Boolean = false,
val showFavoritesButton: Boolean = false,
val callback: (pickedPath: String) -> Unit
) : Breadcrumbs.BreadcrumbsListener {
private var mFirstUpdate = true
private var mPrevPath = ""
@ -204,6 +206,11 @@ class FilePickerDialog(val activity: BaseSimpleActivity,
if ((pickFile && fileDocument.isFile) || (!pickFile && fileDocument.isDirectory)) {
sendSuccess()
}
} else if (activity.isRestrictedAndroidDir(currPath)) {
val document = activity.getSomePrimaryAndroidSAFDocument(currPath) ?: return
if ((pickFile && document.isFile) || (!pickFile && document.isDirectory)) {
sendSuccess()
}
} else {
val file = File(currPath)
if ((pickFile && file.isFile) || (!pickFile && file.isDirectory)) {
@ -233,31 +240,39 @@ class FilePickerDialog(val activity: BaseSimpleActivity,
private fun getRegularItems(path: String, lastModifieds: HashMap<String, Long>, callback: (List<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>()
val base = File(path)
val files = base.listFiles()
if (files == null) {
if (activity.isRestrictedAndroidDir(path)) {
activity.handlePrimarySAFDialog(path) {
activity.getStorageItemsWithTreeUri(path, showHidden) {
callback(it)
}
}
} else {
val base = File(path)
val files = base.listFiles()
if (files == null) {
callback(items)
return
}
for (file in files) {
if (!showHidden && file.name.startsWith('.')) {
continue
}
val curPath = file.absolutePath
val curName = curPath.getFilenameFromPath()
val size = file.length()
var lastModified = lastModifieds.remove(curPath)
val isDirectory = if (lastModified != null) false else file.isDirectory
if (lastModified == null) {
lastModified = 0 // we don't actually need the real lastModified that badly, do not check file.lastModified()
}
val children = if (isDirectory) file.getDirectChildrenCount(activity, showHidden) else 0
items.add(FileDirItem(curPath, curName, isDirectory, children, size, lastModified))
}
callback(items)
return
}
for (file in files) {
if (!showHidden && file.name.startsWith('.')) {
continue
}
val curPath = file.absolutePath
val curName = curPath.getFilenameFromPath()
val size = file.length()
var lastModified = lastModifieds.remove(curPath)
val isDirectory = if (lastModified != null) false else file.isDirectory
if (lastModified == null) {
lastModified = 0 // we don't actually need the real lastModified that badly, do not check file.lastModified()
}
val children = if (isDirectory) file.getDirectChildrenCount(showHidden) else 0
items.add(FileDirItem(curPath, curName, isDirectory, children, size, lastModified))
}
callback(items)
}
private fun containsDirectory(items: List<FileDirItem>) = items.any { it.isDirectory }

View file

@ -88,6 +88,13 @@ class PropertiesDialog() {
} catch (e: Exception) {
return@ensureBackgroundThread
}
} else if (activity.isRestrictedAndroidDir(path)) {
try {
ExifInterface(activity.contentResolver.openInputStream(activity.getPrimaryAndroidSAFUri(path))!!)
} catch (e: Exception) {
e.printStackTrace()
return@ensureBackgroundThread
}
} else {
ExifInterface(fileDirItem.path)
}
@ -110,7 +117,7 @@ class PropertiesDialog() {
when {
fileDirItem.isDirectory -> {
addProperty(R.string.direct_children_count, fileDirItem.getDirectChildrenCount(activity, countHiddenItems).toString())
addProperty(R.string.direct_children_count, fileDirItem.getDirectChildrenCount(activity as BaseSimpleActivity, countHiddenItems).toString())
addProperty(R.string.files_count, "", R.id.properties_file_count)
}
fileDirItem.path.isImageSlow() -> {
@ -144,7 +151,11 @@ class PropertiesDialog() {
if (activity.baseConfig.appId.removeSuffix(".debug") == "com.simplemobiletools.filemanager.pro") {
addProperty(R.string.md5, "", R.id.properties_md5)
ensureBackgroundThread {
val md5 = File(path).md5()
val md5 = if (activity.isRestrictedAndroidDir(path)) {
activity.contentResolver.openInputStream(activity.getPrimaryAndroidSAFUri(path))?.md5()
} else {
File(path).md5()
}
activity.runOnUiThread {
(view.findViewById<LinearLayout>(R.id.properties_md5).property_value as TextView).text = md5
}
@ -220,6 +231,13 @@ class PropertiesDialog() {
} catch (e: Exception) {
return
}
} else if (activity.isRestrictedAndroidDir(path)) {
try {
ExifInterface(activity.contentResolver.openInputStream(activity.getPrimaryAndroidSAFUri(path))!!)
} catch (e: Exception) {
e.printStackTrace()
return
}
} else {
ExifInterface(path)
}

View file

@ -43,6 +43,7 @@ import java.io.OutputStream
import java.util.*
import kotlin.collections.HashMap
import kotlinx.android.synthetic.main.dialog_title.view.*
import org.w3c.dom.Document
fun AppCompatActivity.updateActionBarTitle(text: String, color: Int = baseConfig.primaryColor) {
supportActionBar?.title = Html.fromHtml("<font color='${color.getContrastColor().toHex()}'>$text</font>")
@ -148,7 +149,7 @@ fun BaseSimpleActivity.isShowingSAFDialog(path: String): Boolean {
}
fun BaseSimpleActivity.isShowingSAFPrimaryDialog(path: String): Boolean {
return if (isSAFOnlyRoot(path) && (baseConfig.primaryAndroidTreeUri.isEmpty() || !hasProperStoredPrimaryTreeUri())) {
return if (isRPlus() && isSAFOnlyRoot(path) && (baseConfig.primaryAndroidTreeUri.isEmpty() || !hasProperStoredPrimaryTreeUri())) {
runOnUiThread {
if (!isDestroyed && !isFinishing) {
WritePermissionDialog(this, false) {
@ -628,7 +629,7 @@ fun BaseSimpleActivity.deleteFile(fileDirItem: FileDirItem, allowDeleteFolder: B
fun BaseSimpleActivity.deleteFileBg(fileDirItem: FileDirItem, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
val path = fileDirItem.path
if (isRPlus() && isSAFOnlyRoot(path)) {
if (isRestrictedAndroidDir(path)) {
deleteSAFOnlyDir(path, allowDeleteFolder, callback)
} else {
val file = File(path)
@ -707,7 +708,27 @@ fun Activity.rescanPaths(paths: List<String>, callback: (() -> Unit)? = null) {
}
fun BaseSimpleActivity.renameFile(oldPath: String, newPath: String, callback: ((success: Boolean) -> Unit)? = null) {
if (needsStupidWritePermissions(newPath)) {
if (isRestrictedAndroidDir(oldPath)) {
handlePrimarySAFDialog(oldPath) {
if (!it) {
runOnUiThread {
callback?.invoke(false)
}
return@handlePrimarySAFDialog
}
try {
val success = renameSAFOnlyDocument(oldPath, newPath)
runOnUiThread {
callback?.invoke(success)
}
} catch (e: Exception) {
showErrorToast(e)
runOnUiThread {
callback?.invoke(false)
}
}
}
} else if (needsStupidWritePermissions(newPath)) {
handleSAFDialog(newPath) {
if (!it) {
return@handleSAFDialog
@ -752,49 +773,35 @@ fun BaseSimpleActivity.renameFile(oldPath: String, newPath: String, callback: ((
}
}
} else {
if (isRestrictedAndroidDir(oldPath)) {
try {
val success = renameSAFOnlyDocument(oldPath, newPath)
runOnUiThread {
callback?.invoke(success)
val oldFile = File(oldPath)
val newFile = File(newPath)
val tempFile = oldFile.createTempFile()
val oldToTempSucceeds = oldFile.renameTo(tempFile)
val tempToNewSucceeds = tempFile.renameTo(newFile)
if (oldToTempSucceeds && tempToNewSucceeds) {
if (newFile.isDirectory) {
updateInMediaStore(oldPath, newPath)
rescanPath(newPath) {
runOnUiThread {
callback?.invoke(true)
}
scanPathRecursively(newPath)
}
} catch (e: Exception) {
showErrorToast(e)
runOnUiThread {
callback?.invoke(false)
} else {
if (!baseConfig.keepLastModified) {
newFile.setLastModified(System.currentTimeMillis())
}
updateInMediaStore(oldPath, newPath)
scanPathsRecursively(arrayListOf(newPath)) {
runOnUiThread {
callback?.invoke(true)
}
}
}
} else {
val oldFile = File(oldPath)
val newFile = File(newPath)
val tempFile = oldFile.createTempFile()
val oldToTempSucceeds = oldFile.renameTo(tempFile)
val tempToNewSucceeds = tempFile.renameTo(newFile)
if (oldToTempSucceeds && tempToNewSucceeds) {
if (newFile.isDirectory) {
updateInMediaStore(oldPath, newPath)
rescanPath(newPath) {
runOnUiThread {
callback?.invoke(true)
}
scanPathRecursively(newPath)
}
} else {
if (!baseConfig.keepLastModified) {
newFile.setLastModified(System.currentTimeMillis())
}
updateInMediaStore(oldPath, newPath)
scanPathsRecursively(arrayListOf(newPath)) {
runOnUiThread {
callback?.invoke(true)
}
}
}
} else {
tempFile.delete()
runOnUiThread {
callback?.invoke(false)
}
tempFile.delete()
runOnUiThread {
callback?.invoke(false)
}
}
}
@ -899,6 +906,12 @@ fun BaseSimpleActivity.getFileOutputStreamSync(path: String, mimeType: String, p
showErrorToast(e)
null
}
} else if (isRestrictedAndroidDir(path)) {
val uri = getPrimaryAndroidSAFUri(path)
if (!getDoesFilePathExist(path)) {
createSAFOnlyFile(path)
}
applicationContext.contentResolver.openOutputStream(uri)
} else {
if (targetFile.parentFile?.exists() == false) {
targetFile.parentFile.mkdirs()
@ -1021,6 +1034,10 @@ fun BaseSimpleActivity.createDirectorySync(directory: String): Boolean {
return newDir != null
}
if (isRestrictedAndroidDir(directory)) {
return createSAFOnlyDirectory(directory)
}
return File(directory).mkdirs()
}

View file

@ -153,7 +153,7 @@ fun Context.isPathOnSD(path: String) = sdCardPath.isNotEmpty() && path.startsWit
fun Context.isPathOnOTG(path: String) = otgPath.isNotEmpty() && path.startsWith(otgPath)
val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf("/Android")
val DIRS_ACCESSIBLE_ONLY_WITH_SAF = listOf("/Android/data", "/Android/obb")
fun Context.getSAFOnlyDirs(): List<String> {
return DIRS_ACCESSIBLE_ONLY_WITH_SAF.map { "$internalStoragePath$it" }
@ -480,12 +480,10 @@ fun Context.getOTGItems(path: String, shouldShowHidden: Boolean, getProperFileSi
@RequiresApi(Build.VERSION_CODES.O)
fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean, callback: (ArrayList<FileDirItem>) -> Unit) {
fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean = true, callback: (ArrayList<FileDirItem>) -> Unit) {
val items = ArrayList<FileDirItem>()
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val relativePath = path.substring(baseConfig.internalStoragePath.length).trim('/')
val documentId = "primary:$relativePath"
val documentId = getPrimaryAndroidSAFDocumentId(path)
val childrenUri = try {
DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
} catch (e: Exception) {
@ -499,7 +497,7 @@ fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean,
return
}
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE)
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, Document.COLUMN_LAST_MODIFIED)
val rawCursor = contentResolver.query(childrenUri, projection, null, null)!!
val cursor = ExternalStorageProviderHack.transformQueryResult(childrenUri, rawCursor)
cursor.use {
@ -508,6 +506,7 @@ fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean,
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
val name = cursor.getStringValue(Document.COLUMN_DISPLAY_NAME)
val mimeType = cursor.getStringValue(Document.COLUMN_MIME_TYPE)
val lastModified = cursor.getLongValue(Document.COLUMN_LAST_MODIFIED)
val isDirectory = mimeType == Document.MIME_TYPE_DIR
val filePath = docId.substring("primary:".length)
if (!shouldShowHidden && name.startsWith(".")) {
@ -522,12 +521,11 @@ fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean,
}
val childrenCount = if (isDirectory) {
getChildrenCount(treeUri, docId, shouldShowHidden)
getDirectChildrenCount(treeUri, docId, shouldShowHidden)
} else {
0
}
val lastModified = System.currentTimeMillis()
val fileDirItem = FileDirItem(decodedPath, name, isDirectory, childrenCount, fileSize, lastModified)
items.add(fileDirItem)
} while (cursor.moveToNext())
@ -536,7 +534,7 @@ fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean,
callback(items)
}
fun Context.getChildrenCount(treeUri: Uri, documentId: String, shouldShowHidden: Boolean): Int {
fun Context.getDirectChildrenCount(treeUri: Uri, documentId: String, shouldShowHidden: Boolean): Int {
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID)
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
@ -544,55 +542,152 @@ fun Context.getChildrenCount(treeUri: Uri, documentId: String, shouldShowHidden:
return if (shouldShowHidden) {
cursor.count
} else {
val children = mutableListOf<String>()
var count = 0
cursor.use {
while (cursor.moveToNext()) {
children.add(cursor.getStringValue(Document.COLUMN_DOCUMENT_ID))
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
if (!docId.getFilenameFromPath().startsWith('.') || shouldShowHidden) {
count++
}
}
}
children.filter { !it.getFilenameFromPath().startsWith(".") }.size
count
}
}
fun Context.getProperChildrenCount(treeUri: Uri, documentId: String, shouldShowHidden: Boolean): Int {
val projection = arrayOf(Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE)
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
val rawCursor = contentResolver.query(childrenUri, projection, null, null, null)!!
val cursor = ExternalStorageProviderHack.transformQueryResult(childrenUri, rawCursor)
return if (cursor.count > 0) {
var count = 0
cursor.use {
while (cursor.moveToNext()) {
val docId = cursor.getStringValue(Document.COLUMN_DOCUMENT_ID)
val mimeType = cursor.getStringValue(Document.COLUMN_MIME_TYPE)
if (mimeType == Document.MIME_TYPE_DIR) {
count++
count += getProperChildrenCount(treeUri, docId, shouldShowHidden)
} else if (!docId.getFilenameFromPath().startsWith('.') || shouldShowHidden) {
count++
}
}
}
count
} else {
1
}
}
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)
}
val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return contentResolver.query(documentUri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getLongValue(Document.COLUMN_SIZE) else 0L
} ?: 0L
}
fun Context.getPrimaryAndroidSAFDocumentId(path: String): String {
val relativePath = path.substring(baseConfig.internalStoragePath.length).trim('/')
return "primary:$relativePath"
}
fun Context.getPrimaryAndroidSAFUri(path: String): Uri {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
return DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
}
fun Context.getPrimaryAndroidSAFDocument(path: String): DocumentFile? {
val primaryAndroidPath = File(internalStoragePath, "Android").path
var relativePath = path.substring(primaryAndroidPath.length)
if (relativePath.startsWith(File.separator)) {
relativePath = relativePath.substring(1)
}
return size
return try {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
var document = DocumentFile.fromTreeUri(applicationContext, treeUri)
val files = document?.listFiles()?.map { it.name }
val parts = relativePath.split("/").filter { it.isNotEmpty() }
for (part in parts) {
document = document?.findFile(part)
}
document
} catch (ignored: Exception) {
null
}
}
fun Context.getSomePrimaryAndroidSAFDocument(path: String): DocumentFile? = getFastPrimaryAndroidSAFDocument(path) ?: getPrimaryAndroidSAFDocument(path)
fun Context.getFastPrimaryAndroidSAFDocument(path: String): DocumentFile? {
val uri = getPrimaryAndroidSAFUri(path)
return DocumentFile.fromSingleUri(this, uri)
}
fun Context.getPrimaryAndroidSAFChildrenUri(path: String): Uri {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
return DocumentsContract.buildChildDocumentsUriUsingTree(treeUri, documentId)
}
fun Context.createSAFOnlyDirectory(path: String): Boolean {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val relativePath = path.getParentPath().substring(baseConfig.internalStoragePath.length).trim('/')
val documentId = "primary:$relativePath"
val parentPath = path.getParentPath()
if (!getDoesFilePathExist(parentPath)) {
createSAFOnlyDirectory(parentPath)
}
val documentId = getPrimaryAndroidSAFDocumentId(parentPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return DocumentsContract.createDocument(contentResolver, parentUri, Document.MIME_TYPE_DIR, path.getFilenameFromPath()) != null
val createdUri = DocumentsContract.createDocument(contentResolver, parentUri, Document.MIME_TYPE_DIR, path.getFilenameFromPath())
return createdUri != null
}
fun Context.createSAFOnlyFile(path: String): Boolean {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val relativePath = path.getParentPath().substring(baseConfig.internalStoragePath.length).trim('/')
val documentId = "primary:$relativePath"
val documentId = getPrimaryAndroidSAFDocumentId(path.getParentPath())
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return DocumentsContract.createDocument(contentResolver, parentUri, path.getMimeType(), path.getFilenameFromPath()) != null
}
fun Context.renameSAFOnlyDocument(oldPath: String, newPath: String): Boolean {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val relativePath = oldPath.substring(baseConfig.internalStoragePath.length).trim('/')
val documentId = "primary:$relativePath"
val documentId = getPrimaryAndroidSAFDocumentId(oldPath)
val parentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return DocumentsContract.renameDocument(contentResolver, parentUri, newPath.getFilenameFromPath()) != null
}
private const val TAG = "Context-storage"
fun Context.getSAFOnlyFileSize(path: String): Long {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
val size = getFileSize(treeUri, documentId)
return size
}
fun Context.getSAFOnlyFileCount(path: String, countHidden: Boolean): Int {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
val size = getProperChildrenCount(treeUri, documentId, countHidden)
return size
}
fun Context.getSAFOnlyDirectChildrenCount(path: String, countHidden: Boolean): Int {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
return getDirectChildrenCount(treeUri, documentId, countHidden)
}
fun Context.getSAFOnlyLastModified(path: String): Long {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
val documentId = getPrimaryAndroidSAFDocumentId(path)
val projection = arrayOf(Document.COLUMN_LAST_MODIFIED)
val documentUri = DocumentsContract.buildDocumentUriUsingTree(treeUri, documentId)
return contentResolver.query(documentUri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getLongValue(Document.COLUMN_LAST_MODIFIED) else 0L
} ?: 0L
}
fun Context.deleteSAFOnlyDir(path: String, allowDeleteFolder: Boolean = false, callback: ((wasSuccess: Boolean) -> Unit)? = null) {
val treeUri = baseConfig.primaryAndroidTreeUri.toUri()
@ -633,6 +728,9 @@ fun Context.getFileInputStreamSync(path: String): InputStream? {
return if (isPathOnOTG(path)) {
val fileDocument = getSomeDocumentFile(path)
applicationContext.contentResolver.openInputStream(fileDocument?.uri!!)
} else if (isRestrictedAndroidDir(path)) {
val uri = getPrimaryAndroidSAFUri(path)
applicationContext.contentResolver.openInputStream(uri)
} else {
FileInputStream(File(path))
}
@ -649,7 +747,9 @@ fun Context.updateOTGPathFromPartition() {
fun Context.getDoesFilePathExist(path: String, otgPathToUse: String? = null): Boolean {
val otgPath = otgPathToUse ?: baseConfig.OTGPath
return if (otgPath.isNotEmpty() && path.startsWith(otgPath)) {
return if (isRestrictedAndroidDir(path)) {
getFastPrimaryAndroidSAFDocument(path)?.exists() ?: false
} else if (otgPath.isNotEmpty() && path.startsWith(otgPath)) {
getOTGFastDocumentFile(path)?.exists() ?: false
} else {
File(path).exists()
@ -657,7 +757,9 @@ fun Context.getDoesFilePathExist(path: String, otgPathToUse: String? = null): Bo
}
fun Context.getIsPathDirectory(path: String): Boolean {
return if (isPathOnOTG(path)) {
return if (isRestrictedAndroidDir(path)) {
getFastPrimaryAndroidSAFDocument(path)?.isDirectory ?: false
} else if (isPathOnOTG(path)) {
getOTGFastDocumentFile(path)?.isDirectory ?: false
} else {
File(path).isDirectory

View file

@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.content.pm.ShortcutManager
import android.content.res.Configuration
import android.database.Cursor
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.Point
import android.media.MediaMetadataRetriever
@ -383,7 +384,9 @@ fun Context.getMimeTypeFromUri(uri: Uri): String {
}
fun Context.ensurePublicUri(path: String, applicationId: String): Uri? {
return if (isPathOnOTG(path)) {
return if (isRestrictedAndroidDir(path)) {
getPrimaryAndroidSAFUri(path)
} else if (isPathOnOTG(path)) {
getDocumentFile(path)?.uri
} else {
val uri = Uri.parse(path)
@ -743,7 +746,7 @@ fun Context.getTimeFormat() = if (baseConfig.use24HourFormat) TIME_FORMAT_24 els
fun Context.getResolution(path: String): Point? {
return if (path.isImageFast() || path.isImageSlow()) {
path.getImageResolution()
getImageResolution(path)
} else if (path.isVideoFast() || path.isVideoSlow()) {
getVideoResolution(path)
} else {
@ -751,10 +754,31 @@ fun Context.getResolution(path: String): Point? {
}
}
fun Context.getImageResolution(path: String): Point? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
if (isRestrictedAndroidDir(path)) {
BitmapFactory.decodeStream(contentResolver.openInputStream(getPrimaryAndroidSAFUri(path)), null, options)
} else {
BitmapFactory.decodeFile(path, options)
}
val width = options.outWidth
val height = options.outHeight
return if (width > 0 && height > 0) {
Point(options.outWidth, options.outHeight)
} else {
null
}
}
fun Context.getVideoResolution(path: String): Point? {
var point = try {
val retriever = MediaMetadataRetriever()
retriever.setDataSource(path)
if (isRestrictedAndroidDir(path)) {
retriever.setDataSource(this, getPrimaryAndroidSAFUri(path))
} else {
retriever.setDataSource(path)
}
val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)!!.toInt()
val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)!!.toInt()
Point(width, height)

View file

@ -76,7 +76,7 @@ private fun getDirectoryFileCount(dir: File, countHiddenItems: Boolean): Int {
return count
}
fun File.getDirectChildrenCount(countHiddenItems: Boolean) = listFiles()?.filter { if (countHiddenItems) true else !it.name.startsWith('.') }?.size
fun File.getDirectChildrenCount(context: Context, countHiddenItems: Boolean) = if(context.isSAFOnlyRoot(path)) context.getSAFOnlyDirectChildrenCount(path, countHiddenItems) else listFiles()?.filter { if (countHiddenItems) true else !it.name.startsWith('.') }?.size
?: 0
fun File.toFileDirItem(context: Context) = FileDirItem(absolutePath, name, context.getIsPathDirectory(absolutePath), 0, length(), lastModified())
@ -128,17 +128,7 @@ fun File.doesParentHaveNoMedia(): Boolean {
}
fun File.getDigest(algorithm: String): String {
return inputStream().use { fis ->
val md = MessageDigest.getInstance(algorithm)
val buffer = ByteArray(8192)
generateSequence {
when (val bytesRead = fis.read(buffer)) {
-1 -> null
else -> bytesRead
}
}.forEach { bytesRead -> md.update(buffer, 0, bytesRead) }
md.digest().joinToString("") { "%02x".format(it) }
}
return inputStream().getDigest(algorithm)
}
fun File.md5(): String = this.getDigest(MD5)

View file

@ -71,7 +71,7 @@ fun String.isImageFast() = photoExtensions.any { endsWith(it, true) }
fun String.isAudioFast() = audioExtensions.any { endsWith(it, true) }
fun String.isRawFast() = rawExtensions.any { endsWith(it, true) }
fun String.isImageSlow() = isImageFast() || getMimeType().startsWith("image") || startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
fun String. isImageSlow() = isImageFast() || getMimeType().startsWith("image") || startsWith(MediaStore.Images.Media.EXTERNAL_CONTENT_URI.toString())
fun String.isVideoSlow() = isVideoFast() || getMimeType().startsWith("video") || startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())
fun String.isAudioSlow() = isAudioFast() || getMimeType().startsWith("audio") || startsWith(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.toString())
@ -94,16 +94,21 @@ fun String.getGenericMimeType(): String {
}
fun String.getParentPath() = removeSuffix("/${getFilenameFromPath()}")
fun String.relativizeWith(path: String) = this.substring(path.length)
fun String.containsNoMedia() = File(this).containsNoMedia()
fun String.doesThisOrParentHaveNoMedia(folderNoMediaStatuses: HashMap<String, Boolean>, callback: ((path: String, hasNoMedia: Boolean) -> Unit)?) =
File(this).doesThisOrParentHaveNoMedia(folderNoMediaStatuses, callback)
fun String.getImageResolution(): Point? {
fun String.getImageResolution(context: Context): Point? {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(this, options)
if(context.isRestrictedAndroidDir(this)){
BitmapFactory.decodeStream(context.contentResolver.openInputStream(context.getPrimaryAndroidSAFUri(this)), null, options)
}else{
BitmapFactory.decodeFile(this, options)
}
val width = options.outWidth
val height = options.outHeight
return if (width > 0 && height > 0) {

View file

@ -1,5 +1,6 @@
package com.simplemobiletools.commons.models
import android.app.Activity
import android.content.Context
import android.net.Uri
import com.bumptech.glide.signature.ObjectKey
@ -7,7 +8,14 @@ import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.*
import java.io.File
open class FileDirItem(val path: String, val name: String = "", var isDirectory: Boolean = false, var children: Int = 0, var size: Long = 0L, var modified: Long = 0L) :
open class FileDirItem(
val path: String,
val name: String = "",
var isDirectory: Boolean = false,
var children: Int = 0,
var size: Long = 0L,
var modified: Long = 0L
) :
Comparable<FileDirItem> {
companion object {
var sorting = 0
@ -72,24 +80,42 @@ open class FileDirItem(val path: String, val name: String = "", var isDirectory:
} catch (e: Exception) {
context.getSizeFromContentUri(Uri.parse(path))
}
} else if (context.isRestrictedAndroidDir(path)) {
context.getSAFOnlyFileSize(path)
} else {
File(path).getProperSize(countHidden)
}
}
fun getProperFileCount(context: Context, countHidden: Boolean): Int {
return if (context.isPathOnOTG(path)) {
context.getDocumentFile(path)?.getFileCount(countHidden) ?: 0
} else {
File(path).getFileCount(countHidden)
return when {
context.isPathOnOTG(path) -> {
context.getDocumentFile(path)?.getFileCount(countHidden) ?: 0
}
context.isRestrictedAndroidDir(path) -> {
context.getSAFOnlyFileCount(path, countHidden)
}
else -> {
File(path).getFileCount(countHidden)
}
}
}
fun getDirectChildrenCount(context: Context, countHiddenItems: Boolean): Int {
return if (context.isPathOnOTG(path)) {
context.getDocumentFile(path)?.listFiles()?.filter { if (countHiddenItems) true else !it.name!!.startsWith(".") }?.size ?: 0
} else {
File(path).getDirectChildrenCount(countHiddenItems)
return when {
context.isPathOnOTG(path) -> {
context.getDocumentFile(path)?.listFiles()?.filter { if (countHiddenItems) true else !it.name!!.startsWith(".") }?.size ?: 0
}
context.isRestrictedAndroidDir(path) -> {
try {
context.getSAFOnlyDirectChildrenCount(path, countHiddenItems)
} catch (e: Exception) {
0
}
}
else -> {
File(path).getDirectChildrenCount(context, countHiddenItems)
}
}
}
@ -98,6 +124,8 @@ open class FileDirItem(val path: String, val name: String = "", var isDirectory:
context.getFastDocumentFile(path)?.lastModified() ?: 0L
} else if (isNougatPlus() && path.startsWith("content://")) {
context.getMediaStoreLastModified(path)
} else if (context.isRestrictedAndroidDir(path)) {
context.getSAFOnlyLastModified(path)
} else {
File(path).lastModified()
}
@ -119,7 +147,7 @@ open class FileDirItem(val path: String, val name: String = "", var isDirectory:
fun getVideoResolution(context: Context) = context.getVideoResolution(path)
fun getImageResolution() = path.getImageResolution()
fun getImageResolution(context: Context) = context.getImageResolution(path)
fun getPublicUri(context: Context) = context.getDocumentFile(path)?.uri ?: ""