Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 69 additions & 43 deletions commons/src/main/kotlin/org/fossify/commons/extensions/Activity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.content.Intent
import android.content.Intent.EXTRA_STREAM
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.media.MediaScannerConnection
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
Expand Down Expand Up @@ -89,6 +90,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream
import java.util.TreeSet
import java.util.concurrent.atomic.AtomicInteger

fun Activity.appLaunched(appId: String) {
baseConfig.internalStoragePath = getInternalStoragePath()
Expand Down Expand Up @@ -979,6 +981,56 @@ fun Activity.rescanPaths(paths: List<String>, callback: (() -> Unit)? = null) {
applicationContext.rescanPaths(paths, callback)
}

private fun BaseSimpleActivity.readRenameMediaEntries(path: String): List<Pair<Long, String>> {
if (isRPlus()) return emptyList()
val oldPath = File(path).canonicalPath
val prefix = "$oldPath/"
val entries = ArrayList<Pair<Long, String>>()
contentResolver.query(
MediaStore.Files.getContentUri("external"),
arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DATA),
"${MediaStore.MediaColumns.DATA} = ? OR substr(${MediaStore.MediaColumns.DATA}, 1, ?) = ?",
arrayOf(oldPath, prefix.length.toString(), prefix), null
)?.use { cursor ->
while (cursor.moveToNext()) entries.add(cursor.getLong(0) to cursor.getString(1))
} ?: throw IOException("Could not read media entries before renaming")
return entries
}

private fun BaseSimpleActivity.refreshRenamedPath(
oldPath: String,
newPath: String,
entries: List<Pair<Long, String>>,
callback: ((Boolean, Android30RenameFormat) -> Unit)?
) {
try {
val oldMediaPath = File(oldPath).canonicalPath
val newMediaPath = File(newPath).canonicalPath
// Older providers scan descendants when a directory entry moves, so move the child entries first.
for ((id, path) in entries.sortedByDescending { it.second.length }) {
val renamedPath = newMediaPath + path.removePrefix(oldMediaPath)
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DATA, renamedPath)
put(MediaStore.MediaColumns.DISPLAY_NAME, renamedPath.getFilenameFromPath())
put(MediaStore.MediaColumns.TITLE, renamedPath.getFilenameFromPath())
}
contentResolver.update(MediaStore.Files.getContentUri("external"), values,
"_id = ? AND ${MediaStore.MediaColumns.DATA} = ?", arrayOf(id.toString(), path))
}
val paths = getPaths(File(newMediaPath))
val remaining = AtomicInteger(paths.size)
// Scan the new paths once. A missing old path can match a sibling on older Android versions.
MediaScannerConnection.scanFile(applicationContext, paths.toTypedArray(), null) { _, _ ->
if (remaining.decrementAndGet() == 0) {
runOnUiThread { callback?.invoke(true, Android30RenameFormat.NONE) }
}
}
} catch (e: Exception) {
showErrorToast(e)
runOnUiThread { callback?.invoke(false, Android30RenameFormat.NONE) }
}
}

