diff --git a/gradle.properties b/gradle.properties index 91904d1..416b4a3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -22,7 +22,7 @@ kotlin.code.style=official # For publishing: GROUP=com.anggrayudi POM_ARTIFACT_ID=storage -VERSION_NAME=1.5.5-SNAPSHOT +VERSION_NAME=1.5.6-SNAPSHOT RELEASE_SIGNING_ENABLED=true SONATYPE_AUTOMATIC_RELEASE=true SONATYPE_HOST=DEFAULT diff --git a/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt b/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt index 1736502..0b84baf 100644 --- a/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt +++ b/sample/src/main/java/com/anggrayudi/storage/sample/activity/MainActivity.kt @@ -27,7 +27,18 @@ import com.anggrayudi.storage.callback.FileCallback import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.callback.MultipleFileCallback import com.anggrayudi.storage.extension.launchOnUiThread -import com.anggrayudi.storage.file.* +import com.anggrayudi.storage.file.FileSize +import com.anggrayudi.storage.file.baseName +import com.anggrayudi.storage.file.changeName +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.copyFolderTo +import com.anggrayudi.storage.file.copyTo +import com.anggrayudi.storage.file.fullName +import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.moveFileTo +import com.anggrayudi.storage.file.moveFolderTo +import com.anggrayudi.storage.file.moveTo +import com.anggrayudi.storage.file.openOutputStream import com.anggrayudi.storage.permission.ActivityPermissionRequest import com.anggrayudi.storage.permission.PermissionCallback import com.anggrayudi.storage.permission.PermissionReport @@ -35,7 +46,11 @@ import com.anggrayudi.storage.permission.PermissionResult import com.anggrayudi.storage.sample.R import com.anggrayudi.storage.sample.StorageInfoAdapter import com.anggrayudi.storage.sample.databinding.ActivityMainBinding -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.Runnable +import kotlinx.coroutines.launch import timber.log.Timber import java.io.IOException import kotlin.concurrent.thread @@ -280,7 +295,7 @@ class MainActivity : AppCompatActivity() { binding.layoutMoveMultipleFilesTargetFolder.btnBrowse.setOnClickListener { storageHelper.openFolderPicker(REQUEST_CODE_PICK_TARGET_FOLDER_FOR_MULTIPLE_FILE_MOVE) } - binding.btnStartCopyMultipleFiles.setOnClickListener { + binding.btnStartMoveMultipleFiles.setOnClickListener { val targetFolder = binding.layoutMoveMultipleFilesTargetFolder.tvFilePath.tag as? DocumentFile if (targetFolder == null) { Toast.makeText(this, "Please select target folder", Toast.LENGTH_SHORT).show() diff --git a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt b/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt index 967eb20..36c3cd5 100644 --- a/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt +++ b/storage/src/main/java/com/anggrayudi/storage/file/DocumentFileExt.kt @@ -18,8 +18,35 @@ import androidx.core.content.FileProvider import androidx.core.content.MimeTypeFilter import androidx.documentfile.provider.DocumentFile import com.anggrayudi.storage.SimpleStorage -import com.anggrayudi.storage.callback.* -import com.anggrayudi.storage.extension.* +import com.anggrayudi.storage.callback.BaseFileCallback +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.callback.FileConflictCallback +import com.anggrayudi.storage.callback.FolderCallback +import com.anggrayudi.storage.callback.MultipleFileCallback +import com.anggrayudi.storage.callback.ZipCompressionCallback +import com.anggrayudi.storage.callback.ZipDecompressionCallback +import com.anggrayudi.storage.extension.awaitUiResult +import com.anggrayudi.storage.extension.awaitUiResultWithPending +import com.anggrayudi.storage.extension.childOf +import com.anggrayudi.storage.extension.closeEntryQuietly +import com.anggrayudi.storage.extension.closeStreamQuietly +import com.anggrayudi.storage.extension.fromTreeUri +import com.anggrayudi.storage.extension.getStorageId +import com.anggrayudi.storage.extension.hasParent +import com.anggrayudi.storage.extension.isDocumentsDocument +import com.anggrayudi.storage.extension.isDownloadsDocument +import com.anggrayudi.storage.extension.isExternalStorageDocument +import com.anggrayudi.storage.extension.isKitkatSdCardStorageId +import com.anggrayudi.storage.extension.isMediaDocument +import com.anggrayudi.storage.extension.isRawFile +import com.anggrayudi.storage.extension.isTreeDocumentFile +import com.anggrayudi.storage.extension.openInputStream +import com.anggrayudi.storage.extension.openOutputStream +import com.anggrayudi.storage.extension.parent +import com.anggrayudi.storage.extension.postToUi +import com.anggrayudi.storage.extension.startCoroutineTimer +import com.anggrayudi.storage.extension.toDocumentFile +import com.anggrayudi.storage.extension.trimFileSeparator import com.anggrayudi.storage.file.DocumentFileCompat.removeForbiddenCharsFromFilename import com.anggrayudi.storage.file.StorageId.DATA import com.anggrayudi.storage.file.StorageId.PRIMARY @@ -27,8 +54,13 @@ import com.anggrayudi.storage.media.FileDescription import com.anggrayudi.storage.media.MediaFile import com.anggrayudi.storage.media.MediaStoreCompat import kotlinx.coroutines.Job -import java.io.* -import java.util.* +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.io.InputStream +import java.io.InterruptedIOException +import java.io.OutputStream +import java.util.Date import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream @@ -347,6 +379,7 @@ fun DocumentFile.child(context: Context, path: String, requiresWriteAccess: Bool } file?.takeIfWritable(context, requiresWriteAccess) } + else -> null } } @@ -445,6 +478,7 @@ fun DocumentFile.getBasePath(context: Context): String { else -> path.substringAfterLast(SimpleStorage.externalStoragePath, "").trimFileSeparator() } } + else -> "" } } @@ -898,6 +932,7 @@ fun DocumentFile.search( walkFileTreeForSearch(DocumentFileType.FILE, mimeTypes, name, regex, thread) } } + else -> { var sequence = listFiles().asSequence().filter { it.canRead() } if (regex != null) { @@ -1260,7 +1295,7 @@ fun List.compressToZip( if (success) { if (deleteSourceWhenComplete) { callback.uiScope.postToUi { callback.onDeleteEntryFiles() } - forEach { it.deleteRecursively(context) } + forEach { it.forceDelete(context) } } val sizeReduction = (actualFilesSize - zipFile.length()).toFloat() / actualFilesSize * 100 callback.uiScope.postToUi { callback.onCompleted(zipFile, actualFilesSize, totalFiles, sizeReduction) } @@ -1440,21 +1475,32 @@ private fun List.copyTo( callback.uiScope.postToUi { callback.onCountingFiles() } - class SourceInfo(val children: List, val size: Long, val totalFiles: Int, val conflictResolution: FolderCallback.ConflictResolution) + class SourceInfo(val children: List?, val size: Long, val totalFiles: Int, val conflictResolution: FolderCallback.ConflictResolution) val sourceInfos = validSources.associateWith { src -> - val children = if (skipEmptyFiles) src.walkFileTreeAndSkipEmptyFiles() else src.walkFileTree(context) - var totalFilesToCopy = 0 - var totalSizeToCopy = 0L - children.forEach { - if (it.isFile) { - totalFilesToCopy++ - totalSizeToCopy += it.length() + val resolution = conflictResolutions.find { it.source == src }?.solution ?: FolderCallback.ConflictResolution.CREATE_NEW + if (src.isFile) { + SourceInfo(null, src.length(), 1, resolution) + } else { + val children = if (skipEmptyFiles) src.walkFileTreeAndSkipEmptyFiles() else src.walkFileTree(context) + var totalFilesToCopy = 0 + var totalSizeToCopy = 0L + children.forEach { + if (it.isFile) { + totalFilesToCopy++ + totalSizeToCopy += it.length() + } } + SourceInfo(children, totalSizeToCopy, totalFilesToCopy, resolution) } - val resolution = conflictResolutions.find { it.source == src }?.solution ?: FolderCallback.ConflictResolution.CREATE_NEW - SourceInfo(children, totalSizeToCopy, totalFilesToCopy, resolution) - }.toMutableMap() + // allow empty folders, but empty files need check + }.filterValues { it.children != null || (skipEmptyFiles && it.size > 0 || !skipEmptyFiles) }.toMutableMap() + + if (sourceInfos.isEmpty()) { + val result = MultipleFileCallback.Result(emptyList(), 0, 0, true) + callback.uiScope.postToUi { callback.onCompleted(result) } + return + } // key=src, value=result val results = mutableMapOf() @@ -1507,7 +1553,7 @@ private fun List.copyTo( } val thread = Thread.currentThread() - val totalFilesToCopy = validSources.count { it.isFile } + sourceInfos.values.sumOf { it.totalFiles } + val totalFilesToCopy = sourceInfos.values.sumOf { it.totalFiles } val reportInterval = awaitUiResult(callback.uiScope) { callback.onStart(sourceInfos.map { it.key }, totalFilesToCopy, thread) } if (reportInterval < 0) return @@ -1594,7 +1640,7 @@ private fun List.copyTo( } try { - if (targetRootFile.isFile) { + if (targetRootFile.isFile || info.children == null) { copy(src, targetRootFile) results[src] = targetRootFile continue @@ -1648,7 +1694,7 @@ private fun List.copyTo( timer?.cancel() if (!success || conflictedFiles.isEmpty()) { if (deleteSourceWhenComplete && success) { - sourceInfos.forEach { (src, _) -> src.deleteRecursively(context) } + sourceInfos.forEach { (src, _) -> src.forceDelete(context) } } val result = MultipleFileCallback.Result(results.map { it.value }, totalFilesToCopy, totalCopiedFiles, success) callback.uiScope.postToUi { callback.onCompleted(result) } @@ -1726,6 +1772,7 @@ private fun List.doesMeetCopyRequirements( !it.canRead() -> Pair(it, FolderCallback.ErrorCode.STORAGE_PERMISSION_DENIED) targetParentFolderPath == it.parentFile?.getAbsolutePath(context) -> Pair(it, FolderCallback.ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER) + else -> null } }.toMap() @@ -2752,7 +2799,7 @@ private fun List.handleParentFolderConflict( resolution.forEach { conflict -> when (conflict.solution) { FolderCallback.ConflictResolution.REPLACE -> { - if (!conflict.target.let { it.deleteRecursively(context, true) || !it.exists() }) { + if (!conflict.target.let { it.forceDelete(context, true) || !it.exists() }) { callback.uiScope.postToUi { callback.onFailed(MultipleFileCallback.ErrorCode.CANNOT_CREATE_FILE_IN_TARGET) } return null }