From a78151456745964455f4cff5503cbab439c50ee2 Mon Sep 17 00:00:00 2001 From: darthpaul Date: Tue, 2 Nov 2021 21:54:00 +0000 Subject: [PATCH] handle scoped storage changes --- .../commons/activities/BaseSimpleActivity.kt | 14 +- .../adapters/FilepickerItemsAdapter.kt | 23 ++- .../commons/asynctasks/CopyMoveTask.kt | 19 +- .../commons/dialogs/CreateNewFolderDialog.kt | 3 +- .../commons/dialogs/FilePickerDialog.kt | 79 +++++---- .../commons/dialogs/PropertiesDialog.kt | 22 ++- .../commons/extensions/Activity.kt | 101 ++++++----- .../commons/extensions/Context-storage.kt | 164 ++++++++++++++---- .../commons/extensions/Context.kt | 30 +++- .../commons/extensions/File.kt | 14 +- .../commons/extensions/String.kt | 11 +- .../commons/models/FileDirItem.kt | 48 +++-- 12 files changed, 374 insertions(+), 154 deletions(-) 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 1425a283d..5a0ec2fbc 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/activities/BaseSimpleActivity.kt @@ -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) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/adapters/FilepickerItemsAdapter.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/adapters/FilepickerItemsAdapter.kt index 5f11ba6d5..ec63587a0 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/adapters/FilepickerItemsAdapter.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/adapters/FilepickerItemsAdapter.kt @@ -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, recyclerView: MyRecyclerView, - itemClick: (Any) -> Unit) : MyRecyclerViewAdapter(activity, recyclerView, null, itemClick) { +class FilepickerItemsAdapter( + activity: BaseSimpleActivity, val fileDirItems: List, 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()) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/asynctasks/CopyMoveTask.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/asynctasks/CopyMoveTask.kt index 80627f27c..0b48fdcb3 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/asynctasks/CopyMoveTask.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/asynctasks/CopyMoveTask.kt @@ -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, @@ -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() diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CreateNewFolderDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CreateNewFolderDialog.kt index b7b4d693f..3e4f491f2 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CreateNewFolderDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/CreateNewFolderDialog.kt @@ -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) } diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt index 5ecdcf932..55541ca60 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/FilePickerDialog.kt @@ -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, callback: (List) -> Unit) { val items = ArrayList() - 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) = items.any { it.isDirectory } diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/PropertiesDialog.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/PropertiesDialog.kt index 1bef345a2..bffe119d6 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/PropertiesDialog.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/dialogs/PropertiesDialog.kt @@ -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(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) } 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 79b45bccf..294d445f1 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Activity.kt @@ -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("$text") @@ -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, 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() } 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 a8a7e645c..7781facd9 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 @@ -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 { 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) -> Unit) { +fun Context.getStorageItemsWithTreeUri(path: String, shouldShowHidden: Boolean, getProperFileSize: Boolean = true, callback: (ArrayList) -> Unit) { val items = ArrayList() 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() + 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 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 adcc89660..5a9000506 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/Context.kt @@ -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) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/File.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/File.kt index 2c25eb421..f24ae4f96 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/File.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/File.kt @@ -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) diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt index d71bb7e61..70fa38cb2 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/extensions/String.kt @@ -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, 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) { diff --git a/commons/src/main/kotlin/com/simplemobiletools/commons/models/FileDirItem.kt b/commons/src/main/kotlin/com/simplemobiletools/commons/models/FileDirItem.kt index 5c83bde2f..20308ebbe 100644 --- a/commons/src/main/kotlin/com/simplemobiletools/commons/models/FileDirItem.kt +++ b/commons/src/main/kotlin/com/simplemobiletools/commons/models/FileDirItem.kt @@ -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 { 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 ?: ""