fun BaseSimpleActivity.renameFile(
oldPath: String,
newPath: String,
Expand Down Expand Up @@ -1019,18 +1071,10 @@ fun BaseSimpleActivity.renameFile(

try {
ensureBackgroundThread {
val mediaEntries = readRenameMediaEntries(oldPath)
val success = renameDocumentSdk30(oldPath, newPath)
if (success) {
updateInMediaStore(oldPath, newPath)
rescanPath(newPath) {
runOnUiThread {
callback?.invoke(true, Android30RenameFormat.NONE)
}
if (!oldPath.equals(newPath, true)) {
deleteFromMediaStore(oldPath)
}
scanPathRecursively(newPath)
}
refreshRenamedPath(oldPath, newPath, mediaEntries, callback)
} else {
runOnUiThread {
callback?.invoke(false, Android30RenameFormat.NONE)
Expand Down Expand Up @@ -1062,6 +1106,13 @@ fun BaseSimpleActivity.renameFile(

try {
ensureBackgroundThread {
val mediaEntries = try {
readRenameMediaEntries(oldPath)
} catch (e: Exception) {
showErrorToast(e)
runOnUiThread { callback?.invoke(false, Android30RenameFormat.NONE) }
return@ensureBackgroundThread
}
try {
DocumentsContract.renameDocument(applicationContext.contentResolver, document.uri, newPath.getFilenameFromPath())
} catch (ignored: FileNotFoundException) {
Expand All @@ -1072,16 +1123,10 @@ fun BaseSimpleActivity.renameFile(
return@ensureBackgroundThread
}

updateInMediaStore(oldPath, newPath)
rescanPaths(arrayListOf(oldPath, newPath)) {
if (!baseConfig.keepLastModified) {
updateLastModified(newPath, System.currentTimeMillis())
}
deleteFromMediaStore(oldPath)
runOnUiThread {
callback?.invoke(true, Android30RenameFormat.NONE)
}
if (!baseConfig.keepLastModified) {
updateLastModified(newPath, System.currentTimeMillis())
}
refreshRenamedPath(oldPath, newPath, mediaEntries, callback)
}
} catch (e: Exception) {
showErrorToast(e)
Expand All @@ -1101,7 +1146,9 @@ private fun BaseSimpleActivity.renameCasually(
) {
val oldFile = File(oldPath)
val newFile = File(newPath)
val mediaEntries: List<Pair<Long, String>>
val tempFile = try {
mediaEntries = readRenameMediaEntries(oldPath)
createTempFile(oldFile) ?: return
} catch (exception: Exception) {
if (isRPlus() && exception is java.nio.file.FileSystemException) {
Expand Down Expand Up @@ -1142,31 +1189,10 @@ private fun BaseSimpleActivity.renameCasually(
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, Android30RenameFormat.NONE)
}
if (!oldPath.equals(newPath, true)) {
deleteFromMediaStore(oldPath)
}
scanPathRecursively(newPath)
}
} else {
if (!baseConfig.keepLastModified) {
newFile.setLastModified(System.currentTimeMillis())
}
updateInMediaStore(oldPath, newPath)
scanPathsRecursively(arrayListOf(newPath)) {
if (!oldPath.equals(newPath, true)) {
deleteFromMediaStore(oldPath)
}
runOnUiThread {
callback?.invoke(true, Android30RenameFormat.NONE)
}
}
if (!newFile.isDirectory && !baseConfig.keepLastModified) {
newFile.setLastModified(System.currentTimeMillis())
}
refreshRenamedPath(oldPath, newPath, mediaEntries, callback)
} else {
tempFile.delete()
newFile.delete()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import java.util.Locale

class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(context, attrs), RenameTab {
var ignoreClicks = false
var stopLooping = false // we should request the permission on Android 30+ for all uris at once, not one by one
var currentIncrementalNumber = 1
var numbersCnt = 0
var activity: BaseSimpleActivity? = null
Expand All @@ -43,7 +42,6 @@ class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(c
}

override fun dialogConfirmed(useMediaFileExtension: Boolean, callback: (success: Boolean) -> Unit) {
stopLooping = false
if (ignoreClicks) {
return
}
Expand Down Expand Up @@ -79,40 +77,53 @@ class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(c
}

ignoreClicks = true
var pathsCnt = validPaths.size
numbersCnt = pathsCnt.toString().length
for (path in validPaths) {
if (stopLooping) {
return@checkManageMediaOrHandleSAFDialogSdk30
numbersCnt = validPaths.size.toString().length
fun renameNext(index: Int) {
if (index == validPaths.size) {
ignoreClicks = false
callback(true)
return
}

val path = validPaths[index]
val incrementalNumber = currentIncrementalNumber
try {
val newPath = getNewPath(path, useMediaFileExtension) ?: continue
val newPath = getNewPath(path, useMediaFileExtension, newNameRaw)
if (newPath == null) {
ignoreClicks = false
callback(false)
return
}
activity?.renameFile(path, newPath, true) { success, android30Format ->
if (success) {
pathsCnt--
if (pathsCnt == 0) {
callback(true)
}
renameNext(index + 1)
} else {
ignoreClicks = false
if (android30Format != Android30RenameFormat.NONE) {
currentIncrementalNumber = 1
stopLooping = true
renameAllFiles(validPaths, useMediaFileExtension, android30Format, callback)
currentIncrementalNumber = incrementalNumber
renameAllFiles(
validPaths.drop(index),
useMediaFileExtension,
android30Format,
newNameRaw,
callback
)
} else {
callback(false)
}
}
}
} catch (e: Exception) {
activity?.showErrorToast(e)
ignoreClicks = false
callback(false)
}
}
stopLooping = false
renameNext(0)
}
}
}

private fun getNewPath(path: String, useMediaFileExtension: Boolean): String? {
private fun getNewPath(path: String, useMediaFileExtension: Boolean, renamePattern: String): String? {
try {
val exif = ExifInterface(path)
var dateTime = if (isNougatPlus()) {
Expand Down Expand Up @@ -140,7 +151,7 @@ class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(c
val minutes = (cal.get(Calendar.MINUTE)).ensureTwoDigits()
val seconds = (cal.get(Calendar.SECOND)).ensureTwoDigits()

var newName = binding.renameItemsValue.value
var newName = renamePattern
.replace("%Y", year, false)
.replace("%M", month, false)
.replace("%D", day, false)
Expand Down Expand Up @@ -185,6 +196,7 @@ class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(c
paths: List<String>,
useMediaFileExtension: Boolean,
android30Format: Android30RenameFormat,
renamePattern: String,
callback: (success: Boolean) -> Unit,
) {
val fileDirItems = paths.map { File(it).toFileDirItem(context) }
Expand All @@ -197,7 +209,8 @@ class RenamePatternTab(context: Context, attrs: AttributeSet) : RelativeLayout(c
try {
uris.forEachIndexed { index, uri ->
val path = validPaths[index]
val newFileName = getNewPath(path, useMediaFileExtension)?.getFilenameFromPath() ?: return@forEachIndexed
val newFileName = getNewPath(path, useMediaFileExtension, renamePattern)
?.getFilenameFromPath() ?: return@forEachIndexed
when (android30Format) {
Android30RenameFormat.SAF -> {
val sourceFile = File(path).toFileDirItem(context)
Expand Down
Loading